├── .ruby-version ├── .swift-version ├── GEMFILE ├── .swiftlint.yml ├── Tests ├── LinuxMain.swift └── AnyCodableTests │ ├── KeyedContainerProtocol+EvaluatingNilTests.swift │ ├── KeyedEncodingContainerProtocol+AnyCodableTests.swift │ └── AnyCodableTests.swift ├── Package.swift ├── Sources └── AnyCodable │ ├── KeyedEncodingContainerProtocol+AnyCodable.swift │ ├── KeyedContainerProtocol+EvaluatingNil.swift │ └── AnyCodable.swift ├── AnyCodable.podspec ├── .travis.yml ├── LICENSE ├── .gitignore ├── README.md └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.2 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0.3 2 | -------------------------------------------------------------------------------- /GEMFILE: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bundler' 4 | gem 'cocoapods', '1.4.0' 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - cyclomatic_complexity 3 | - function_body_length 4 | - syntactic_sugar 5 | - force_cast 6 | - line_length 7 | included: 8 | - Package.swift 9 | - Sources 10 | - Tests 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxMain.swift 3 | // AnyCodable 4 | // 5 | // Created by Valerio Mazzeo on 14/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | #if os(Linux) 10 | 11 | import XCTest 12 | @testable import AnyCodableTests 13 | 14 | XCTMain([ 15 | testCase(AnyCodableTests.allTests), 16 | testCase(KeyedContainerProtocolEvaluatingNilTests.allTests) 17 | ]) 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | // 4 | // Package.swift 5 | // AnyCodable 6 | // 7 | // Created by Valerio Mazzeo on 14/03/2018. 8 | // Copyright © 2018 Asensei Inc. All rights reserved. 9 | // 10 | 11 | import PackageDescription 12 | 13 | let package = Package( 14 | 15 | name: "AnyCodable", 16 | 17 | products: [ 18 | .library(name: "AnyCodable", targets: ["AnyCodable"]) 19 | ], 20 | 21 | targets: [ 22 | .target(name: "AnyCodable"), 23 | .testTarget(name: "AnyCodableTests", dependencies: ["AnyCodable"]) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Sources/AnyCodable/KeyedEncodingContainerProtocol+AnyCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedEncodingContainerProtocol+AnyCodable.swift 3 | // AnyCodable 4 | // 5 | // Created by Valerio Mazzeo on 22/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension KeyedEncodingContainerProtocol { 12 | 13 | public mutating func encodeIfPresent(_ value: AnyCodable?, forKey key: Self.Key) throws { 14 | 15 | guard let someValue = value, someValue.value != nil else { 16 | return 17 | } 18 | 19 | try self.encode(someValue, forKey: key) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AnyCodable.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'AnyCodable' 3 | spec.version = '1.2.0' 4 | spec.swift_version = '4.0.3' 5 | spec.license = { :type => 'MIT' } 6 | spec.homepage = 'https://github.com/asensei/AnyCodable' 7 | spec.authors = { 'Asensei' => 'info@asensei.com' } 8 | spec.summary = 'Generic Any? data encapsulation meant to facilitate the transformation of loosely typed objects using Codable.' 9 | spec.source = { :git => "https://github.com/asensei/AnyCodable.git", :tag => "#{spec.version}" } 10 | spec.ios.deployment_target = "9.0" 11 | spec.osx.deployment_target = "10.11" 12 | spec.watchos.deployment_target = "2.0" 13 | spec.tvos.deployment_target = "9.0" 14 | spec.requires_arc = true 15 | spec.source_files = "Sources/**/*.{swift}" 16 | spec.framework = 'Foundation' 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode9.2 8 | 9 | before_install: 10 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 11 | eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; 12 | elif [ $TRAVIS_OS_NAME == "osx" ]; then 13 | rvm use 2.4.2 --install --binary --fuzzy; 14 | ruby --version; 15 | gem install bundler; 16 | bundle --version; 17 | brew update; 18 | brew outdated swiftlint || brew upgrade swiftlint; 19 | fi 20 | 21 | install: 22 | - if [ $TRAVIS_OS_NAME == "osx" ]; then 23 | bundle install --jobs=3 --retry=3 --deployment; 24 | fi 25 | 26 | script: 27 | - swift --version 28 | - swift build 29 | - swift build -c release 30 | - swift test 31 | - if [ $TRAVIS_OS_NAME == "osx" ]; then 32 | swiftlint; 33 | bundle exec pod lib lint; 34 | fi 35 | 36 | notifications: 37 | email: 38 | on_success: never 39 | on_failure: change 40 | -------------------------------------------------------------------------------- /Sources/AnyCodable/KeyedContainerProtocol+EvaluatingNil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedDecodingContainerProtocol+EvaluatingNil.swift 3 | // AnyCodable 4 | // 5 | // Created by Valerio Mazzeo on 21/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension KeyedDecodingContainerProtocol { 12 | 13 | /** 14 | Decoding method that makes a distinction between keys which are not present, and keys which are explicitily set to `null`. 15 | 16 | - `nil` or `.none`: represents a missing key. 17 | - `"value"` or `.some("value")`: represents a value. 18 | - `.some(nil)`: represents `null`. 19 | */ 20 | public func decodeIfPresentEvaluatingNil(_ type: T.Type, forKey key: Self.Key) throws -> T?? where T: Decodable { 21 | 22 | switch try? self.decodeNil(forKey: key) { 23 | case .some(true): 24 | return .some(nil) 25 | case .some(false): 26 | return try self.decodeIfPresent(T.self, forKey: key) 27 | case .none: 28 | return nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Asensei 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | *.xcodeproj 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository is no longer maintained. It was meant as an experiment to explore if it was possible to make Codable works with undefined data structures. 2 | 3 | We came to the conclusion that there are too many edge cases, and we have since then completely moved away from fuzzy decoding of Any types. 4 | 5 | # AnyCodable 6 | 7 | ![Swift](https://img.shields.io/badge/swift-4.0.3-brightgreen.svg) 8 | [![Build Status](https://travis-ci.org/asensei/AnyCodable.svg?branch=master)](https://travis-ci.org/asensei/AnyCodable) 9 | [![CocoaPods](https://img.shields.io/cocoapods/v/AnyCodable.svg)](https://cocoapods.org/) 10 | 11 | ## Overview 12 | 13 | Generic `Any?` data encapsulation meant to facilitate the transformation of loosely typed objects using Codable. 14 | 15 | ## License 16 | 17 | Copyright © 2018 [Asensei](https://www.asensei.com). All rights reserved. 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /Tests/AnyCodableTests/KeyedContainerProtocol+EvaluatingNilTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedContainerProtocol+EvaluatingNilTests.swift 3 | // AnyCodableTests 4 | // 5 | // Created by Valerio Mazzeo on 21/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import AnyCodable 11 | 12 | class KeyedContainerProtocolEvaluatingNilTests: XCTestCase { 13 | 14 | static let allTests = [ 15 | ("testSome", testSome), 16 | ("testMissingKey", testMissingKey), 17 | ("testNilKey", testNilKey) 18 | ] 19 | 20 | func testSome() throws { 21 | 22 | let data = try JSONEncoder().encode(MockCodable("test")) 23 | 24 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 25 | 26 | XCTAssertTrue(decoded.attribute != nil) 27 | XCTAssertEqual(decoded.attribute!, "test") 28 | XCTAssertEqual(decoded.attribute!!, "test") 29 | } 30 | 31 | func testMissingKey() throws { 32 | 33 | let data = try JSONEncoder().encode(MockCodable(nil)) 34 | 35 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 36 | 37 | XCTAssertTrue(decoded.attribute == nil) 38 | } 39 | 40 | func testNilKey() throws { 41 | 42 | let data = try JSONEncoder().encode(MockCodable(.some(nil))) 43 | 44 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 45 | 46 | XCTAssertNil(decoded.attribute!) 47 | XCTAssertTrue(decoded.attribute != nil) 48 | } 49 | } 50 | 51 | extension KeyedContainerProtocolEvaluatingNilTests { 52 | 53 | struct MockCodable: Codable { 54 | 55 | let attribute: String?? 56 | 57 | init(_ attribute: String??) { 58 | self.attribute = attribute 59 | } 60 | 61 | init(from decoder: Decoder) throws { 62 | 63 | let container = try decoder.container(keyedBy: CodingKeys.self) 64 | 65 | self.attribute = try container.decodeIfPresentEvaluatingNil(String.self, forKey: .attribute) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.6) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.2) 11 | claide (1.0.2) 12 | cocoapods (1.4.0) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.4.0) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.1.3, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.0, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.4) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.1) 30 | xcodeproj (>= 1.5.4, < 2.0) 31 | cocoapods-core (1.4.0) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.2) 36 | cocoapods-downloader (1.1.3) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.0.0) 41 | cocoapods-trunk (1.3.0) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.0.5) 47 | escape (0.0.4) 48 | fourflusher (2.0.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.4) 55 | nanaimo (0.2.3) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.1.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.5.6) 63 | CFPropertyList (~> 2.3.3) 64 | atomos (~> 0.1.2) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.3) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | bundler 74 | cocoapods (= 1.4.0) 75 | 76 | BUNDLED WITH 77 | 1.16.1 78 | -------------------------------------------------------------------------------- /Tests/AnyCodableTests/KeyedEncodingContainerProtocol+AnyCodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedEncodingContainerProtocol+AnyCodableTests.swift 3 | // AnyCodableTests 4 | // 5 | // Created by Dale Buckley on 22/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import AnyCodable 11 | 12 | class KeyedEncodingContainerProtocolAnyCodableTests: XCTestCase { 13 | 14 | static let allTests = [ 15 | ("testEncodingEmptyAnyCodable", testEncodingEmptyAnyCodable), 16 | ("testEncodingNSNullAnyCodable", testEncodingNSNullAnyCodable), 17 | ("testEncodingNonEmptyAnyCodable", testEncodingNonEmptyAnyCodable), 18 | ("testEncodingEmptyAutoSynthesizedAnyCodable", testEncodingEmptyAutoSynthesizedAnyCodable), 19 | ("testEncodingNSNullAutoSynthesizedAnyCodable", testEncodingNSNullAutoSynthesizedAnyCodable), 20 | ("testEncodingNonEmptyAutoSynthesizedAnyCodable", testEncodingNonEmptyAutoSynthesizedAnyCodable) 21 | ] 22 | 23 | func testEncodingEmptyAnyCodable() throws { 24 | 25 | let mock = MockCodable(nil) 26 | let data = try JSONEncoder().encode(mock) 27 | 28 | XCTAssertFalse(String(data: data, encoding: .utf8)!.contains("\"any\":")) 29 | 30 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 31 | 32 | XCTAssertNil(decoded.any) 33 | } 34 | 35 | func testEncodingNSNullAnyCodable() throws { 36 | 37 | let mock = MockCodable(NSNull()) 38 | let data = try JSONEncoder().encode(mock) 39 | 40 | XCTAssertTrue(String(data: data, encoding: .utf8)!.contains("\"any\":null")) 41 | 42 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 43 | 44 | XCTAssertNil(decoded.any) 45 | } 46 | 47 | func testEncodingNonEmptyAnyCodable() throws { 48 | 49 | let mock = MockCodable("test") 50 | let data = try JSONEncoder().encode(mock) 51 | 52 | XCTAssertTrue(String(data: data, encoding: .utf8)!.contains("\"any\":\"test\"")) 53 | 54 | let decoded = try JSONDecoder().decode(MockCodable.self, from: data) 55 | 56 | XCTAssertEqual(decoded.any as! String, "test") 57 | } 58 | 59 | func testEncodingEmptyAutoSynthesizedAnyCodable() throws { 60 | 61 | let mock = SynthesizedMockCodable(anyCodable: nil) 62 | let data = try JSONEncoder().encode(mock) 63 | 64 | XCTAssertFalse(String(data: data, encoding: .utf8)!.contains("\"anyCodable\":")) 65 | 66 | let decoded = try JSONDecoder().decode(SynthesizedMockCodable.self, from: data) 67 | 68 | XCTAssertNil(decoded.anyCodable?.value) 69 | } 70 | 71 | func testEncodingNSNullAutoSynthesizedAnyCodable() throws { 72 | 73 | let mock = SynthesizedMockCodable(anyCodable: AnyCodable(NSNull())) 74 | let data = try JSONEncoder().encode(mock) 75 | 76 | XCTAssertTrue(String(data: data, encoding: .utf8)!.contains("\"anyCodable\":null")) 77 | 78 | let decoded = try JSONDecoder().decode(SynthesizedMockCodable.self, from: data) 79 | 80 | XCTAssertNil(decoded.anyCodable?.value) 81 | } 82 | 83 | func testEncodingNonEmptyAutoSynthesizedAnyCodable() throws { 84 | 85 | let mock = SynthesizedMockCodable(anyCodable: AnyCodable("test")) 86 | let data = try JSONEncoder().encode(mock) 87 | 88 | XCTAssertTrue(String(data: data, encoding: .utf8)!.contains("\"anyCodable\":\"test\"")) 89 | 90 | let decoded = try JSONDecoder().decode(SynthesizedMockCodable.self, from: data) 91 | 92 | XCTAssertEqual(decoded.anyCodable?.value as! String, "test") 93 | } 94 | } 95 | 96 | extension KeyedEncodingContainerProtocolAnyCodableTests { 97 | 98 | struct MockCodable: Codable { 99 | 100 | let any: Any? 101 | 102 | public init(_ any: Any? = nil) { 103 | 104 | self.any = any 105 | } 106 | 107 | // MARK: Codable 108 | 109 | private enum CodingKeys: String, CodingKey { 110 | case any 111 | } 112 | 113 | public init(from decoder: Decoder) throws { 114 | 115 | let container = try decoder.container(keyedBy: CodingKeys.self) 116 | 117 | self.any = try container.decodeIfPresent(AnyCodable.self, forKey: .any)?.value 118 | } 119 | 120 | public func encode(to encoder: Encoder) throws { 121 | 122 | var container = encoder.container(keyedBy: CodingKeys.self) 123 | 124 | try container.encodeIfPresent(AnyCodable(self.any), forKey: .any) 125 | } 126 | } 127 | 128 | struct SynthesizedMockCodable: Codable { 129 | 130 | let anyCodable: AnyCodable? 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/AnyCodableTests/AnyCodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCodableTests.swift 3 | // AnyCodableTests 4 | // 5 | // Created by Valerio Mazzeo on 14/03/2018. 6 | // Copyright © 2018 Asensei. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import AnyCodable 11 | 12 | class AnyCodableTests: XCTestCase { 13 | 14 | static let allTests = [ 15 | ("testEncodable", testEncodable), 16 | ("testDecodable", testDecodable) 17 | ] 18 | 19 | override func setUp() { 20 | super.setUp() 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | } 26 | 27 | func testEncodable() throws { 28 | 29 | let anyCodable = AnyCodable(AnyCodableTests.mockDataStructure) 30 | let data = try JSONEncoder().encode(anyCodable) 31 | 32 | let dataString = String(data: data, encoding: .utf8)! 33 | 34 | XCTAssertNoThrow(try JSONDecoder().decode(AnyCodable.self, from: data)) 35 | XCTAssertTrue(dataString.contains("\"dictionary\":{\"key\":\"value\"}")) 36 | XCTAssertTrue(dataString.contains("\"optionalDictionary\":{\"key\":\"value\",\"nil\":null}") || dataString.contains("\"optionalDictionary\":{\"nil\":null,\"key\":\"value\"}")) 37 | XCTAssertTrue(dataString.contains("\"array\":[\"a\",\"b\",\"c\"]")) 38 | XCTAssertTrue(dataString.contains("\"optionalArray\":[\"a\",\"b\",\"c\",null]")) 39 | XCTAssertTrue(dataString.contains("\"bool\":true")) 40 | XCTAssertTrue(dataString.contains("\"int\":1")) 41 | XCTAssertTrue(dataString.contains("\"int8\":1")) 42 | XCTAssertTrue(dataString.contains("\"int16\":1")) 43 | XCTAssertTrue(dataString.contains("\"int32\":1")) 44 | XCTAssertTrue(dataString.contains("\"int64\":1")) 45 | XCTAssertTrue(dataString.contains("\"uint\":1")) 46 | XCTAssertTrue(dataString.contains("\"uint8\":1")) 47 | XCTAssertTrue(dataString.contains("\"uint16\":1")) 48 | XCTAssertTrue(dataString.contains("\"uint32\":1")) 49 | XCTAssertTrue(dataString.contains("\"uint64\":1")) 50 | XCTAssertTrue(dataString.contains("\"float\":1.1")) 51 | XCTAssertTrue(dataString.contains("\"double\":1.1")) 52 | XCTAssertTrue(dataString.contains("\"double-rounded\":2")) 53 | XCTAssertTrue(dataString.contains("\"string\":\"test\"")) 54 | XCTAssertTrue(dataString.contains("\"nil\":null")) 55 | } 56 | 57 | func testDecodable() throws { 58 | 59 | let data = try JSONSerialization.data(withJSONObject: AnyCodableTests.mockDataStructure) 60 | 61 | let anyCodable = try JSONDecoder().decode(AnyCodable.self, from: data) 62 | let any = anyCodable.value as! [String: Any?] 63 | 64 | XCTAssertEqual(any["dictionary"] as! [String: String], ["key": "value"]) 65 | XCTAssertEqual((any["optionalDictionary"] as! [String: Any?])["key"] as? String, "value") 66 | XCTAssertNil((any["optionalDictionary"] as! [String: Any?])["nil"]!) 67 | XCTAssertEqual(any["array"] as! [String], ["a", "b", "c"]) 68 | XCTAssertEqual((any["optionalArray"] as! [String?])[0], "a") 69 | XCTAssertEqual((any["optionalArray"] as! [String?])[1], "b") 70 | XCTAssertEqual((any["optionalArray"] as! [String?])[2], "c") 71 | XCTAssertNil((any["optionalArray"] as! [String?])[3]) 72 | XCTAssertEqual(any["bool"] as! Bool, true) 73 | XCTAssertEqual(any["int"] as! Int, Int(1)) 74 | XCTAssertEqual(any["int8"] as! Int, Int(1)) 75 | XCTAssertEqual(any["int16"] as! Int, Int(1)) 76 | XCTAssertEqual(any["int32"] as! Int, Int(1)) 77 | XCTAssertEqual(any["int64"] as! Int, Int(1)) 78 | XCTAssertEqual(any["uint"] as! Int, Int(1)) 79 | XCTAssertEqual(any["uint8"] as! Int, Int(1)) 80 | XCTAssertEqual(any["uint16"] as! Int, Int(1)) 81 | XCTAssertEqual(any["uint32"] as! Int, Int(1)) 82 | XCTAssertEqual(any["uint64"] as! Int, Int(1)) 83 | XCTAssertEqual(any["float"] as! Double, Double(1.1), accuracy: 0.001) 84 | XCTAssertEqual(any["double"] as! Double, Double(1.1), accuracy: 0.001) 85 | XCTAssertEqual(any["double-rounded"] as! Int, Int(2)) 86 | XCTAssertEqual(any["string"] as! String, "test") 87 | XCTAssertNil(any["nil"]!) 88 | } 89 | } 90 | 91 | extension AnyCodableTests { 92 | 93 | static let mockDataStructure: [String: Any] = [ 94 | "dictionary": ["key": "value"], 95 | "optionalDictionary": ["key": "value", "nil": nil], 96 | "array": ["a", "b", "c"], 97 | "optionalArray": ["a", "b", "c", nil], 98 | "bool": true, 99 | "int": Int(1), 100 | "int8": Int8(1), 101 | "int16": Int16(1), 102 | "int32": Int32(1), 103 | "int64": Int64(1), 104 | "uint": UInt(1), 105 | "uint8": UInt8(1), 106 | "uint16": UInt16(1), 107 | "uint32": UInt32(1), 108 | "uint64": UInt64(1), 109 | "float": Float(1.1), 110 | "double": Double(1.1), 111 | "double-rounded": Double(2.00), 112 | "string": "test", 113 | "nil": NSNull(), 114 | "decimal": Decimal(1.23), 115 | "decimalNumber": NSDecimalNumber(decimal: 1.23), 116 | "number-bool": NSNumber(value: 1), 117 | "number-int": NSNumber(value: 2) 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /Sources/AnyCodable/AnyCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCodable.swift 3 | // AnyCodable 4 | // 5 | // Created by Valerio Mazzeo on 14/03/2018. 6 | // Copyright © 2018 Asensei Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct AnyCodable { 12 | 13 | // MARK: Initialization 14 | 15 | public init(_ value: Any?) { 16 | self.value = value 17 | } 18 | 19 | // MARK: Accessing Attributes 20 | 21 | public let value: Any? 22 | } 23 | 24 | public extension AnyCodable { 25 | 26 | public func assertValue(_ type: T.Type) throws -> T { 27 | 28 | switch type { 29 | case is NSNull.Type where self.value == nil: 30 | return NSNull() as! T 31 | default: 32 | guard let value = self.value as? T else { 33 | throw Error.typeMismatch(Swift.type(of: self.value)) 34 | } 35 | 36 | return value 37 | } 38 | } 39 | } 40 | 41 | public extension AnyCodable { 42 | 43 | public enum Error: Swift.Error { 44 | case typeMismatch(Any.Type) 45 | } 46 | } 47 | 48 | extension AnyCodable: Codable { 49 | 50 | public init(from decoder: Decoder) throws { 51 | 52 | let container = try decoder.singleValueContainer() 53 | 54 | if let value = try? container.decode(String.self) { 55 | self.value = value 56 | } else if let value = try? container.decode(Bool.self) { 57 | self.value = value 58 | } else if container.decodeNil() { 59 | self.value = nil 60 | } else if let value = try? container.decode([String: AnyCodable].self) { 61 | self.value = value.mapValues { $0.value } 62 | } else if let value = try? container.decode([AnyCodable].self) { 63 | self.value = value.map { $0.value } 64 | } else if let value = try? container.decode(Double.self) { 65 | switch value { 66 | case value.rounded(): 67 | self.value = Int(value) 68 | default: 69 | self.value = value 70 | } 71 | } else { 72 | throw DecodingError.dataCorruptedError( 73 | in: container, 74 | debugDescription: "Invalid value cannot be decoded" 75 | ) 76 | } 77 | } 78 | 79 | public func encode(to encoder: Encoder) throws { 80 | 81 | var container = encoder.singleValueContainer() 82 | 83 | guard let value = self.value else { 84 | try container.encodeNil() 85 | return 86 | } 87 | 88 | switch value { 89 | case let value as String: 90 | try container.encode(value) 91 | case let value as Bool: 92 | try container.encode(value) 93 | case let value as Int: 94 | try container.encode(value) 95 | case let value as Int8: 96 | try container.encode(value) 97 | case let value as Int16: 98 | try container.encode(value) 99 | case let value as Int32: 100 | try container.encode(value) 101 | case let value as Int64: 102 | try container.encode(value) 103 | case let value as UInt: 104 | try container.encode(value) 105 | case let value as UInt8: 106 | try container.encode(value) 107 | case let value as UInt16: 108 | try container.encode(value) 109 | case let value as UInt32: 110 | try container.encode(value) 111 | case let value as UInt64: 112 | try container.encode(value) 113 | case let value as Array: 114 | try container.encode(value.map { AnyCodable($0) }) 115 | case let value as Dictionary: 116 | try container.encode(value.mapValues { AnyCodable($0) }) 117 | case let value as Float: 118 | try container.encode(value) 119 | case let value as Double: 120 | try container.encode(value) 121 | case let value as Decimal: 122 | try container.encode(value) 123 | case let value as NSDecimalNumber: 124 | try container.encode(value.decimalValue) 125 | case is NSNull: 126 | try container.encodeNil() 127 | case let value as NSNumber: 128 | try container.encode(value.doubleValue) 129 | default: 130 | throw EncodingError.invalidValue( 131 | value, 132 | EncodingError.Context( 133 | codingPath: container.codingPath, 134 | debugDescription: "Invalid value cannot be encoded" 135 | ) 136 | ) 137 | } 138 | } 139 | } 140 | 141 | extension AnyCodable: Equatable { 142 | 143 | public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { 144 | 145 | switch (lhs.value, rhs.value) { 146 | case (let lhs as String, let rhs as String): 147 | return lhs == rhs 148 | case (let lhs as Bool, let rhs as Bool): 149 | return lhs == rhs 150 | case (let lhs as Int, let rhs as Int): 151 | return lhs == rhs 152 | case (let lhs as Int8, let rhs as Int8): 153 | return lhs == rhs 154 | case (let lhs as Int16, let rhs as Int16): 155 | return lhs == rhs 156 | case (let lhs as Int32, let rhs as Int32): 157 | return lhs == rhs 158 | case (let lhs as Int64, let rhs as Int64): 159 | return lhs == rhs 160 | case (let lhs as UInt, let rhs as UInt): 161 | return lhs == rhs 162 | case (let lhs as UInt8, let rhs as UInt8): 163 | return lhs == rhs 164 | case (let lhs as UInt16, let rhs as UInt16): 165 | return lhs == rhs 166 | case (let lhs as UInt32, let rhs as UInt32): 167 | return lhs == rhs 168 | case (let lhs as UInt64, let rhs as UInt64): 169 | return lhs == rhs 170 | case (let lhs as Float, let rhs as Float): 171 | return lhs == rhs 172 | case (let lhs as Double, let rhs as Double): 173 | return lhs == rhs 174 | case (let lhs as [String: AnyCodable], let rhs as [String: AnyCodable]): 175 | return lhs == rhs 176 | case (let lhs as [AnyCodable], let rhs as [AnyCodable]): 177 | return lhs == rhs 178 | case (is NSNull, is NSNull): 179 | return true 180 | default: 181 | return false 182 | } 183 | } 184 | } 185 | 186 | extension AnyCodable: CustomStringConvertible { 187 | 188 | public var description: String { 189 | 190 | switch self.value { 191 | case let value as CustomStringConvertible: 192 | return value.description 193 | default: 194 | return String(describing: self.value) 195 | } 196 | } 197 | } 198 | 199 | extension AnyCodable: CustomDebugStringConvertible { 200 | 201 | public var debugDescription: String { 202 | 203 | switch self.value { 204 | case let value as CustomDebugStringConvertible: 205 | return value.debugDescription 206 | default: 207 | return self.description 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------