diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1cfa351..b4471cb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,16 +2,17 @@ name: Build and Test on: push: - branches: [ master ] + branches: [ master, develop ] pull_request: - branches: [ master ] + branches: [ master, develop ] workflow_dispatch: jobs: build: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@v1 - name: Build run: swift build -v - name: Run tests diff --git a/ExCodable.podspec b/ExCodable.podspec index b595796..1cb4459 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -3,14 +3,14 @@ Pod::Spec.new do |s| # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.name = "ExCodable" # export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) - s.version = ENV["LIB_VERSION"] || "0.6.0" + s.version = ENV["LIB_VERSION"] || "1.0.0" s.summary = "Key-Mapping Extensions for Swift Codable" # s.description = "Key-Mapping Extensions for Swift Codable." s.homepage = "https://github.com/iwill/ExCodable" # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.license = "MIT" - s.author = { "Mr. Ming" => "i+ExCodable@iwill.im" } + s.author = { "Míng" => "i+ExCodable@iwill.im" } s.social_media_url = "https://iwill.im/about/" # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.osx.deployment_target = "10.10" s.watchos.deployment_target = "2.0" - s.swift_version = "5.0" + s.swift_version = "5.9" # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.source = { :git => "https://github.com/iwill/ExCodable.git", :tag => s.version.to_s } @@ -43,4 +43,9 @@ Pod::Spec.new do |s| # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } # s.dependency "pod", "~> 1.0.0" + # use <"> but not <'> for #{s.name} and #{s.version} + s.pod_target_xcconfig = { + "OTHER_SWIFT_FLAGS" => "$(inherited) -Xfrontend -module-interface-preserve-types-as-written", + } + end diff --git a/LICENSE b/LICENSE index f66b212..0977b98 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Mr. Ming +Copyright (c) 2023 Míng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift index 0f9b40c..10a6f7a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,30 +1,26 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "ExCodable", platforms: [ - .iOS(.v9), - .tvOS(.v9), - .macOS(.v10_10), - .watchOS(.v2) + .iOS(.v12), + .tvOS(.v12), + .macOS(.v10_13), + .watchOS(.v4) ], products: [ - .library( - name: "ExCodable", - targets: ["ExCodable"]), + .library(name: "ExCodable", targets: ["ExCodable"]), + // .library(name: "ExCodable-Static", type: .static, targets: ["ExCodable"]), + .library(name: "ExCodable-Dynamic", type: .dynamic, targets: ["ExCodable"]) ], dependencies: [ // .package(url: "https://github.com/user/repo", from: "1.0.0") ], targets: [ - .target( - name: "ExCodable", - dependencies: []), - .testTarget( - name: "ExCodableTests", - dependencies: ["ExCodable"]), + .target(name: "ExCodable", dependencies: []), + .testTarget(name: "ExCodableTests", dependencies: ["ExCodable"]) ], swiftLanguageVersions: [.v5] ) diff --git a/README.md b/README.md index 0154806..ce0792f 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org/) [![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) [![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#readme) +
[![Build and Test](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml) [![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/iwill/ExCodable.svg?sort=semver)](https://github.com/iwill/ExCodable/releases) [![Deploy to CocoaPods](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml) [![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable) +
[![LICENSE](https://img.shields.io/github/license/iwill/ExCodable.svg)](https://github.com/iwill/ExCodable/blob/master/LICENSE) [![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) @@ -17,6 +19,7 @@ En | [中文](https://iwill.im/ExCodable/) - [Features](#features) - [Usage](#usage) - [Requirements](#requirements) +- [Migration Guides](#migration-guides) - [Installation](#installation) - [Credits](#credits) - [License](#license) @@ -24,7 +27,7 @@ En | [中文](https://iwill.im/ExCodable/) ## Features - Extends Swift `Codable` - `Encodable & Decodable`; -- Supports Key-Mapping via `KeyPath` and Coding-Key: +- Supports Key-Mapping via **Property-Wrapper** `ExCodable` + `String`: - `ExCodable` did not read/write memory via unsafe pointers; - No need to encode/decode properties one by one; - Just requires using `var` to declare properties and provide default values; @@ -42,9 +45,13 @@ En | [中文](https://iwill.im/ExCodable/) ## Usage -### 0. `Codable`: +### 0. Star this repo ⭐️ -With `Codable`, it just needs to adop the `Codable` protocol without implementing any method of it. +🤭 + +### 1. `Codable` vs `ExCodable`: + +With `Codable`, it just needs to adopt the `Codable` protocol without implementing any method of it. ```swift struct TestAutoCodable: Codable, Equatable { @@ -57,7 +64,7 @@ struct TestAutoCodable: Codable, Equatable { ``` -But, if you have to encode/decode manually for some reason, e.g. Alternative-Keys and Nested-Keys ... +But, if you have to encode/decode manually for some reason, e.g. Default-Value, Alternative-Keys, Nested-Keys or Type-Conversions ... ```swift struct TestManualCodable: Equatable { @@ -115,7 +122,7 @@ extension TestExCodable: ExCodable { ``` -### 1. Key-Mapping for `struct`: +### 2. Key-Mapping for `struct`: With `ExCodable`, it needs to to declare properties with `var` and provide default values. @@ -147,7 +154,7 @@ extension TestStruct: ExCodable { ``` -### 2. Alternative-Keys: +### 3. Alternative-Keys: ```swift static let keyMapping: [KeyMap] = [ @@ -157,7 +164,7 @@ static let keyMapping: [KeyMap] = [ ``` -### 3. Nested-Keys: +### 4. Nested-Keys: ```swift static let keyMapping: [KeyMap] = [ @@ -167,7 +174,7 @@ static let keyMapping: [KeyMap] = [ ``` -### 4. Custom encode/decode: +### 5. Custom encode/decode: ```swift struct TestCustomEncodeDecode: Equatable { @@ -217,7 +224,7 @@ extension TestCustomEncodeDecode: ExCodable { ``` -### 5. Encode/decode constant properties with subscripts: +### 6. Encode/decode constant properties with subscripts: Using `let` to declare properties without default values. @@ -256,13 +263,13 @@ extension TestSubscript: Encodable, Decodable { ``` -### 6. Custom Type-Conversions: +### 7. Custom Type-Conversions: -Declare struct `FloatToBoolDecodingTypeConverter` with protocol `ExCodableDecodingTypeConverter` and implement its method, decode values in alternative types and convert to target type: +A. For a specific type: ```swift -struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { - public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { +extension TestTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { // Bool -> Double if type is Double.Type || type is Double?.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { @@ -282,13 +289,59 @@ struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { ``` -Register `FloatToBoolDecodingTypeConverter` with an instance: +B. For multiple types: ```swift -register(FloatToBoolDecodingTypeConverter()) +extension ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // Bool -> Double + if type is Double.Type || type is Double?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Bool -> Float + else if type is Float.Type || type is Float?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Double or Float NOT found + return nil + } +} + +extension TestTypeConverter001: ExCodableDecodingTypeConverter {} +extension TestTypeConverter002: ExCodableDecodingTypeConverter {} +extension TestTypeConverter003: ExCodableDecodingTypeConverter {} + +``` + +C. For all types - DONOT do this in public frameworks: + +```swift +extension KeyedDecodingContainer: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // Bool -> Double + if type is Double.Type || type is Double?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Bool -> Float + else if type is Float.Type || type is Float?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Double or Float NOT found + return nil + } +} + ``` -### 7. Key-Mapping for `class`: +### 8. Key-Mapping for `class`: Cannot adopt `ExCodable` in extension of classes. @@ -317,7 +370,7 @@ class TestClass: ExCodable, Equatable { ``` -### 8. Key-Mapping for subclass: +### 9. Key-Mapping for subclass: Requires declaring another static Key-Mapping for subclass. @@ -351,7 +404,7 @@ class TestSubclass: TestClass { ``` -### 9. Encode/decode with Type-Inference: +### 10. Encode/decode with Type-Inference: ```swift let test = TestStruct(int: 304, string: "Not Modified") @@ -374,21 +427,21 @@ XCTAssertEqual(copy2, test) - [Swift Package Manager](https://swift.org/package-manager/): ```swift -.package(url: "https://github.com/iwill/ExCodable", from: "0.6.0") +.package(url: "https://github.com/iwill/ExCodable", from: "1.0.0") ``` - [CocoaPods](http://cocoapods.org): ```ruby -pod 'ExCodable', '~> 0.6.0' +pod 'ExCodable', '~> 1.0.0' ``` - Code Snippets: > Title: ExCodable -> Summary: Adopte to ExCodable protocol +> Summary: Adopt to ExCodable protocol > Language: Swift > Platform: All > Completion: ExCodable @@ -425,7 +478,7 @@ Hope you like this project, don't forget to give it a star [⭐](https://github. ## Connect with me -- Mr. Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im +- Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im ## License diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift new file mode 100644 index 0000000..c686bfe --- /dev/null +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -0,0 +1,125 @@ +// +// ExCodable.swift +// ExCodable +// +// Created by Míng on 2021-07-01. +// Copyright (c) 2023 Míng . Released under the MIT license. +// + +import Foundation + +// MARK: - keyMapping + +@available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") +public protocol ExCodableProtocol: Codable { + associatedtype Root = Self where Root: ExCodableProtocol + static var keyMapping: [KeyMap] { get } +} + +@available(*, deprecated) +public extension ExCodableProtocol where Root == Self { + // default implementation of ExCodableProtocol + static var keyMapping: [KeyMap] { [] } + // default implementation of Encodable + func encode(to encoder: Encoder) throws { + try encode(to: encoder, with: Self.keyMapping) + try encode(to: encoder, nonnull: false, throws: false) + } + func decode(from decoder: Decoder) throws { + try decode(from: decoder, nonnull: false, throws: false) + } +} + +@available(*, deprecated) +public extension ExCodableProtocol { + func encode(to encoder: Encoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { + try keyMapping.forEach { try $0.encode(self, encoder, nonnull, `throws`) } + } + mutating func decode(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { + try keyMapping.forEach { try $0.decode?(&self, decoder, nonnull, `throws`) } + } + func decodeReference(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { + try keyMapping.forEach { try $0.decodeReference?(self, decoder, nonnull, `throws`) } + } +} + +@available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") +public final class KeyMap { + fileprivate let encode: (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void + fileprivate let decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)? + fileprivate let decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)? + private init(encode: @escaping (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void, + decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?, + decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?) { + (self.encode, self.decode, self.decodeReference) = (encode, decode, decodeReference) + } +} + +@available(*, deprecated) +public extension KeyMap { + convenience init(_ keyPath: WritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) { + self.init(encode: { root, encoder, nonnullAll, throwsAll in + try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) + }, decode: { root, decoder, nonnullAll, throwsAll in + if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { + root[keyPath: keyPath] = value + } + }, decodeReference: nil) + } + convenience init(_ keyPath: WritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) { + self.init(encode: { root, encoder, nonnullAll, throwsAll in + try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) + }, decode: { root, decoder, nonnullAll, throwsAll in + if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { + root[keyPath: keyPath] = value + } + }, decodeReference: nil) + } + convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) { + self.init(encode: { root, encoder, nonnullAll, throwsAll in + try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) + }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in + if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { + root[keyPath: keyPath] = value + } + }) + } + convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) { + self.init(encode: { root, encoder, nonnullAll, throwsAll in + try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) + }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in + if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { + root[keyPath: keyPath] = value + } + }) + } +} + +// MARK: - + +@available(*, deprecated) +public extension ExCodableProtocol { + @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)") + func encode(with keyMapping: [KeyMap], using encoder: Encoder) { + try? encode(to: encoder, with: keyMapping) + } + @available(*, deprecated, renamed: "decode(from:with:nonnull:throws:)") + mutating func decode(with keyMapping: [KeyMap], using decoder: Decoder) { + try? decode(from: decoder, with: keyMapping) + } + @available(*, deprecated, renamed: "decodeReference(from:with:nonnull:throws:)") + func decodeReference(with keyMapping: [KeyMap], using decoder: Decoder) { + try? decodeReference(from: decoder, with: keyMapping) + } +} + +@available(*, deprecated, renamed: "append(decodingTypeConverter:)") +public protocol KeyedDecodingContainerCustomTypeConversion: ExCodableDecodingTypeConverter { + func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? +} +@available(*, deprecated) +public extension KeyedDecodingContainerCustomTypeConversion { + func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? { + return decodeForTypeConversion(container, codingKey: codingKey, as: type) + } +} diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 8ef6c24..51b4efc 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -2,8 +2,8 @@ // ExCodable.swift // ExCodable // -// Created by Mr. Ming on 2021-02-10. -// Copyright (c) 2021 Mr. Ming . Released under the MIT license. +// Created by Míng on 2021-02-10. +// Copyright (c) 2023 Míng . Released under the MIT license. // import Foundation @@ -11,89 +11,126 @@ import Foundation /** * # ExCodable * - * A protocol extends `Encodable` & `Decodable` with `keyMapping` + * - `ExCodable`: A property-wrapper for mapping properties to JSON keys. + * - `ExAutoEncodable` & `ExAutoDecodable`: Protocols with default implementation for Encodable & Decodable. + * - `ExAutoCodable`: A typealias for `ExAutoEncodable & ExAutoDecodable`. + * - `Encodable` & `Decodable` extensions for encode/decode-ing from internal/external. + * - `Encoder` & `Encoder` extensions for encode/decode-ing properties one by one. + * - Supports Alternative-Keys, Nested-Keys, Type-Conversions and Default-Values. + * * <#swift#> <#codable#> <#json#> <#model#> <#type-inference#> - * <#key-mapping#> <#keypath#> <#codingkey#> <#subscript#> - * <#alternative-keys#> <#nested-keys#> <#type-conversion#> + * <#key-mapping#> <#property-wrapper#> <#coding-key#> <#subscript#> + * <#alternative-keys#> <#nested-keys#> <#type-conversions#> * - * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from GitGub - * - seealso: `ExCodableTests.swift` form the source code + * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from the `README.md` + * - seealso: `ExCodableTests.swift` from the `Tests` + * - seealso: [Decoding and overriding](https://www.swiftbysundell.com/articles/property-wrappers-in-swift/#decoding-and-overriding) + * and [Useful Codable extensions](https://www.swiftbysundell.com/tips/useful-codable-extensions/), by John Sundell. */ -public protocol ExCodable: Codable { - associatedtype Root = Self where Root: ExCodable - static var keyMapping: [KeyMap] { get } -} -public extension ExCodable { - func encode(to encoder: Encoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { - try keyMapping.forEach { try $0.encode(self, encoder, nonnull, `throws`) } +@propertyWrapper +public final class ExCodable { + fileprivate let stringKeys: [String]? + fileprivate let nonnull, `throws`: Bool? + fileprivate let encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)? + public var wrappedValue: Value + private init(wrappedValue initialValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?) { + (self.wrappedValue, self.stringKeys, self.nonnull, self.throws, self.encode, self.decode) = (initialValue, stringKeys, nonnull, `throws`, encode, decode) } - mutating func decode(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { - try keyMapping.forEach { try $0.decode?(&self, decoder, nonnull, `throws`) } + public convenience init(wrappedValue initialValue: Value, _ stringKey: String? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) { + self.init(wrappedValue: initialValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) } - func decodeReference(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws { - try keyMapping.forEach { try $0.decodeReference?(self, decoder, nonnull, `throws`) } + public convenience init(wrappedValue initialValue: Value, _ stringKeys: String..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) { + self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) + } + public convenience init(wrappedValue initialValue: Value, _ codingKeys: CodingKey..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) { + self.init(wrappedValue: initialValue, stringKeys: codingKeys.map { $0.stringValue }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) } } -public extension ExCodable where Root == Self { - func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.keyMapping) +extension ExCodable: Equatable where Value: Equatable { + public static func == (lhs: ExCodable, rhs: ExCodable) -> Bool { + return lhs.wrappedValue == rhs.wrappedValue } } +extension ExCodable: CustomStringConvertible { // CustomDebugStringConvertible + public var description: String { String(describing: wrappedValue) } + // public var debugDescription: String { "\(type(of: self))(\(wrappedValue))" } +} -// MARK: - +fileprivate protocol EncodablePropertyWrapper { + func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws +} +extension ExCodable: EncodablePropertyWrapper where Value: Encodable { + fileprivate func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws { + if encode != nil { try encode!(encoder, wrappedValue) } + else { + let value = deepUnwrap(wrappedValue) + if value != nil || self.nonnull ?? nonnull { + try encoder.encode(wrappedValue, for: stringKeys?.first ?? String(label), nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`) + } + } + } +} -public final class KeyMap { - fileprivate let encode: (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void - fileprivate let decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)? - fileprivate let decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)? - private init(encode: @escaping (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void, - decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?, - decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?) { - (self.encode, self.decode, self.decodeReference) = (encode, decode, decodeReference) +fileprivate protocol DecodablePropertyWrapper { + func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws +} +extension ExCodable: DecodablePropertyWrapper where Value: Decodable { + fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws { + if let value = (decode != nil + ? try decode!(decoder) + : try decoder.decode(stringKeys ?? [String(label)], nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`, typeConverter: typeConverter)) { + wrappedValue = value + } } } -public extension KeyMap { - convenience init(_ keyPath: WritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) { - self.init(encode: { root, encoder, nonnullAll, throwsAll in - try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) - }, decode: { root, decoder, nonnullAll, throwsAll in - if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { - root[keyPath: keyPath] = value - } - }, decodeReference: nil) - } - convenience init(_ keyPath: WritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) { - self.init(encode: { root, encoder, nonnullAll, throwsAll in - try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) - }, decode: { root, decoder, nonnullAll, throwsAll in - if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { - root[keyPath: keyPath] = value - } - }, decodeReference: nil) - } - convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) { - self.init(encode: { root, encoder, nonnullAll, throwsAll in - try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) - }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in - if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { - root[keyPath: keyPath] = value +// MARK: - Auto implementation for Encodable & Decodable + +public protocol ExAutoEncodable: Encodable {} +public extension ExAutoEncodable { + func encode(to encoder: Encoder) throws { + try encode(to: encoder, nonnull: false, throws: false) + } +} + +public protocol ExAutoDecodable: Decodable { init() } +public extension ExAutoDecodable { + init(from decoder: Decoder) throws { + self.init() + try decode(from: decoder, nonnull: false, throws: false) + } +} + +public typealias ExAutoCodable = ExAutoEncodable & ExAutoDecodable + +// MARK: - Encodable & Decodable - internal + +public extension Encodable { + func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws { + var mirror: Mirror! = Mirror(reflecting: self) + while mirror != nil { + for child in mirror.children where child.label != nil { + try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } - }) - } - convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) { - self.init(encode: { root, encoder, nonnullAll, throwsAll in - try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) - }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in - if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { - root[keyPath: keyPath] = value + mirror = mirror.superclassMirror + } + } +} + +public extension Decodable { + func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws { + var mirror: Mirror! = Mirror(reflecting: self) + while mirror != nil { + for child in mirror.children where child.label != nil { + try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false, typeConverter: Self.self as? ExCodableDecodingTypeConverter.Type) } - }) + mirror = mirror.superclassMirror + } } } -// MARK: - subscript +// MARK: - Encoder & Decoder public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false subscript(stringKey: String) -> T? { get { return nil } @@ -105,22 +142,20 @@ public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError } public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false - subscript(stringKeys: [String]) -> T? { - return decode(stringKeys, as: T.self) + subscript(stringKeys: [String], typeConverter typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(stringKeys, as: T.self, typeConverter: typeConverter) } - subscript(stringKeys: String ...) -> T? { - return decode(stringKeys, as: T.self) + subscript(stringKeys: String ..., typeConverter typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(stringKeys, as: T.self, typeConverter: typeConverter) } - subscript(codingKeys: [K]) -> T? { - return decode(codingKeys, as: T.self) + subscript(codingKeys: [K], typeConverter typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(codingKeys, as: T.self, typeConverter: typeConverter) } - subscript(codingKeys: K ...) -> T? { - return decode(codingKeys, as: T.self) + subscript(codingKeys: K ..., typeConverter typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(codingKeys, as: T.self, typeConverter: typeConverter) } } -// MARK: - - public extension Encoder { func encodeNonnullThrows(_ value: T, for stringKey: String) throws { @@ -132,7 +167,7 @@ public extension Encoder { func encode(_ value: T?, for stringKey: String) { try? encode(value, for: stringKey, nonnull: false, throws: false) } - fileprivate func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { + internal/* fileprivate */ func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { let dot: Character = "." guard stringKey.contains(dot), stringKey.count > 1 else { @@ -163,7 +198,7 @@ public extension Encoder { func encode(_ value: T?, for codingKey: K) { try? encode(value, for: codingKey, nonnull: false, throws: false) } - fileprivate func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { + internal/* fileprivate */ func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { var container = self.container(keyedBy: K.self) do { if nonnull { try container.encode(value, forKey: codingKey) } @@ -175,75 +210,91 @@ public extension Encoder { public extension Decoder { - func decodeNonnullThrows(_ stringKeys: String ..., as type: T.Type = T.self) throws -> T { - return try decodeNonnullThrows(stringKeys, as: type) + func decodeNonnullThrows(_ stringKeys: String ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decodeNonnullThrows(stringKeys, as: type, typeConverter: typeConverter) } - func decodeNonnullThrows(_ stringKeys: [String], as type: T.Type = T.self) throws -> T { - return try decode(stringKeys, as: type, nonnull: true, throws: true)! + func decodeNonnullThrows(_ stringKeys: [String], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decode(stringKeys, as: type, nonnull: true, throws: true, typeConverter: typeConverter)! } - func decodeThrows(_ stringKeys: String ..., as type: T.Type = T.self) throws -> T? { - return try decodeThrows(stringKeys, as: type) + func decodeThrows(_ stringKeys: String ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decodeThrows(stringKeys, as: type, typeConverter: typeConverter) } - func decodeThrows(_ stringKeys: [String], as type: T.Type = T.self) throws -> T? { - return try decode(stringKeys, as: type, nonnull: false, throws: true) + func decodeThrows(_ stringKeys: [String], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decode(stringKeys, as: type, nonnull: false, throws: true, typeConverter: typeConverter) } - func decode(_ stringKeys: String ..., as type: T.Type = T.self) -> T? { - return decode(stringKeys, as: type) + func decode(_ stringKeys: String ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return decode(stringKeys, as: type, typeConverter: typeConverter) } - func decode(_ stringKeys: [String], as type: T.Type = T.self) -> T? { - return try? decode(stringKeys, as: type, nonnull: false, throws: false) + func decode(_ stringKeys: [String], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return try? decode(stringKeys, as: type, nonnull: false, throws: false, typeConverter: typeConverter) } - fileprivate func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? { - return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`) + // TODO: DEPRECATED + internal func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { + return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) } + // fileprivate func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + // return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) + // } - func decodeNonnullThrows(_ codingKeys: K ..., as type: T.Type = T.self) throws -> T { - return try decodeNonnullThrows(codingKeys, as: type) + func decodeNonnullThrows(_ codingKeys: K ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decodeNonnullThrows(codingKeys, as: type, typeConverter: typeConverter) } - func decodeNonnullThrows(_ codingKeys: [K], as type: T.Type = T.self) throws -> T { - return try decode(codingKeys, as: type, nonnull: true, throws: true)! + func decodeNonnullThrows(_ codingKeys: [K], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decode(codingKeys, as: type, nonnull: true, throws: true, typeConverter: typeConverter)! } - func decodeThrows(_ codingKeys: K ..., as type: T.Type = T.self) throws -> T? { - return try decodeThrows(codingKeys, as: type) + func decodeThrows(_ codingKeys: K ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decodeThrows(codingKeys, as: type, typeConverter: typeConverter) } - func decodeThrows(_ codingKeys: [K], as type: T.Type = T.self) throws -> T? { - return try decode(codingKeys, as: type, nonnull: false, throws: true) + func decodeThrows(_ codingKeys: [K], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decode(codingKeys, as: type, nonnull: false, throws: true, typeConverter: typeConverter) } - func decode(_ codingKeys: K ..., as type: T.Type = T.self) -> T? { - return decode(codingKeys, as: type) + func decode(_ codingKeys: K ..., as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return decode(codingKeys, as: type, typeConverter: typeConverter) } - func decode(_ codingKeys: [K], as type: T.Type = T.self) -> T? { - return try? decode(codingKeys, as: type, nonnull: false, throws: false) + func decode(_ codingKeys: [K], as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return try? decode(codingKeys, as: type, nonnull: false, throws: false, typeConverter: typeConverter) } - fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? { + // TODO: DEPRECATED + internal func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, typeConverter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { do { let container = try self.container(keyedBy: K.self) - return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`) + return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) } catch { if `throws` || nonnull { throw error } } return nil } + // fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + // do { + // let container = try self.container(keyedBy: K.self) + // return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) + // } + // catch { if `throws` || nonnull { throw error } } + // return nil + // } +} + +// MARK: - ExCodingKey + +private struct ExCodingKey { + public let stringValue: String, intValue: Int? + init(_ stringValue: S) { (self.stringValue, self.intValue) = (stringValue as? String ?? String(stringValue), nil) } } -private struct ExCodingKey: CodingKey { - let stringValue: String, intValue: Int? - init(_ stringValue: String) { (self.stringValue, self.intValue) = (stringValue, nil) } - init(_ stringValue: Substring) { self.init(String(stringValue)) } - init?(stringValue: String) { self.init(stringValue) } - init(_ intValue: Int) { (self.intValue, self.stringValue) = (intValue, String(intValue)) } - init?(intValue: Int) { self.init(intValue) } +extension ExCodingKey: CodingKey { + public init?(stringValue: String) { self.init(stringValue) } + public init?(intValue: Int) { self.init(intValue) } } -// MARK: - alternative-keys + nested-keys + type-conversion +// MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversions -private extension KeyedDecodingContainer { +fileprivate extension KeyedDecodingContainer { - func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { let codingKey = codingKeys.first! - if let value = try decodeForNestedKeys(codingKey, as: type, nonnull: nonnull, throws: `throws`) { + if let value = try decodeForNestedKeys(codingKey, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) { return value } } @@ -251,7 +302,7 @@ private extension KeyedDecodingContainer { let codingKeys = Array(codingKeys.dropFirst()) if !codingKeys.isEmpty, - let value = try? decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`) { + let value = try? decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) { return value } @@ -259,11 +310,11 @@ private extension KeyedDecodingContainer { return nil } - func decodeForNestedKeys(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForNestedKeys(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { - if let value = try decodeForValue(codingKey, as: type, nonnull: nonnull, throws: `throws`) { + if let value = try decodeForValue(codingKey, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) { return value } } @@ -276,7 +327,7 @@ private extension KeyedDecodingContainer { if !keys.isEmpty, let container = nestedContainer(with: keys.dropLast()), let codingKey = keys.last, - let value = try? container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`) { + let value = try? container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`, typeConverter: typeConverter) { return value } } @@ -294,28 +345,73 @@ private extension KeyedDecodingContainer { return container } - func decodeForValue(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForValue(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { - if let value = nonnull - ? (`throws` ? try decode(type, forKey: codingKey) : try? decode(type, forKey: codingKey)) - : (`throws` ? try decodeIfPresent(type, forKey: codingKey) : try? decodeIfPresent(type, forKey: codingKey)) { + if let value = (nonnull + ? (`throws` ? try decode(type, forKey: codingKey) : try? decode(type, forKey: codingKey)) + : (`throws` ? try decodeIfPresent(type, forKey: codingKey) : try? decodeIfPresent(type, forKey: codingKey))) { return value } } catch { firstError = error } - if contains(codingKey), - let value = decodeForTypeConversion(codingKey, as: type) { - return value + if contains(codingKey) { + if let value = decodeForTypeConversion(codingKey, as: type, typeConverter: typeConverter) { + return value + } + if #available(iOS 16.0.0, *) { + + // TODO: how to fix "Type 'any Decodable' cannot conform to 'Decodable'" + // if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + + if let rawType = type as? any (Decodable&RawRepresentable< Bool >).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + + if let rawType = type as? any (Decodable&RawRepresentable< Int >).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< Int8 >).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< Int16>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< Int32>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< Int64>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + + if let rawType = type as? any (Decodable&RawRepresentable< UInt >).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< UInt8 >).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< UInt16>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< UInt32>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable< UInt64>).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + // if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + // if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + + if let rawType = type as? any (Decodable&RawRepresentable).Type { return decodeForRawRepresentable(codingKey, as: rawType, typeConverter: typeConverter) as? T } + } } if firstError != nil && (`throws` || nonnull) { throw firstError! } return nil } - func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self) -> T? { + // TODO: how to fix "Type 'any Decodable' cannot conform to 'Decodable'" + // func decodeForRawRepresentable>(_ codingKey: Self.Key, as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + // if let rawValue = decodeForTypeConversion(codingKey, as: T.RawValue.self, typeConverter: typeConverter) { + // return type.init(rawValue: rawValue) + // } + // return nil + // } + + func decodeForRawRepresentable(_ codingKey: Self.Key, as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? where T: RawRepresentable, T.RawValue: Decodable { + if let rawValue = decodeForTypeConversion(codingKey, as: T.RawValue.self, typeConverter: typeConverter) { + return type.init(rawValue: rawValue) + } + return nil + } + + func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, typeConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { if type is Bool.Type { if let int = try? decodeIfPresent(Int.self, forKey: codingKey) { @@ -359,6 +455,7 @@ private extension KeyedDecodingContainer { else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int64(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int64(string) { return value as? T } } + else if type is UInt.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt(string) { return value as? T } @@ -388,20 +485,43 @@ private extension KeyedDecodingContainer { if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float(int64) as? T } // include all Int types else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float(string) { return value as? T } } - else if type is String.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return String(describing: bool) as? T } else if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return String(describing: int64) as? T } // include all Int types else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return String(describing: double) as? T } // include Float } - for conversion in _decodingTypeConverters { - if let value = try? conversion.decode(self, codingKey: codingKey, as: type) { - return value + if #available(iOS 14.0, *) { + if type is Float16.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float16(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float16(string) { return value as? T } + } + else if type is Float32.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float32(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float32(string) { return value as? T } + } + else if type is Float64.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float64(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float64(string) { return value as? T } } + // else if type is Float80.Type { + // if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float80(int64) as? T } // include all Int types + // else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float80(string) { return value as? T } + // } + // else if type is Float96.Type { + // if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float96(int64) as? T } // include all Int types + // else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float96(string) { return value as? T } + // } + } + + // specific converter for type `T`, via `extension T: ExCodableDecodingTypeConverter` + if let typeConverter, + let value = try? typeConverter.decode(self, codingKey: codingKey, as: type) { + return value } - if let custom = self as? ExCodableDecodingTypeConverter, - let value = try? custom.decode(self, codingKey: codingKey, as: type) { + // global converter for all types, via `extension KeyedDecodingContainer: ExCodableDecodingTypeConverter` + if let selfConverter = Self.self as? ExCodableDecodingTypeConverter.Type, + let value = try? selfConverter.decode(self, codingKey: codingKey, as: type) { return value } @@ -410,16 +530,10 @@ private extension KeyedDecodingContainer { } public protocol ExCodableDecodingTypeConverter { - func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? + static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? } -private var _decodingTypeConverters: [ExCodableDecodingTypeConverter] = [] -public func register(_ decodingTypeConverter: ExCodableDecodingTypeConverter) { - _decodingTypeConverters.append(decodingTypeConverter) -} - -// MARK: - Encodable/Decodable -// - seealso: [Codextended](https://github.com/JohnSundell/Codextended) +// MARK: - Encodable & Decodable - external // Encodable.encode() -> Data? public extension Encodable { @@ -494,35 +608,26 @@ public protocol DataDecoder { extension JSONEncoder: DataEncoder {} extension JSONDecoder: DataDecoder {} -#if canImport(ObjectiveC) || swift(>=5.1) +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension PropertyListEncoder: DataEncoder {} +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension PropertyListDecoder: DataDecoder {} -#endif -// MARK: - #### DEPRECATED #### +// MARK: Optional -public extension ExCodable { - @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)") - func encode(with keyMapping: [KeyMap], using encoder: Encoder) { - try? encode(to: encoder, with: keyMapping) - } - @available(*, deprecated, renamed: "decode(from:with:nonnull:throws:)") - mutating func decode(with keyMapping: [KeyMap], using decoder: Decoder) { - try? decode(from: decoder, with: keyMapping) - } - @available(*, deprecated, renamed: "decodeReference(from:with:nonnull:throws:)") - func decodeReference(with keyMapping: [KeyMap], using decoder: Decoder) { - try? decodeReference(from: decoder, with: keyMapping) - } +private protocol OptionalProtocol { + var deepWrapped: Any? { get } } - -@available(*, deprecated, renamed: "append(decodingTypeConverter:)") -public protocol KeyedDecodingContainerCustomTypeConversion: ExCodableDecodingTypeConverter { - func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? +extension Optional: OptionalProtocol { + var deepWrapped: Any? { + guard case let .some(wrapped) = self else { return nil } + guard let wrapped = wrapped as? OptionalProtocol else { return wrapped } + return wrapped.deepWrapped + } } -@available(*, deprecated) -public extension KeyedDecodingContainerCustomTypeConversion { - func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? { - return decodeForTypeConversion(container, codingKey: codingKey, as: type) +private func deepUnwrap(_ any: Any) -> Any? { + if let any = any as? OptionalProtocol { + return any.deepWrapped } + return any } diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 333f83f..d6e8363 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -2,8 +2,8 @@ // ExCodableTests.swift // ExCodable // -// Created by Mr. Ming on 2021-02-10. -// Copyright (c) 2021 Mr. Ming . Released under the MIT license. +// Created by Míng on 2021-02-10. +// Copyright (c) 2023 Míng . Released under the MIT license. // import XCTest @@ -59,93 +59,66 @@ extension TestManualCodable: Codable { // MARK: struct struct TestStruct: Equatable { + @ExCodable("int") private(set) var int: Int = 0 - private(set) var string: String? + @ExCodable("string") + private(set) var string: String? = nil var bool: Bool! } +extension TestStruct: ExAutoCodable {} -extension TestStruct: ExCodable { - - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int"), - KeyMap(\.string, to: "string"), - ] - - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - } - // `encode` with default implementation can be omitted - // func encode(to encoder: Encoder) throws { - // try encode(to: encoder, with: Self.keyMapping) - // } +// MARK: struct with enum + +enum TestEnum: Int, Codable { + case zero = 0, one = 1 } +struct TestStructWithEnum: Equatable { + @ExCodable("enum") + private(set) var `enum`: TestEnum = .zero +} +extension TestStructWithEnum: ExAutoCodable {} + // MARK: alternative-keys & alternative-keyMapping struct TestAlternativeKeys: Equatable { + @ExCodable("int", "i") var int: Int = 0 - var string: String! -} - -extension TestAlternativeKeys: ExCodable { - - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int", "i"), - KeyMap(\.string, to: "string", "str", "s") - ] - - static let keyMappingFromLocal: [KeyMap] = [ - KeyMap(\.int, to: "INT"), - KeyMap(\.string, to: "STRING") - ] - - enum LocalKeys: String, CodingKey { - case isLocal = "_IS_LOCAL_" - } - - init(from decoder: Decoder) throws { - let isLocal = decoder[LocalKeys.isLocal] ?? false - try decode(from: decoder, with: isLocal ? Self.keyMappingFromLocal : Self.keyMapping) - } - func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.keyMappingFromLocal) - encoder[LocalKeys.isLocal] = true - } + @ExCodable("string", "str", "s") + var string: String! = nil } +extension TestAlternativeKeys: ExAutoCodable {} // MARK: nested-keys struct TestNestedKeys: Equatable { + @ExCodable var int: Int = 0 - var string: String! -} - -extension TestNestedKeys: ExCodable { - - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int"), - KeyMap(\.string, to: "nested.string") - ] - - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - } - // func encode(to encoder: Encoder) throws { - // try encode(to: encoder, with: Self.keyMapping) - // } + @ExCodable("nested.nested.string") + var string: String! = nil } +extension TestNestedKeys: ExAutoCodable {} // MARK: custom encode/decode struct TestCustomEncodeDecode: Equatable { + + @ExCodable(Keys.int) var int: Int = 0 - var string: String? + + @ExCodable(encode: { encoder, value in encoder["nested.nested.string"] = value }, + decode: { decoder in return decoder["nested.nested.string"/*, typeConverter: Self.self*/] }) + var string: String? = nil + + @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value }, + decode: { decoder in return decoder[Keys.bool/*, typeConverter: Self.self*/] }) + var bool: Bool = false } -extension TestCustomEncodeDecode: ExCodable { +extension TestCustomEncodeDecode: Codable { private enum Keys: CodingKey { - case int, string + case int, bool } private static let dddd = "dddd" private func string(for int: Int) -> String { @@ -162,20 +135,16 @@ extension TestCustomEncodeDecode: ExCodable { } } - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: Keys.int), - ] - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - string = decoder[Keys.string] + try decode(from: decoder, nonnull: false, throws: false) + string = decoder["nested.nested.string"/*, typeConverter: Self.self*/] if string == nil || string == Self.dddd { string = string(for: int) } } func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.keyMapping) - encoder[Keys.string] = Self.dddd + try encode(to: encoder, nonnull: false, throws: false) + encoder["nested.nested.string"] = Self.dddd } } @@ -194,17 +163,17 @@ extension TestSubscript: Encodable, Decodable { init(from decoder: Decoder) throws { // - seealso: - // string = decoder.decode(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeNonnullThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - int = decoder[Keys.int] ?? 0 - string = decoder[Keys.string] ?? "" + // string = decoder.decode(Keys.string, as: String.self, typeConverter: Self.self)! + // string = try decoder.decodeThrows(Keys.string, as: String.self, typeConverter: Self.self)! + // string = try decoder.decodeNonnullThrows(Keys.string, as: String.self, typeConverter: Self.self) + int = decoder[Keys.int/*, typeConverter: Self.self*/] ?? 0 + string = decoder[Keys.string/*, typeConverter: Self.self*/] ?? "" } func encode(to encoder: Encoder) throws { // - seealso: - // encoder.encode(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeThrows(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeNonnullThrows(<#T##value: Encodable##Encodable#>, for: <#T##CodingKey#>) + // encoder.encode(string, for: Keys.string) + // try encoder.encodeThrows(string, for: Keys.string) + // try encoder.encodeNonnullThrows(string, for: Keys.string) encoder[Keys.int] = int encoder[Keys.string] = string } @@ -233,20 +202,20 @@ extension TestTypeConversions: Encodable, Decodable { } init(from decoder: Decoder) throws { - boolFromInt = decoder[Keys.boolFromInt] - boolFromString = decoder[Keys.boolFromString] - intFromBool = decoder[Keys.intFromBool] - intFromDouble = decoder[Keys.intFromDouble] - intFromString = decoder[Keys.intFromString] - uIntFromBool = decoder[Keys.uIntFromBool] - uIntFromString = decoder[Keys.uIntFromString] - doubleFromInt64 = decoder[Keys.doubleFromInt64] - doubleFromString = decoder[Keys.doubleFromString] - floatFromInt64 = decoder[Keys.floatFromInt64] - floatFromString = decoder[Keys.floatFromString] - stringFromBool = decoder[Keys.stringFromBool] - stringFromInt64 = decoder[Keys.stringFromInt64] - stringFromDouble = decoder[Keys.stringFromDouble] + boolFromInt = decoder[Keys.boolFromInt/*, typeConverter: Self.self*/] + boolFromString = decoder[Keys.boolFromString/*, typeConverter: Self.self*/] + intFromBool = decoder[Keys.intFromBool/*, typeConverter: Self.self*/] + intFromDouble = decoder[Keys.intFromDouble/*, typeConverter: Self.self*/] + intFromString = decoder[Keys.intFromString/*, typeConverter: Self.self*/] + uIntFromBool = decoder[Keys.uIntFromBool/*, typeConverter: Self.self*/] + uIntFromString = decoder[Keys.uIntFromString/*, typeConverter: Self.self*/] + doubleFromInt64 = decoder[Keys.doubleFromInt64/*, typeConverter: Self.self*/] + doubleFromString = decoder[Keys.doubleFromString/*, typeConverter: Self.self*/] + floatFromInt64 = decoder[Keys.floatFromInt64/*, typeConverter: Self.self*/] + floatFromString = decoder[Keys.floatFromString/*, typeConverter: Self.self*/] + stringFromBool = decoder[Keys.stringFromBool/*, typeConverter: Self.self*/] + stringFromInt64 = decoder[Keys.stringFromInt64/*, typeConverter: Self.self*/] + stringFromDouble = decoder[Keys.stringFromDouble/*, typeConverter: Self.self*/] } func encode(to encoder: Encoder) throws { @@ -289,8 +258,23 @@ extension TestTypeConversions: Encodable, Decodable { // } // } -struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { - public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { +struct TestCustomTypeConverter: Equatable { + @ExCodable("enum") + private(set) var `enum`: TestEnum = .zero + @ExCodable("doubleFromBool") + var doubleFromBool: Double? = nil +} +extension TestCustomTypeConverter: ExAutoCodable {} + +extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { + static public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // String -> TestEnum + if type is TestEnum.Type || type is TestEnum?.Type { + if let string = try? container.decodeIfPresent(String.self, forKey: codingKey), + let int = Int(string) { + return TestEnum(rawValue: int) as? T + } + } // Bool -> Double if type is Double.Type || type is Double?.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { @@ -308,45 +292,24 @@ struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { } } -struct TestCustomTypeConverter: Equatable { - var doubleFromBool: Double? = nil -} - -extension TestCustomTypeConverter: ExCodable { - - static let keyMapping: [KeyMap] = [ - KeyMap(\.doubleFromBool, to: "doubleFromBool") - ] - - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - } - // func encode(to encoder: Encoder) throws { - // try encode(to: encoder, with: Self.keyMapping) - // } -} - // MARK: class -class TestClass: ExCodable, Equatable { +class TestClass: Codable, Equatable { + @ExCodable("int") var int: Int = 0 + @ExCodable("string") var string: String? = nil init(int: Int, string: String?) { (self.int, self.string) = (int, string) } - static let keyMapping: [KeyMap] = [ - KeyMap(ref: \.int, to: "int"), - KeyMap(ref: \.string, to: "string") - ] - required init(from decoder: Decoder) throws { - try decodeReference(from: decoder, with: Self.keyMapping) + try decode(from: decoder, nonnull: false, throws: false) + } + func encode(to encoder: Encoder) throws { + try encode(to: encoder, nonnull: false, throws: false) } - // func encode(to encoder: Encoder) throws { - // try encode(to: encoder, with: Self.keyMapping) - // } static func == (lhs: TestClass, rhs: TestClass) -> Bool { return lhs.int == rhs.int && lhs.string == rhs.string @@ -356,53 +319,34 @@ class TestClass: ExCodable, Equatable { // MARK: subclass class TestSubclass: TestClass { + + @ExCodable("bool") var bool: Bool = false required init(int: Int, string: String, bool: Bool) { self.bool = bool super.init(int: int, string: string) } - static let keyMappingForTestSubclass: [KeyMap] = [ - KeyMap(ref: \.bool, to: "bool") - ] - required init(from decoder: Decoder) throws { try super.init(from: decoder) - try decodeReference(from: decoder, with: Self.keyMappingForTestSubclass) - } - override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - try encode(to: encoder, with: Self.keyMappingForTestSubclass) } static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool { - return lhs.int == rhs.int - && lhs.string == rhs.string - && lhs.bool == rhs.bool + return (lhs.int == rhs.int + && lhs.string == rhs.string + && lhs.bool == rhs.bool) } } // MARK: ExCodable struct TestExCodable: Equatable { + @ExCodable("int") private(set) var int: Int = 0 - private(set) var string: String? -} - -extension TestExCodable: ExCodable { - - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int"), - KeyMap(\.string, to: "string") - ] - - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - } - // func encode(to encoder: Encoder) throws { - // try encode(to: encoder, with: Self.keyMapping) - // } + @ExCodable("string") + private(set) var string: String? = nil } +extension TestExCodable: ExAutoCodable {} // MARK: - Tests @@ -412,7 +356,7 @@ final class ExCodableTests: XCTestCase { let test = TestAutoCodable(int: 100, string: "Continue") if let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestAutoCodable, - let json: [String: Any] = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { XCTAssertEqual(copy, test) XCTAssertEqual(NSDictionary(dictionary: json), ["i": 100, "s": "Continue"]) } @@ -442,6 +386,61 @@ final class ExCodableTests: XCTestCase { let copy2 = try? TestStruct.decoded(from: data) { XCTAssertEqual(copy1, test) XCTAssertEqual(copy2, test) + + let string = "string: \(test)" + print(string) + print(test) + debugPrint(test) + } + else { + XCTFail() + } + } + + func testStructWithEnum() { + let test = TestStructWithEnum(enum: .one) + if let data = try? test.encoded() as Data, + let copy1 = try? data.decoded() as TestStructWithEnum, + let copy2 = try? TestStructWithEnum.decoded(from: data) { + XCTAssertEqual(copy1, test) + XCTAssertEqual(copy2, test) + + let string = String(data: data, encoding: .utf8) + print("TestStructWithEnum: \(string ?? "")") + } + else { + XCTFail() + } + } + + func testStructWithEnumFromJSON() { + let json = Data(#"{"enum":1}"#.utf8) + if let test = try? json.decoded() as TestStructWithEnum, + let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestStructWithEnum { + XCTAssertEqual(test, TestStructWithEnum(enum: .one)) + XCTAssertEqual(copy, test) + let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: localJSON), [ + "enum": 1 + ]) + } + else { + XCTFail() + } + } + + func testStructWithEnumFromJSONWithString() { + let json = Data(#"{"enum":"1"}"#.utf8) + if let test = try? json.decoded() as TestStructWithEnum, + let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestStructWithEnum { + XCTAssertEqual(test, TestStructWithEnum(enum: .one)) + XCTAssertEqual(copy, test) + let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: localJSON), [ + "enum": 1 + ]) } else { XCTFail() @@ -457,9 +456,8 @@ final class ExCodableTests: XCTestCase { XCTAssertEqual(copy, test) let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] XCTAssertEqual(NSDictionary(dictionary: localJSON), [ - "_IS_LOCAL_": true, - "INT": 403, - "STRING": "Forbidden" + "int": 403, + "string": "Forbidden" ]) } else { @@ -477,7 +475,9 @@ final class ExCodableTests: XCTestCase { XCTAssertEqual(NSDictionary(dictionary: json), [ "int": 404, "nested": [ - "string": "Not Found" + "nested": [ + "string": "Not Found" + ] ] ]) } @@ -487,7 +487,7 @@ final class ExCodableTests: XCTestCase { } func testCustomEncodeDecode() { - let test = TestCustomEncodeDecode(int: 418, string: "I'm a teapot") + let test = TestCustomEncodeDecode(int: 418, string: "I'm a teapot", bool: true) if let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestCustomEncodeDecode { XCTAssertEqual(copy, test) @@ -495,7 +495,12 @@ final class ExCodableTests: XCTestCase { debugPrint(json) XCTAssertEqual(NSDictionary(dictionary: json), [ "int": 418, - "string": "dddd" + "nested": [ + "nested": [ + "string": "dddd" + ] + ], + "bool": true ]) } else { @@ -594,14 +599,14 @@ final class ExCodableTests: XCTestCase { } func testCustomTypeConverter() { - register(FloatToBoolDecodingTypeConverter()) let data = Data(#""" { + "enum": "1", "doubleFromBool": true } """#.utf8) if let test = try? data.decoded() as TestCustomTypeConverter { - XCTAssertEqual(test, TestCustomTypeConverter(doubleFromBool: 1.0)) + XCTAssertEqual(test, TestCustomTypeConverter(enum: .one, doubleFromBool: 1.0)) } else { XCTFail() @@ -748,20 +753,4 @@ final class ExCodableTests: XCTestCase { let milliseconds = elapsed / 1_000_000 print("elapsed: \(milliseconds) ms") } - - static var allTests = [ - ("testAutoCodable", testAutoCodable), - ("testManualCodable", testManualCodable), - ("testStruct", testStruct), - ("testAlternativeKeys", testAlternativeKeys), - ("testNestedKeys", testNestedKeys), - ("testCustomEncodeDecode", testCustomEncodeDecode), - ("testSubscript", testSubscript), - ("testTypeConversions", testTypeConversions), - ("testCustomTypeConverter", testCustomTypeConverter), - ("testClass", testClass), - ("testSubclass", testSubclass), - ("testExCodable", testExCodable), - ("testElapsed", testElapsed) - ] } diff --git a/Tests/ExCodableTests/XCTestManifests.swift b/Tests/ExCodableTests/XCTestManifests.swift deleted file mode 100644 index d608652..0000000 --- a/Tests/ExCodableTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(ExCodableTests.allTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 863417c..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import ExCodableTests - -var tests = [XCTestCaseEntry]() -tests += ExCodableTests.allTests() -XCTMain(tests)