├── .spi.yml ├── Helpers ├── DynamicCodableKit.xcconfig └── DynamicCodableKit.podspec ├── Tests ├── LinuxMain.swift └── DynamicCodableKitTests │ ├── DynamicDecodingContextContainerCodingKey │ ├── Data │ │ ├── PathCodingKeyData.swift │ │ ├── ContainerDecodeWithInvalidData.swift │ │ ├── ContainerDecode.swift │ │ ├── ContainerCollectionDecodeWithInvalidData.swift │ │ └── ContainerCollectionDecode.swift │ ├── Models │ │ ├── PostContainer.swift │ │ └── PostPath.swift │ ├── DynamicDecodingDictionaryWrapper.swift │ ├── DynamicDecodingCollectionDictionaryWrapper.swift │ └── PathCodingKeyWrapper.swift │ ├── DynamicDecodingContextCodingKey │ ├── Data │ │ ├── IdentifierDecodeWithInvalidData.swift │ │ ├── IdentifierDecode.swift │ │ ├── IdentifierCollectionDecode.swift │ │ └── IdentifierCollectionDecodeWithInvalidData.swift │ ├── Models │ │ ├── PostPage.swift │ │ ├── SocialMediaPost.swift │ │ └── DynamicBaseType.swift │ ├── DynamicDecodingWrapper.swift │ └── DynamicDecodingCollectionWrapper.swift │ ├── DynamicDecodingContextProvider │ ├── Data │ │ ├── ProviderCollectionDecode.swift │ │ └── ProviderCollectionDecodeWithInvalidData.swift │ ├── Models │ │ └── ProviderPost.swift │ ├── DynamicDecodingCollectionContextBasedWrapper.swift │ └── DynamicDecodingContextBasedWrapper.swift │ ├── Models │ ├── Post.swift │ └── HashablePost.swift │ ├── DynamicDecodingContext.swift │ └── DynamicDecodable.swift ├── .vscode └── settings.json ├── Sources └── DynamicCodableKit │ ├── DynamicCodableKit.docc │ ├── Resources │ │ ├── container-json.png │ │ ├── identifier-class.png │ │ ├── identifier-json.png │ │ ├── container-json~dark.png │ │ ├── context-provider-class.png │ │ ├── identifier-class~dark.png │ │ ├── identifier-json~dark.png │ │ └── context-provider-class~dark.png │ ├── Guides │ │ ├── NoThrowDecoding.md │ │ ├── CollectionDecoding.md │ │ ├── ContextProvider.md │ │ ├── ContainerCodingKey.md │ │ └── TypeIdentifier.md │ ├── Extensions │ │ └── DynamicDecodable.md │ └── DynamicCodableKit.md │ ├── Extensions │ ├── UnkeyedDecodingContainer.swift │ ├── KeyedDecodingContainerProtocol.swift │ ├── Decoder.swift │ └── DecodingError.swift │ ├── DynamicDecodingContextProvider │ ├── DynamicDecodingContextBasedWrapper.swift │ ├── DynamicDecodingContextProvider.swift │ ├── DynamicDecodingDefaultValueContextBasedWrapper.swift │ └── DynamicDecodingCollectionContextBasedWrapper.swift │ ├── DynamicDecodingContextCodingKey │ ├── DynamicDecodingWrapper.swift │ ├── DynamicDecodingContextCodingKey.swift │ └── DynamicDecodingDefaultValueWrapper.swift │ ├── DynamicDecodingContextContainerCodingKey │ ├── PathCodingKeyWrapper.swift │ ├── DynamicDecodingDictionaryWrapper.swift │ ├── PathCodingKeyDefaultValueWrapper.swift │ └── DynamicDecodingContextContainerCodingKey.swift │ ├── DynamicDecodingConfiguration.swift │ ├── DynamicDecodingContext.swift │ ├── DynamicDecodable.swift │ └── DynamicEncodable.swift ├── .github ├── config │ ├── spellcheck.yaml │ └── spellcheck-wordlist.txt ├── dependabot.yml ├── FUNDING.yml └── workflows │ └── main.yml ├── .swift-format ├── Package.swift ├── Package@swift-5.5.swift ├── Package@swift-5.6.swift ├── DynamicCodableKit.xcodeproj ├── DynamicCodableKit_Info.plist ├── DynamicCodableKitTests_Info.plist └── xcshareddata │ └── xcschemes │ └── DynamicCodableKit-Package.xcscheme ├── LICENSE ├── package.json ├── DynamicCodableKit.podspec ├── CONTRIBUTING.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── .gitignore └── README.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [DynamicCodableKit] -------------------------------------------------------------------------------- /Helpers/DynamicCodableKit.xcconfig: -------------------------------------------------------------------------------- 1 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES 2 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 3 | RUN_DOCUMENTATION_COMPILER = YES 4 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DynamicCodableKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DynamicCodableKitTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.build": true, 4 | "**/.swiftpm": true, 5 | "**/.docc-build": true, 6 | "**/node_modules": true 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/container-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/container-json.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-class.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-json.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/container-json~dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/container-json~dark.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/context-provider-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/context-provider-class.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-class~dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-class~dark.png -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-json~dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/identifier-json~dark.png -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Data/PathCodingKeyData.swift: -------------------------------------------------------------------------------- 1 | let pathCodingKeyData = 2 | """ 3 | { 4 | "text": [ 5 | {} 6 | ] 7 | } 8 | """.data(using: .utf8)! 9 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/context-provider-class~dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftyLab/DynamicCodableKit/HEAD/Sources/DynamicCodableKit/DynamicCodableKit.docc/Resources/context-provider-class~dark.png -------------------------------------------------------------------------------- /.github/config/spellcheck.yaml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | aspell: 4 | lang: en 5 | dictionary: 6 | wordlists: 7 | - .github/config/spellcheck-wordlist.txt 8 | encoding: utf-8 9 | pipeline: 10 | - pyspelling.filters.markdown: 11 | markdown_extensions: 12 | - pymdownx.superfences: 13 | - pyspelling.filters.html: 14 | comments: false 15 | ignores: 16 | - code 17 | - pre 18 | sources: 19 | - '**/*.md' 20 | default_encoding: utf-8 21 | -------------------------------------------------------------------------------- /.github/config/spellcheck-wordlist.txt: -------------------------------------------------------------------------------- 1 | CocoaPods 2 | Codable 3 | CodingKey 4 | DocC 5 | DocumentationExtension 6 | DynamicCodableKit 7 | Github 8 | JSON 9 | SPM 10 | Submodule 11 | XCFramework 12 | Xcode 13 | ae 14 | automagically 15 | ba 16 | bab 17 | casted 18 | ceb 19 | eb 20 | faq 21 | fc 22 | fd 23 | fe 24 | gmail 25 | html 26 | https 27 | lossy 28 | macOS 29 | mahunt 30 | mergeBehavior 31 | podspec 32 | pre 33 | prebuilt 34 | sexualized 35 | socio 36 | soumya 37 | submodule 38 | tvOS 39 | vscode 40 | watchOS 41 | www 42 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 80, 4 | "indentation": { 5 | "spaces": 4 6 | }, 7 | "maximumBlankLines": 1, 8 | "respectsExistingLineBreaks": true, 9 | "indentConditionalCompilationBlocks": false, 10 | "rules": { 11 | "AllPublicDeclarationsHaveDocumentation": true, 12 | "NeverForceUnwrap": true, 13 | "NeverUseForceTry": true, 14 | "NoAccessLevelOnExtensionDeclaration": false, 15 | "ValidateDocumentationComments": true 16 | } 17 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | # - package-ecosystem: swift-package 5 | # directory: / 6 | # schedule: 7 | # interval: weekly 8 | # commit-message: 9 | # prefix: 'deps: ' 10 | 11 | - package-ecosystem: github-actions 12 | directory: / 13 | schedule: 14 | interval: monthly 15 | commit-message: 16 | prefix: 'ci(Deps): ' 17 | 18 | - package-ecosystem: npm 19 | directory: / 20 | schedule: 21 | interval: monthly 22 | commit-message: 23 | prefix: 'ci(Deps): ' 24 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Data/IdentifierDecodeWithInvalidData.swift: -------------------------------------------------------------------------------- 1 | let identifierDecodeWithInvalidData = 2 | """ 3 | { 4 | "content": { 5 | "id": "4c76f901-3c4f-482c-8663-600a73416773", 6 | "type": "invalid", 7 | "author": "026d7a8a-12b1-4193-8a0d-415bc8f80c1a", 8 | "likes": 25, 9 | "createdAt": "2021-07-23T09:33:48Z", 10 | "url": "https://a.url.com/to/a/audio.aac", 11 | "duration": 60 12 | }, 13 | "next": "https://a.url.com/to/next/page" 14 | } 15 | """.data(using: .utf8)! 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DynamicCodableKit", 7 | platforms: [ 8 | .iOS(.v8), 9 | .macOS(.v10_10), 10 | .tvOS(.v9), 11 | .watchOS(.v2) 12 | ], 13 | products: [ 14 | .library(name: "DynamicCodableKit", targets: ["DynamicCodableKit"]), 15 | ], 16 | targets: [ 17 | .target(name: "DynamicCodableKit", dependencies: []), 18 | .testTarget(name: "DynamicCodableKitTests", dependencies: ["DynamicCodableKit"]), 19 | ], 20 | swiftLanguageVersions: [.v5] 21 | ) 22 | -------------------------------------------------------------------------------- /Package@swift-5.5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DynamicCodableKit", 7 | platforms: [ 8 | .iOS(.v8), 9 | .macOS(.v10_10), 10 | .tvOS(.v9), 11 | .watchOS(.v2), 12 | .macCatalyst(.v13) 13 | ], 14 | products: [ 15 | .library(name: "DynamicCodableKit", targets: ["DynamicCodableKit"]), 16 | ], 17 | targets: [ 18 | .target(name: "DynamicCodableKit", dependencies: []), 19 | .testTarget(name: "DynamicCodableKitTests", dependencies: ["DynamicCodableKit"]), 20 | ], 21 | swiftLanguageVersions: [.v5] 22 | ) 23 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/Extensions/UnkeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | extension UnkeyedDecodingContainer { 2 | /// Returns decoded valid data from container, 3 | /// move to decoding next item if data is invalid or corrupt. 4 | /// 5 | /// - Parameter type: The type to decode. 6 | /// 7 | /// - Returns: The decoded value or nil if decoding fails. 8 | mutating func lossyDecode(_ type: T.Type) -> T? { 9 | do { return try self.decode(T.self) } 10 | catch { _ = try? self.decode(AnyDecodableValue.self) } 11 | return nil 12 | } 13 | } 14 | 15 | /// Any value decodable type. 16 | private struct AnyDecodableValue: Decodable {} 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [soumyamahunt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /Package@swift-5.6.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DynamicCodableKit", 7 | platforms: [ 8 | .iOS(.v8), 9 | .macOS(.v10_10), 10 | .tvOS(.v9), 11 | .watchOS(.v2), 12 | .macCatalyst(.v13), 13 | ], 14 | products: [ 15 | .library(name: "DynamicCodableKit", targets: ["DynamicCodableKit"]), 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 19 | .package(url: "https://github.com/apple/swift-format", from: "0.50600.1"), 20 | ], 21 | targets: [ 22 | .target(name: "DynamicCodableKit", dependencies: []), 23 | .testTarget(name: "DynamicCodableKitTests", dependencies: ["DynamicCodableKit"]), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /DynamicCodableKit.xcodeproj/DynamicCodableKit_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | -------------------------------------------------------------------------------- /DynamicCodableKit.xcodeproj/DynamicCodableKitTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SwiftyLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextProvider/DynamicDecodingContextBasedWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes dynamic value based on dynamic 2 | /// decoding context provided by ``DynamicDecodingContextProvider``. 3 | @frozen 4 | @propertyWrapper 5 | public struct DynamicDecodingContextBasedWrapper< 6 | Provider: DynamicDecodingContextProvider 7 | >: PropertyWrapperCodable { 8 | /// The underlying dynamic value referenced. 9 | public var wrappedValue: Provider.Identified 10 | 11 | /// Creates new instance with a dynamic value. 12 | /// 13 | /// - Parameters: 14 | /// - wrappedValue: An initial dynamic value. 15 | public init(wrappedValue: Provider.Identified) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | /// Creates a new instance by decoding from the given decoder. 19 | /// 20 | /// - Parameters: 21 | /// - decoder: The decoder to read data from. 22 | /// 23 | /// - Throws: `DecodingError` if data is invalid or corrupt. 24 | public init(from decoder: Decoder) throws { 25 | self.wrappedValue = try Provider.context( 26 | from: decoder 27 | ).decodeFrom(decoder) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Data/IdentifierDecode.swift: -------------------------------------------------------------------------------- 1 | let identifierDecode = 2 | """ 3 | { 4 | "content": { 5 | "id": "98765432-abcd-efab-0123-456789abcdef", 6 | "type": "video", 7 | "author": "04355678-abcd-efab-0123-456789abcdef", 8 | "likes": 2345, 9 | "createdAt": "2021-07-23T09:36:38Z", 10 | "url": "https://a.url.com/to/a/video.mp4", 11 | "duration": 460, 12 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 13 | }, 14 | "next": "https://a.url.com/to/next/page" 15 | } 16 | """.data(using: .utf8)! 17 | 18 | let identifierMetaDecode = 19 | """ 20 | { 21 | "content": { 22 | "id": "98765432-abcd-efab-0123-456789abcdef", 23 | "type": "video", 24 | "author": "04355678-abcd-efab-0123-456789abcdef", 25 | "likes": 2345, 26 | "createdAt": "2021-07-23T09:36:38Z", 27 | "metadata": { 28 | "url": "https://a.url.com/to/a/video.mp4", 29 | "duration": 460, 30 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 31 | } 32 | }, 33 | "next": "https://a.url.com/to/next/page" 34 | } 35 | """.data(using: .utf8)! 36 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Models/PostContainer.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | struct ThrowingKeyedPostPage: Codable { 5 | typealias PostWrapper = StrictDynamicDecodingDictionaryWrapper 6 | let next: URL 7 | @PostWrapper var content: [PostType: Post] 8 | } 9 | 10 | struct LossyKeyedPostPage: Codable { 11 | typealias PostWrapper = LossyDynamicDecodingDictionaryWrapper 12 | let next: URL 13 | @PostWrapper var content: [PostType: Post] 14 | } 15 | 16 | struct ThrowingKeyedPostPageCollection: Codable { 17 | typealias PostWrapper = StrictDynamicDecodingArrayDictionaryWrapper< 18 | PostType 19 | > 20 | let next: URL 21 | @PostWrapper var content: [PostType: [Post]] 22 | } 23 | 24 | struct DefaultValueKeyedPostPageCollection: Codable { 25 | typealias PostWrapper = DefaultValueDynamicDecodingArrayDictionaryWrapper< 26 | PostType 27 | > 28 | let next: URL 29 | @PostWrapper var content: [PostType: [Post]] 30 | } 31 | 32 | struct LossyKeyedPostPageCollection: Codable { 33 | typealias PostWrapper = LossyDynamicDecodingArrayDictionaryWrapper 34 | let next: URL 35 | @PostWrapper var content: [PostType: [Post]] 36 | } 37 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Guides/NoThrowDecoding.md: -------------------------------------------------------------------------------- 1 | # Using Default Value if Decoding Fails 2 | 3 | Customize behavior if decoding fails by implementing ``DynamicDecodingDefaultValueProvider``. 4 | 5 | ## Overview 6 | 7 | All the dynamic decoding scenario has specific property wrappers that accepts decoding a type implementing ``DynamicDecodingDefaultValueProvider``. The ``DynamicDecodingDefaultValueProvider/default`` value is used in the decoding failure scenario, due to invalid or corrupt data, instead of throwing error. 8 | 9 | 10 | By default, library provides implementation for `Optional` types. If decoding `Optional` type fails ``DynamicDecodingDefaultValueProvider/default`` value `nil` is used. 11 | ```swift 12 | extension Optional: DynamicDecodingDefaultValueProvider { 13 | public static var `default`: Self { 14 | return nil 15 | } 16 | } 17 | ``` 18 | 19 | ## Topics 20 | 21 | ### Protocols 22 | 23 | - ``DynamicDecodingDefaultValueProvider`` 24 | 25 | ### Property Wrappers 26 | 27 | - ``DynamicDecodingDefaultValueWrapper`` 28 | - ``PathCodingKeyDefaultValueWrapper`` 29 | - ``DynamicDecodingDefaultValueContextBasedWrapper`` 30 | 31 | ### Type Aliases 32 | 33 | - ``OptionalDynamicDecodingWrapper`` 34 | - ``OptionalPathCodingKeyWrapper`` 35 | - ``OptionalDynamicDecodingContextBasedWrapper`` -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextProvider/Data/ProviderCollectionDecode.swift: -------------------------------------------------------------------------------- 1 | let providerCollectionDecode = 2 | """ 3 | { 4 | "content": [ 5 | { 6 | "id": "98765432-abcd-efab-0123-456789abcdef", 7 | "author": "04355678-abcd-efab-0123-456789abcdef", 8 | "likes": 2345, 9 | "createdAt": "2021-07-23T09:36:38Z", 10 | "url": "https://a.url.com/to/a/video.mp4", 11 | "duration": 460, 12 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 13 | }, 14 | { 15 | "id": "98765432-abcd-efab-0123-456789abcdef", 16 | "author": "04355678-abcd-efab-0123-456789abcdef", 17 | "likes": 2345, 18 | "createdAt": "2021-07-23T09:36:38Z", 19 | "url": "https://a.url.com/to/a/video.mp4", 20 | "duration": 460, 21 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 22 | }, 23 | { 24 | "id": "98765432-abcd-efab-0123-456789abcdef", 25 | "author": "04355678-abcd-efab-0123-456789abcdef", 26 | "likes": 2345, 27 | "createdAt": "2021-07-23T09:36:38Z", 28 | "url": "https://a.url.com/to/a/video.mp4", 29 | "duration": 460, 30 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 31 | } 32 | ], 33 | "next": "https://a.url.com/to/next/page" 34 | } 35 | """.data(using: .utf8)! 36 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Models/PostPage.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | struct SinglePostPage: Codable { 5 | let next: URL 6 | @DynamicDecodingWrapper var content: Post 7 | } 8 | 9 | struct OptionalSinglePostPage: Codable { 10 | let next: URL 11 | @OptionalDynamicDecodingWrapper var content: Post? 12 | } 13 | 14 | struct ThrowingPostPage: Codable { 15 | let next: URL 16 | @StrictDynamicDecodingArrayWrapper var content: [Post] 17 | } 18 | 19 | struct DefaultPostPage: Codable { 20 | let next: URL 21 | @DefaultValueDynamicDecodingArrayWrapper var content: [Post] 22 | } 23 | 24 | struct LossyPostPage: Codable { 25 | let next: URL 26 | @LossyDynamicDecodingArrayWrapper var content: [Post] 27 | } 28 | 29 | struct ThrowingPostPageSet: Codable { 30 | let next: URL 31 | @StrictDynamicDecodingCollectionWrapper< 32 | PostSetCodingKey, Set> 33 | > var content: Set> 34 | } 35 | 36 | struct DefaultPostPageSet: Codable { 37 | let next: URL 38 | @DefaultValueDynamicDecodingCollectionWrapper< 39 | PostSetCodingKey, Set> 40 | > var content: Set> 41 | } 42 | 43 | struct LossyPostPageSet: Codable { 44 | let next: URL 45 | @LossyDynamicDecodingCollectionWrapper>> 46 | var content: Set> 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DynamicCodableKit", 3 | "version": "1.0.0", 4 | "summary": "DynamicCodableKit helps you to implement dynamic JSON decoding.", 5 | "description": "DynamicCodableKit helps you to implement dynamic JSON decoding within the constraints of Swift's sound type system by working on top of Swift's Codable implementations.", 6 | "homepage": "https://github.com/SwiftyLab/DynamicCodableKit", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Soumya Ranjan Mahunt", 10 | "email": "soumya.mahunt@gmail.com", 11 | "url": "https://twitter.com/soumya_mahunt" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/SwiftyLab/DynamicCodableKit.git" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "swiftylab-ci": "github:SwiftyLab/ci" 20 | }, 21 | "scripts": { 22 | "build": "npm exec --package=swiftylab-ci -- build.js", 23 | "xcodebuild": "npm exec --package=swiftylab-ci -- xcodebuild.js", 24 | "test": "npm exec --package=swiftylab-ci -- test.js", 25 | "archive": "npm exec --package=swiftylab-ci -- archive.js", 26 | "pod-lint": "npm exec --package=swiftylab-ci -- pod-lint.js", 27 | "generate": "npm exec --package=swiftylab-ci -- generate.js --generate-linuxmain", 28 | "preview-doc": "npm exec --package=swiftylab-ci -- preview-doc.js DynamicCodableKit", 29 | "build-doc": "npm exec --package=swiftylab-ci -- build-doc.js DynamicCodableKit", 30 | "serve-doc": "npm exec --package=swiftylab-ci -- serve-doc.js DynamicCodableKit" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Helpers/DynamicCodableKit.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | Pod::Spec.new do |s| 4 | package = JSON.parse(File.read('package.json'), {object_class: OpenStruct}) 5 | 6 | s.name = 'DynamicCodableKit' 7 | s.version = package.version.to_s 8 | s.homepage = package.homepage 9 | s.summary = package.summary 10 | s.description = package.description 11 | s.license = { :type => package.license, :file => 'LICENSE' } 12 | s.social_media_url = package.author.url 13 | s.readme = "#{s.homepage}/blob/main/README.md" 14 | s.changelog = "#{s.homepage}/blob/main/CHANGELOG.md" 15 | s.documentation_url = "https://swiftylab.github.io/DynamicCodableKit/#{s.version}/documentation/#{s.name.downcase}/" 16 | 17 | s.source = { 18 | package.repository.type.to_sym => package.repository.url, 19 | :tag => "v#{s.version}" 20 | } 21 | 22 | s.authors = { 23 | package.author.name => package.author.email 24 | } 25 | 26 | s.swift_version = '5.0' 27 | s.ios.deployment_target = '8.0' 28 | s.macos.deployment_target = '10.10' 29 | s.tvos.deployment_target = '9.0' 30 | s.watchos.deployment_target = '2.0' 31 | s.osx.deployment_target = '10.10' 32 | 33 | s.vendored_frameworks = "#{s.name}.xcframework" 34 | # @todo: Enable when CocoaPods starts supporting docc 35 | # s.source_files = "#{s.name}.docc" 36 | # s.pod_target_xcconfig = { 37 | # 'CLANG_WARN_DOCUMENTATION_COMMENTS' => 'YES', 38 | # 'RUN_DOCUMENTATION_COMPILER' => 'YES' 39 | # } 40 | end 41 | -------------------------------------------------------------------------------- /DynamicCodableKit.xcodeproj/xcshareddata/xcschemes/DynamicCodableKit-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/DynamicDecodingDictionaryWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingDictionaryWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = containerDecode 7 | let decoder = JSONDecoder() 8 | let postPage = try decoder.decode( 9 | ThrowingKeyedPostPage.self, 10 | from: data 11 | ) 12 | XCTAssertEqual(postPage.content.count, 4) 13 | XCTAssertEqual( 14 | Set(postPage.content.map(\.value.type)), 15 | Set([.text, .picture, .audio, .video]) 16 | ) 17 | postPage.content.forEach { XCTAssertEqual($1.type, $0) } 18 | } 19 | 20 | func testInvalidDataDecodingWithThrowConfig() throws { 21 | let data = containerDecodeWithInvalidData 22 | let decoder = JSONDecoder() 23 | XCTAssertThrowsError( 24 | try decoder.decode(ThrowingKeyedPostPage.self, from: data) 25 | ) 26 | } 27 | 28 | func testInvalidDataDecodingWithLossyConfig() throws { 29 | let data = containerDecodeWithInvalidData 30 | let decoder = JSONDecoder() 31 | let postPage = try decoder.decode(LossyKeyedPostPage.self, from: data) 32 | XCTAssertEqual(postPage.content.count, 3) 33 | XCTAssertEqual( 34 | Set(postPage.content.map(\.value.type)), 35 | Set([.text, .picture, .video]) 36 | ) 37 | postPage.content.forEach { XCTAssertEqual($1.type, $0) } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Data/ContainerDecodeWithInvalidData.swift: -------------------------------------------------------------------------------- 1 | let containerDecodeWithInvalidData = 2 | """ 3 | { 4 | "content": { 5 | "text": { 6 | "id": "00005678-abcd-efab-0123-456789abcdef", 7 | "author": "12345678-abcd-efab-0123-456789abcdef", 8 | "likes": 145, 9 | "createdAt": "2021-07-23T07:36:43Z", 10 | "text": "Lorem Ipsium" 11 | }, 12 | "picture": { 13 | "id": "43215678-abcd-efab-0123-456789abcdef", 14 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 15 | "likes": 370, 16 | "createdAt": "2021-07-23T09:32:13Z", 17 | "url": "https://a.url.com/to/a/picture.png", 18 | "caption": "Lorem Ipsium" 19 | }, 20 | "audio": { 21 | "id": "4c76f901-3c4f-482c-8663-600a73416773", 22 | "author": "026d7a8a-12b1-4193-8a0d-415bc8f80c1a", 23 | "likes": 25, 24 | "createdAt": "2021-07-23T09:33:48Z", 25 | "url": "https://a.url.com/to/a/audio.aac" 26 | }, 27 | "video": { 28 | "id": "98765432-abcd-efab-0123-456789abcdef", 29 | "author": "04355678-abcd-efab-0123-456789abcdef", 30 | "likes": 2345, 31 | "createdAt": "2021-07-23T09:36:38Z", 32 | "url": "https://a.url.com/to/a/video.mp4", 33 | "duration": 460, 34 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 35 | } 36 | }, 37 | "next": "https://a.url.com/to/next/page" 38 | } 39 | """.data(using: .utf8)! 40 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/Extensions/KeyedDecodingContainerProtocol.swift: -------------------------------------------------------------------------------- 1 | public extension KeyedDecodingContainerProtocol { 2 | /// Returns decoding error for coding key not found in `codingPath`. 3 | /// 4 | /// - Parameters: 5 | /// - type: The coding key type. 6 | /// 7 | /// - Returns: The value not found decoding error. 8 | func keyNotFound(ofType type: K.Type) -> DecodingError { 9 | return .keyNotFound(ofType: K.self, codingPath: codingPath) 10 | } 11 | /// Returns coding key of provided type from `codingPath`. 12 | /// 13 | /// - Parameters: 14 | /// - type: The type of coding key to retrieve. 15 | /// 16 | /// - Returns: The coding key of required type in `codingPath` 17 | /// or nil if found none. 18 | func codingKeyFromPath(ofType type: K.Type) -> K? { 19 | return self.codingPath.first(where: { $0 is K }) as? K 20 | } 21 | /// Returns coding key of provided type from `codingPath`. 22 | /// 23 | /// - Parameters: 24 | /// - type: The type of coding key to retrieve. 25 | /// 26 | /// - Returns: The coding key of required type in `codingPath`. 27 | /// 28 | /// - Throws: `DecodingError.valueNotFound` if coding key 29 | /// of provided type not found in `codingPath`. 30 | func codingKeyFromPath(ofType type: K.Type) throws -> K { 31 | guard 32 | let key = self.codingPath.first(where: { $0 is K }) as? K 33 | else { throw self.keyNotFound(ofType: K.self) } 34 | return key 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Data/ContainerDecode.swift: -------------------------------------------------------------------------------- 1 | let containerDecode = 2 | """ 3 | { 4 | "content": { 5 | "text": { 6 | "id": "00005678-abcd-efab-0123-456789abcdef", 7 | "author": "12345678-abcd-efab-0123-456789abcdef", 8 | "likes": 145, 9 | "createdAt": "2021-07-23T07:36:43Z", 10 | "text": "Lorem Ipsium" 11 | }, 12 | "picture": { 13 | "id": "43215678-abcd-efab-0123-456789abcdef", 14 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 15 | "likes": 370, 16 | "createdAt": "2021-07-23T09:32:13Z", 17 | "url": "https://a.url.com/to/a/picture.png", 18 | "caption": "Lorem Ipsium" 19 | }, 20 | "audio": { 21 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 22 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 23 | "likes": 25, 24 | "createdAt": "2021-07-23T09:33:48Z", 25 | "url": "https://a.url.com/to/a/audio.aac", 26 | "duration": 60 27 | }, 28 | "video": { 29 | "id": "98765432-abcd-efab-0123-456789abcdef", 30 | "author": "04355678-abcd-efab-0123-456789abcdef", 31 | "likes": 2345, 32 | "createdAt": "2021-07-23T09:36:38Z", 33 | "url": "https://a.url.com/to/a/video.mp4", 34 | "duration": 460, 35 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 36 | } 37 | }, 38 | "next": "https://a.url.com/to/next/page" 39 | } 40 | """.data(using: .utf8)! 41 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextProvider/Models/ProviderPost.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | extension CodingUserInfoKey { 5 | static let postKey = CodingUserInfoKey(rawValue: "post_key")! 6 | } 7 | 8 | struct PostDecodingProvider: UserInfoDynamicDecodingContextProvider { 9 | typealias Identified = Post 10 | static var infoKey: CodingUserInfoKey { .postKey } 11 | } 12 | 13 | struct ProviderBasedSinglePostPage: Decodable { 14 | let next: URL 15 | @DynamicDecodingContextBasedWrapper var content: Post 16 | } 17 | 18 | struct ProviderBasedOptionalSinglePostPage: Decodable { 19 | typealias PostWrapper = OptionalDynamicDecodingContextBasedWrapper< 20 | PostDecodingProvider 21 | > 22 | let next: URL 23 | @PostWrapper var content: Post? 24 | } 25 | 26 | struct ProviderBasedThrowingPostPage: Decodable { 27 | typealias PostWrapper = StrictDynamicDecodingArrayContextBasedWrapper< 28 | PostDecodingProvider 29 | > 30 | let next: URL 31 | @PostWrapper var content: [Post] 32 | } 33 | 34 | struct ProviderBasedDefaultPostPage: Decodable { 35 | typealias PostWrapper = DefaultValueDynamicDecodingArrayContextBasedWrapper< 36 | PostDecodingProvider 37 | > 38 | let next: URL 39 | @PostWrapper var content: [Post] 40 | } 41 | 42 | struct ProviderBasedLossyPostPage: Decodable { 43 | typealias PostWrapper = LossyDynamicDecodingArrayContextBasedWrapper< 44 | PostDecodingProvider 45 | > 46 | let next: URL 47 | @PostWrapper var content: [Post] 48 | } 49 | -------------------------------------------------------------------------------- /DynamicCodableKit.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | Pod::Spec.new do |s| 4 | package = JSON.parse(File.read('package.json'), {object_class: OpenStruct}) 5 | 6 | s.name = 'DynamicCodableKit' 7 | s.version = package.version.to_s 8 | s.homepage = package.homepage 9 | s.summary = package.summary 10 | s.description = package.description 11 | s.license = { :type => package.license, :file => 'LICENSE' } 12 | s.social_media_url = package.author.url 13 | s.readme = "#{s.homepage}/blob/main/README.md" 14 | s.changelog = "#{s.homepage}/blob/main/CHANGELOG.md" 15 | s.documentation_url = "https://swiftylab.github.io/DynamicCodableKit/#{s.version}/documentation/#{s.name.downcase}/" 16 | 17 | s.source = { 18 | package.repository.type.to_sym => package.repository.url, 19 | :tag => "v#{s.version}" 20 | } 21 | 22 | s.authors = { 23 | package.author.name => package.author.email 24 | } 25 | 26 | s.swift_version = '5.0' 27 | s.ios.deployment_target = '8.0' 28 | s.macos.deployment_target = '10.10' 29 | s.tvos.deployment_target = '9.0' 30 | s.watchos.deployment_target = '2.0' 31 | s.osx.deployment_target = '10.10' 32 | 33 | s.source_files = "Sources/#{s.name}/**/*.swift", "Sources/#{s.name}/*.docc" 34 | s.preserve_paths = "{Sources,Tests}/#{s.name}*/**/*", "*.md" 35 | s.pod_target_xcconfig = { 36 | 'CLANG_WARN_DOCUMENTATION_COMMENTS' => 'YES', 37 | 'RUN_DOCUMENTATION_COMPILER' => 'YES' 38 | } 39 | 40 | s.test_spec do |ts| 41 | ts.source_files = "Tests/#{s.name}Tests/**/*.swift" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Data/IdentifierCollectionDecode.swift: -------------------------------------------------------------------------------- 1 | let identifierCollectionDecode = 2 | """ 3 | { 4 | "content": [ 5 | { 6 | "id": "00005678-abcd-efab-0123-456789abcdef", 7 | "type": "text", 8 | "author": "12345678-abcd-efab-0123-456789abcdef", 9 | "likes": 145, 10 | "createdAt": "2021-07-23T07:36:43Z", 11 | "text": "Lorem Ipsium" 12 | }, 13 | { 14 | "id": "43215678-abcd-efab-0123-456789abcdef", 15 | "type": "picture", 16 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 17 | "likes": 370, 18 | "createdAt": "2021-07-23T09:32:13Z", 19 | "url": "https://a.url.com/to/a/picture.png", 20 | "caption": "Lorem Ipsium" 21 | }, 22 | { 23 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 24 | "type": "audio", 25 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 26 | "likes": 25, 27 | "createdAt": "2021-07-23T09:33:48Z", 28 | "url": "https://a.url.com/to/a/audio.aac", 29 | "duration": 60 30 | }, 31 | { 32 | "id": "98765432-abcd-efab-0123-456789abcdef", 33 | "type": "video", 34 | "author": "04355678-abcd-efab-0123-456789abcdef", 35 | "likes": 2345, 36 | "createdAt": "2021-07-23T09:36:38Z", 37 | "url": "https://a.url.com/to/a/video.mp4", 38 | "duration": 460, 39 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 40 | } 41 | ], 42 | "next": "https://a.url.com/to/next/page" 43 | } 44 | """.data(using: .utf8)! 45 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextProvider/DynamicDecodingContextProvider.swift: -------------------------------------------------------------------------------- 1 | /// A type that decides dynamic decoding context based on provided `Decoder`. 2 | public protocol DynamicDecodingContextProvider { 3 | /// The base type or base element type in case of collection, that will be decoded. 4 | associatedtype Identified 5 | /// Decides dynamic decoding context based on provided `Decoder`. 6 | /// 7 | /// - Parameters: 8 | /// - decoder: The `Decoder` to analyse. 9 | /// 10 | /// - Returns: Dynamic decoding context to use on `decoder`. 11 | static func context( 12 | from decoder: Decoder 13 | ) throws -> DynamicDecodingContext 14 | } 15 | 16 | /// A ``DynamicDecodingContextProvider`` type that decides dynamic decoding context 17 | /// based on decoding context contained by ``infoKey``. 18 | public protocol UserInfoDynamicDecodingContextProvider: 19 | DynamicDecodingContextProvider 20 | { 21 | /// User info key that contains decoding context to use. 22 | static var infoKey: CodingUserInfoKey { get } 23 | } 24 | 25 | public extension UserInfoDynamicDecodingContextProvider { 26 | /// Provides dynamic decoding context contained in `Decoder`'s 27 | /// `userInfo` property for key ``infoKey``. 28 | /// 29 | /// - Parameters: 30 | /// - decoder: The `Decoder` to analyse. 31 | /// 32 | /// - Returns: Dynamic decoding context to use on `decoder`. 33 | static func context( 34 | from decoder: Decoder 35 | ) throws -> DynamicDecodingContext { 36 | guard 37 | let context = decoder.userInfo[infoKey] 38 | as? DynamicDecodingContext 39 | else { 40 | throw decoder.typeMismatch(Identified.self) 41 | } 42 | return context 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/Extensions/Decoder.swift: -------------------------------------------------------------------------------- 1 | extension Decoder { 2 | /// Returns decoding error for type mismatch. 3 | /// 4 | /// - Parameters: 5 | /// - type: The mismatching type. 6 | /// 7 | /// - Returns: The type mismatch decoding error. 8 | func typeMismatch(_ type: T.Type) -> DecodingError { 9 | .typeMismatch(type, codingPath: codingPath) 10 | } 11 | /// Returns decoding error for coding key not found in `codingPath`. 12 | /// 13 | /// - Parameters: 14 | /// - type: The coding key type. 15 | /// 16 | /// - Returns: The value not found decoding error. 17 | func keyNotFound(ofType type: K.Type) -> DecodingError { 18 | return .valueNotFound( 19 | type, 20 | .init( 21 | codingPath: self.codingPath, 22 | debugDescription: 23 | "CodingKey of type \(type) not found in coding path" 24 | ) 25 | ) 26 | } 27 | /// Returns coding key of provided type from `codingPath`. 28 | /// 29 | /// - Parameters: 30 | /// - type: The type of coding key to retrieve. 31 | /// 32 | /// - Returns: The coding key of required type in `codingPath` 33 | /// or nil if found none. 34 | func codingKeyFromPath(ofType type: K.Type) -> K? { 35 | return self.codingPath.first(where: { $0 is K }) as? K 36 | } 37 | /// Returns coding key of provided type from `codingPath`. 38 | /// 39 | /// - Parameters: 40 | /// - type: The type of coding key to retrieve. 41 | /// 42 | /// - Returns: The coding key of required type in `codingPath`. 43 | /// 44 | /// - Throws: `DecodingError.valueNotFound` if coding key 45 | /// of provided type not found in `codingPath`. 46 | func codingKeyFromPath(ofType type: K.Type) throws -> K { 47 | guard 48 | let key = self.codingPath.first(where: { $0 is K }) as? K 49 | else { throw self.keyNotFound(ofType: K.self) } 50 | return key 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextProvider/Data/ProviderCollectionDecodeWithInvalidData.swift: -------------------------------------------------------------------------------- 1 | let providerCollectionDecodeWithInvalidData = 2 | """ 3 | { 4 | "content": [ 5 | { 6 | "id": "00005678-abcd-efab-0123-456789abcdef", 7 | "author": "12345678-abcd-efab-0123-456789abcdef", 8 | "likes": 145, 9 | "createdAt": "2021-07-23T07:36:43Z", 10 | "text": "Lorem Ipsium" 11 | }, 12 | { 13 | "id": "00005678-abcd-efab-0123-456789abcdef", 14 | "author": "12345678-abcd-efab-0123-456789abcdef", 15 | "likes": 145, 16 | "createdAt": "2021-07-23T07:36:43Z", 17 | "text": "Lorem Ipsium" 18 | }, 19 | { 20 | "id": "00005678-abcd-efab-0123-456789abcdef", 21 | "author": "12345678-abcd-efab-0123-456789abcdef", 22 | "likes": 145, 23 | "createdAt": "2021-07-23T07:36:43Z", 24 | "text": "Lorem Ipsium" 25 | }, 26 | { 27 | "id": "98765432-abcd-efab-0123-456789abcdef", 28 | "author": "04355678-abcd-efab-0123-456789abcdef", 29 | "likes": 2345, 30 | "createdAt": "2021-07-23T09:36:38Z", 31 | "url": "https://a.url.com/to/a/video.mp4", 32 | "duration": 460, 33 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 34 | }, 35 | { 36 | "id": "98765432-abcd-efab-0123-456789abcdef", 37 | "author": "04355678-abcd-efab-0123-456789abcdef", 38 | "likes": 2345, 39 | "createdAt": "2021-07-23T09:36:38Z", 40 | "url": "https://a.url.com/to/a/video.mp4", 41 | "duration": 460, 42 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 43 | }, 44 | { 45 | "likes": 2345, 46 | "createdAt": "2021-07-23T09:36:38Z", 47 | "url": "https://a.url.com/to/a/video.mp4", 48 | "duration": 460, 49 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 50 | } 51 | ], 52 | "next": "https://a.url.com/to/next/page" 53 | } 54 | """.data(using: .utf8)! 55 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/Extensions/DecodingError.swift: -------------------------------------------------------------------------------- 1 | private extension String { 2 | /// Returns description for type mismatch error. 3 | /// 4 | /// - Parameters: 5 | /// - type: The type mismatched. 6 | /// 7 | /// - Returns: The type mismatch error description. 8 | static func typeMismatchDesc(_ type: T.Type) -> Self { 9 | return "Failed to cast to \(type)" 10 | } 11 | /// Returns description for coding key not found error. 12 | /// 13 | /// - Parameters: 14 | /// - type: The coding key type not found. 15 | /// 16 | /// - Returns: The coding key value not found error description. 17 | static func codingKeyNotFoundDesc(_ type: K.Type) -> Self { 18 | return "CodingKey of type \(type) not found in coding path" 19 | } 20 | } 21 | 22 | extension DecodingError { 23 | /// Returns decoding error for type mismatch. 24 | /// 25 | /// - Parameters: 26 | /// - type: The type mismatched. 27 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 28 | /// 29 | /// - Returns: The type mismatch decoding error. 30 | static func typeMismatch( 31 | _ type: T.Type, 32 | codingPath: [CodingKey] 33 | ) -> Self { 34 | return .typeMismatch( 35 | type, 36 | .init( 37 | codingPath: codingPath, 38 | debugDescription: .typeMismatchDesc(T.self) 39 | ) 40 | ) 41 | } 42 | /// Returns decoding error for coding key not found. 43 | /// 44 | /// - Parameters: 45 | /// - type: The coding key type not found. 46 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 47 | /// 48 | /// - Returns: The coding key type value not found error. 49 | static func keyNotFound( 50 | ofType type: K.Type, 51 | codingPath: [CodingKey] 52 | ) -> Self { 53 | return .valueNotFound( 54 | type, 55 | .init( 56 | codingPath: codingPath, 57 | debugDescription: .codingKeyNotFoundDesc(K.self) 58 | ) 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextCodingKey/DynamicDecodingWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes dynamic value based on dynamic 2 | /// decoding context provided by ``DynamicDecodingContextCodingKey``. 3 | @frozen 4 | @propertyWrapper 5 | public struct DynamicDecodingWrapper< 6 | ContextCodingKey: DynamicDecodingContextCodingKey 7 | >: PropertyWrapperCodable { 8 | /// The underlying dynamic value referenced. 9 | public var wrappedValue: ContextCodingKey.Identified 10 | 11 | /// Creates new instance with a dynamic value. 12 | /// 13 | /// - Parameters: 14 | /// - wrappedValue: An initial dynamic value. 15 | public init(wrappedValue: ContextCodingKey.Identified) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | /// Creates a new instance by decoding from the given decoder. 19 | /// 20 | /// - Parameters: 21 | /// - decoder: The decoder to read data from. 22 | /// 23 | /// - Throws: `DecodingError` if data is invalid or corrupt. 24 | public init(from decoder: Decoder) throws { 25 | let container = try decoder.container(keyedBy: ContextCodingKey.self) 26 | self.wrappedValue = try ContextCodingKey.context( 27 | forContainer: container 28 | ).decodeFrom(decoder) 29 | } 30 | } 31 | 32 | public extension KeyedDecodingContainerProtocol 33 | where Key: DynamicDecodingContextCodingKey { 34 | /// Decodes a value of dynamic ``DynamicDecodingWrapper`` 35 | /// type for the given coding key. 36 | /// 37 | /// - Parameters: 38 | /// - type: The type of value to decode. 39 | /// - key: The coding key. 40 | /// 41 | /// - Returns: A dynamic value wrapped in ``DynamicDecodingWrapper``. 42 | /// 43 | /// - Throws: `DecodingError` if data is invalid or corrupt. 44 | func decode( 45 | _ type: DynamicDecodingWrapper.Type, 46 | forKey key: Key 47 | ) throws -> DynamicDecodingWrapper { 48 | let context = try Key.context(forContainer: self) 49 | let decoder = try self.superDecoder(forKey: key) 50 | let value = try context.decodeFrom(decoder) 51 | return DynamicDecodingWrapper(wrappedValue: value) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/Models/Post.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | enum PostType: String, 5 | Hashable, 6 | DynamicDecodingContextIdentifierKey, 7 | DynamicDecodingContextContainerCodingKey 8 | { 9 | case text, picture, audio, video 10 | 11 | var associatedContext: DynamicDecodingContext { 12 | switch self { 13 | case .text: 14 | return DynamicDecodingContext(decoding: TextPost.self) 15 | case .picture: 16 | return DynamicDecodingContext(decoding: PicturePost.self) 17 | case .audio: 18 | return DynamicDecodingContext(decoding: AudioPost.self) 19 | case .video: 20 | return DynamicDecodingContext(decoding: VideoPost.self) 21 | } 22 | } 23 | } 24 | 25 | protocol Post: DynamicDecodable { 26 | var id: UUID { get } 27 | var type: PostType { get } 28 | var author: UUID { get } 29 | var likes: Int { get } 30 | var createdAt: String { get } 31 | } 32 | 33 | struct TextPost: Post, Hashable, DynamicCodable { 34 | let id: UUID 35 | let author: UUID 36 | let likes: Int 37 | let createdAt: String 38 | let text: String 39 | var type: PostType { .text } 40 | } 41 | 42 | struct PicturePost: Post, Hashable, Encodable { 43 | let id: UUID 44 | let author: UUID 45 | let likes: Int 46 | let createdAt: String 47 | let url: URL 48 | let caption: String 49 | var type: PostType { .picture } 50 | } 51 | 52 | struct AudioPost: Post, Hashable { 53 | let id: UUID 54 | let author: UUID 55 | let likes: Int 56 | let createdAt: String 57 | let url: URL 58 | let duration: Int 59 | var type: PostType { .audio } 60 | } 61 | 62 | struct VideoPost: Post, Hashable { 63 | let id: UUID 64 | let author: UUID 65 | let likes: Int 66 | let createdAt: String 67 | let url: URL 68 | let duration: Int 69 | let thumbnail: URL 70 | var type: PostType { .video } 71 | } 72 | 73 | enum PostCodingKey: String, DynamicDecodingContextIdentifierCodingKey { 74 | typealias Identifier = PostType 75 | typealias Identified = Post 76 | case type 77 | static var identifierCodingKey: Self { .type } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Extensions/DynamicDecodable.md: -------------------------------------------------------------------------------- 1 | # ``DynamicCodableKit/DynamicDecodable`` 2 | 3 | @Metadata { 4 | @DocumentationExtension(mergeBehavior: append) 5 | } 6 | 7 | Provides `castAs` methods to customize dynamic casting to a provided type where provided type can be optional type or collection type as well. Default implementations are provided to work well with down casting, however custom types can provide their own casting behavior. 8 | 9 | For example, in , to use `Set` of `Post` type instead of `Array`, a box type for `Post` type can be created that will confirm `Hashable`: 10 | ```swift 11 | @dynamicMemberLookup 12 | struct AnyPost

: Hashable { 13 | let value: P 14 | 15 | init(withValue value: P) { 16 | self.value = value 17 | } 18 | 19 | static func == (lhs: AnyPost

, rhs: AnyPost

) -> Bool { 20 | lhs.value as! AnyHashable == rhs.value as! AnyHashable 21 | } 22 | 23 | func hash(into hasher: inout Hasher) { 24 | (value as! AnyHashable).hash(into: &hasher) 25 | } 26 | 27 | subscript(dynamicMember path: KeyPath) -> T { 28 | return value[keyPath: path] 29 | } 30 | } 31 | ``` 32 | 33 | and custom `castAs` implementation can be provided for casting to `AnyPost` box type: 34 | ```swift 35 | extension DynamicDecodable where Self: Post { 36 | func castAs(type: T.Type, codingPath: [CodingKey]) throws -> T { 37 | switch self { 38 | case let value as T: 39 | return value 40 | case _ where T.self is AnyPost.Type: 41 | return AnyPost(withValue: self as Post) as! T 42 | default: 43 | throw DecodingError.typeMismatch(T.self, codingPath: codingPath) 44 | } 45 | } 46 | func castAs(type: T.Type, codingPath: [CodingKey]) -> T { 47 | return self as? T ?? AnyPost(withValue: self as Post) as? T ?? nil 48 | } 49 | } 50 | ``` 51 | 52 | Finally, set of posts can be decoded with following `Decodable` model: 53 | ```swift 54 | struct ThrowingPostPageSet: Decodable { 55 | let next: URL 56 | @StrictDynamicDecodingCollectionWrapper>> var content: Set> 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextProvider/DynamicDecodingCollectionContextBasedWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingCollectionContextBasedWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = identifierCollectionDecode 7 | let decoder = JSONDecoder() 8 | decoder.userInfo[.postKey] = DynamicDecodingContext( 9 | withKey: PostCodingKey.self 10 | ) 11 | let postPage = try decoder.decode( 12 | ProviderBasedThrowingPostPage.self, 13 | from: data 14 | ) 15 | XCTAssertEqual(postPage.content.count, 4) 16 | XCTAssertEqual( 17 | postPage.content.map(\.type), 18 | [.text, .picture, .audio, .video] 19 | ) 20 | } 21 | 22 | func testInvalidDataDecodingWithThrowConfig() throws { 23 | let data = identifierCollectionDecodeWithInvalidData 24 | let decoder = JSONDecoder() 25 | XCTAssertThrowsError( 26 | try decoder.decode(ProviderBasedThrowingPostPage.self, from: data) 27 | ) 28 | } 29 | 30 | func testInvalidDataDecodingWithDefaultConfig() throws { 31 | let data = identifierCollectionDecodeWithInvalidData 32 | let decoder = JSONDecoder() 33 | decoder.userInfo[.postKey] = DynamicDecodingContext( 34 | withKey: PostCodingKey.self 35 | ) 36 | let postPage = try decoder.decode( 37 | ProviderBasedDefaultPostPage.self, 38 | from: data 39 | ) 40 | XCTAssertEqual(postPage.content.count, 0) 41 | } 42 | 43 | func testInvalidDataDecodingWithLossyConfig() throws { 44 | let data = identifierCollectionDecodeWithInvalidData 45 | let decoder = JSONDecoder() 46 | decoder.userInfo[.postKey] = DynamicDecodingContext( 47 | withKey: PostCodingKey.self 48 | ) 49 | let postPage = try decoder.decode( 50 | ProviderBasedLossyPostPage.self, 51 | from: data 52 | ) 53 | XCTAssertEqual(postPage.content.count, 4) 54 | XCTAssertEqual( 55 | postPage.content.map(\.type), 56 | [.text, .picture, .audio, .video] 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/DynamicDecodingCollectionDictionaryWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingCollectionDictionaryWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = containerCollectionDecode 7 | let decoder = JSONDecoder() 8 | let postPage = try decoder.decode( 9 | ThrowingKeyedPostPageCollection.self, 10 | from: data 11 | ) 12 | XCTAssertEqual(postPage.content.count, 4) 13 | postPage.content.forEach { type, posts in 14 | XCTAssertEqual(posts.count, 3) 15 | posts.forEach { XCTAssertEqual($0.type, type) } 16 | } 17 | } 18 | 19 | func testInvalidDataDecodingWithThrowConfig() throws { 20 | let data = containerCollectionDecodeWithInvalidData 21 | let decoder = JSONDecoder() 22 | XCTAssertThrowsError( 23 | try decoder.decode(ThrowingKeyedPostPageCollection.self, from: data) 24 | ) 25 | } 26 | 27 | func testInvalidDataDecodingWithDefaultConfig() throws { 28 | let data = containerCollectionDecodeWithInvalidData 29 | let decoder = JSONDecoder() 30 | let postPage = try decoder.decode( 31 | DefaultValueKeyedPostPageCollection.self, 32 | from: data 33 | ) 34 | XCTAssertEqual(postPage.content.count, 2) 35 | postPage.content.forEach { type, posts in 36 | XCTAssertEqual(posts.count, 3) 37 | posts.forEach { XCTAssertEqual($0.type, type) } 38 | } 39 | } 40 | 41 | func testInvalidDataDecodingWithLossyConfig() throws { 42 | let data = containerCollectionDecodeWithInvalidData 43 | let decoder = JSONDecoder() 44 | let postPage = try decoder.decode( 45 | LossyKeyedPostPageCollection.self, 46 | from: data 47 | ) 48 | XCTAssertEqual(postPage.content.count, 4) 49 | postPage.content.forEach { type, posts in 50 | switch type { 51 | case .audio, .video: 52 | XCTAssertEqual(posts.count, 2) 53 | default: 54 | XCTAssertEqual(posts.count, 3) 55 | } 56 | posts.forEach { XCTAssertEqual($0.type, type) } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Data/IdentifierCollectionDecodeWithInvalidData.swift: -------------------------------------------------------------------------------- 1 | let identifierCollectionDecodeWithInvalidData = 2 | """ 3 | { 4 | "content": [ 5 | { 6 | "id": "00005678-abcd-efab-0123-456789abcdef", 7 | "type": "text", 8 | "author": "12345678-abcd-efab-0123-456789abcdef", 9 | "likes": 145, 10 | "createdAt": "2021-07-23T07:36:43Z", 11 | "text": "Lorem Ipsium" 12 | }, 13 | { 14 | "id": "43215678-abcd-efab-0123-456789abcdef", 15 | "type": "picture", 16 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 17 | "likes": 370, 18 | "createdAt": "2021-07-23T09:32:13Z", 19 | "url": "https://a.url.com/to/a/picture.png", 20 | "caption": "Lorem Ipsium" 21 | }, 22 | { 23 | "id": "4c76f901-3c4f-482c-8663-600a73416773", 24 | "type": "invalid", 25 | "author": "026d7a8a-12b1-4193-8a0d-415bc8f80c1a", 26 | "likes": 25, 27 | "createdAt": "2021-07-23T09:33:48Z", 28 | "url": "https://a.url.com/to/a/audio.aac", 29 | "duration": 60 30 | }, 31 | { 32 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 33 | "type": "audio", 34 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 35 | "likes": 25, 36 | "createdAt": "2021-07-23T09:33:48Z", 37 | "url": "https://a.url.com/to/a/audio.aac", 38 | "duration": 60 39 | }, 40 | { 41 | "type": "video", 42 | "likes": 2345, 43 | "createdAt": "2021-07-23T09:36:38Z", 44 | "url": "https://a.url.com/to/a/video.mp4", 45 | "duration": 460, 46 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 47 | }, 48 | { 49 | "id": "98765432-abcd-efab-0123-456789abcdef", 50 | "type": "video", 51 | "author": "04355678-abcd-efab-0123-456789abcdef", 52 | "likes": 2345, 53 | "createdAt": "2021-07-23T09:36:38Z", 54 | "url": "https://a.url.com/to/a/video.mp4", 55 | "duration": 460, 56 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 57 | } 58 | ], 59 | "next": "https://a.url.com/to/next/page" 60 | } 61 | """.data(using: .utf8)! 62 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/Models/HashablePost.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | enum PostSetType: String, DynamicDecodingContextIdentifierKey { 5 | case text, picture, audio, video 6 | 7 | var associatedContext: DynamicDecodingContext> { 8 | switch self { 9 | case .text: 10 | return DynamicDecodingContext(decoding: TextPost.self) 11 | case .picture: 12 | return DynamicDecodingContext(decoding: PicturePost.self) 13 | case .audio: 14 | return DynamicDecodingContext(decoding: AudioPost.self) 15 | case .video: 16 | return DynamicDecodingContext(decoding: VideoPost.self) 17 | } 18 | } 19 | } 20 | 21 | extension DynamicDecodable where Self: Post { 22 | func castAs(type: T.Type, codingPath: [CodingKey]) throws -> T { 23 | switch self { 24 | case let value as T: 25 | return value 26 | case _ where T.self is AnyPost.Type: 27 | return AnyPost(withValue: self as Post) as! T 28 | default: 29 | throw DecodingError.typeMismatch(T.self, codingPath: codingPath) 30 | } 31 | } 32 | func castAs( 33 | type: T.Type, codingPath: [CodingKey] 34 | ) -> T { 35 | return self as? T ?? AnyPost(withValue: self as Post) as? T ?? nil 36 | } 37 | // func castAs(type: AnyPost.Type, codingPath: [CodingKey]) throws -> AnyPost { 38 | // return AnyPost(withValue: self as Post) 39 | // } 40 | } 41 | 42 | @dynamicMemberLookup 43 | struct AnyPost

: Hashable { 44 | let value: P 45 | 46 | init(withValue value: P) { 47 | self.value = value 48 | } 49 | 50 | static func == (lhs: AnyPost

, rhs: AnyPost

) -> Bool { 51 | lhs.value as! AnyHashable == rhs.value as! AnyHashable 52 | } 53 | 54 | func hash(into hasher: inout Hasher) { 55 | (value as! AnyHashable).hash(into: &hasher) 56 | } 57 | 58 | subscript(dynamicMember path: KeyPath) -> T { 59 | return value[keyPath: path] 60 | } 61 | } 62 | 63 | enum PostSetCodingKey: String, DynamicDecodingContextIdentifierCodingKey { 64 | typealias Identifier = PostSetType 65 | typealias Identified = AnyPost 66 | case type 67 | static var identifierCodingKey: Self { .type } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextProvider/DynamicDecodingContextBasedWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingContextBasedWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = identifierDecode 7 | let decoder = JSONDecoder() 8 | decoder.userInfo[.postKey] = DynamicDecodingContext( 9 | decoding: VideoPost.self 10 | ) 11 | let postPage = try decoder.decode( 12 | ProviderBasedSinglePostPage.self, 13 | from: data 14 | ) 15 | XCTAssertEqual(postPage.content.type, .video) 16 | XCTAssertEqual(postPage.content.likes, 2345) 17 | } 18 | 19 | func testOptionalDecoding() throws { 20 | let data = identifierDecode 21 | let decoder = JSONDecoder() 22 | decoder.userInfo[.postKey] = DynamicDecodingContext( 23 | decoding: VideoPost.self 24 | ) 25 | let postPage = try decoder.decode( 26 | ProviderBasedOptionalSinglePostPage.self, 27 | from: data 28 | ) 29 | XCTAssertEqual(postPage.content?.type, .video) 30 | XCTAssertEqual(postPage.content?.likes, 2345) 31 | } 32 | 33 | func testInvalidDataDecodingWithThrowConfig() throws { 34 | let data = identifierDecodeWithInvalidData 35 | let decoder = JSONDecoder() 36 | XCTAssertThrowsError( 37 | try decoder.decode(ProviderBasedSinglePostPage.self, from: data) 38 | ) 39 | } 40 | 41 | func testInvalidDataDecodingWithDefaultConfig() throws { 42 | let data = identifierDecodeWithInvalidData 43 | let decoder = JSONDecoder() 44 | let postPage = try decoder.decode( 45 | ProviderBasedOptionalSinglePostPage.self, 46 | from: data 47 | ) 48 | XCTAssertNil(postPage.content) 49 | } 50 | 51 | func testIdentifierKeyContextDecoding() throws { 52 | let data = identifierDecode 53 | let decoder = JSONDecoder() 54 | decoder.userInfo[.postKey] = DynamicDecodingContext( 55 | withKey: PostCodingKey.self 56 | ) 57 | let postPage = try decoder.decode( 58 | ProviderBasedSinglePostPage.self, 59 | from: data 60 | ) 61 | XCTAssertEqual(postPage.content.type, .video) 62 | XCTAssertEqual(postPage.content.likes, 2345) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextProvider/DynamicDecodingDefaultValueContextBasedWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes dynamic value in a no throw approach based on 2 | /// dynamic decoding context provided by ``DynamicDecodingContextProvider``. 3 | /// 4 | /// This can be used as an alternative to ``DynamicDecodingContextBasedWrapper`` 5 | /// where instead of throwing error when decoding fails, 6 | /// ``DynamicDecodingDefaultValueProvider/default`` value provided by 7 | /// ``DynamicDecodingDefaultValueProvider`` is used, 8 | /// i.e. ``OptionalDynamicDecodingContextBasedWrapper`` uses `nil` 9 | /// as default value in case of failure. 10 | @frozen 11 | @propertyWrapper 12 | public struct DynamicDecodingDefaultValueContextBasedWrapper< 13 | Provider: DynamicDecodingContextProvider, 14 | Wrapped: DynamicDecodingDefaultValueProvider 15 | >: PropertyWrapperCodable where Wrapped.Wrapped == Provider.Identified { 16 | /// The underlying ``DynamicDecodingDefaultValueProvider`` 17 | /// that wraps dynamic value value referenced. 18 | public var wrappedValue: Wrapped 19 | 20 | /// Creates new instance with an underlying dynamic wrapped value. 21 | /// 22 | /// - Parameters: 23 | /// - wrappedValue: An initial value with wrapped dynamic value. 24 | public init(wrappedValue: Wrapped) { 25 | self.wrappedValue = wrappedValue 26 | } 27 | /// Creates a new instance by decoding from the given decoder. 28 | /// 29 | /// - Parameters: 30 | /// - decoder: The decoder to read data from. 31 | public init(from decoder: Decoder) { 32 | guard 33 | let value = try? Provider.context(from: decoder).decodeFrom(decoder) 34 | else { 35 | self.wrappedValue = .default 36 | return 37 | } 38 | self.wrappedValue = .init(value) 39 | } 40 | } 41 | 42 | /// A property wrapper type that decodes optional dynamic value based on dynamic 43 | /// decoding context provided by ``DynamicDecodingContextProvider``. 44 | /// 45 | /// `OptionalDynamicDecodingContextBasedWrapper` is a type alias for 46 | /// ``DynamicDecodingDefaultValueContextBasedWrapper``, 47 | /// with wrapped value as optional dynamic value. If decoding fails, 48 | /// `nil` is used as underlying value instead of throwing error. 49 | public typealias OptionalDynamicDecodingContextBasedWrapper< 50 | Provider: DynamicDecodingContextProvider 51 | > = DynamicDecodingDefaultValueContextBasedWrapper< 52 | Provider, 53 | Optional 54 | > 55 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Guides/CollectionDecoding.md: -------------------------------------------------------------------------------- 1 | # Configure Collection Type Decoding 2 | 3 | Customize collection decoding failure handling with ``CollectionDecodeFailConfiguration`` value. 4 | 5 | ## Overview 6 | 7 | `DynamicCodableKit` provides property wrappers for decoding collections. 8 | These wrappers accept ``DynamicDecodingCollectionConfigurationProvider`` that provides configuration of type ``CollectionDecodeFailConfiguration`` with ``DynamicDecodingCollectionConfigurationProvider/failConfig``. 9 | 10 | 11 | Implementation for all the configuration scenarios are provided with type aliases, depending upon following naming convention. 12 | 13 | - Aliases starting with `Strict..` indicate the if decoding fails then error is thrown. 14 | - Aliases starting with `DefaultValue..` indicate that in the event of decoding fail, empty collection is used. 15 | - Aliases starting with `Lossy..` provide safest decoding configuration. Each item in collection is decoded one by one, if the data for item is intact and valid item is added to collection, otherwise item is ignored. 16 | 17 | In the topic, ``StrictDynamicDecodingArrayWrapper`` will throw error decoding following response, while, ``DefaultValueDynamicDecodingArrayWrapper`` will decode an empty array and ``LossyDynamicDecodingArrayWrapper`` will ignore first two items and only decode the last item. 18 | ```json 19 | { 20 | "content": [ 21 | { 22 | "id": "4c76f901-3c4f-482c-8663-600a73416773", 23 | "type": "invalid", 24 | "author": "026d7a8a-12b1-4193-8a0d-415bc8f80c1a", 25 | "likes": 25, 26 | "createdAt": "2021-07-23T09:33:48Z", 27 | "url": "https://a.url.com/to/a/audio.aac", 28 | "duration": 60 29 | }, 30 | { 31 | "type": "video", 32 | "likes": 2345, 33 | "createdAt": "2021-07-23T09:36:38Z", 34 | "url": "https://a.url.com/to/a/video.mp4", 35 | "duration": 460, 36 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 37 | }, 38 | { 39 | "id": "98765432-abcd-efab-0123-456789abcdef", 40 | "type": "video", 41 | "author": "04355678-abcd-efab-0123-456789abcdef", 42 | "likes": 2345, 43 | "createdAt": "2021-07-23T09:36:38Z", 44 | "url": "https://a.url.com/to/a/video.mp4", 45 | "duration": 460, 46 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 47 | } 48 | ], 49 | "next": "https://a.url.com/to/next/page" 50 | } 51 | 52 | ``` 53 | 54 | ## Topics 55 | 56 | ### Protocols 57 | 58 | - ``DynamicDecodingCollectionConfigurationProvider`` 59 | 60 | ### Structures 61 | 62 | - ``StrictCollectionConfiguration`` 63 | - ``DefaultValueCollectionConfiguration`` 64 | - ``LossyCollectionConfiguration`` 65 | 66 | ### Enumerations 67 | 68 | - ``CollectionDecodeFailConfiguration`` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains information and guidelines about contributing to this project. 4 | Please read it before you start participating. 5 | 6 | _See also: [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md)_ 7 | 8 | ## Submitting Pull Requests 9 | 10 | You can contribute by fixing bugs or adding new features. For larger code changes, we first recommend discussing them in our [Github issues](https://github.com/SwiftyLab/DynamicCodableKit/issues). When submitting a pull request, please add relevant tests and ensure your changes don't break any existing tests (see [Automated Tests](#automated-tests) below). 11 | 12 | ### Things you will need 13 | 14 | * Linux, Mac OS (preferred), or Windows. 15 | * Git 16 | * [Swift](https://www.swift.org/getting-started/#installing-swift) 17 | * Optional 18 | * Xcode and [CocoaPods], to test [CocoaPods] integration 19 | * [Node], to use helper scripts in [Scripts](Scripts/) folder. 20 | 21 | ### Automated Tests 22 | 23 | GitHub action is already setup to run tests on pull requests targeting `main` branch. However, to reduce heavy usage of GitHub runners, run the following commands in your terminal to test: 24 | 25 | | Test category | With [Node] | Manually | 26 | | --- | --- | --- | 27 | | SPM integration | Run `npm run test` | Run `swift test` | 28 | | [CocoaPods] integration (Requires Xcode) | Run `npm run pod-lint` | Run `pod lib lint --no-clean --allow-warnings` | 29 | 30 | ## Developer's Certificate of Origin 1.1 31 | 32 | By making a contribution to this project, I certify that: 33 | 34 |

    35 |
  1. 36 | The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or 37 |
  2. 38 |
  3. 39 | The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or 40 |
  4. 41 |
  5. 42 | The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. 43 |
  6. 44 |
  7. 45 | I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. 46 |
  8. 47 |
48 | 49 | [CocoaPods]: https://cocoapods.org/ 50 | [Node]: https://nodejs.org/ 51 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/PathCodingKeyWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that exposes value of provided coding key type 2 | /// from the path of coding keys taken to get to decoding this type. 3 | @frozen 4 | @propertyWrapper 5 | public struct PathCodingKeyWrapper< 6 | Key: CodingKey 7 | >: PropertyWrapperDecodableEmptyCodable { 8 | /// The underlying coding key value referenced. 9 | public var wrappedValue: Key 10 | 11 | /// Creates new instance with a coding key wrapped value. 12 | /// 13 | /// - Parameters: 14 | /// - wrappedValue: An initial coding key wrapped value. 15 | public init(wrappedValue: Key) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | /// Creates a new instance by decoding from the given decoder. 19 | /// 20 | /// - Parameters: 21 | /// - decoder: The decoder to read data from. 22 | /// 23 | /// - Throws: `DecodingError.valueNotFound` if coding key 24 | /// of provided type not found in `decoder.codingPath`. 25 | public init(from decoder: Decoder) throws { 26 | self.wrappedValue = try decoder.codingKeyFromPath(ofType: Key.self) 27 | } 28 | } 29 | 30 | public extension KeyedDecodingContainer { 31 | /// Decodes a value of the type ``PathCodingKeyWrapper`` 32 | /// for the given `PathKey` coding key. 33 | /// 34 | /// - Parameters: 35 | /// - type: The type of value to decode. 36 | /// - key: The coding key. 37 | /// 38 | /// - Returns: A value of the type ``PathCodingKeyWrapper`` 39 | /// for the given `PathKey` coding key. 40 | func decode( 41 | _ type: PathCodingKeyWrapper.Type, 42 | forKey key: K 43 | ) throws -> PathCodingKeyWrapper { 44 | return try self.decode(type) 45 | } 46 | } 47 | 48 | public extension KeyedDecodingContainerProtocol { 49 | /// Decodes a value of the type ``PathCodingKeyWrapper`` from coding key path. 50 | /// 51 | /// - Parameters: 52 | /// - type: The type of value to decode. 53 | /// 54 | /// - Returns: A value of the type ``PathCodingKeyWrapper`` from coding key path. 55 | fileprivate func decode( 56 | _ type: PathCodingKeyWrapper.Type 57 | ) throws -> PathCodingKeyWrapper { 58 | return try .init( 59 | wrappedValue: self.codingKeyFromPath(ofType: PathKey.self) 60 | ) 61 | } 62 | /// Decodes a value of the type ``PathCodingKeyWrapper`` 63 | /// for the given `PathKey` coding key. 64 | /// 65 | /// - Parameters: 66 | /// - type: The type of value to decode. 67 | /// - key: The coding key. 68 | /// 69 | /// - Returns: A value of the type ``PathCodingKeyWrapper`` 70 | /// for the given `PathKey` coding key. 71 | func decode( 72 | _ type: PathCodingKeyWrapper.Type, 73 | forKey key: Key 74 | ) throws -> PathCodingKeyWrapper { 75 | return try self.decode(type) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/DynamicDecodingWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = identifierDecode 7 | let decoder = JSONDecoder() 8 | let postPage = try decoder.decode(SinglePostPage.self, from: data) 9 | XCTAssertEqual(postPage.content.type, .video) 10 | XCTAssertEqual(postPage.content.likes, 2345) 11 | } 12 | 13 | func testOptionalDecoding() throws { 14 | let data = identifierDecode 15 | let decoder = JSONDecoder() 16 | let postPage = try decoder.decode( 17 | OptionalSinglePostPage.self, 18 | from: data 19 | ) 20 | XCTAssertEqual(postPage.content?.type, .video) 21 | XCTAssertEqual(postPage.content?.likes, 2345) 22 | } 23 | 24 | func testInvalidDataDecodingWithThrowConfig() throws { 25 | let data = identifierDecodeWithInvalidData 26 | let decoder = JSONDecoder() 27 | XCTAssertThrowsError( 28 | try decoder.decode(SinglePostPage.self, from: data) 29 | ) 30 | } 31 | 32 | func testInvalidDataDecodingWithDefaultConfig() throws { 33 | let data = identifierDecodeWithInvalidData 34 | let decoder = JSONDecoder() 35 | let postPage = try decoder.decode( 36 | OptionalSinglePostPage.self, 37 | from: data 38 | ) 39 | XCTAssertNil(postPage.content) 40 | } 41 | 42 | func testDynamicTypeDecodingWithSelfCodingKeyContext() throws { 43 | let data = #"{"value": 86}"#.data(using: .utf8)! 44 | let decoder = JSONDecoder() 45 | let container = try decoder.decode( 46 | VariableBaseDataTypeContainer.self, 47 | from: data 48 | ) 49 | XCTAssertEqual(container.value as? Int, 86) 50 | } 51 | 52 | func testOptionalDynamicTypeDecodingWithSelfCodingKeyContext() throws { 53 | let data = #"{"value": 86}"#.data(using: .utf8)! 54 | let decoder = JSONDecoder() 55 | let container = try decoder.decode( 56 | OptionalVariableBaseDataTypeContainer.self, 57 | from: data 58 | ) 59 | XCTAssertEqual(container.value as? Int, 86) 60 | } 61 | 62 | func 63 | testInvalidDataDynamicTypeDecodingWithSelfCodingKeyContextWithThrowConfig() 64 | throws 65 | { 66 | let data = #"{"value": 86.89}"#.data(using: .utf8)! 67 | let decoder = JSONDecoder() 68 | XCTAssertThrowsError( 69 | try decoder.decode(VariableBaseDataTypeContainer.self, from: data) 70 | ) 71 | } 72 | 73 | func 74 | testInvalidDataDynamicTypeDecodingWithSelfCodingKeyContextWithDefaultConfig() 75 | throws 76 | { 77 | let data = #"{"value": 86.89}"#.data(using: .utf8)! 78 | let decoder = JSONDecoder() 79 | let container = try decoder.decode( 80 | OptionalVariableBaseDataTypeContainer.self, 81 | from: data 82 | ) 83 | XCTAssertNil(container.value) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (2022-06-05) 2 | 3 | 4 | ### ✅ Tests 5 | 6 | * add `DynamicDecodable` unit tests ([96d4ba6](https://github.com/SwiftyLab/DynamicCodableKit/commit/96d4ba6a2cadf6dfad141316632feada67537fcc)) 7 | * add tests for dynamic JSON decoding with type identifiers ([9fc73b7](https://github.com/SwiftyLab/DynamicCodableKit/commit/9fc73b71f9fd1541e59a53ffce2137ef0491906c)) 8 | * add tests for dynamic JSON decoding with type(s) parent coding key ([c222eb1](https://github.com/SwiftyLab/DynamicCodableKit/commit/c222eb1835a8f461a6ade1f02d1968b097df54c1)) 9 | * add tests for dynamic JSON decoding with user context provider ([74fd0b4](https://github.com/SwiftyLab/DynamicCodableKit/commit/74fd0b4664cfd383617160d26f9700c8557a97db)) 10 | * add type identifier decoding tests ([3ae17b3](https://github.com/SwiftyLab/DynamicCodableKit/commit/3ae17b37997167ec03bc64868a50c6d676fca83e)) 11 | 12 | 13 | ### 🚀 Features 14 | 15 | * add Carthage prebuilt frameworks support ([c0f6918](https://github.com/SwiftyLab/DynamicCodableKit/commit/c0f6918d252c187a21aac2566e9e02dcfdf8ef77)) 16 | * add Carthage support ([107a5c7](https://github.com/SwiftyLab/DynamicCodableKit/commit/107a5c77e86cfc8d2865209b36d64ddd3b12a724)) 17 | * add CocoaPods support ([4e53ceb](https://github.com/SwiftyLab/DynamicCodableKit/commit/4e53ceb853542deae30446fc48ed29a6d134ed4c)) 18 | * add dynamic encoding ([ac18636](https://github.com/SwiftyLab/DynamicCodableKit/commit/ac1863697b82923ed6e488e5bd7dd3560a8e89ed)) 19 | * add dynamic JSON decoding with type identifiers ([63bab18](https://github.com/SwiftyLab/DynamicCodableKit/commit/63bab18f80d087d9538c17b4dd8189a7fef5a6c6)) 20 | * add dynamic JSON decoding with type(s) parent coding key ([5c5bf0a](https://github.com/SwiftyLab/DynamicCodableKit/commit/5c5bf0a2567ad21927ea7a6a2cb4bfa1ab5ed2da)) 21 | * add dynamic JSON decoding with user context provider ([8a61ceb](https://github.com/SwiftyLab/DynamicCodableKit/commit/8a61cebe6727159e88829e0b2f41bc521d3614ce)) 22 | * add ignoring key with invalid data for parent coding key based dynamic decoding ([79935b4](https://github.com/SwiftyLab/DynamicCodableKit/commit/79935b448af7465b8fded0b23cce1367919dab15)) 23 | 24 | 25 | ### 🐛 Fixes 26 | 27 | * **CocoaPods:** fix podspec source tag attribute ([9072d1e](https://github.com/SwiftyLab/DynamicCodableKit/commit/9072d1e675f4f23a1930eb5101b21b13af2f369b)) 28 | 29 | 30 | ### 📚 Documentation 31 | 32 | * add about and contributing guidelines ([1c3f7d1](https://github.com/SwiftyLab/DynamicCodableKit/commit/1c3f7d12132eaa634b44076118f9989e0c7d144a)) 33 | * add DocC documentation with articles ([9575dd3](https://github.com/SwiftyLab/DynamicCodableKit/commit/9575dd32f56bae8b2e5a6bb6c02fc4555fb654b0)) 34 | * add manual installation guides ([c51c8c9](https://github.com/SwiftyLab/DynamicCodableKit/commit/c51c8c974a81dce9a0307c96677a6a79ca8ace19)) 35 | 36 | 37 | ### 💄 Styles 38 | 39 | * add swift-format for code formatting ([0a719fe](https://github.com/SwiftyLab/DynamicCodableKit/commit/0a719fee49c725afc352278eb0129c16d739e693)) 40 | 41 | 42 | ### ⏪ Reverts 43 | 44 | * Revert "chore: add single major swift tool package manifest" ([19044fd](https://github.com/SwiftyLab/DynamicCodableKit/commit/19044fd8c2f5092f44e77d7143ee62ebb769853d)) 45 | 46 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Models/SocialMediaPost.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | struct SocialMediaPost: Codable { 5 | typealias CodingKeys = SocialMediaPostCodingKey 6 | typealias MetaData = DynamicDecodingWrapper 7 | let id: UUID 8 | let author: UUID 9 | let likes: Int 10 | let createdAt: String 11 | @MetaData var metadata: PostMetaData 12 | 13 | init(from decoder: Decoder) throws { 14 | let container = try decoder.container(keyedBy: CodingKeys.self) 15 | self.id = try container.decode(UUID.self, forKey: .id) 16 | self.author = try container.decode(UUID.self, forKey: .author) 17 | self.likes = try container.decode(Int.self, forKey: .likes) 18 | self.createdAt = try container.decode(String.self, forKey: .createdAt) 19 | self._metadata = try container.decode(MetaData.self, forKey: .metadata) 20 | } 21 | } 22 | 23 | struct OptionalMetadataPost: Codable { 24 | typealias CodingKeys = SocialMediaPostCodingKey 25 | typealias MetaData = OptionalDynamicDecodingWrapper 26 | let id: UUID 27 | let author: UUID 28 | let likes: Int 29 | let createdAt: String 30 | @MetaData var metadata: PostMetaData? 31 | 32 | init(from decoder: Decoder) throws { 33 | let container = try decoder.container(keyedBy: CodingKeys.self) 34 | self.id = try container.decode(UUID.self, forKey: .id) 35 | self.author = try container.decode(UUID.self, forKey: .author) 36 | self.likes = try container.decode(Int.self, forKey: .likes) 37 | self.createdAt = try container.decode(String.self, forKey: .createdAt) 38 | self._metadata = container.decode(MetaData.self, forKey: .metadata) 39 | } 40 | } 41 | 42 | enum PostMetaDataType: String, 43 | Hashable, 44 | DynamicDecodingContextIdentifierKey 45 | { 46 | case text, picture, audio, video 47 | 48 | var associatedContext: DynamicDecodingContext { 49 | switch self { 50 | case .text: 51 | return DynamicDecodingContext(decoding: TextPostMetaData.self) 52 | case .picture: 53 | return DynamicDecodingContext(decoding: PicturePostMetaData.self) 54 | case .audio: 55 | return DynamicDecodingContext(decoding: AudioPostMetaData.self) 56 | case .video: 57 | return DynamicDecodingContext(decoding: VideoPostMetaData.self) 58 | } 59 | } 60 | } 61 | 62 | enum SocialMediaPostCodingKey: String, 63 | DynamicDecodingContextIdentifierCodingKey 64 | { 65 | typealias Identifier = PostMetaDataType 66 | typealias Identified = PostMetaData 67 | case id 68 | case author 69 | case likes 70 | case createdAt 71 | case metadata 72 | static var identifierCodingKey: Self { .metadata } 73 | } 74 | 75 | protocol PostMetaData: DynamicDecodable {} 76 | 77 | struct TextPostMetaData: PostMetaData { 78 | let text: String 79 | } 80 | 81 | struct PicturePostMetaData: PostMetaData { 82 | let url: URL 83 | let caption: String 84 | } 85 | 86 | struct AudioPostMetaData: PostMetaData { 87 | let url: URL 88 | let duration: Int 89 | } 90 | 91 | struct VideoPostMetaData: PostMetaData { 92 | let url: URL 93 | let duration: Int 94 | let thumbnail: URL 95 | } 96 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/PathCodingKeyWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class PathCodingKeyWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = containerCollectionDecode 7 | let decoder = JSONDecoder() 8 | let postPage = try decoder.decode(CommonPostPage.self, from: data) 9 | XCTAssertEqual(postPage.content.count, 4) 10 | postPage.content.forEach { type, posts in 11 | XCTAssertEqual(posts.count, 3) 12 | posts.forEach { XCTAssertEqual($0.type, type) } 13 | } 14 | } 15 | 16 | func testOptionalDecoding() throws { 17 | let data = containerCollectionDecode 18 | let decoder = JSONDecoder() 19 | let postPage = try decoder.decode( 20 | OptionalCommonPostPage.self, 21 | from: data 22 | ) 23 | XCTAssertEqual(postPage.content.count, 4) 24 | postPage.content.forEach { type, posts in 25 | XCTAssertEqual(posts.count, 3) 26 | posts.forEach { XCTAssertEqual($0.type, type) } 27 | } 28 | } 29 | 30 | func testInvalidDataDecodingWithThrowConfig() throws { 31 | let data = containerCollectionDecode 32 | let decoder = JSONDecoder() 33 | XCTAssertThrowsError( 34 | try decoder.decode(ThrowingCommonPostPage.self, from: data) 35 | ) 36 | } 37 | 38 | func testInvalidDataDecodingWithDefaultConfig() throws { 39 | let data = containerCollectionDecode 40 | let decoder = JSONDecoder() 41 | let postPage = try decoder.decode( 42 | OptionalTypeCommonPostPage.self, 43 | from: data 44 | ) 45 | XCTAssertEqual(postPage.content.count, 4) 46 | postPage.content.forEach { type, posts in 47 | XCTAssertEqual(posts.count, 3) 48 | posts.forEach { XCTAssertNil($0.type) } 49 | } 50 | } 51 | 52 | func testPathCodingKeyWrapperDecoding() throws { 53 | let value = try JSONDecoder().decode( 54 | ContainedPathCodingKeyWrapper.self, 55 | from: pathCodingKeyData 56 | ) 57 | XCTAssertEqual(value.text.count, 1) 58 | XCTAssertEqual(value.text.first?.wrappedValue, .text) 59 | } 60 | 61 | func testOptionalPathCodingKeyWrapperDecoding() throws { 62 | let value = try JSONDecoder().decode( 63 | ContainedOptionalPathCodingKeyWrapper.self, 64 | from: pathCodingKeyData 65 | ) 66 | XCTAssertEqual(value.text.count, 1) 67 | XCTAssertEqual(value.text.first?.wrappedValue, .text) 68 | } 69 | 70 | func testInvalidPathCodingKeyWrapperDecoding() throws { 71 | XCTAssertThrowsError( 72 | try JSONDecoder().decode( 73 | ContainedInvalidPathCodingKeyWrapper.self, 74 | from: pathCodingKeyData 75 | ) 76 | ) 77 | } 78 | 79 | func testInvalidOptionalPathCodingKeyWrapperDecoding() throws { 80 | let value = try JSONDecoder().decode( 81 | ContainedInvalidOptionalPathCodingKeyWrapper.self, 82 | from: pathCodingKeyData 83 | ) 84 | XCTAssertEqual(value.text.count, 1) 85 | XCTAssertNil(value.text.first?.wrappedValue) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContext.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingContextTests: XCTestCase { 5 | func testFallbackDecodingWithPrimarySucess() throws { 6 | let data = identifierDecode 7 | let decoder = JSONDecoder() 8 | decoder.userInfo[.postKey] = DynamicDecodingContext( 9 | decoding: VideoPost.self, 10 | fallback: TextPost.self 11 | ) 12 | let postPage = try decoder.decode( 13 | ProviderBasedSinglePostPage.self, 14 | from: data 15 | ) 16 | XCTAssertEqual(postPage.content.type, .video) 17 | XCTAssertEqual(postPage.content.likes, 2345) 18 | } 19 | 20 | func testFallbackDecodingWithFallbackSucess() throws { 21 | let data = identifierDecode 22 | let decoder = JSONDecoder() 23 | decoder.userInfo[.postKey] = DynamicDecodingContext( 24 | decoding: TextPost.self, 25 | fallback: VideoPost.self 26 | ) 27 | let postPage = try decoder.decode( 28 | ProviderBasedSinglePostPage.self, 29 | from: data 30 | ) 31 | XCTAssertEqual(postPage.content.type, .video) 32 | XCTAssertEqual(postPage.content.likes, 2345) 33 | } 34 | 35 | func testFallbackArrayDecoding() throws { 36 | let data = providerCollectionDecode 37 | let decoder = JSONDecoder() 38 | decoder.userInfo[.postKey] = DynamicDecodingContext( 39 | decoding: TextPost.self, 40 | fallback: VideoPost.self 41 | ) 42 | let postPage = try decoder.decode( 43 | ProviderBasedThrowingPostPage.self, 44 | from: data 45 | ) 46 | XCTAssertEqual(postPage.content.count, 3) 47 | } 48 | 49 | func testInvalidDataDecodingWithThrowConfig() throws { 50 | let data = providerCollectionDecodeWithInvalidData 51 | let decoder = JSONDecoder() 52 | decoder.userInfo[.postKey] = DynamicDecodingContext( 53 | decoding: TextPost.self, 54 | fallback: VideoPost.self 55 | ) 56 | XCTAssertThrowsError( 57 | try decoder.decode(ProviderBasedThrowingPostPage.self, from: data) 58 | ) 59 | } 60 | 61 | func testInvalidDataDecodingWithDefaultConfig() throws { 62 | let data = providerCollectionDecodeWithInvalidData 63 | let decoder = JSONDecoder() 64 | decoder.userInfo[.postKey] = DynamicDecodingContext( 65 | decoding: TextPost.self, 66 | fallback: VideoPost.self 67 | ) 68 | let postPage = try decoder.decode( 69 | ProviderBasedDefaultPostPage.self, 70 | from: data 71 | ) 72 | XCTAssertEqual(postPage.content.count, 0) 73 | } 74 | 75 | func testInvalidDataDecodingWithLossyConfig() throws { 76 | let data = providerCollectionDecodeWithInvalidData 77 | let decoder = JSONDecoder() 78 | decoder.userInfo[.postKey] = DynamicDecodingContext( 79 | decoding: TextPost.self, 80 | fallback: VideoPost.self 81 | ) 82 | let postPage = try decoder.decode( 83 | ProviderBasedLossyPostPage.self, 84 | from: data 85 | ) 86 | XCTAssertEqual(postPage.content.count, 5) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingConfiguration.swift: -------------------------------------------------------------------------------- 1 | /// A type that can be dynamically casted to multiple type, 2 | /// allowing dynamic decoding. 3 | public protocol DynamicDecodingDefaultValueProvider { 4 | /// A type representing the stored value. 5 | associatedtype Wrapped 6 | /// Creates an instance that stores the given value. 7 | /// 8 | /// - Parameters: 9 | /// - wrapped: The value to wrap/store. 10 | init(_ wrapped: Wrapped) 11 | /// Default value to use when decoding fails. 12 | static var `default`: Self { get } 13 | } 14 | 15 | extension Optional: DynamicDecodingDefaultValueProvider { 16 | /// Use `nil` as default value when decoding 17 | /// ``DynamicDecodingDefaultValueProvider/Wrapped`` type fails. 18 | public static var `default`: Self { nil } 19 | } 20 | 21 | /// Available dynamic array decoding configuration. 22 | @frozen 23 | public enum CollectionDecodeFailConfiguration { 24 | /// Throw error if decoding fails. 25 | case `throw` 26 | /// Use default value if decoding fails. 27 | case `default` 28 | /// Ignore elements with invalid or corrupt data. 29 | case lossy 30 | } 31 | 32 | /// A type that can be initialized with a finite sequence of items. 33 | public protocol SequenceInitializable { 34 | /// A type representing the sequence’s elements. 35 | associatedtype Element 36 | /// Creates a new instance, from no collection item. 37 | init() 38 | /// Creates a new instance from a finite sequence of items. 39 | /// 40 | /// - Parameters: 41 | /// - sequence: The elements to use as members of the new instance. 42 | init(_ sequence: S) where S: Sequence, Element == S.Element 43 | /// A Boolean value indicating whether the collection is empty. 44 | var isEmpty: Bool { get } 45 | } 46 | 47 | extension Array: SequenceInitializable {} 48 | extension Set: SequenceInitializable {} 49 | 50 | /// A type that provides configuration for dynamic array decoding. 51 | public protocol DynamicDecodingCollectionConfigurationProvider { 52 | /// Configuration to use if decoding fails. 53 | static var failConfig: CollectionDecodeFailConfiguration { get } 54 | } 55 | 56 | /// Provides strict configuration for dynamic array decoding, 57 | /// throw error if decoding fails. 58 | @frozen 59 | public struct StrictCollectionConfiguration: 60 | DynamicDecodingCollectionConfigurationProvider 61 | { 62 | /// Use ``CollectionDecodeFailConfiguration/throw`` configuration to throw error if decoding fails. 63 | public static var failConfig: CollectionDecodeFailConfiguration { .throw } 64 | } 65 | 66 | /// Provides default configuration for dynamic array decoding, 67 | /// use default value if decoding fails. 68 | @frozen 69 | public struct DefaultValueCollectionConfiguration: 70 | DynamicDecodingCollectionConfigurationProvider 71 | { 72 | /// Use ``CollectionDecodeFailConfiguration/default`` configuration 73 | /// to use default empty collection value if decoding fails. 74 | public static var failConfig: CollectionDecodeFailConfiguration { 75 | .`default` 76 | } 77 | } 78 | 79 | /// Provides lossy configuration for dynamic array decoding, 80 | /// ignore elements with invalid or corrupt data. 81 | @frozen 82 | public struct LossyCollectionConfiguration: 83 | DynamicDecodingCollectionConfigurationProvider 84 | { 85 | /// Use ``CollectionDecodeFailConfiguration/lossy`` configuration 86 | /// to only decode items with valid data while ignoring the rest. 87 | public static var failConfig: CollectionDecodeFailConfiguration { .lossy } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/Models/DynamicBaseType.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | extension Int: DynamicDecodable {} 5 | extension String: DynamicDecodable {} 6 | 7 | enum DynamicBaseDataTypeCodingKeys: String, CodingKey, 8 | DynamicDecodingContextCodingKey 9 | { 10 | typealias Identified = Decodable 11 | case value 12 | 13 | static func context( 14 | forContainer container: Container 15 | ) throws -> DynamicDecodingContext where Self == Container.Key { 16 | return DynamicDecodingContext(decoding: Int.self, fallback: String.self) 17 | } 18 | } 19 | 20 | enum DynamicBaseDataTypeCollectionCodingKeys: String, CodingKey, 21 | DynamicDecodingContextCodingKey 22 | { 23 | typealias Identified = Decodable 24 | case values 25 | 26 | static func context( 27 | forContainer container: Container 28 | ) throws -> DynamicDecodingContext where Self == Container.Key { 29 | return DynamicDecodingContext(decoding: Int.self, fallback: String.self) 30 | } 31 | } 32 | 33 | struct VariableBaseDataTypeContainer: Decodable { 34 | typealias CodingKeys = DynamicBaseDataTypeCodingKeys 35 | @DynamicDecodingWrapper var value: Decodable 36 | 37 | init(from decoder: Decoder) throws { 38 | let container = try decoder.container(keyedBy: CodingKeys.self) 39 | self._value = try container.decode( 40 | DynamicDecodingWrapper.self, forKey: .value) 41 | } 42 | } 43 | 44 | struct OptionalVariableBaseDataTypeContainer: Decodable { 45 | typealias CodingKeys = DynamicBaseDataTypeCodingKeys 46 | @OptionalDynamicDecodingWrapper var value: Decodable? 47 | 48 | init(from decoder: Decoder) throws { 49 | let container = try decoder.container(keyedBy: CodingKeys.self) 50 | self._value = container.decode( 51 | OptionalDynamicDecodingWrapper.self, forKey: .value) 52 | } 53 | } 54 | 55 | struct StrictVariableBaseDataTypeContainer: Decodable { 56 | typealias CodingKeys = DynamicBaseDataTypeCollectionCodingKeys 57 | @StrictDynamicDecodingArrayWrapper var values: [Decodable] 58 | 59 | init(from decoder: Decoder) throws { 60 | let container = try decoder.container(keyedBy: CodingKeys.self) 61 | self._values = try container.decode( 62 | StrictDynamicDecodingArrayWrapper.self, forKey: .values) 63 | } 64 | } 65 | 66 | struct DefaultVariableBaseDataTypeContainer: Decodable { 67 | typealias CodingKeys = DynamicBaseDataTypeCollectionCodingKeys 68 | @DefaultValueDynamicDecodingArrayWrapper var values: [Decodable] 69 | 70 | init(from decoder: Decoder) throws { 71 | let container = try decoder.container(keyedBy: CodingKeys.self) 72 | self._values = try container.decode( 73 | DefaultValueDynamicDecodingArrayWrapper.self, 74 | forKey: .values) 75 | } 76 | } 77 | 78 | struct LossyVariableBaseDataTypeContainer: Decodable { 79 | typealias CodingKeys = DynamicBaseDataTypeCollectionCodingKeys 80 | @LossyDynamicDecodingArrayWrapper var values: [Decodable] 81 | 82 | init(from decoder: Decoder) throws { 83 | let container = try decoder.container(keyedBy: CodingKeys.self) 84 | self._values = try container.decode( 85 | LossyDynamicDecodingArrayWrapper.self, forKey: .values) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 0 * * *' 10 | workflow_dispatch: 11 | inputs: 12 | release: 13 | description: Create release 14 | required: false 15 | type: boolean 16 | version: 17 | description: New version to release 18 | required: false 19 | type: string 20 | 21 | concurrency: 22 | group: ci/cd-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | # analyze: 27 | # name: Analyze 28 | # if: github.event_name != 'workflow_dispatch' 29 | # uses: SwiftyLab/ci/.github/workflows/codeql-analysis.yml@main 30 | # permissions: 31 | # actions: read 32 | # contents: read 33 | # security-events: write 34 | 35 | spell-check: 36 | name: Run spell check 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v3 41 | - name: Spellcheck Action 42 | uses: rojopolis/spellcheck-github-actions@0.35.0 43 | with: 44 | config_path: .github/config/spellcheck.yml 45 | 46 | swift-package-test: 47 | name: Swift Package 48 | uses: SwiftyLab/ci/.github/workflows/swift-package.yml@main 49 | secrets: inherit 50 | with: 51 | matrix: > 52 | { 53 | "include": [ 54 | { 55 | "os": "ubuntu-latest", 56 | "swift": "5.7" 57 | }, 58 | { 59 | "os": "ubuntu-20.04", 60 | "swift": "5.2" 61 | } 62 | ] 63 | } 64 | # { 65 | # "os": "windows-latest", 66 | # "swift": "5.7" 67 | # }, 68 | # { 69 | # "os": "windows-latest", 70 | # "swift": "5.3" 71 | # } 72 | 73 | cocoapods-test: 74 | name: CocoaPods 75 | uses: SwiftyLab/ci/.github/workflows/cocoapods.yml@main 76 | strategy: 77 | matrix: 78 | platforms: ['macos tvos', 'ios'] 79 | with: 80 | platforms: ${{ matrix.platforms }} 81 | 82 | xcode-test: 83 | name: Xcode 84 | uses: SwiftyLab/ci/.github/workflows/xcode.yml@main 85 | with: 86 | os: macos-11 87 | xcode: '11.7' 88 | 89 | ci: 90 | name: CI 91 | if: github.event_name == 'push' 92 | needs: [swift-package-test, xcode-test, cocoapods-test, spell-check] 93 | # needs: [analyze, swift-package-test, xcode-test, cocoapods-test] 94 | uses: SwiftyLab/ci/.github/workflows/ci.yml@main 95 | 96 | cd: 97 | name: CD 98 | if: | 99 | (github.event_name == 'push' && needs.ci.outputs.release == 'true') || 100 | (always() && 101 | github.event_name == 'workflow_dispatch' && 102 | github.event.inputs.release == 'true' && 103 | needs.swift-package-test.result == 'success' && 104 | needs.xcode-test.result == 'success' && 105 | needs.cocoapods-test.result == 'success' && 106 | (needs.ci.result == 'success' || needs.ci.result == 'skipped')) 107 | # (needs.analyze.result == 'success' || needs.analyze.result == 'skipped') 108 | needs: [ci, swift-package-test, xcode-test, cocoapods-test, spell-check] 109 | # needs: [ci, analyze, swift-package-test, xcode-test, cocoapods-test] 110 | uses: SwiftyLab/ci/.github/workflows/cd.yml@main 111 | with: 112 | version: ${{ github.event.inputs.version }} 113 | secrets: 114 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at soumya.mahunt@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/DynamicDecodingDictionaryWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes a dictionary value of ``DynamicDecodingContextContainerCodingKey`` coding key 2 | /// and their dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value. 3 | @frozen 4 | @propertyWrapper 5 | public struct DynamicDecodingDictionaryWrapper< 6 | ContainerCodingKey: DynamicDecodingContextContainerCodingKey, 7 | Config: DynamicDecodingCollectionConfigurationProvider 8 | >: PropertyWrapperCodable where ContainerCodingKey: Hashable { 9 | /// The underlying dictionary of ``DynamicDecodingContextContainerCodingKey`` key 10 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value. 11 | public var wrappedValue: [ContainerCodingKey: ContainerCodingKey.Contained] 12 | 13 | /// Creates new instance with an underlying dictionary value. 14 | /// 15 | /// - Parameters: 16 | /// - wrappedValue: A dictionary ``DynamicDecodingContextContainerCodingKey`` key and 17 | /// dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value. 18 | public init( 19 | wrappedValue: [ContainerCodingKey: ContainerCodingKey.Contained] 20 | ) { 21 | self.wrappedValue = wrappedValue 22 | } 23 | /// Creates a new instance by decoding from the given decoder. 24 | /// 25 | /// - Parameters: 26 | /// - decoder: The decoder to read data from. 27 | /// 28 | /// - Throws: `DecodingError` if ``DynamicDecodingCollectionConfigurationProvider/failConfig`` 29 | /// is ``CollectionDecodeFailConfiguration/throw`` and data is invalid or corrupt. 30 | public init(from decoder: Decoder) throws { 31 | switch Config.failConfig { 32 | case .throw: 33 | let container = try decoder.container( 34 | keyedBy: ContainerCodingKey.self 35 | ) 36 | self.wrappedValue = try container.decode() 37 | default: 38 | guard 39 | let container = try? decoder.container( 40 | keyedBy: ContainerCodingKey.self 41 | ) 42 | else { self.wrappedValue = [:]; return } 43 | self.wrappedValue = container.lossyDecode() 44 | } 45 | } 46 | } 47 | 48 | /// A property wrapper type that strictly decodes a dictionary value of ``DynamicDecodingContextContainerCodingKey`` 49 | /// coding key and their dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value and 50 | /// throws error if decoding fails. 51 | /// 52 | /// `StrictDynamicDecodingDictionaryWrapper` is a type alias for 53 | /// ``DynamicDecodingDictionaryWrapper``, 54 | /// with ``DynamicDecodingCollectionConfigurationProvider`` as 55 | /// ``StrictCollectionConfiguration`` 56 | public typealias StrictDynamicDecodingDictionaryWrapper< 57 | ContainerCodingKey: DynamicDecodingContextContainerCodingKey 58 | > = DynamicDecodingDictionaryWrapper< 59 | ContainerCodingKey, 60 | StrictCollectionConfiguration 61 | > where ContainerCodingKey: Hashable 62 | 63 | /// A property wrapper type that decodes valid data into a dictionary value of 64 | /// ``DynamicDecodingContextContainerCodingKey`` coding key and 65 | /// their dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 66 | /// value while ignoring invalid data. 67 | /// 68 | /// `LossyDynamicDecodingDictionaryWrapper` is a type alias for 69 | /// ``DynamicDecodingDictionaryWrapper``, 70 | /// with ``DynamicDecodingCollectionConfigurationProvider`` as 71 | /// ``LossyCollectionConfiguration`` 72 | public typealias LossyDynamicDecodingDictionaryWrapper< 73 | ContainerCodingKey: DynamicDecodingContextContainerCodingKey 74 | > = DynamicDecodingDictionaryWrapper< 75 | ContainerCodingKey, 76 | LossyCollectionConfiguration 77 | > where ContainerCodingKey: Hashable 78 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Data/ContainerCollectionDecodeWithInvalidData.swift: -------------------------------------------------------------------------------- 1 | let containerCollectionDecodeWithInvalidData = 2 | """ 3 | { 4 | "content": { 5 | "text": [ 6 | { 7 | "id": "00005678-abcd-efab-0123-456789abcdef", 8 | "author": "12345678-abcd-efab-0123-456789abcdef", 9 | "likes": 145, 10 | "createdAt": "2021-07-23T07:36:43Z", 11 | "text": "Lorem Ipsium" 12 | }, 13 | { 14 | "id": "00005678-abcd-efab-0123-456789abcdef", 15 | "author": "12345678-abcd-efab-0123-456789abcdef", 16 | "likes": 145, 17 | "createdAt": "2021-07-23T07:36:43Z", 18 | "text": "Lorem Ipsium" 19 | }, 20 | { 21 | "id": "00005678-abcd-efab-0123-456789abcdef", 22 | "author": "12345678-abcd-efab-0123-456789abcdef", 23 | "likes": 145, 24 | "createdAt": "2021-07-23T07:36:43Z", 25 | "text": "Lorem Ipsium" 26 | } 27 | ], 28 | "picture": [ 29 | { 30 | "id": "43215678-abcd-efab-0123-456789abcdef", 31 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 32 | "likes": 370, 33 | "createdAt": "2021-07-23T09:32:13Z", 34 | "url": "https://a.url.com/to/a/picture.png", 35 | "caption": "Lorem Ipsium" 36 | }, 37 | { 38 | "id": "43215678-abcd-efab-0123-456789abcdef", 39 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 40 | "likes": 370, 41 | "createdAt": "2021-07-23T09:32:13Z", 42 | "url": "https://a.url.com/to/a/picture.png", 43 | "caption": "Lorem Ipsium" 44 | }, 45 | { 46 | "id": "43215678-abcd-efab-0123-456789abcdef", 47 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 48 | "likes": 370, 49 | "createdAt": "2021-07-23T09:32:13Z", 50 | "url": "https://a.url.com/to/a/picture.png", 51 | "caption": "Lorem Ipsium" 52 | } 53 | ], 54 | "audio": [ 55 | { 56 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 57 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 58 | "likes": 25, 59 | "createdAt": "2021-07-23T09:33:48Z", 60 | "url": "https://a.url.com/to/a/audio.aac", 61 | "duration": 60 62 | }, 63 | { 64 | "id": "4c76f901-3c4f-482c-8663-600a73416773", 65 | "author": "026d7a8a-12b1-4193-8a0d-415bc8f80c1a", 66 | "likes": 25, 67 | "createdAt": "2021-07-23T09:33:48Z", 68 | "url": "https://a.url.com/to/a/audio.aac" 69 | }, 70 | { 71 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 72 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 73 | "likes": 25, 74 | "createdAt": "2021-07-23T09:33:48Z", 75 | "url": "https://a.url.com/to/a/audio.aac", 76 | "duration": 60 77 | } 78 | ], 79 | "video": [ 80 | { 81 | "id": "98765432-abcd-efab-0123-456789abcdef", 82 | "author": "04355678-abcd-efab-0123-456789abcdef", 83 | "likes": 2345, 84 | "createdAt": "2021-07-23T09:36:38Z", 85 | "url": "https://a.url.com/to/a/video.mp4", 86 | "duration": 460, 87 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 88 | }, 89 | { 90 | "id": "98765432-abcd-efab-0123-456789abcdef", 91 | "author": "04355678-abcd-efab-0123-456789abcdef", 92 | "likes": 2345, 93 | "createdAt": "2021-07-23T09:36:38Z", 94 | "url": "https://a.url.com/to/a/video.mp4", 95 | "duration": 460, 96 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 97 | }, 98 | { 99 | "likes": 2345, 100 | "createdAt": "2021-07-23T09:36:38Z", 101 | "url": "https://a.url.com/to/a/video.mp4", 102 | "duration": 460, 103 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 104 | } 105 | ] 106 | }, 107 | "next": "https://a.url.com/to/next/page" 108 | } 109 | """.data(using: .utf8)! 110 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Data/ContainerCollectionDecode.swift: -------------------------------------------------------------------------------- 1 | let containerCollectionDecode = 2 | """ 3 | { 4 | "content": { 5 | "text": [ 6 | { 7 | "id": "00005678-abcd-efab-0123-456789abcdef", 8 | "author": "12345678-abcd-efab-0123-456789abcdef", 9 | "likes": 145, 10 | "createdAt": "2021-07-23T07:36:43Z", 11 | "text": "Lorem Ipsium" 12 | }, 13 | { 14 | "id": "00005678-abcd-efab-0123-456789abcdef", 15 | "author": "12345678-abcd-efab-0123-456789abcdef", 16 | "likes": 145, 17 | "createdAt": "2021-07-23T07:36:43Z", 18 | "text": "Lorem Ipsium" 19 | }, 20 | { 21 | "id": "00005678-abcd-efab-0123-456789abcdef", 22 | "author": "12345678-abcd-efab-0123-456789abcdef", 23 | "likes": 145, 24 | "createdAt": "2021-07-23T07:36:43Z", 25 | "text": "Lorem Ipsium" 26 | } 27 | ], 28 | "picture": [ 29 | { 30 | "id": "43215678-abcd-efab-0123-456789abcdef", 31 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 32 | "likes": 370, 33 | "createdAt": "2021-07-23T09:32:13Z", 34 | "url": "https://a.url.com/to/a/picture.png", 35 | "caption": "Lorem Ipsium" 36 | }, 37 | { 38 | "id": "43215678-abcd-efab-0123-456789abcdef", 39 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 40 | "likes": 370, 41 | "createdAt": "2021-07-23T09:32:13Z", 42 | "url": "https://a.url.com/to/a/picture.png", 43 | "caption": "Lorem Ipsium" 44 | }, 45 | { 46 | "id": "43215678-abcd-efab-0123-456789abcdef", 47 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 48 | "likes": 370, 49 | "createdAt": "2021-07-23T09:32:13Z", 50 | "url": "https://a.url.com/to/a/picture.png", 51 | "caption": "Lorem Ipsium" 52 | } 53 | ], 54 | "audio": [ 55 | { 56 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 57 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 58 | "likes": 25, 59 | "createdAt": "2021-07-23T09:33:48Z", 60 | "url": "https://a.url.com/to/a/audio.aac", 61 | "duration": 60 62 | }, 63 | { 64 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 65 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 66 | "likes": 25, 67 | "createdAt": "2021-07-23T09:33:48Z", 68 | "url": "https://a.url.com/to/a/audio.aac", 69 | "duration": 60 70 | }, 71 | { 72 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 73 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 74 | "likes": 25, 75 | "createdAt": "2021-07-23T09:33:48Z", 76 | "url": "https://a.url.com/to/a/audio.aac", 77 | "duration": 60 78 | } 79 | ], 80 | "video": [ 81 | { 82 | "id": "98765432-abcd-efab-0123-456789abcdef", 83 | "author": "04355678-abcd-efab-0123-456789abcdef", 84 | "likes": 2345, 85 | "createdAt": "2021-07-23T09:36:38Z", 86 | "url": "https://a.url.com/to/a/video.mp4", 87 | "duration": 460, 88 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 89 | }, 90 | { 91 | "id": "98765432-abcd-efab-0123-456789abcdef", 92 | "author": "04355678-abcd-efab-0123-456789abcdef", 93 | "likes": 2345, 94 | "createdAt": "2021-07-23T09:36:38Z", 95 | "url": "https://a.url.com/to/a/video.mp4", 96 | "duration": 460, 97 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 98 | }, 99 | { 100 | "id": "98765432-abcd-efab-0123-456789abcdef", 101 | "author": "04355678-abcd-efab-0123-456789abcdef", 102 | "likes": 2345, 103 | "createdAt": "2021-07-23T09:36:38Z", 104 | "url": "https://a.url.com/to/a/video.mp4", 105 | "duration": 460, 106 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 107 | } 108 | ] 109 | }, 110 | "next": "https://a.url.com/to/next/page" 111 | } 112 | """.data(using: .utf8)! 113 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Guides/ContextProvider.md: -------------------------------------------------------------------------------- 1 | # Decoding with Provided Context 2 | 3 | Decode dynamic JSON objects based on previous responses or actions by providing decoding context to top level decoder, i.e. `JSONDecoder`. 4 | 5 | ## Overview 6 | 7 | In certain scenarios, the data type present in JSON response will be dependant on previous JSON responses or a specific action performed. For example, in a social media post detail response, the data present will be indicated by the post type the detail is for, i.e. text based post, picture, audio or video post: 8 | ```json 9 | { 10 | "id": "00005678-abcd-efab-0123-456789abcdef", 11 | "author": "12345678-abcd-efab-0123-456789abcdef", 12 | "likes": 145, 13 | "createdAt": "2021-07-23T07:36:43Z", 14 | "title": "Lorem Ipsium", 15 | "description": "Lorem Ipsium", 16 | "text": "Lorem Ipsium" 17 | } 18 | ``` 19 | ```json 20 | { 21 | "id": "43215678-abcd-efab-0123-456789abcdef", 22 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 23 | "likes": 370, 24 | "createdAt": "2021-07-23T09:32:13Z", 25 | "url": "https://a.url.com/to/a/picture.png", 26 | "caption": "Lorem Ipsium", 27 | "size": { 28 | "hight": 1080, 29 | "width": 1920 30 | } 31 | } 32 | ``` 33 | ```json 34 | { 35 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 36 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 37 | "likes": 25, 38 | "createdAt": "2021-07-23T09:33:48Z", 39 | "url": "https://a.url.com/to/a/audio.aac", 40 | "duration": 60, 41 | "lyrics": "https://a.url.com/to/a/text.txt", 42 | } 43 | ``` 44 | ```json 45 | { 46 | "id": "98765432-abcd-efab-0123-456789abcdef", 47 | "author": "04355678-abcd-efab-0123-456789abcdef", 48 | "likes": 2345, 49 | "createdAt": "2021-07-23T09:36:38Z", 50 | "url": "https://a.url.com/to/a/video.mp4", 51 | "duration": 460, 52 | "thumbnail": "https://a.url.com/to/a/thumbnail.png", 53 | "subTitles": "https://a.url.com/to/a/subTitles.srt", 54 | "aspectRatio": { 55 | "hight": 9, 56 | "width": 16 57 | } 58 | } 59 | ``` 60 | 61 | To decode post detail JSON dynamically, type representing every post type: `TextPostDetail`, `PicturePostDetail`, `AudioPostDetail`, `VideoPostDetail` can be created, each confirming to ``DynamicDecodable`` and to protocol `PostDetail` which represents common post detail type. 62 | ![Decoded models hierarchy.](context-provider-class) 63 | 64 | A ``DynamicDecodingContextProvider`` can be created to provide dynamic decoding context. For the current example, a type confirming ``UserInfoDynamicDecodingContextProvider`` can be created that provides the decoding context present in `Decoder`'s `userInfo` property associated to ``UserInfoDynamicDecodingContextProvider/infoKey``. 65 | ```swift 66 | extension CodingUserInfoKey { 67 | static let postDetailDecodingContext = CodingUserInfoKey(rawValue: "post_detail_decoding_context")! 68 | } 69 | 70 | struct PostDetailDecodingContextProvider: UserInfoDynamicDecodingContextProvider { 71 | static var infoKey: CodingUserInfoKey { .postDetailDecodingContext } 72 | } 73 | ``` 74 | 75 | Finally, the top `Decodable` model can be created with ``DynamicDecodingContextBasedWrapper`` wrapped property to consume the decoding context provided: 76 | ```swift 77 | struct PostDetailPage: Decodable { 78 | let next: URL 79 | @DynamicDecodingContextBasedWrapper var data: PostDetail 80 | } 81 | ``` 82 | 83 | Before decoding, the context can be provided in the top level decoder's `userInfo` property to be consumed later. If a text post is clicked, then assuming the `TextPostDetail` type would be decoded: 84 | ```swift 85 | let decoder = JSONDecoder() 86 | decoder.userInfo[.postDetailDecodingContext] = DynamicDecodingContext(decoding: TextPostDetail.self) 87 | let detailPage = try decoder.decode(PostDetailPage.self, from: json) 88 | ``` 89 | 90 | ## Topics 91 | 92 | ### Protocols 93 | 94 | - ``UserInfoDynamicDecodingContextProvider`` 95 | - ``DynamicDecodingContextProvider`` 96 | 97 | ### Property Wrappers 98 | 99 | - ``DynamicDecodingContextBasedWrapper`` 100 | - ``DynamicDecodingDefaultValueContextBasedWrapper`` 101 | - ``DynamicDecodingCollectionContextBasedWrapper`` 102 | 103 | ### Type Aliases 104 | 105 | - ``OptionalDynamicDecodingContextBasedWrapper`` 106 | - ``StrictDynamicDecodingArrayContextBasedWrapper`` 107 | - ``DefaultValueDynamicDecodingArrayContextBasedWrapper`` 108 | - ``LossyDynamicDecodingArrayContextBasedWrapper`` 109 | - ``StrictDynamicDecodingCollectionContextBasedWrapper`` 110 | - ``DefaultValueDynamicDecodingCollectionContextBasedWrapper`` 111 | - ``LossyDynamicDecodingCollectionContextBasedWrapper`` -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/PathCodingKeyDefaultValueWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that exposes value of provided coding key type 2 | /// in a no throw approach from the path of coding keys taken to get to decoding this type. 3 | /// 4 | /// This can be used as an alternative to ``PathCodingKeyWrapper`` where instead of 5 | /// throwing error when finding value of coding key type fails, 6 | /// ``DynamicDecodingDefaultValueProvider/default`` value provided by 7 | /// ``DynamicDecodingDefaultValueProvider`` is used, 8 | /// i.e. ``OptionalPathCodingKeyWrapper`` uses `nil` 9 | /// as default value in case of failure. 10 | @frozen 11 | @propertyWrapper 12 | public struct PathCodingKeyDefaultValueWrapper< 13 | Value: DynamicDecodingDefaultValueProvider 14 | >: PropertyWrapperDecodableEmptyCodable where Value.Wrapped: CodingKey { 15 | /// The underlying ``DynamicDecodingDefaultValueProvider`` 16 | /// that wraps coding key value referenced. 17 | public var wrappedValue: Value 18 | 19 | /// Creates new instance with a value that has underlying coding key value. 20 | /// 21 | /// - Parameters: 22 | /// - wrappedValue: An initial value that has 23 | /// underlying coding key value. 24 | public init(wrappedValue: Value) { 25 | self.wrappedValue = wrappedValue 26 | } 27 | /// Creates a new instance by decoding from the given decoder. 28 | /// 29 | /// If coding key of provided type not found in `decoder.codingPath`, 30 | /// default value is used for provided ``DynamicDecodingDefaultValueProvider``. 31 | /// 32 | /// - Parameters: 33 | /// - decoder: The decoder to read data from. 34 | public init(from decoder: Decoder) { 35 | guard 36 | let value = decoder.codingKeyFromPath(ofType: Value.Wrapped.self) 37 | else { self.wrappedValue = .default; return } 38 | self.wrappedValue = .init(value) 39 | } 40 | } 41 | 42 | public extension KeyedDecodingContainer { 43 | /// Decodes a value of the type ``DynamicDecodingDefaultValueProvider`` 44 | /// for the wrapped value type provided. 45 | /// 46 | /// - Parameters: 47 | /// - type: The type of value to decode. 48 | /// - key: The coding key. 49 | /// 50 | /// - Returns: A value of the type ``DynamicDecodingDefaultValueProvider`` 51 | /// for the wrapped value type provided. 52 | func decode( 53 | _ type: PathCodingKeyDefaultValueWrapper.Type, 54 | forKey key: K 55 | ) -> PathCodingKeyDefaultValueWrapper { 56 | return self.decode(type) 57 | } 58 | } 59 | 60 | public extension KeyedDecodingContainerProtocol { 61 | /// Decodes a value of the type ``DynamicDecodingDefaultValueProvider`` from coding key path. 62 | /// 63 | /// - Parameters: 64 | /// - type: The type of value to decode. 65 | /// - key: The coding key. 66 | /// 67 | /// - Returns: A value of the type ``DynamicDecodingDefaultValueProvider`` 68 | /// from coding key path. 69 | fileprivate func decode( 70 | _ type: PathCodingKeyDefaultValueWrapper.Type 71 | ) -> PathCodingKeyDefaultValueWrapper { 72 | guard 73 | let value = self.codingKeyFromPath(ofType: Value.Wrapped.self) 74 | else { return .init(wrappedValue: .default) } 75 | return .init(wrappedValue: .init(value)) 76 | } 77 | /// Decodes a value of the type ``DynamicDecodingDefaultValueProvider`` 78 | /// for the wrapped value type provided. 79 | /// 80 | /// - Parameters: 81 | /// - type: The type of value to decode. 82 | /// - key: The coding key. 83 | /// 84 | /// - Returns: A value of the type ``DynamicDecodingDefaultValueProvider`` 85 | /// for the wrapped value type provided. 86 | func decode( 87 | _ type: PathCodingKeyDefaultValueWrapper.Type, 88 | forKey key: Key 89 | ) -> PathCodingKeyDefaultValueWrapper { 90 | return self.decode(type) 91 | } 92 | } 93 | 94 | /// A property wrapper type that exposes optional value of provided coding key type 95 | /// from the path of coding keys taken to get to decoding this type. 96 | /// 97 | /// `OptionalPathCodingKeyWrapper` is a type alias for ``PathCodingKeyDefaultValueWrapper``, 98 | /// with `Value` generic type as `Optional`. If coding key value of provided type is not found, 99 | /// `nil` is used as underlying value instead of throwing error. 100 | public typealias OptionalPathCodingKeyWrapper = 101 | PathCodingKeyDefaultValueWrapper> 102 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextContainerCodingKey/Models/PostPath.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | struct CommonPost: Decodable { 5 | let id: UUID 6 | let author: UUID 7 | let likes: Int 8 | let createdAt: String 9 | @PathCodingKeyWrapper var type: PostType 10 | } 11 | 12 | struct OptionalTypeCommonPost: Decodable { 13 | let id: UUID 14 | let author: UUID 15 | let likes: Int 16 | let createdAt: String 17 | @OptionalPathCodingKeyWrapper var type: PostType? 18 | } 19 | 20 | struct CommonPostPage: Decodable { 21 | let next: URL 22 | @PostData var content: [PostType: [CommonPost]] 23 | } 24 | 25 | struct OptionalCommonPostPage: Decodable { 26 | let next: URL 27 | @OptionalPostData var content: [PostType: [OptionalTypeCommonPost]] 28 | } 29 | 30 | struct ThrowingCommonPostPage: Decodable { 31 | let next: URL 32 | var content: [String: [CommonPost]] 33 | } 34 | 35 | struct OptionalTypeCommonPostPage: Decodable { 36 | let next: URL 37 | var content: [String: [OptionalTypeCommonPost]] 38 | } 39 | 40 | @propertyWrapper 41 | struct PostData: Decodable { 42 | public var wrappedValue: [PostType: [CommonPost]] 43 | 44 | public init(wrappedValue: [PostType: [CommonPost]]) { 45 | self.wrappedValue = wrappedValue 46 | } 47 | 48 | public init(from decoder: Decoder) throws { 49 | let container = try decoder.container(keyedBy: PostType.self) 50 | self.wrappedValue = try container.allKeys.reduce( 51 | into: [:], 52 | { values, key in 53 | values[key] = try container.decode( 54 | [CommonPost].self, 55 | forKey: key 56 | ) 57 | } 58 | ) 59 | } 60 | } 61 | 62 | @propertyWrapper 63 | struct OptionalPostData: Decodable { 64 | public var wrappedValue: [PostType: [OptionalTypeCommonPost]] 65 | 66 | public init(wrappedValue: [PostType: [OptionalTypeCommonPost]]) { 67 | self.wrappedValue = wrappedValue 68 | } 69 | 70 | public init(from decoder: Decoder) throws { 71 | let container = try decoder.container(keyedBy: PostType.self) 72 | self.wrappedValue = try container.allKeys.reduce( 73 | into: [:], 74 | { values, key in 75 | values[key] = try container.decode( 76 | [OptionalTypeCommonPost].self, 77 | forKey: key 78 | ) 79 | } 80 | ) 81 | } 82 | } 83 | 84 | struct ContainedPathCodingKeyWrapper: Decodable { 85 | let text: [PathCodingKeyWrapper] 86 | 87 | init(from decoder: Decoder) throws { 88 | let container = try decoder.container(keyedBy: CodingKeys.self) 89 | self.text = try container.decode( 90 | [PathCodingKeyWrapper].self, 91 | forKey: .text 92 | ) 93 | } 94 | 95 | enum CodingKeys: String, CodingKey { 96 | case text 97 | } 98 | } 99 | 100 | struct ContainedInvalidPathCodingKeyWrapper: Decodable { 101 | typealias Value = Array< 102 | PathCodingKeyWrapper 103 | > 104 | let text: Value 105 | 106 | init(from decoder: Decoder) throws { 107 | let container = try decoder.container(keyedBy: CodingKeys.self) 108 | self.text = try container.decode(Value.self, forKey: .text) 109 | } 110 | 111 | enum CodingKeys: String, CodingKey { 112 | case text 113 | } 114 | } 115 | 116 | struct ContainedOptionalPathCodingKeyWrapper: Decodable { 117 | typealias Value = [OptionalPathCodingKeyWrapper] 118 | let text: Value 119 | 120 | init(from decoder: Decoder) throws { 121 | let container = try decoder.container(keyedBy: CodingKeys.self) 122 | self.text = try container.decode(Value.self, forKey: .text) 123 | } 124 | 125 | enum CodingKeys: String, CodingKey { 126 | case text 127 | } 128 | } 129 | 130 | struct ContainedInvalidOptionalPathCodingKeyWrapper: Decodable { 131 | typealias Value = Array< 132 | OptionalPathCodingKeyWrapper< 133 | ContainedOptionalPathCodingKeyWrapper.CodingKeys 134 | > 135 | > 136 | let text: 137 | Array< 138 | OptionalPathCodingKeyWrapper< 139 | ContainedOptionalPathCodingKeyWrapper.CodingKeys 140 | > 141 | > 142 | 143 | init(from decoder: Decoder) throws { 144 | let container = try decoder.container(keyedBy: CodingKeys.self) 145 | self.text = try container.decode(Value.self, forKey: .text) 146 | } 147 | 148 | enum CodingKeys: String, CodingKey { 149 | case text 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Guides/ContainerCodingKey.md: -------------------------------------------------------------------------------- 1 | # Decoding with Parent CodingKey 2 | 3 | Decode dynamic JSON objects based on parent coding key that indicates the actual type to decode. 4 | 5 | ## Overview 6 | 7 | Alternative to , in some JSON responses different types of objects are already grouped with a specific parent coding key. Below is a JSON response for a social media page that contains different types of posts that are already grouped by post type: 8 | ```json 9 | { 10 | "content": { 11 | "text": [ 12 | { 13 | "id": "00005678-abcd-efab-0123-456789abcdef", 14 | "type": "text", 15 | "author": "12345678-abcd-efab-0123-456789abcdef", 16 | "likes": 145, 17 | "createdAt": "2021-07-23T07:36:43Z", 18 | "text": "Lorem Ipsium" 19 | } 20 | ], 21 | "picture": [ 22 | { 23 | "id": "43215678-abcd-efab-0123-456789abcdef", 24 | "type": "picture", 25 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 26 | "likes": 370, 27 | "createdAt": "2021-07-23T09:32:13Z", 28 | "url": "https://a.url.com/to/a/picture.png", 29 | "caption": "Lorem Ipsium" 30 | } 31 | ], 32 | "audio": [ 33 | { 34 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 35 | "type": "audio", 36 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 37 | "likes": 25, 38 | "createdAt": "2021-07-23T09:33:48Z", 39 | "url": "https://a.url.com/to/a/audio.aac", 40 | "duration": 60 41 | } 42 | ], 43 | "video": [ 44 | { 45 | "id": "98765432-abcd-efab-0123-456789abcdef", 46 | "type": "video", 47 | "author": "04355678-abcd-efab-0123-456789abcdef", 48 | "likes": 2345, 49 | "createdAt": "2021-07-23T09:36:38Z", 50 | "url": "https://a.url.com/to/a/video.mp4", 51 | "duration": 460, 52 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 53 | } 54 | ] 55 | }, 56 | "next": "https://a.url.com/to/next/page" 57 | } 58 | ``` 59 | 60 | ### Create Types to be Decoded 61 | 62 | To summarize above JSON, posts can be either **Text** based, includes **Picture**, **Audio** or **Video**. The `type` field indicates the object type and indicates additional fields available while fields like `id`, `author`, `likes`, `createdAt` are common for all types. 63 | ![Social media page posts JSON data hierarchy.](container-json) 64 | 65 | Decoding dynamically can be handled by creating types representing every post type: `TextPost`, `PicturePost`, `AudioPost`, `VideoPost` each confirming to ``DynamicDecodable`` and to protocol `Post` which represents common post type. 66 | ![Decoded models hierarchy.](identifier-class) 67 | 68 | ### Implement Dynamic Decoding Contexts 69 | 70 | Now `Post` type can be dynamically decoded to its concrete type based on value of `PostCodingKey` key. `PostCodingKey` can be created as an `Enum` confirming to ``DynamicDecodingContextContainerCodingKey`` while implementing the decoding context to use with ``DynamicDecodingContextContainerCodingKey/containedContext-15p5u``. 71 | ```swift 72 | enum PostCodingKey: String, Hashable, DynamicDecodingContextContainerCodingKey { 73 | case text, picture, audio, video 74 | 75 | var containedContext: DynamicDecodingContext { 76 | switch self { 77 | case .text: 78 | return DynamicDecodingContext(decoding: TextPost.self) 79 | case .picture: 80 | return DynamicDecodingContext(decoding: PicturePost.self) 81 | case .audio: 82 | return DynamicDecodingContext(decoding: AudioPost.self) 83 | case .video: 84 | return DynamicDecodingContext(decoding: VideoPost.self) 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | Finally, `PostPage` can be decoded with all the dynamic post contents by using ``DynamicDecodingCollectionDictionaryWrapper`` property wrapper to wrap `content` dictionary. 91 | ```swift 92 | struct PostPage: Decodable { 93 | let next: URL 94 | @StrictDynamicDecodingArrayDictionaryWrapper var content: [PostCodingKey: Post] 95 | } 96 | ``` 97 | > Tip: You can use the lossy versions, i.e. ``LossyDynamicDecodingArrayDictionaryWrapper`` to decode only valid post data while ignoring the rest. See more in . 98 | 99 | ## Topics 100 | 101 | ### Protocols 102 | 103 | - ``DynamicDecodingContextContainerCodingKey`` 104 | 105 | ### Property Wrappers 106 | 107 | - ``DynamicDecodingDictionaryWrapper`` 108 | - ``DynamicDecodingCollectionDictionaryWrapper`` 109 | - ``PathCodingKeyWrapper`` 110 | - ``PathCodingKeyDefaultValueWrapper`` 111 | 112 | ### Type Aliases 113 | 114 | - ``StrictDynamicDecodingDictionaryWrapper`` 115 | - ``LossyDynamicDecodingDictionaryWrapper`` 116 | - ``StrictDynamicDecodingArrayDictionaryWrapper`` 117 | - ``DefaultValueDynamicDecodingArrayDictionaryWrapper`` 118 | - ``LossyDynamicDecodingArrayDictionaryWrapper`` 119 | - ``StrictDynamicDecodingCollectionDictionaryWrapper`` 120 | - ``DefaultValueDynamicDecodingCollectionDictionaryWrapper`` 121 | - ``LossyDynamicDecodingCollectionDictionaryWrapper`` 122 | - ``OptionalPathCodingKeyWrapper`` 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | # OS generated files # 26 | ###################### 27 | .DS_Store 28 | .DS_Store? 29 | ._* 30 | .Spotlight-V100 31 | .Trashes 32 | ehthumbs.db 33 | Thumbs.db 34 | 35 | ## Obj-C/Swift specific 36 | *.hmap 37 | 38 | ## App packaging 39 | *.ipa 40 | *.dSYM.zip 41 | *.dSYM 42 | 43 | ## Playgrounds 44 | timeline.xctimeline 45 | playground.xcworkspace 46 | 47 | # Swift Package Manager 48 | # 49 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 50 | Packages/ 51 | Package.pins 52 | Package.resolved 53 | 54 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 55 | # hence it is not needed unless you have added a package configuration file to your project 56 | .swiftpm 57 | 58 | .build/ 59 | 60 | # CocoaPods 61 | # 62 | # We recommend against adding the Pods directory to your .gitignore. However 63 | # you should judge for yourself, the pros and cons are mentioned at: 64 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | # 66 | Pods/ 67 | # 68 | # Add this line if you want to avoid checking in source code from the Xcode workspace 69 | *.xcworkspace 70 | 71 | # Carthage 72 | # 73 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 74 | Carthage/Checkouts 75 | 76 | Carthage/Build/ 77 | 78 | # Add Xcode project related files required by Carthage 79 | DynamicCodableKit.xcodeproj/* 80 | !DynamicCodableKit.xcodeproj/*.pbxproj 81 | !DynamicCodableKit.xcodeproj/*.plist 82 | !DynamicCodableKit.xcodeproj/xcshareddata 83 | 84 | # Accio dependency management 85 | Dependencies/ 86 | .accio/ 87 | 88 | # fastlane 89 | # 90 | # It is recommended to not store the screenshots in the git repo. 91 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 92 | # For more information about the recommended setup visit: 93 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 94 | 95 | fastlane/report.xml 96 | fastlane/Preview.html 97 | fastlane/screenshots/**/*.png 98 | fastlane/test_output 99 | 100 | # Code Injection 101 | # 102 | # After new code Injection tools there's a generated folder /iOSInjectionProject 103 | # https://github.com/johnno1962/injectionforxcode 104 | 105 | iOSInjectionProject/ 106 | 107 | # DocC 108 | .netrc 109 | .docc-build 110 | *.doccarchive* 111 | 112 | # Built Products 113 | *.xcframework* 114 | *.zip 115 | *.tar* 116 | 117 | # Tuist 118 | Derived/ 119 | 120 | ## Node-Js ignores 121 | # Logs 122 | logs 123 | *.log 124 | npm-debug.log* 125 | yarn-debug.log* 126 | yarn-error.log* 127 | lerna-debug.log* 128 | 129 | # Diagnostic reports (https://nodejs.org/api/report.html) 130 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 131 | 132 | # Runtime data 133 | pids 134 | *.pid 135 | *.seed 136 | *.pid.lock 137 | 138 | # Directory for instrumented libs generated by jscoverage/JSCover 139 | lib-cov 140 | 141 | # Coverage directory used by tools like istanbul 142 | coverage 143 | *.lcov 144 | 145 | # nyc test coverage 146 | .nyc_output 147 | 148 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 149 | .grunt 150 | 151 | # Bower dependency directory (https://bower.io/) 152 | bower_components 153 | 154 | # node-waf configuration 155 | .lock-wscript 156 | 157 | # Compiled binary addons (https://nodejs.org/api/addons.html) 158 | build/Release 159 | 160 | # Dependency directories 161 | node_modules/ 162 | jspm_packages/ 163 | 164 | # TypeScript v1 declaration files 165 | typings/ 166 | 167 | # TypeScript cache 168 | *.tsbuildinfo 169 | 170 | # Optional npm cache directory 171 | .npm 172 | 173 | # Optional eslint cache 174 | .eslintcache 175 | 176 | # Microbundle cache 177 | .rpt2_cache/ 178 | .rts2_cache_cjs/ 179 | .rts2_cache_es/ 180 | .rts2_cache_umd/ 181 | 182 | # Optional REPL history 183 | .node_repl_history 184 | 185 | # Output of 'npm pack' 186 | *.tgz 187 | 188 | # Yarn Integrity file 189 | .yarn-integrity 190 | 191 | # dotenv environment variables file 192 | .env 193 | .env.test 194 | 195 | # parcel-bundler cache (https://parceljs.org/) 196 | .cache 197 | 198 | # Next.js build output 199 | .next 200 | 201 | # Nuxt.js build / generate output 202 | .nuxt 203 | dist 204 | 205 | # Gatsby files 206 | .cache/ 207 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 208 | # https://nextjs.org/blog/next-9-1#public-directory-support 209 | # public 210 | 211 | # vuepress build output 212 | .vuepress/dist 213 | 214 | # Serverless directories 215 | .serverless/ 216 | 217 | # FuseBox cache 218 | .fusebox/ 219 | 220 | # DynamoDB Local files 221 | .dynamodb/ 222 | 223 | # TernJS port file 224 | .tern-port 225 | 226 | # NPM package lock 227 | package-lock.json 228 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/Guides/TypeIdentifier.md: -------------------------------------------------------------------------------- 1 | # Decoding with Type Identifiers 2 | 3 | Decode dynamic JSON objects based on single or multiple identifiers that indicates the actual type to decode. 4 | 5 | ## Overview 6 | 7 | It is quite common in JSON responses to have objects that have some common fields while providing some additional fields based on object type. Below is a JSON response for a social media page that contains different types of posts: 8 | ```json 9 | { 10 | "content": [ 11 | { 12 | "id": "00005678-abcd-efab-0123-456789abcdef", 13 | "type": "text", 14 | "author": "12345678-abcd-efab-0123-456789abcdef", 15 | "likes": 145, 16 | "createdAt": "2021-07-23T07:36:43Z", 17 | "text": "Lorem Ipsium" 18 | }, 19 | { 20 | "id": "43215678-abcd-efab-0123-456789abcdef", 21 | "type": "picture", 22 | "author": "abcd5678-abcd-efab-0123-456789abcdef", 23 | "likes": 370, 24 | "createdAt": "2021-07-23T09:32:13Z", 25 | "url": "https://a.url.com/to/a/picture.png", 26 | "caption": "Lorem Ipsium" 27 | }, 28 | { 29 | "id": "64475bcb-caff-48c1-bb53-8376628b350b", 30 | "type": "audio", 31 | "author": "4c17c269-1c56-45ab-8863-d8924ece1d0b", 32 | "likes": 25, 33 | "createdAt": "2021-07-23T09:33:48Z", 34 | "url": "https://a.url.com/to/a/audio.aac", 35 | "duration": 60 36 | }, 37 | { 38 | "id": "98765432-abcd-efab-0123-456789abcdef", 39 | "type": "video", 40 | "author": "04355678-abcd-efab-0123-456789abcdef", 41 | "likes": 2345, 42 | "createdAt": "2021-07-23T09:36:38Z", 43 | "url": "https://a.url.com/to/a/video.mp4", 44 | "duration": 460, 45 | "thumbnail": "https://a.url.com/to/a/thumbnail.png" 46 | } 47 | ], 48 | "next": "https://a.url.com/to/next/page" 49 | } 50 | ``` 51 | 52 | ### Create Types to be Decoded 53 | 54 | To summarize above JSON, posts can be either **Text** based, includes **Picture**, **Audio** or **Video**. The `type` field indicates the object type and indicates additional fields available while fields like `id`, `author`, `likes`, `createdAt` are common for all types. 55 | ![Social media page posts JSON data hierarchy.](identifier-json) 56 | 57 | Decoding dynamically can be handled by creating types representing every post type: `TextPost`, `PicturePost`, `AudioPost`, `VideoPost` each confirming to ``DynamicDecodable`` and to protocol `Post` which represents common post type. 58 | ![Decoded models hierarchy.](identifier-class) 59 | 60 | ### Implement Dynamic Decoding Contexts 61 | 62 | Now `Post` type can be dynamically decoded to its concrete type based on value of `PostType` identifier. `PostType` can be created as an `Enum` confirming to ``DynamicDecodingContextIdentifierKey`` while implementing the decoding context to use with ``DynamicDecodingContextIdentifierKey/associatedContext``. To allow `Decoder` to decode `PostType` and get dynamic decoding context, additional `CodingKey` type `PostCodingKey` need to be defined confirming to ``DynamicDecodingContextCodingKey``. Since current example only has one identifier to decode confirming to ``DynamicDecodingContextIdentifierCodingKey`` and providing key that contains the identifier in ``DynamicDecodingContextIdentifierCodingKey/identifierCodingKey`` will suffice. 63 | ```swift 64 | enum PostCodingKey: String, DynamicDecodingContextIdentifierCodingKey { 65 | typealias Identifier = PostType 66 | typealias Identified = Post 67 | case type 68 | static var identifierCodingKey: Self { .type } 69 | } 70 | 71 | enum PostType: String, DynamicDecodingContextIdentifierKey { 72 | case text, picture, audio, video 73 | 74 | var associatedContext: DynamicDecodingContext { 75 | switch self { 76 | case .text: 77 | return DynamicDecodingContext(decoding: TextPost.self) 78 | case .picture: 79 | return DynamicDecodingContext(decoding: PicturePost.self) 80 | case .audio: 81 | return DynamicDecodingContext(decoding: AudioPost.self) 82 | case .video: 83 | return DynamicDecodingContext(decoding: VideoPost.self) 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | Finally, `PostPage` can be decoded with all the dynamic post contents by using ``DynamicDecodingCollectionWrapper`` property wrapper to wrap `content` array. 90 | ```swift 91 | struct PostPage: Decodable { 92 | let next: URL 93 | @StrictDynamicDecodingArrayWrapper var content: [Post] 94 | } 95 | ``` 96 | > Tip: You can use the lossy versions, i.e. ``LossyDynamicDecodingArrayWrapper`` to decode only valid post data while ignoring the rest. See more in . 97 | 98 | ## Topics 99 | 100 | ### Protocols 101 | 102 | - ``DynamicDecodingContextIdentifierKey`` 103 | - ``DynamicDecodingContextIdentifierCodingKey`` 104 | - ``DynamicDecodingContextCodingKey`` 105 | 106 | ### Property Wrappers 107 | 108 | - ``DynamicDecodingWrapper`` 109 | - ``DynamicDecodingDefaultValueWrapper`` 110 | - ``DynamicDecodingCollectionWrapper`` 111 | 112 | ### Type Aliases 113 | 114 | - ``OptionalDynamicDecodingWrapper`` 115 | - ``StrictDynamicDecodingArrayWrapper`` 116 | - ``DefaultValueDynamicDecodingArrayWrapper`` 117 | - ``LossyDynamicDecodingArrayWrapper`` 118 | - ``StrictDynamicDecodingCollectionWrapper`` 119 | - ``DefaultValueDynamicDecodingCollectionWrapper`` 120 | - ``LossyDynamicDecodingCollectionWrapper`` 121 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextCodingKey/DynamicDecodingContextCodingKey.swift: -------------------------------------------------------------------------------- 1 | /// A type that provides dynamic decoding context based on its value. 2 | public protocol DynamicDecodingContextIdentifierKey: Decodable { 3 | /// The base type or base element type in case of collection, that will be decoded. 4 | associatedtype Identified 5 | /// The associated dynamic decoding context. 6 | var associatedContext: DynamicDecodingContext { get } 7 | } 8 | 9 | /// A coding key type that decides dynamic decoding context based on its associated `KeyedDecodingContainer`. 10 | public protocol DynamicDecodingContextCodingKey: CodingKey { 11 | /// The base type or base element type in case of collection, that will be decoded. 12 | associatedtype Identified 13 | /// Decides dynamic decoding context based on provided `KeyedDecodingContainer`. 14 | /// 15 | /// - Parameters: 16 | /// - container: The `KeyedDecodingContainer` to analyse. 17 | /// 18 | /// - Returns: Dynamic decoding context to use on `container`. 19 | static func context( 20 | forContainer container: Container 21 | ) throws -> DynamicDecodingContext where Container.Key == Self 22 | } 23 | 24 | /// A ``DynamicDecodingContextCodingKey`` type that decides dynamic decoding context based on its associated 25 | /// ``DynamicDecodingContextIdentifierKey`` contained by ``identifierCodingKey``. 26 | public protocol DynamicDecodingContextIdentifierCodingKey: 27 | DynamicDecodingContextCodingKey 28 | { 29 | /// The ``DynamicDecodingContextIdentifierKey`` type that ``identifierCodingKey`` contains. 30 | associatedtype Identifier: DynamicDecodingContextIdentifierKey 31 | where Identifier.Identified == Identified 32 | 33 | /// The coding key value that contains ``DynamicDecodingContextIdentifierKey``. 34 | static var identifierCodingKey: Self { get } 35 | } 36 | 37 | public extension DynamicDecodingContextIdentifierCodingKey { 38 | /// Decides dynamic decoding context based on provided 39 | /// ``Identifier`` contained by ``identifierCodingKey``. 40 | /// 41 | /// Decodes ``Identifier`` key contained by ``identifierCodingKey`` 42 | /// from provided `KeyedDecodingContainer` 43 | /// and exposes its ``DynamicDecodingContextIdentifierKey/associatedContext``. 44 | /// 45 | /// - Parameters: 46 | /// - container: The `KeyedDecodingContainer` to analyse. 47 | /// 48 | /// - Returns: Dynamic decoding context to use on `container`. 49 | static func context( 50 | forContainer container: Container 51 | ) throws -> DynamicDecodingContext where Container.Key == Self { 52 | return try container.decode( 53 | Identifier.self, 54 | forKey: Self.identifierCodingKey 55 | ).associatedContext 56 | } 57 | } 58 | 59 | // MARK: - Parse by DynamicDecodingContextCodingKey 60 | public extension DynamicDecodingContext { 61 | /// Creates new context from ``DynamicDecodingContextCodingKey`` coding key. 62 | /// 63 | /// Uses ``DynamicDecodingContextCodingKey/context(forContainer:)`` 64 | /// to decode dynamic type. 65 | /// 66 | /// ```swift 67 | /// let context: DynamicDecodingContext = DynamicDecodingContext(withKey: CodingKeys.self) 68 | /// ``` 69 | /// 70 | /// - Parameters: 71 | /// - key: The coding key type. 72 | init( 73 | withKey key: Key.Type 74 | ) where Key.Identified == Base { 75 | // MARK: - type decoding 76 | decodeFrom = { decoder in 77 | let container = try decoder.container(keyedBy: Key.self) 78 | return try Key.context(forContainer: container).decodeFrom(decoder) 79 | } 80 | // MARK: - array decoding 81 | decodeArrayFrom = { decoder in 82 | var values: [B] = [] 83 | var container = try decoder.unkeyedContainer() 84 | while !container.isAtEnd { 85 | let iteratorDecoder = try container.superDecoder() 86 | let iteratorContainer = try iteratorDecoder.container( 87 | keyedBy: Key.self 88 | ) 89 | let value = try Key.context( 90 | forContainer: iteratorContainer 91 | ).decodeFrom(iteratorDecoder) 92 | values.append(value) 93 | } 94 | return values 95 | } 96 | // MARK: - lossy array decoding 97 | decodeLossyArrayFrom = { decoder in 98 | guard 99 | var container = try? decoder.unkeyedContainer() 100 | else { return [] } 101 | 102 | var values: [B] = [] 103 | while !container.isAtEnd { 104 | guard 105 | let iteratorDecoder = try? container.superDecoder(), 106 | let iteratorContainer = try? iteratorDecoder.container( 107 | keyedBy: Key.self 108 | ), 109 | let value = try? Key.context( 110 | forContainer: iteratorContainer 111 | ).decodeFrom(iteratorDecoder) 112 | else { continue } 113 | values.append(value) 114 | } 115 | return values 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodingContextCodingKey/DynamicDecodingCollectionWrapper.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodingCollectionWrapperTests: XCTestCase { 5 | func testDecoding() throws { 6 | let data = identifierCollectionDecode 7 | let decoder = JSONDecoder() 8 | let postPage = try decoder.decode(ThrowingPostPage.self, from: data) 9 | XCTAssertEqual(postPage.content.count, 4) 10 | XCTAssertEqual( 11 | postPage.content.map(\.type), [.text, .picture, .audio, .video] 12 | ) 13 | } 14 | 15 | func testInvalidDataDecodingWithThrowConfig() throws { 16 | let data = identifierCollectionDecodeWithInvalidData 17 | let decoder = JSONDecoder() 18 | XCTAssertThrowsError( 19 | try decoder.decode(ThrowingPostPage.self, from: data) 20 | ) 21 | } 22 | 23 | func testInvalidDataDecodingWithDefaultConfig() throws { 24 | let data = identifierCollectionDecodeWithInvalidData 25 | let decoder = JSONDecoder() 26 | let postPage = try decoder.decode(DefaultPostPage.self, from: data) 27 | XCTAssertEqual(postPage.content.count, 0) 28 | } 29 | 30 | func testInvalidDataDecodingWithLossyConfig() throws { 31 | let data = identifierCollectionDecodeWithInvalidData 32 | let decoder = JSONDecoder() 33 | let postPage = try decoder.decode(LossyPostPage.self, from: data) 34 | XCTAssertEqual(postPage.content.count, 4) 35 | XCTAssertEqual( 36 | postPage.content.map(\.type), [.text, .picture, .audio, .video] 37 | ) 38 | } 39 | 40 | // func testLossyDecodingPerformance() throws { 41 | // let data = identifierCollectionDecodeWithInvalidData 42 | // measure { 43 | // let decoder = JSONDecoder() 44 | // for _ in 0..<100 { 45 | // _ = try? decoder.decode(LossyPostPage.self, from: data) 46 | // } 47 | // } 48 | // } 49 | 50 | func testDecodingWithSet() throws { 51 | let data = identifierCollectionDecode 52 | let decoder = JSONDecoder() 53 | let postPage = try decoder.decode(ThrowingPostPageSet.self, from: data) 54 | XCTAssertEqual(postPage.content.count, 4) 55 | let decodedPostTypes = Set(postPage.content.map(\.type)) 56 | let expectedPostTypes: Set = [ 57 | .text, .picture, .audio, .video, 58 | ] 59 | XCTAssertEqual(decodedPostTypes, expectedPostTypes) 60 | } 61 | 62 | func testInvalidDataDecodingWithThrowConfigWithSet() throws { 63 | let data = identifierCollectionDecodeWithInvalidData 64 | let decoder = JSONDecoder() 65 | XCTAssertThrowsError( 66 | try decoder.decode(ThrowingPostPageSet.self, from: data) 67 | ) 68 | } 69 | 70 | func testInvalidDataDecodingWithDefaultConfigWithSet() throws { 71 | let data = identifierCollectionDecodeWithInvalidData 72 | let decoder = JSONDecoder() 73 | let postPage = try decoder.decode(DefaultPostPageSet.self, from: data) 74 | XCTAssertEqual(postPage.content.count, 0) 75 | } 76 | 77 | func testInvalidDataDecodingWithLossyConfigWithSet() throws { 78 | let data = identifierCollectionDecodeWithInvalidData 79 | let decoder = JSONDecoder() 80 | let postPage = try decoder.decode(LossyPostPageSet.self, from: data) 81 | XCTAssertEqual(postPage.content.count, 4) 82 | let decodedPostTypes = Set(postPage.content.map(\.type)) 83 | let expectedPostTypes: Set = [ 84 | .text, .picture, .audio, .video, 85 | ] 86 | XCTAssertEqual(decodedPostTypes, expectedPostTypes) 87 | } 88 | 89 | func testDynamicTypeDecodingWithSelfCodingKeyContext() throws { 90 | let data = #"{"values": [86, 46, 94]}"#.data(using: .utf8)! 91 | let decoder = JSONDecoder() 92 | let container = try decoder.decode( 93 | StrictVariableBaseDataTypeContainer.self, 94 | from: data 95 | ) 96 | XCTAssertEqual(container.values as? [Int], [86, 46, 94]) 97 | } 98 | 99 | func 100 | testInvalidDataDynamicTypeDecodingWithSelfCodingKeyContextWithThrowConfig() 101 | throws 102 | { 103 | let data = #"{"values": [86.89, 46, 94]}"#.data(using: .utf8)! 104 | let decoder = JSONDecoder() 105 | XCTAssertThrowsError( 106 | try decoder.decode( 107 | StrictVariableBaseDataTypeContainer.self, 108 | from: data 109 | ) 110 | ) 111 | } 112 | 113 | func 114 | testInvalidDataDynamicTypeDecodingWithSelfCodingKeyContextWithDefaultConfig() 115 | throws 116 | { 117 | let data = #"{"values": [86.89, 46, 94]}"#.data(using: .utf8)! 118 | let decoder = JSONDecoder() 119 | let container = try decoder.decode( 120 | DefaultVariableBaseDataTypeContainer.self, 121 | from: data 122 | ) 123 | XCTAssertEqual(container.values.count, 0) 124 | } 125 | 126 | func 127 | testInvalidDataDynamicTypeDecodingWithSelfCodingKeyContextWithLossyConfig() 128 | throws 129 | { 130 | let data = #"{"values": [86.89, 46, 94]}"#.data(using: .utf8)! 131 | let decoder = JSONDecoder() 132 | let container = try decoder.decode( 133 | LossyVariableBaseDataTypeContainer.self, 134 | from: data 135 | ) 136 | XCTAssertEqual(container.values.count, 2) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextCodingKey/DynamicDecodingDefaultValueWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes dynamic value in a no throw approach based on 2 | /// dynamic decoding context provided by ``DynamicDecodingContextCodingKey``. 3 | /// 4 | /// This can be used as an alternative to ``DynamicDecodingWrapper`` 5 | /// where instead of throwing error when decoding fails, 6 | /// ``DynamicDecodingDefaultValueProvider/default`` value provided by 7 | /// ``DynamicDecodingDefaultValueProvider`` is used, 8 | /// i.e. ``OptionalDynamicDecodingWrapper`` uses `nil` as default value in case of failure. 9 | @frozen 10 | @propertyWrapper 11 | public struct DynamicDecodingDefaultValueWrapper< 12 | ContextCodingKey: DynamicDecodingContextCodingKey, 13 | Wrapped: DynamicDecodingDefaultValueProvider 14 | >: PropertyWrapperCodable where Wrapped.Wrapped == ContextCodingKey.Identified { 15 | /// The underlying ``DynamicDecodingDefaultValueProvider`` 16 | /// that wraps dynamic value value referenced. 17 | public var wrappedValue: Wrapped 18 | 19 | /// Creates new instance with an underlying dynamic wrapped value. 20 | /// 21 | /// - Parameters: 22 | /// - wrappedValue: An initial value with wrapped dynamic value. 23 | public init(wrappedValue: Wrapped) { 24 | self.wrappedValue = wrappedValue 25 | } 26 | /// Creates a new instance by decoding from the given decoder. 27 | /// 28 | /// - Parameters: 29 | /// - decoder: The decoder to read data from. 30 | public init(from decoder: Decoder) { 31 | guard 32 | let container = try? decoder.container( 33 | keyedBy: ContextCodingKey.self 34 | ), 35 | let value = try? ContextCodingKey.context( 36 | forContainer: container 37 | ).decodeFrom(decoder) 38 | else { 39 | self.wrappedValue = Wrapped.default 40 | return 41 | } 42 | self.wrappedValue = .init(value) 43 | } 44 | } 45 | 46 | /// Decodes a value of dynamic ``DynamicDecodingDefaultValueWrapper`` 47 | /// type for the given keyed container and coding key. 48 | /// 49 | /// - Parameters: 50 | /// - container: The keyed container to cdecode from. 51 | /// - type: The type of value to decode. 52 | /// - key: The coding key. 53 | /// 54 | /// - Returns: A dynamic value wrapped in ``DynamicDecodingDefaultValueWrapper`` 55 | /// or a default value provided by ``DynamicDecodingDefaultValueProvider`` 56 | /// if decoding fails. 57 | fileprivate func decode( 58 | from container: Container, 59 | ofType type: DynamicDecodingDefaultValueWrapper< 60 | Container.Key, Wrapped 61 | >.Type, 62 | forKey key: Container.Key 63 | ) -> DynamicDecodingDefaultValueWrapper 64 | where Container: KeyedDecodingContainerProtocol { 65 | typealias Key = Container.Key 66 | guard 67 | let context = try? Key.context(forContainer: container), 68 | let decoder = try? container.superDecoder(forKey: key), 69 | let value = try? context.decodeFrom(decoder) 70 | else { 71 | return DynamicDecodingDefaultValueWrapper(wrappedValue: .default) 72 | } 73 | return DynamicDecodingDefaultValueWrapper(wrappedValue: .init(value)) 74 | } 75 | 76 | public extension KeyedDecodingContainer 77 | where K: DynamicDecodingContextCodingKey { 78 | /// Decodes a value of dynamic ``DynamicDecodingDefaultValueWrapper`` 79 | /// type for the given coding key. 80 | /// 81 | /// - Parameters: 82 | /// - type: The type of value to decode. 83 | /// - key: The coding key. 84 | /// 85 | /// - Returns: A dynamic value wrapped in ``DynamicDecodingDefaultValueWrapper`` 86 | /// or a default value provided by ``DynamicDecodingDefaultValueProvider`` 87 | /// if decoding fails. 88 | func decode( 89 | _ type: DynamicDecodingDefaultValueWrapper.Type, 90 | forKey key: K 91 | ) -> DynamicDecodingDefaultValueWrapper { 92 | return DynamicCodableKit.decode(from: self, ofType: type, forKey: key) 93 | } 94 | } 95 | 96 | public extension KeyedDecodingContainerProtocol 97 | where Key: DynamicDecodingContextCodingKey { 98 | /// Decodes a value of dynamic ``DynamicDecodingDefaultValueWrapper`` 99 | /// type for the given coding key. 100 | /// 101 | /// - Parameters: 102 | /// - type: The type of value to decode. 103 | /// - key: The coding key. 104 | /// 105 | /// - Returns: A dynamic value wrapped in ``DynamicDecodingDefaultValueWrapper`` 106 | /// or a default value provided by ``DynamicDecodingDefaultValueProvider`` 107 | /// if decoding fails. 108 | func decode( 109 | _ type: DynamicDecodingDefaultValueWrapper.Type, 110 | forKey key: Key 111 | ) -> DynamicDecodingDefaultValueWrapper { 112 | return DynamicCodableKit.decode(from: self, ofType: type, forKey: key) 113 | } 114 | } 115 | 116 | /// A property wrapper type that decodes optional dynamic value based on dynamic 117 | /// decoding context provided by ``DynamicDecodingContextCodingKey``. 118 | /// 119 | /// `OptionalDynamicDecodingWrapper` is a type alias for 120 | /// ``DynamicDecodingDefaultValueWrapper``, 121 | /// with wrapped value as optional dynamic value. If decoding fails, 122 | /// `nil` is used as underlying value instead of throwing error. 123 | public typealias OptionalDynamicDecodingWrapper< 124 | ContextCodingKey: DynamicDecodingContextCodingKey 125 | > = DynamicDecodingDefaultValueWrapper< 126 | ContextCodingKey, 127 | Optional 128 | > 129 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicCodableKit.docc/DynamicCodableKit.md: -------------------------------------------------------------------------------- 1 | # ``DynamicCodableKit`` 2 | 3 | Access essential data types, property wrappers, and protocols to implement dynamic JSON decoding functionality working with Swift's sound type system. 4 | 5 | ## Overview 6 | 7 | `DynamicCodableKit` framework provides a base layer of functionality to allow dynamic JSON data decoding. It does so by providing a ``DynamicDecodingContext`` that decodes a generic type, during initialization any concrete ``DynamicDecodable`` type can be provided to be decoded and then casted to the required generic type by using type's implemented ``DynamicDecodable/castAs(type:codingPath:)-4hwd``. 8 | 9 | 10 | The data types, protocols, and property wrappers defined by `DynamicCodableKit` can be used to provide dynamic decoding functionality to swift's `Decodable` types. 11 | 12 | ## Requirements 13 | 14 | | Platform | Minimum Swift Version | Installation | Status | 15 | | --- | --- | --- | --- | 16 | | iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ | 5.1 | CocoaPods, Carthage, Swift Package Manager, Manual | Fully Tested | 17 | | Linux | 5.1 | Swift Package Manager | Fully Tested | 18 | | Windows | 5.3 | Swift Package Manager | Fully Tested | 19 | 20 | ## Installation 21 | 22 | ### CocoaPods 23 | 24 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate `DynamicCodableKit` into your Xcode project using CocoaPods, specify it in your `Podfile`: 25 | 26 | ```ruby 27 | pod 'DynamicCodableKit' 28 | ``` 29 | 30 | Optionally, you can also use the pre-built XCFramework from the GitHub releases page by replacing `{version}` with the required version you want to use: 31 | 32 | ```ruby 33 | pod 'DynamicCodableKit', :http => 'https://github.com/SwiftyLab/DynamicCodableKit/releases/download/v{version}/DynamicCodableKit-{version}.xcframework.zip' 34 | ``` 35 | 36 | ### Carthage 37 | 38 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate `DynamicCodableKit` into your Xcode project using Carthage, specify it in your `Cartfile`: 39 | 40 | ```ogdl 41 | github "SwiftyLab/DynamicCodableKit" 42 | ``` 43 | 44 | ### Swift Package Manager 45 | 46 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. 47 | 48 | Once you have your Swift package set up, adding `DynamicCodableKit` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 49 | 50 | ```swift 51 | .package(url: "https://github.com/SwiftyLab/DynamicCodableKit.git", from: "1.0.0"), 52 | ``` 53 | 54 | Optionally, you can also use the pre-built XCFramework from the GitHub releases page by replacing `{version}` and `{checksum}` with the required version and checksum of artifact you want to use: 55 | 56 | ```swift 57 | .binaryTarget(name: "DynamicCodableKit", url: "https://github.com/SwiftyLab/DynamicCodableKit/releases/download/v{version}/DynamicCodableKit-{version}.xcframework.zip", checksum: "{checksum}"), 58 | ``` 59 | 60 | ### Manually 61 | 62 | If you prefer not to use any of the aforementioned dependency managers, you can integrate `DynamicCodableKit` into your project manually. 63 | 64 | #### Git Submodule 65 | 66 | - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: 67 | 68 | ```bash 69 | $ git init 70 | ``` 71 | 72 | - Add `DynamicCodableKit` as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: 73 | 74 | ```bash 75 | $ git submodule add https://github.com/SwiftyLab/DynamicCodableKit.git 76 | ``` 77 | 78 | - Open the new `DynamicCodableKit` folder, and drag the `DynamicCodableKit.xcodeproj` into the Project Navigator of your application's Xcode project or existing workspace. 79 | 80 | > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. 81 | 82 | - Select the `DynamicCodableKit.xcodeproj` in the Project Navigator and verify the deployment target satisfies that of your application target (should be less or equal). 83 | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the `Targets` heading in the sidebar. 84 | - In the tab bar at the top of that window, open the "General" panel. 85 | - Click on the `+` button under the `Frameworks and Libraries` section. 86 | - You will see `DynamicCodableKit.xcodeproj` folder with `DynamicCodableKit.framework` nested inside. 87 | - Select the `DynamicCodableKit.framework` and that's it! 88 | 89 | > The `DynamicCodableKit.framework` is automagically added as a target dependency, linked framework and embedded framework in build phase which is all you need to build on the simulator and a device. 90 | 91 | #### XCFramework 92 | 93 | You can also directly download the pre-built artifact from the GitHub releases page: 94 | 95 | - Download the artifact from the GitHub releases page of the format `DynamicCodableKit-{version}.xcframework.zip` where `{version}` is the version you want to use. 96 | - Extract the XCFramework from the archive, and drag the `DynamicCodableKit.xcframework` into the Project Navigator of your application's target folder in your Xcode project. 97 | - Select `Copy items if needed` and that's it! 98 | 99 | > The `DynamicCodableKit.xcframework` is automagically added in the embedded `Frameworks and Libraries` section, an in turn the linked framework in build phase. 100 | 101 | ## Topics 102 | 103 | ### Common Scenarios 104 | 105 | - 106 | - 107 | - 108 | 109 | ### Decoding Configurations 110 | 111 | - 112 | - 113 | 114 | -------------------------------------------------------------------------------- /Tests/DynamicCodableKitTests/DynamicDecodable.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicCodableKit 3 | 4 | final class DynamicDecodableTests: XCTestCase { 5 | func testDefaultDownCasting() throws { 6 | let value: Decodable = try 5.castAs( 7 | type: Decodable.self, 8 | codingPath: [] 9 | ) 10 | XCTAssertEqual(value as? Int, 5) 11 | } 12 | 13 | func testDefaultDownCastingFailure() throws { 14 | XCTAssertThrowsError(try 5.castAs(type: String.self, codingPath: [])) 15 | } 16 | 17 | func testDefaultOptionalDownCasting() throws { 18 | let value: Decodable? = 5.castAs(type: Decodable?.self, codingPath: []) 19 | XCTAssertEqual(value as? Int, 5) 20 | } 21 | 22 | func testDefaultOptionalDownCastingFailure() throws { 23 | let value: String? = 5.castAs(type: String?.self, codingPath: []) 24 | XCTAssertNil(value) 25 | } 26 | 27 | func testDefaultCollectionDownCasting() throws { 28 | let value: [Decodable] = try [5, 6, 7].castAs( 29 | type: [Decodable].self, 30 | codingPath: [] 31 | ) 32 | XCTAssertEqual(value as! Array, [5, 6, 7]) 33 | let set: Set = try ([5, 6, 7] as Set).castAs( 34 | type: Set.self, 35 | codingPath: [] 36 | ) 37 | XCTAssertEqual(set, [5, 6, 7] as Set) 38 | } 39 | 40 | func testDefaultCollectionCastingForSingleValue() throws { 41 | let value: [Decodable] = try 5.castAs( 42 | type: [Decodable].self, 43 | codingPath: [] 44 | ) 45 | XCTAssertEqual(value as! Array, [5]) 46 | let set: Set = try 5.castAs( 47 | type: Set.self, 48 | codingPath: [] 49 | ) 50 | XCTAssertEqual(set, [5] as Set) 51 | } 52 | 53 | func testDefaultCollectionCastingForSingleValueFailure() throws { 54 | XCTAssertThrowsError(try 5.castAs(type: [String].self, codingPath: [])) 55 | } 56 | 57 | func testDefaultCollectionDownCastingFailure() throws { 58 | XCTAssertThrowsError( 59 | try [5, 6, 7].castAs(type: [String].self, codingPath: []) 60 | ) 61 | } 62 | 63 | func testCastingToExistential() throws { 64 | let textPost = TextPost( 65 | id: UUID(), 66 | author: UUID(), 67 | likes: 78, 68 | createdAt: "2021-07-23T07:36:43Z", 69 | text: "Lorem Ipsium" 70 | ) 71 | let post = try textPost.castAs(type: Post.self, codingPath: []) 72 | XCTAssertEqual(post.type, .text) 73 | } 74 | 75 | func testCastingToBoxType() throws { 76 | let textPost = TextPost( 77 | id: UUID(), 78 | author: UUID(), 79 | likes: 78, 80 | createdAt: "2021-07-23T07:36:43Z", 81 | text: "Lorem Ipsium" 82 | ) 83 | let post = try textPost.castAs(type: AnyPost.self, codingPath: []) 84 | XCTAssertEqual(post.type, .text) 85 | } 86 | 87 | func testOptionalCastingToExistential() throws { 88 | let textPost = TextPost( 89 | id: UUID(), 90 | author: UUID(), 91 | likes: 78, 92 | createdAt: "2021-07-23T07:36:43Z", 93 | text: "Lorem Ipsium" 94 | ) 95 | let post = textPost.castAs(type: Post?.self, codingPath: []) 96 | XCTAssertEqual(post?.type, .text) 97 | } 98 | 99 | func testOptionalCastingToBoxType() throws { 100 | let textPost = TextPost( 101 | id: UUID(), 102 | author: UUID(), 103 | likes: 78, 104 | createdAt: "2021-07-23T07:36:43Z", 105 | text: "Lorem Ipsium" 106 | ) 107 | let post = textPost.castAs(type: AnyPost?.self, codingPath: []) 108 | XCTAssertEqual(post?.type, .text) 109 | } 110 | 111 | func testArrayCastingToExistentialArray() throws { 112 | let textPosts = Array( 113 | repeating: TextPost( 114 | id: UUID(), 115 | author: UUID(), 116 | likes: 78, 117 | createdAt: "2021-07-23T07:36:43Z", 118 | text: "Lorem Ipsium" 119 | ), 120 | count: 10 121 | ) 122 | let posts = try textPosts.castAs(type: [Post].self, codingPath: []) 123 | posts.forEach { XCTAssertEqual($0.type, .text) } 124 | } 125 | 126 | func testArrayCastingToBoxTypeArray() throws { 127 | let textPosts = Array( 128 | repeating: TextPost( 129 | id: UUID(), 130 | author: UUID(), 131 | likes: 78, 132 | createdAt: "2021-07-23T07:36:43Z", 133 | text: "Lorem Ipsium" 134 | ), 135 | count: 10 136 | ) 137 | let posts = try textPosts.castAs( 138 | type: [AnyPost].self, 139 | codingPath: [] 140 | ) 141 | posts.forEach { XCTAssertEqual($0.type, .text) } 142 | } 143 | 144 | func testSetCastingToBoxTypeSet() throws { 145 | let textPosts: Set = [ 146 | TextPost( 147 | id: UUID(), 148 | author: UUID(), 149 | likes: 78, 150 | createdAt: "2021-07-23T07:36:43Z", 151 | text: "Lorem Ipsium" 152 | ), 153 | TextPost( 154 | id: UUID(), 155 | author: UUID(), 156 | likes: 88, 157 | createdAt: "2021-06-23T07:36:43Z", 158 | text: "Lorem Ipsium" 159 | ), 160 | TextPost( 161 | id: UUID(), 162 | author: UUID(), 163 | likes: 887, 164 | createdAt: "2021-06-28T07:36:43Z", 165 | text: "Lorem Ipsium" 166 | ), 167 | ] 168 | let posts = try textPosts.castAs( 169 | type: [AnyPost].self, 170 | codingPath: [] 171 | ) 172 | posts.forEach { XCTAssertEqual($0.type, .text) } 173 | let postSet = try textPosts.castAs( 174 | type: Set>.self, 175 | codingPath: [] 176 | ) 177 | postSet.forEach { XCTAssertEqual($0.type, .text) } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContext.swift: -------------------------------------------------------------------------------- 1 | /// Stores context for decoding a dynamic type or dynamic array. 2 | /// 3 | /// After this struct has been initialized, you can decode dynamic 4 | /// type by passing `Decoder` in ``decodeFrom``. 5 | /// 6 | /// ```swift 7 | /// let value: Base = context.decodeFrom(decoder) 8 | /// ``` 9 | @frozen 10 | public struct DynamicDecodingContext { 11 | internal typealias B = Base 12 | /// Decode `Base` type from `Decoder`. 13 | /// 14 | /// ```swift 15 | /// let value: Base = context.decodeFrom(decoder) 16 | /// ``` 17 | /// 18 | /// - Parameter decoder: The `Decoder` to decode from. 19 | /// 20 | /// - Returns: The decoded `Base` type. 21 | /// 22 | /// - Throws: `DecodingError` if data invalid. 23 | public let decodeFrom: (Decoder) throws -> Base 24 | /// Decode `Array` type from `Decoder`. 25 | /// 26 | /// ```swift 27 | /// let value: Array = context.decodeArrayFrom(decoder) 28 | /// ``` 29 | /// 30 | /// - Parameter decoder: The `Decoder` to decode from. 31 | /// 32 | /// - Returns: The decoded `Array` type. 33 | /// 34 | /// - Throws: `DecodingError` if any element data invalid. 35 | public let decodeArrayFrom: (Decoder) throws -> [Base] 36 | /// Decode valid data into `Array` type from `Decoder`, 37 | /// while ignoring invalid data. 38 | /// 39 | /// ```swift 40 | /// let value: Array = context.decodeLossyArrayFrom(decoder) 41 | /// ``` 42 | /// 43 | /// - Parameter decoder: The `Decoder` to decode from. 44 | /// 45 | /// - Returns: The decoded `Array` type. 46 | public let decodeLossyArrayFrom: (Decoder) -> [Base] 47 | } 48 | 49 | // MARK: - Parse by type 50 | public extension DynamicDecodingContext { 51 | /// Creates new context for decoding provided ``DynamicDecodable`` type or array. 52 | /// 53 | /// ```swift 54 | /// let context: DynamicDecodingContext = DynamicDecodingContext(decoding: Int.self) 55 | /// ``` 56 | /// 57 | /// - Parameter type: The actual type to decode. 58 | init(decoding type: Dynamic.Type) { 59 | // MARK: - type decoding 60 | decodeFrom = { decoder in 61 | return try type.init(from: decoder).castAs( 62 | type: B.self, 63 | codingPath: decoder.codingPath 64 | ) 65 | } 66 | // MARK: - array decoding 67 | decodeArrayFrom = { decoder in 68 | return try array(for: type).init(from: decoder).castAs( 69 | type: [B].self, 70 | codingPath: decoder.codingPath 71 | ) 72 | } 73 | // MARK: - lossy array decoding 74 | decodeLossyArrayFrom = { decoder in 75 | guard 76 | var container = try? decoder.unkeyedContainer() 77 | else { return [] } 78 | 79 | var values: [B] = [] 80 | while !container.isAtEnd { 81 | guard 82 | let value = try? container.lossyDecode( 83 | type 84 | )?.castAs( 85 | type: B.self, 86 | codingPath: container.codingPath 87 | ) 88 | else { continue } 89 | values.append(value) 90 | } 91 | return values 92 | } 93 | } 94 | } 95 | 96 | // MARK: - Parse by type with fallback 97 | public extension DynamicDecodingContext { 98 | /// Creates new context for decoding provided ``DynamicDecodable`` type or array. 99 | /// If decoding fails, decodes with provided fallback type. 100 | /// 101 | /// ```swift 102 | /// let context: DynamicDecodingContext = DynamicDecodingContext(decoding: Int.self, fallback: String.self) 103 | /// ``` 104 | /// 105 | /// - Parameters: 106 | /// - type: The primary type to decode. 107 | /// - fallbackType: The fallback type to decode if primary type decoding fails. 108 | init( 109 | decoding type: Original.Type, 110 | fallback fallbackType: Fallback.Type 111 | ) { 112 | // MARK: - type decoding 113 | decodeFrom = { decoder in 114 | do { 115 | return try type.init(from: decoder).castAs( 116 | type: B.self, 117 | codingPath: decoder.codingPath 118 | ) 119 | } catch { 120 | return try fallbackType.init(from: decoder).castAs( 121 | type: B.self, 122 | codingPath: decoder.codingPath 123 | ) 124 | } 125 | } 126 | // MARK: - array decoding 127 | decodeArrayFrom = { decoder in 128 | do { 129 | return try array(for: type).init(from: decoder).castAs( 130 | type: [B].self, 131 | codingPath: decoder.codingPath 132 | ) 133 | } catch { 134 | return try array(for: fallbackType).init(from: decoder).castAs( 135 | type: [B].self, 136 | codingPath: decoder.codingPath 137 | ) 138 | } 139 | } 140 | // MARK: - lossy array decoding 141 | decodeLossyArrayFrom = { decoder in 142 | guard 143 | var container = try? decoder.unkeyedContainer() 144 | else { return [] } 145 | 146 | var values: [B] = [] 147 | while !container.isAtEnd { 148 | do { 149 | let value = try container.decode( 150 | type 151 | ).castAs( 152 | type: B.self, 153 | codingPath: container.codingPath 154 | ) 155 | values.append(value) 156 | } catch { 157 | guard 158 | let value = try? container.lossyDecode( 159 | fallbackType 160 | )?.castAs( 161 | type: B.self, 162 | codingPath: container.codingPath 163 | ) 164 | else { continue } 165 | values.append(value) 166 | } 167 | } 168 | return values 169 | } 170 | } 171 | } 172 | 173 | /// Gets array type of `Array` for generic type `T`, 174 | /// while ignoring invalid data. 175 | /// 176 | /// ```swift 177 | /// let arrType: Array = array(for: Int.self) 178 | /// ``` 179 | /// 180 | /// - Parameter type: The type to get array type for. 181 | /// 182 | /// - Returns: The array type `Array` for type `T`. 183 | internal func array(for type: T.Type) -> Array.Type { Array.self } 184 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextProvider/DynamicDecodingCollectionContextBasedWrapper.swift: -------------------------------------------------------------------------------- 1 | /// A property wrapper type that decodes collection of dynamic value based on dynamic 2 | /// decoding context provided by ``DynamicDecodingContextProvider``. 3 | @frozen 4 | @propertyWrapper 5 | public struct DynamicDecodingCollectionContextBasedWrapper< 6 | Provider: DynamicDecodingContextProvider, 7 | DynamicCollection: SequenceInitializable, 8 | Config: DynamicDecodingCollectionConfigurationProvider 9 | >: PropertyWrapperCodable 10 | where DynamicCollection.Element == Provider.Identified { 11 | /// The underlying dynamic value collection referenced. 12 | public var wrappedValue: DynamicCollection 13 | 14 | /// Creates new instance with a dynamic collection value. 15 | /// 16 | /// - Parameters: 17 | /// - wrappedValue: An initial dynamic collection value. 18 | public init(wrappedValue: DynamicCollection) { 19 | self.wrappedValue = wrappedValue 20 | } 21 | /// Creates a new instance by decoding from the given decoder. 22 | /// 23 | /// - Parameters: 24 | /// - decoder: The decoder to read data from. 25 | /// 26 | /// - Throws: `DecodingError` if ``DynamicDecodingCollectionConfigurationProvider/failConfig`` 27 | /// is ``CollectionDecodeFailConfiguration/throw`` and data is invalid or corrupt. 28 | public init(from decoder: Decoder) throws { 29 | switch Config.failConfig { 30 | case .throw, .`default`: 31 | do { 32 | let context = try Provider.context(from: decoder) 33 | self.wrappedValue = try .init(context.decodeArrayFrom(decoder)) 34 | } catch { 35 | if Config.failConfig == .throw { throw error } 36 | self.wrappedValue = .init() 37 | } 38 | case .lossy: 39 | guard 40 | let context = try? Provider.context(from: decoder) 41 | else { 42 | self.wrappedValue = .init() 43 | return 44 | } 45 | self.wrappedValue = .init(context.decodeLossyArrayFrom(decoder)) 46 | } 47 | } 48 | } 49 | 50 | /// A property wrapper type that strictly decodes array of dynamic value based on dynamic 51 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 52 | /// throws error if decoding fails. 53 | /// 54 | /// `StrictDynamicDecodingArrayContextBasedWrapper` is a type alias for 55 | /// ``DynamicDecodingCollectionContextBasedWrapper``, with collection type 56 | /// `Array` and ``DynamicDecodingCollectionConfigurationProvider`` as 57 | /// ``StrictCollectionConfiguration`` 58 | public typealias StrictDynamicDecodingArrayContextBasedWrapper< 59 | Provider: DynamicDecodingContextProvider 60 | > = DynamicDecodingCollectionContextBasedWrapper< 61 | Provider, 62 | Array, 63 | StrictCollectionConfiguration 64 | > 65 | 66 | /// A property wrapper type that decodes array of dynamic value based on dynamic 67 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 68 | /// assigns default value if decoding fails. 69 | /// 70 | /// `DefaultValueDynamicDecodingArrayContextBasedWrapper` is a type alias for 71 | /// ``DynamicDecodingCollectionContextBasedWrapper``, with collection type 72 | /// `Array` and ``DynamicDecodingCollectionConfigurationProvider`` as 73 | /// ``DefaultValueCollectionConfiguration`` 74 | public typealias DefaultValueDynamicDecodingArrayContextBasedWrapper< 75 | Provider: DynamicDecodingContextProvider 76 | > = DynamicDecodingCollectionContextBasedWrapper< 77 | Provider, 78 | Array, 79 | DefaultValueCollectionConfiguration 80 | > 81 | 82 | /// A property wrapper type that decodes valid data into array of dynamic value based on dynamic 83 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 84 | /// ignores invalid data. 85 | /// 86 | /// `LossyDynamicDecodingArrayContextBasedWrapper` is a type alias for 87 | /// ``DynamicDecodingCollectionContextBasedWrapper``, with collection type 88 | /// `Array` and ``DynamicDecodingCollectionConfigurationProvider`` as 89 | /// ``LossyCollectionConfiguration`` 90 | public typealias LossyDynamicDecodingArrayContextBasedWrapper< 91 | Provider: DynamicDecodingContextProvider 92 | > = DynamicDecodingCollectionContextBasedWrapper< 93 | Provider, 94 | Array, 95 | LossyCollectionConfiguration 96 | > 97 | 98 | /// A property wrapper type that strictly decodes collection of dynamic value based on dynamic 99 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 100 | /// throws error if decoding fails. 101 | /// 102 | /// `StrictDynamicDecodingCollectionContextBasedWrapper` is a type alias for 103 | /// ``DynamicDecodingCollectionContextBasedWrapper``, with 104 | /// ``DynamicDecodingCollectionConfigurationProvider`` as 105 | /// ``StrictCollectionConfiguration`` 106 | public typealias StrictDynamicDecodingCollectionContextBasedWrapper< 107 | Provider: DynamicDecodingContextProvider, 108 | DynamicCollection: SequenceInitializable 109 | > = DynamicDecodingCollectionContextBasedWrapper< 110 | Provider, 111 | DynamicCollection, 112 | StrictCollectionConfiguration 113 | > where DynamicCollection.Element == Provider.Identified 114 | 115 | /// A property wrapper type that decodes collection of dynamic value based on dynamic 116 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 117 | /// assigns default value if decoding fails. 118 | /// 119 | /// `DefaultValueDynamicDecodingCollectionContextBasedWrapper` is a type 120 | /// alias for ``DynamicDecodingCollectionContextBasedWrapper``, with 121 | /// ``DynamicDecodingCollectionConfigurationProvider`` as 122 | /// ``DefaultValueCollectionConfiguration`` 123 | public typealias DefaultValueDynamicDecodingCollectionContextBasedWrapper< 124 | Provider: DynamicDecodingContextProvider, 125 | DynamicCollection: SequenceInitializable 126 | > = DynamicDecodingCollectionContextBasedWrapper< 127 | Provider, 128 | DynamicCollection, 129 | DefaultValueCollectionConfiguration 130 | > where DynamicCollection.Element == Provider.Identified 131 | 132 | /// A property wrapper type that decodes valid data into collection of dynamic value based on dynamic 133 | /// decoding context provided by ``DynamicDecodingContextProvider`` and 134 | /// ignores invalid data. 135 | /// 136 | /// `LossyDynamicDecodingCollectionContextBasedWrapper` is a type alias for 137 | /// ``DynamicDecodingCollectionContextBasedWrapper``, with 138 | /// ``DynamicDecodingCollectionConfigurationProvider`` as 139 | /// ``LossyCollectionConfiguration`` 140 | public typealias LossyDynamicDecodingCollectionContextBasedWrapper< 141 | Provider: DynamicDecodingContextProvider, 142 | DynamicCollection: SequenceInitializable 143 | > = DynamicDecodingCollectionContextBasedWrapper< 144 | Provider, 145 | DynamicCollection, 146 | LossyCollectionConfiguration 147 | > where DynamicCollection.Element == Provider.Identified 148 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/DynamicDecodingContextContainerCodingKey.swift: -------------------------------------------------------------------------------- 1 | /// A coding key type that can dynamically decode contained data. 2 | public protocol DynamicDecodingContextContainerCodingKey: CodingKey { 3 | /// The base type or base element type in case of collection, 4 | /// for the data contained by this key. 5 | associatedtype Contained 6 | /// The dynamic decoding context for the data contained by this key. 7 | var containedContext: DynamicDecodingContext { get } 8 | } 9 | 10 | public extension DynamicDecodingContextContainerCodingKey 11 | where 12 | Self: DynamicDecodingContextIdentifierKey, 13 | Self.Contained == Self.Identified 14 | { 15 | /// The associated dynamic decoding context. 16 | var containedContext: DynamicDecodingContext { 17 | return associatedContext 18 | } 19 | } 20 | 21 | public extension KeyedDecodingContainerProtocol 22 | where Key: DynamicDecodingContextContainerCodingKey { 23 | /// Decodes a value of dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 24 | /// type for the given ``DynamicDecodingContextContainerCodingKey`` coding key. 25 | /// 26 | /// - Parameters: 27 | /// - type: The type of value to decode. 28 | /// - key: The coding key. 29 | /// 30 | /// - Returns: A value of the type ``DynamicDecodingContextContainerCodingKey/Contained``. 31 | func decode( 32 | _ type: Key.Contained.Type, 33 | forKey key: Key 34 | ) throws -> Key.Contained { 35 | let decoder = try self.superDecoder(forKey: key) 36 | return try key.containedContext.decodeFrom(decoder) 37 | } 38 | /// Decodes value of dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 39 | /// collection type for the given ``DynamicDecodingContextContainerCodingKey`` coding key. 40 | /// 41 | /// - Parameters: 42 | /// - type: The type of value to decode. 43 | /// - key: The coding key. 44 | /// 45 | /// - Returns: A value of ``DynamicDecodingContextContainerCodingKey/Contained`` 46 | /// collection type. 47 | func decode( 48 | _ type: Value.Type, 49 | forKey key: Key 50 | ) throws -> Value where Value.Element == Key.Contained { 51 | let decoder = try self.superDecoder(forKey: key) 52 | let items = try key.containedContext.decodeArrayFrom(decoder) 53 | return .init(items) 54 | } 55 | /// Decodes value of dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 56 | /// collection type for the given ``DynamicDecodingContextContainerCodingKey`` coding key. 57 | /// Ignores invalid data instead of throwing error. 58 | /// 59 | /// - Parameters: 60 | /// - type: The type of value to decode. 61 | /// - key: The coding key. 62 | /// 63 | /// - Returns: A value of ``DynamicDecodingContextContainerCodingKey/Contained`` 64 | /// collection type. 65 | func lossyDecode( 66 | _ type: Value.Type, 67 | forKey key: Key 68 | ) -> Value where Value.Element == Key.Contained { 69 | guard 70 | let decoder = try? self.superDecoder(forKey: key) 71 | else { return .init() } 72 | let items = key.containedContext.decodeLossyArrayFrom(decoder) 73 | return .init(items) 74 | } 75 | } 76 | 77 | public extension KeyedDecodingContainerProtocol 78 | where 79 | Key: DynamicDecodingContextContainerCodingKey, 80 | Key: Hashable 81 | { 82 | /// Decodes a dictionary of ``DynamicDecodingContextContainerCodingKey`` key 83 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value 84 | /// from the container. 85 | /// 86 | /// - Returns: A dictionary of keyed by ``DynamicDecodingContextContainerCodingKey`` 87 | /// and ``DynamicDecodingContextContainerCodingKey/Contained`` value. 88 | /// 89 | /// - Throws: `DecodingError` if invalid or corrupt data. 90 | func decode() throws -> [Key: Key.Contained] { 91 | return try self.allKeys.reduce(into: [:]) { values, key in 92 | values[key] = try self.decode(Key.Contained.self, forKey: key) 93 | } 94 | } 95 | /// Decodes a dictionary of ``DynamicDecodingContextContainerCodingKey`` key 96 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value 97 | /// from the container. Ignores invalid data instead of throwing error. 98 | /// 99 | /// - Returns: A dictionary of keyed by ``DynamicDecodingContextContainerCodingKey`` 100 | /// and ``DynamicDecodingContextContainerCodingKey/Contained`` value. 101 | func lossyDecode() -> [Key: Key.Contained] { 102 | return self.allKeys.reduce(into: [:]) { values, key in 103 | guard let value = try? self.decode(Key.Contained.self, forKey: key) 104 | else { return } 105 | values[key] = value 106 | } 107 | } 108 | /// Decodes a dictionary of ``DynamicDecodingContextContainerCodingKey`` key 109 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 110 | /// collection value from the container. 111 | /// 112 | /// - Returns: A dictionary of keyed by ``DynamicDecodingContextContainerCodingKey`` 113 | /// and ``DynamicDecodingContextContainerCodingKey/Contained`` 114 | /// collection value. 115 | /// 116 | /// - Throws: `DecodingError` if invalid or corrupt data. 117 | func decode() throws -> [Key: Value] 118 | where Value.Element == Key.Contained { 119 | return try self.allKeys.reduce(into: [:]) { values, key in 120 | values[key] = try self.decode(Value.self, forKey: key) 121 | } 122 | } 123 | /// Decodes a dictionary of ``DynamicDecodingContextContainerCodingKey`` key 124 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 125 | /// collection value from the container. Ignores keys with invalid data instead of throwing error. 126 | /// 127 | /// - Returns: A dictionary of keyed by ``DynamicDecodingContextContainerCodingKey`` 128 | /// and ``DynamicDecodingContextContainerCodingKey/Contained`` 129 | /// collection value. 130 | func decodeValidContainers() -> [Key: Value] 131 | where Value.Element == Key.Contained { 132 | return self.allKeys.reduce(into: [:]) { values, key in 133 | guard 134 | let items = try? self.decode(Value.self, forKey: key), 135 | !items.isEmpty 136 | else { return } 137 | values[key] = .init(items) 138 | } 139 | } 140 | /// Decodes a dictionary of ``DynamicDecodingContextContainerCodingKey`` key 141 | /// and dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` 142 | /// collection value from the container. Ignores invalid data instead of throwing error. 143 | /// 144 | /// - Returns: A dictionary of keyed by ``DynamicDecodingContextContainerCodingKey`` 145 | /// and ``DynamicDecodingContextContainerCodingKey/Contained`` 146 | /// collection value. 147 | func lossyDecode() -> [Key: Value] 148 | where Value.Element == Key.Contained { 149 | return self.allKeys.reduce(into: [:]) { values, key in 150 | guard 151 | case let items = self.lossyDecode(Value.self, forKey: key), 152 | !items.isEmpty 153 | else { return } 154 | values[key] = .init(items) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicDecodable.swift: -------------------------------------------------------------------------------- 1 | /// A type that can be dynamically casted to multiple type, 2 | /// allowing dynamic decoding. 3 | public protocol DynamicDecodable: Decodable { 4 | /// Cast self as generic type `T`. 5 | /// 6 | /// - Parameters: 7 | /// - type: The type to cast as. 8 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 9 | /// 10 | /// - Returns: Self as type `T`. 11 | /// 12 | /// - Throws: `DecodingError.typeMismatch` if casting fails. 13 | func castAs(type: T.Type, codingPath: [CodingKey]) throws -> T 14 | /// Cast self as optional type `T` or initialize `T` with `nil` value. 15 | /// 16 | /// - Parameters: 17 | /// - type: The optional type to cast as. 18 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 19 | /// 20 | /// - Returns: Self as optional type `T`. 21 | func castAs( 22 | type: T.Type, 23 | codingPath: [CodingKey] 24 | ) -> T 25 | /// Cast self as collection type `T`. 26 | /// 27 | /// - Parameters: 28 | /// - type: The collection type to cast as. 29 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 30 | /// 31 | /// - Returns: Self as collection type `T`. 32 | /// 33 | /// - Throws: `DecodingError.typeMismatch` if casting fails. 34 | func castAs( 35 | type: T.Type, 36 | codingPath: [CodingKey] 37 | ) throws -> T 38 | } 39 | 40 | public extension DynamicDecodable { 41 | /// Cast self as generic type `T`. 42 | /// 43 | /// Tries dynamically casting `self` to `T`, 44 | /// throws error if instance can not be converted to destination type `T`. 45 | /// 46 | /// - Parameters: 47 | /// - type: The type to cast as. 48 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 49 | /// 50 | /// - Returns: Self as type `T`. 51 | /// 52 | /// - Throws: `DecodingError.typeMismatch` if casting fails. 53 | func castAs(type: T.Type, codingPath: [CodingKey]) throws -> T { 54 | switch self { 55 | case let value as T: 56 | return value 57 | default: 58 | throw DecodingError.typeMismatch(T.self, codingPath: codingPath) 59 | } 60 | } 61 | /// Cast self as optional type `T` or initialize `T` with `nil` value. 62 | /// 63 | /// Tries dynamically casting `self` to optional type `T`, 64 | /// initializes type `T` with `nil` if instance can not be converted to destination type `T`. 65 | /// 66 | /// - Parameters: 67 | /// - type: The optional type to cast as. 68 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 69 | /// 70 | /// - Returns: Self as optional type `T`. 71 | func castAs( 72 | type: T.Type, 73 | codingPath: [CodingKey] 74 | ) -> T { 75 | return self as? T ?? nil 76 | } 77 | /// Cast self as collection type `T`. 78 | /// 79 | /// First tries dynamically casting `self` to collection type `T`, 80 | /// falls back to dynamically casting each element 81 | /// to provided collection element type using ``castAs(type:codingPath:)-4hwd``. 82 | /// Throws error if both casting approaches fail. 83 | /// 84 | /// - Parameters: 85 | /// - type: The collection type to cast as. 86 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 87 | /// 88 | /// - Returns: Self as collection type `T`. 89 | /// 90 | /// - Throws: `DecodingError.typeMismatch` if casting fails. 91 | func castAs( 92 | type: T.Type, 93 | codingPath: [CodingKey] 94 | ) throws -> T { 95 | switch self { 96 | case let value as T: 97 | return value 98 | case let value as T.Element: 99 | return .init([value]) 100 | default: 101 | throw DecodingError.typeMismatch(T.self, codingPath: codingPath) 102 | } 103 | } 104 | } 105 | 106 | public extension DynamicDecodable 107 | where 108 | Self: Sequence, 109 | Element: DynamicDecodable 110 | { 111 | /// Cast self as collection type `T`. 112 | /// 113 | /// First tries dynamically casting `self` to collection type `T`, 114 | /// falls back to dynamically casting `self` to collection element type 115 | /// and returns a collection with single element if succeeds. 116 | /// Throws error if both casting approaches fail. 117 | /// 118 | /// - Parameters: 119 | /// - type: The collection type to cast as. 120 | /// - codingPath: The path of coding keys taken to get to this point in decoding. 121 | /// 122 | /// - Returns: Self as collection type `T`. 123 | /// 124 | /// - Throws: `DecodingError.typeMismatch` if casting fails. 125 | func castAs( 126 | type: T.Type, 127 | codingPath: [CodingKey] 128 | ) throws -> T { 129 | switch self { 130 | case let value as T: 131 | return value 132 | default: 133 | let values = try self.map { 134 | try $0.castAs(type: T.Element.self, codingPath: codingPath) 135 | } 136 | return T.init(values) 137 | } 138 | } 139 | } 140 | 141 | extension Optional: DynamicDecodable where Wrapped: DynamicDecodable {} 142 | extension Array: DynamicDecodable where Element: DynamicDecodable {} 143 | extension ClosedRange: DynamicDecodable where Bound: Decodable {} 144 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 145 | extension CollectionDifference: DynamicDecodable where ChangeElement: Codable {} 146 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 147 | extension CollectionDifference.Change: DynamicDecodable 148 | where ChangeElement: Codable {} 149 | extension ContiguousArray: DynamicDecodable where Element: Decodable {} 150 | extension PartialRangeFrom: DynamicDecodable where Bound: Decodable {} 151 | extension PartialRangeThrough: DynamicDecodable where Bound: Decodable {} 152 | extension PartialRangeUpTo: DynamicDecodable where Bound: Decodable {} 153 | extension Range: DynamicDecodable where Bound: Decodable {} 154 | extension Set: DynamicDecodable where Element: DynamicDecodable {} 155 | 156 | #if canImport(TabularData) 157 | import TabularData 158 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 159 | extension Column: DynamicDecodable where WrappedElement: Decodable {} 160 | #endif 161 | 162 | #if canImport(MusicKit) 163 | import MusicKit 164 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 165 | extension MusicCatalogResourceResponse: DynamicDecodable 166 | where MusicItemType: Decodable {} 167 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 168 | extension MusicItemCollection: DynamicDecodable 169 | where MusicItemType: Decodable {} 170 | #endif 171 | 172 | #if canImport(Combine) 173 | import Combine 174 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 175 | extension Record: DynamicDecodable where Output: Codable, Failure: Codable {} 176 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 177 | extension Record.Recording: DynamicDecodable 178 | where Output: Codable, Failure: Codable {} 179 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 180 | extension Subscribers.Completion: DynamicDecodable where Failure: Decodable {} 181 | #endif 182 | 183 | /// A type that can be dynamically casted to multiple type and can be dynamically converted to external representation, 184 | /// allowing dynamic decoding and encoding 185 | /// 186 | /// `DynamicCodable` is a type alias for the ``DynamicDecodable``, ``DynamicEncodable`` 187 | /// and `Encodable` protocols. 188 | public typealias DynamicCodable = DynamicDecodable & DynamicEncodable & Codable 189 | -------------------------------------------------------------------------------- /Sources/DynamicCodableKit/DynamicEncodable.swift: -------------------------------------------------------------------------------- 1 | /// A type that can be dynamically converted to external representation, 2 | /// allowing dynamic encoding. 3 | public protocol DynamicEncodable { 4 | /// Encodes dynamic value into the given encoder. 5 | /// 6 | /// - Parameters: 7 | /// - encoder: The encoder to write data to. 8 | func dynamicEncode(to encoder: Encoder) throws 9 | } 10 | 11 | public extension DynamicEncodable where Self: Encodable { 12 | /// Encodes this value into the given encoder. 13 | /// 14 | /// - Parameters: 15 | /// - encoder: The encoder to write data to. 16 | func dynamicEncode(to encoder: Encoder) throws { 17 | try self.encode(to: encoder) 18 | } 19 | } 20 | 21 | public extension DynamicEncodable where Self: Sequence { 22 | /// Encodes the dynamic elements of this sequence 23 | /// into the given encoder in an unkeyed container. 24 | /// 25 | /// - Parameters: 26 | /// - encoder: The encoder to write data to. 27 | func dynamicEncode(to encoder: Encoder) throws { 28 | switch self { 29 | case let value as Encodable: 30 | try value.encode(to: encoder) 31 | default: 32 | var container = encoder.unkeyedContainer() 33 | try self.forEach { element in 34 | switch element { 35 | case let value as DynamicEncodable: 36 | try value.dynamicEncode(to: container.superEncoder()) 37 | case let value as Encodable: 38 | try value.encode(to: container.superEncoder()) 39 | default: 40 | break 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | extension Optional: DynamicEncodable { 48 | /// Encodes wrapped dynamic value or the wrapped value into the given encoder. 49 | /// 50 | /// - Parameters: 51 | /// - encoder: The encoder to write data to. 52 | public func dynamicEncode(to encoder: Encoder) throws { 53 | switch self { 54 | case .some(let value as DynamicEncodable): 55 | try value.dynamicEncode(to: encoder) 56 | case .some(let value as Encodable): 57 | try value.encode(to: encoder) 58 | default: 59 | break 60 | } 61 | } 62 | } 63 | 64 | /// A type that can be dynamically converted to external representation, 65 | /// based on key value pairs. 66 | protocol KeyedDynamicEncodable { 67 | /// Encodes the dynamic contents of this dictionary 68 | /// into the given encoder in a keyed container of dictionary key type. 69 | /// 70 | /// - Parameters: 71 | /// - encoder: The encoder to write data to. 72 | func keyedEncode(to encoder: Encoder) throws 73 | } 74 | 75 | extension Dictionary: KeyedDynamicEncodable where Key: CodingKey { 76 | /// Encodes the dynamic contents of this dictionary 77 | /// into the given encoder in a keyed container of dictionary key type. 78 | /// 79 | /// - Parameters: 80 | /// - encoder: The encoder to write data to. 81 | func keyedEncode(to encoder: Encoder) throws { 82 | var container = encoder.container(keyedBy: Key.self) 83 | try self.forEach { key, value in 84 | switch value { 85 | case let value as DynamicEncodable: 86 | try value.dynamicEncode(to: container.superEncoder(forKey: key)) 87 | case let value as Encodable: 88 | try value.encode(to: container.superEncoder(forKey: key)) 89 | default: 90 | break 91 | } 92 | } 93 | } 94 | } 95 | 96 | extension Dictionary: DynamicEncodable { 97 | /// Encodes the dynamic contents of this dictionary into the given encoder. 98 | /// 99 | /// - Parameters: 100 | /// - encoder: The encoder to write data to. 101 | public func dynamicEncode(to encoder: Encoder) throws { 102 | switch self { 103 | case let value as KeyedDynamicEncodable: 104 | try value.keyedEncode(to: encoder) 105 | case let value as Encodable: 106 | try value.encode(to: encoder) 107 | default: 108 | break 109 | } 110 | } 111 | } 112 | 113 | /// A property wrapper type that can convert itself into and out of an external representation. 114 | /// 115 | /// Default implementation allows converting into external representation 116 | /// by converting wrapped value. 117 | protocol PropertyWrapperCodable: Codable { 118 | /// The type of value that is wrapped. 119 | associatedtype Wrapped 120 | /// The wrapped value. 121 | var wrappedValue: Wrapped { get } 122 | } 123 | 124 | extension PropertyWrapperCodable { 125 | /// Encodes wrapped value into the given encoder. 126 | /// 127 | /// If wrapped value isn't an `Encodable` type or fails to encode anything, 128 | /// encoder will encode an empty keyed container in its place. 129 | /// This function throws an error if any values are invalid for the given encoder’s format. 130 | /// 131 | /// - Parameters: 132 | /// - encoder: The encoder to write data to. 133 | public func encode(to encoder: Encoder) throws { 134 | switch wrappedValue { 135 | case let value as DynamicEncodable: 136 | try value.dynamicEncode(to: encoder) 137 | case let value as Encodable: 138 | try value.encode(to: encoder) 139 | default: 140 | break 141 | } 142 | } 143 | } 144 | 145 | /// A property wrapper type that can convert itself out of an external representation 146 | /// depending on decoding path or info value. 147 | /// 148 | /// The value itself doesn't have external representation 149 | /// but can be associated with an external representation. 150 | protocol PropertyWrapperDecodableEmptyCodable: PropertyWrapperCodable {} 151 | extension PropertyWrapperDecodableEmptyCodable { 152 | /// Encodes an empty keyed container into the given encoder. 153 | /// 154 | /// - Parameters: 155 | /// - encoder: The encoder to write data to. 156 | public func encode(to encoder: Encoder) throws { 157 | // Do nothing 158 | } 159 | } 160 | 161 | extension Array: DynamicEncodable {} 162 | extension ClosedRange: DynamicEncodable where Bound: Encodable {} 163 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 164 | extension CollectionDifference: DynamicEncodable {} 165 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 166 | extension CollectionDifference.Change: DynamicEncodable 167 | where ChangeElement: Codable {} 168 | extension ContiguousArray: DynamicEncodable {} 169 | extension PartialRangeFrom: DynamicEncodable where Bound: Encodable {} 170 | extension PartialRangeThrough: DynamicEncodable where Bound: Encodable {} 171 | extension PartialRangeUpTo: DynamicEncodable where Bound: Encodable {} 172 | extension Range: DynamicEncodable where Bound: Encodable {} 173 | extension Set: DynamicEncodable {} 174 | 175 | #if canImport(TabularData) 176 | import TabularData 177 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 178 | extension Column: DynamicEncodable {} 179 | #endif 180 | 181 | #if canImport(MusicKit) 182 | import MusicKit 183 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 184 | extension MusicCatalogResourceResponse: DynamicEncodable 185 | where MusicItemType: Encodable {} 186 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8, *) 187 | extension MusicItemCollection: DynamicEncodable {} 188 | #endif 189 | 190 | #if canImport(Combine) 191 | import Combine 192 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 193 | extension Record: DynamicEncodable where Output: Codable, Failure: Codable {} 194 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 195 | extension Record.Recording: DynamicEncodable 196 | where Output: Codable, Failure: Codable {} 197 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 198 | extension Subscribers.Completion: DynamicEncodable where Failure: Encodable {} 199 | #endif 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamicCodableKit 2 | 3 | [![API Docs](http://img.shields.io/badge/Read_the-docs-2196f3.svg)](https://swiftylab.github.io/DynamicCodableKit/documentation/dynamiccodablekit/) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/DynamicCodableKit.svg?label=CocoaPods&color=C90005)](https://badge.fury.io/co/DynamicCodableKit) 5 | [![Swift Package Manager Compatible](https://img.shields.io/github/v/tag/SwiftyLab/DynamicCodableKit?label=SPM&color=orange)](https://badge.fury.io/gh/SwiftyLab%2FDynamicCodableKit) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg)](https://github.com/Carthage/Carthage) 7 | [![Swift](https://img.shields.io/badge/Swift-5-orange)](https://img.shields.io/badge/Swift-5-DE5D43) 8 | [![Platforms](https://img.shields.io/badge/Platforms-all-sucess)](https://img.shields.io/badge/Platforms-all-sucess) 9 | [![CI/CD](https://github.com/SwiftyLab/DynamicCodableKit/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/SwiftyLab/DynamicCodableKit/actions/workflows/main.yml) 10 | [![CodeFactor](https://www.codefactor.io/repository/github/swiftylab/dynamiccodablekit/badge)](https://www.codefactor.io/repository/github/swiftylab/dynamiccodablekit) 11 | [![codecov](https://codecov.io/gh/SwiftyLab/DynamicCodableKit/branch/main/graph/badge.svg?token=QIM4SKWNCS)](https://codecov.io/gh/SwiftyLab/DynamicCodableKit) 12 | 13 | 14 | > [!IMPORTANT] 15 | **DynamicCodableKit** no longer actively developped, migrate to [**MetaCodable**](https://github.com/SwiftyLab/MetaCodable) instead 16 | 17 | **DynamicCodableKit** helps you to implement dynamic JSON decoding within the constraints of Swift's sound type system by working on top of Swift's Codable implementations. 18 | 19 | The data types, protocols, and property wrappers defined by **DynamicCodableKit** can be used to provide dynamic decoding functionality to swift's `Decodable` types. 20 | 21 | ## Features 22 | 23 | - Decode dynamic type based on JSON keys. 24 | - Decode dynamic type based on parent JSON key. 25 | - Dynamically decode types with default value if decoding fails. 26 | - Decode dynamic type array/set with option to ignore invalid elements. 27 | - Decode dynamic data based on user actions. 28 | 29 | ## Requirements 30 | 31 | | Platform | Minimum Swift Version | Installation | Status | 32 | | --- | --- | --- | --- | 33 | | iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ | 5.1 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | 34 | | Linux | 5.1 | [Swift Package Manager](#swift-package-manager) | Fully Tested | 35 | | Windows | 5.3 | [Swift Package Manager](#swift-package-manager) | Fully Tested | 36 | 37 | ## Installation 38 | 39 | ### CocoaPods 40 | 41 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate `DynamicCodableKit` into your Xcode project using CocoaPods, specify it in your `Podfile`: 42 | 43 | ```ruby 44 | pod 'DynamicCodableKit' 45 | ``` 46 | 47 | Optionally, you can also use the pre-built XCFramework from the GitHub releases page by replacing `{version}` with the required version you want to use: 48 | 49 | ```ruby 50 | pod 'DynamicCodableKit', :http => 'https://github.com/SwiftyLab/DynamicCodableKit/releases/download/v{version}/DynamicCodableKit-{version}.xcframework.zip' 51 | ``` 52 | 53 | ### Carthage 54 | 55 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate `DynamicCodableKit` into your Xcode project using Carthage, specify it in your `Cartfile`: 56 | 57 | ```ogdl 58 | github "SwiftyLab/DynamicCodableKit" 59 | ``` 60 | 61 | ### Swift Package Manager 62 | 63 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. 64 | 65 | Once you have your Swift package set up, adding `DynamicCodableKit` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 66 | 67 | ```swift 68 | .package(url: "https://github.com/SwiftyLab/DynamicCodableKit.git", from: "1.0.0"), 69 | ``` 70 | 71 | Optionally, you can also use the pre-built XCFramework from the GitHub releases page by replacing `{version}` and `{checksum}` with the required version and checksum of artifact you want to use: 72 | 73 | ```swift 74 | .binaryTarget(name: "DynamicCodableKit", url: "https://github.com/SwiftyLab/DynamicCodableKit/releases/download/v{version}/DynamicCodableKit-{version}.xcframework.zip", checksum: "{checksum}"), 75 | ``` 76 | 77 | ### Manually 78 | 79 | If you prefer not to use any of the aforementioned dependency managers, you can integrate `DynamicCodableKit` into your project manually. 80 | 81 | #### Git Submodule 82 | 83 | - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: 84 | 85 | ```bash 86 | $ git init 87 | ``` 88 | 89 | - Add `DynamicCodableKit` as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: 90 | 91 | ```bash 92 | $ git submodule add https://github.com/SwiftyLab/DynamicCodableKit.git 93 | ``` 94 | 95 | - Open the new `DynamicCodableKit` folder, and drag the `DynamicCodableKit.xcodeproj` into the Project Navigator of your application's Xcode project or existing workspace. 96 | 97 | > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. 98 | 99 | - Select the `DynamicCodableKit.xcodeproj` in the Project Navigator and verify the deployment target satisfies that of your application target (should be less or equal). 100 | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the `Targets` heading in the sidebar. 101 | - In the tab bar at the top of that window, open the "General" panel. 102 | - Click on the `+` button under the `Frameworks and Libraries` section. 103 | - You will see `DynamicCodableKit.xcodeproj` folder with `DynamicCodableKit.framework` nested inside. 104 | - Select the `DynamicCodableKit.framework` and that's it! 105 | 106 | > The `DynamicCodableKit.framework` is automagically added as a target dependency, linked framework and embedded framework in build phase which is all you need to build on the simulator and a device. 107 | 108 | #### XCFramework 109 | 110 | You can also directly download the pre-built artifact from the GitHub releases page: 111 | 112 | - Download the artifact from the GitHub releases page of the format `DynamicCodableKit-{version}.xcframework.zip` where `{version}` is the version you want to use. 113 | - Extract the XCFramework from the archive, and drag the `DynamicCodableKit.xcframework` into the Project Navigator of your application's target folder in your Xcode project. 114 | - Select `Copy items if needed` and that's it! 115 | 116 | > The `DynamicCodableKit.xcframework` is automagically added in the embedded `Frameworks and Libraries` section, an in turn the linked framework in build phase. 117 | 118 | ## Usage 119 | 120 | See the full [documentation](https://swiftylab.github.io/DynamicCodableKit/documentation/dynamiccodablekit/) for API details and articles on sample scenarios. 121 | 122 | ## Contributing 123 | 124 | If you wish to contribute a change, suggest any improvements, 125 | please review our [contribution guide](CONTRIBUTING.md), 126 | check for open [issues](https://github.com/SwiftyLab/DynamicCodableKit/issues), if it is already being worked upon 127 | or open a [pull request](https://github.com/SwiftyLab/DynamicCodableKit/pulls). 128 | 129 | ## License 130 | 131 | `DynamicCodableKit` is released under the MIT license. [See LICENSE](LICENSE) for details. 132 | --------------------------------------------------------------------------------