├── .swift-version ├── logo.png ├── Tests ├── LinuxMain.swift └── WrapTests │ ├── WrapTests+Linux.swift │ └── WrapTests.swift ├── Wrap.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ ├── Wrap-watchOS.xcscheme │ │ ├── Wrap-iOS.xcscheme │ │ ├── Wrap-tvOS.xcscheme │ │ └── Wrap-macOS.xcscheme └── project.pbxproj ├── .travis.yml ├── buddybuild_postbuild.sh ├── Package.swift ├── Configs ├── WrapTests.plist └── Wrap.plist ├── Wrap.podspec ├── LICENSE ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md └── Sources └── Wrap.swift /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/Wrap/HEAD/logo.png -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import WrapTests 3 | 4 | XCTMain([ 5 | testCase(WrapTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: generic 3 | sudo: required 4 | dist: trusty 5 | install: 6 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)" 7 | script: 8 | - swift test 9 | -------------------------------------------------------------------------------- /buddybuild_postbuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run tests using SPM for macOS 4 | swift test 5 | 6 | # Run tests for tvOS 7 | xcodebuild clean test -quiet -project Wrap.xcodeproj -scheme Wrap-tvOS -destination "platform=tvOS Simulator,name=Apple TV 1080p" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 8 | 9 | # Build for watchOS 10 | xcodebuild clean build -project Wrap.xcodeproj -scheme Wrap-watchOS CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | /** 4 | * Wrap 5 | * Copyright (c) John Sundell 2017 6 | * Licensed under the MIT license. See LICENSE file. 7 | */ 8 | 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "Wrap", 13 | products: [ 14 | .library(name: "Wrap", targets: ["Wrap"]) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "Wrap", 19 | path: "Sources" 20 | ), 21 | .testTarget( 22 | name: "WrapTests", 23 | dependencies: ["Wrap"] 24 | ) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Configs/WrapTests.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 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Configs/Wrap.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 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 - 2017 John Sundell. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Wrap.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Wrap" 3 | s.version = "3.0.0" 4 | s.summary = "The easy to use Swift JSON encoder" 5 | s.description = <<-DESC 6 | Wrap is an easy to use Swift JSON encoder. Don't spend hours writing JSON encoding code - just wrap it instead! 7 | 8 | Using Wrap is as easy as calling Wrap() on any instance of a class or struct that you wish to encode. It automatically encodes all of your type’s properties, including nested objects, collections, enums and more! 9 | DESC 10 | s.homepage = "https://github.com/JohnSundell/Wrap" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "John Sundell" => "john@sundell.co" } 13 | s.social_media_url = "https://twitter.com/johnsundell" 14 | s.ios.deployment_target = "8.0" 15 | s.osx.deployment_target = "10.9" 16 | s.watchos.deployment_target = "2.0" 17 | s.tvos.deployment_target = "9.0" 18 | s.source = { :git => "https://github.com/JohnSundell/Wrap.git", :tag => s.version.to_s } 19 | s.source_files = "Sources/Wrap.swift" 20 | s.framework = "Foundation" 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2017 John Sundell 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 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/xcshareddata/xcschemes/Wrap-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 73 | 74 | 75 | 76 | 78 | 79 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Wrap Code of Conduct 2 | 3 | Below is the Code of Conduct that all contributors and participants in the Wrap community are expected to adhere to. 4 | It's adopted from the [Contributor Covenant Code of Conduct][homepage]. 5 | 6 | ## Our Pledge 7 | 8 | In the interest of fostering an open and welcoming environment, we as 9 | contributors and maintainers pledge to making participation in our project and 10 | our community a harassment-free experience for everyone, regardless of age, body 11 | size, disability, ethnicity, gender identity and expression, level of experience, 12 | nationality, personal appearance, race, religion, or sexual identity and 13 | orientation. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | * Using welcoming and inclusive language 21 | * Being respectful of differing viewpoints and experiences 22 | * Gracefully accepting constructive criticism 23 | * Focusing on what is best for the community 24 | * Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | * The use of sexualized language or imagery and unwelcome sexual attention or 29 | advances 30 | * Trolling, insulting/derogatory comments, and personal or political attacks 31 | * Public or private harassment 32 | * Publishing others' private information, such as a physical or electronic 33 | address, without explicit permission 34 | * Other conduct which could reasonably be considered inappropriate in a 35 | professional setting 36 | 37 | ## Our Responsibilities 38 | 39 | Project maintainers are responsible for clarifying the standards of acceptable 40 | behavior and are expected to take appropriate and fair corrective action in 41 | response to any instances of unacceptable behavior. 42 | 43 | Project maintainers have the right and responsibility to remove, edit, or 44 | reject comments, commits, code, wiki edits, issues, and other contributions 45 | that are not aligned to this Code of Conduct, or to ban temporarily or 46 | permanently any contributor for other behaviors that they deem inappropriate, 47 | threatening, offensive, or harmful. 48 | 49 | ## Scope 50 | 51 | This Code of Conduct applies both within project spaces and in public spaces 52 | when an individual is representing the project or its community. Examples of 53 | representing a project or community include using an official project e-mail 54 | address, posting via an official social media account, or acting as an appointed 55 | representative at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | ## Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 61 | reported by contacting the project leader at john@sundell.co. All 62 | complaints will be reviewed and investigated and will result in a response that 63 | is deemed necessary and appropriate to the circumstances. The project team is 64 | obligated to maintain confidentiality with regard to the reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 74 | available at [http://contributor-covenant.org/version/1/4][version] 75 | 76 | [homepage]: http://contributor-covenant.org 77 | [version]: http://contributor-covenant.org/version/1/4/ 78 | -------------------------------------------------------------------------------- /Tests/WrapTests/WrapTests+Linux.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrap 3 | * 4 | * Copyright (c) 2015 - 2017 John Sundell. Licensed under the MIT license, as follows: 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | import Foundation 26 | 27 | #if os(Linux) 28 | extension WrapTests { 29 | static var allTests = [ 30 | ("testBasicStruct", testBasicStruct), 31 | ("testOptionalProperties", testOptionalProperties), 32 | ("testProtocolProperties", testProtocolProperties), 33 | ("testRootEnum", testRootEnum), 34 | ("testEnumProperties", testEnumProperties), 35 | ("testDateProperty", testDateProperty), 36 | ("testDatePropertyWithCustomizableStruct", testDatePropertyWithCustomizableStruct), 37 | ("testEmptyStruct", testEmptyStruct), 38 | ("testNestedEmptyStruct", testNestedEmptyStruct), 39 | ("testArrayProperties", testArrayProperties), 40 | ("testDictionaryProperties", testDictionaryProperties), 41 | ("testHomogeneousSetProperty", testHomogeneousSetProperty), 42 | ("testNSURLProperty", testNSURLProperty), 43 | ("testURLProperty", testURLProperty), 44 | ("test64BitIntegerProperties", test64BitIntegerProperties), 45 | ("testRootSubclass", testRootSubclass), 46 | ("testRootNSObjectSubclass", testRootNSObjectSubclass), 47 | ("testRootDictionary", testRootDictionary), 48 | ("testNestedStruct", testNestedStruct), 49 | ("testNestedArrayOfStructs", testNestedArrayOfStructs), 50 | ("testNestedDictionariesOfStructs", testNestedDictionariesOfStructs), 51 | ("testNestedSubclass", testNestedSubclass), 52 | ("testDeepNesting", testDeepNesting), 53 | ("testWrappableKey", testWrappableKey), 54 | ("testKeyCustomization", testKeyCustomization), 55 | ("testCustomWrapping", testCustomWrapping), 56 | ("testCustomWrappingCallingWrapFunction", testCustomWrappingCallingWrapFunction), 57 | ("testCustomWrappingForSingleProperty", testCustomWrappingForSingleProperty), 58 | ("testCustomWrappingFailureThrows", testCustomWrappingFailureThrows), 59 | ("testCustomWrappingForSinglePropertyFailureThrows", testCustomWrappingForSinglePropertyFailureThrows), 60 | ("testInvalidRootObjectThrows", testInvalidRootObjectThrows), 61 | ("testDataWrapping", testDataWrapping), 62 | ("testWrappingArray", testWrappingArray), 63 | ("testSnakeCasedKeyWrapping", testSnakeCasedKeyWrapping), 64 | ("testContext", testContext), 65 | ("testInheritance", testInheritance), 66 | ("testIgnoringClosureProperties", testIgnoringClosureProperties) 67 | ] 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/xcshareddata/xcschemes/Wrap-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/xcshareddata/xcschemes/Wrap-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/xcshareddata/xcschemes/Wrap-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ **DEPRECATED** 2 | 3 | Wrap is deprecated in favor of Swift’s built-in `Codable` API and [the Codextended project](https://github.com/JohnSundell/Codextended). All current users are highly encouraged to migrate to `Codable` as soon as possible. 4 | 5 | --- 6 | 7 |

8 | Wrap 9 |

10 | 11 |

12 | Unbox 13 | | 14 | Wrap 15 |

16 | 17 |

18 | 19 | BuddyBuild 20 | 21 | 22 | 23 | CocoaPods 24 | 25 | 26 | Carthage 27 | 28 | 29 | Twitter: @johnsundell 30 | 31 |

32 | 33 | Wrap is an easy to use Swift JSON encoder. Don't spend hours writing JSON encoding code - just wrap it instead! 34 | 35 | Using Wrap is as easy as calling `wrap()` on any instance of a `class` or `struct` that you wish to encode. It automatically encodes all of your type’s properties, including nested objects, collections, enums and more! 36 | 37 | It also provides a suite of simple but powerful customization APIs that enables you to use it on any model setup with ease. 38 | 39 | ## Basic example 40 | 41 | Say you have your usual-suspect `User` model: 42 | 43 | ```swift 44 | struct User { 45 | let name: String 46 | let age: Int 47 | } 48 | 49 | let user = User(name: "John", age: 28) 50 | ``` 51 | 52 | Using `wrap()` you can now encode a `User` instance with one command: 53 | 54 | ```swift 55 | let dictionary: [String : Any] = try wrap(user) 56 | ``` 57 | 58 | Which will produce the following `Dictionary`: 59 | 60 | ```json 61 | { 62 | "name": "John", 63 | "age": 28 64 | } 65 | ``` 66 | 67 | ## Advanced example 68 | 69 | The first was a pretty simple example, but Wrap can encode even the most complicated structures for you, with both optional, non-optional and custom type values, all without any extra code on your part. Let’s say we have the following model setup: 70 | 71 | ```swift 72 | struct SpaceShip { 73 | let type: SpaceShipType 74 | let weight: Double 75 | let engine: Engine 76 | let passengers: [Astronaut] 77 | let launchLiveStreamURL: URL? 78 | let lastPilot: Astronaut? 79 | } 80 | 81 | enum SpaceShipType: Int, WrappableEnum { 82 | case apollo 83 | case sputnik 84 | } 85 | 86 | struct Engine { 87 | let manufacturer: String 88 | let fuelConsumption: Float 89 | } 90 | 91 | struct Astronaut { 92 | let name: String 93 | } 94 | ``` 95 | 96 | Let’s create an instance of `SpaceShip`: 97 | 98 | ```swift 99 | let ship = SpaceShip( 100 | type: .apollo, 101 | weight: 3999.72, 102 | engine: Engine( 103 | manufacturer: "The Space Company", 104 | fuelConsumption: 17.2321 105 | ), 106 | passengers: [ 107 | Astronaut(name: "Mike"), 108 | Astronaut(name: "Amanda") 109 | ], 110 | launchLiveStreamURL: URL(string: "http://livestream.com"), 111 | lastPilot: nil 112 | ) 113 | ``` 114 | 115 | And now let’s encode it with one call to `wrap()`: 116 | 117 | ```swift 118 | let dictionary: WrappedDictionary = try wrap(ship) 119 | ``` 120 | 121 | Which will produce the following dictionary: 122 | 123 | ```json 124 | { 125 | "type": 0, 126 | "weight": 3999.72, 127 | "engine": { 128 | "manufacturer": "The Space Company", 129 | "fuelConsumption": 17.2321 130 | }, 131 | "passengers": [ 132 | {"name": "Mike"}, 133 | {"name": "Amanda"} 134 | ], 135 | "launchLiveStreamURL": "http://livestream.com" 136 | } 137 | ``` 138 | 139 | As you can see, Wrap automatically encoded the `URL` property to its `absoluteString`, and ignored any properties that were `nil` (reducing the size of the produced JSON). 140 | 141 | ## Customization 142 | 143 | While automation is awesome, customization is just as important. Thankfully, Wrap provides several override points that enables you to easily tweak its default behavior. 144 | 145 | ### Customizing keys 146 | 147 | Per default Wrap uses the property names of a type as its encoding keys, but sometimes this is not what you’re looking for. You can choose to override any or all of a type’s encoding keys by making it conform to `WrapCustomizable` and implementing `keyForWrapping(propertyNamed:)`, like this: 148 | 149 | ```swift 150 | struct Book: WrapCustomizable { 151 | let title: String 152 | let authorName: String 153 | 154 | func keyForWrapping(propertyNamed propertyName: String) -> String? { 155 | if propertyName == "authorName" { 156 | return "author_name" 157 | } 158 | 159 | return propertyName 160 | } 161 | } 162 | ``` 163 | 164 | You can also use the `keyForWrapping(propertyNamed:)` API to skip a property entirely, by returning nil from this method for it. 165 | 166 | ### Custom key types 167 | 168 | You might have nested dictionaries that are not keyed on `Strings`, and for those Wrap provides the `WrappableKey` protocol. This enables you to easily convert any type into a string that can be used as a JSON key. 169 | 170 | ### Encoding keys as snake_case 171 | 172 | If you want the dictionary that Wrap produces to have snake_cased keys rather than the default (which is matching the names of the properties that were encoded), you can easily do so by conforming to `WrapCustomizable` and returning `.convertToSnakeCase` from the `wrapKeyStyle` property. Doing that will, for example, convert the property name `myProperty` into the key `my_property`. 173 | 174 | ### Customized wrapping 175 | 176 | For some nested types, you might want to handle the wrapping yourself. This may be especially true for any custom collections, or types that have a completely different representation when encoded. To do that, make a type conform to `WrapCustomizable` and implement `wrap(context:dateFormatter:)`, like this: 177 | 178 | ```swift 179 | struct Library: WrapCustomizable { 180 | private let booksByID: [String : Book] 181 | 182 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 183 | return Wrapper(context: context, dateFormatter: dateFormatter).wrap(self.booksByID) 184 | } 185 | } 186 | ``` 187 | 188 | ## Enum support 189 | 190 | Wrap also makes it super easy to encode any `enum` values that your types are using. If an `enum` is based on a raw type (such as `String` or `Int`), all you have to do is to declare conformance to `WrappableEnum`, and the rest is taken care of for you. 191 | 192 | Non-raw type `enum` values are also automatically encoded. The default behavior encodes any values that don’t have associated values as their string representation, and those that do have associated values as a dictionary (with the string representation as the key), like this: 193 | 194 | ```swift 195 | enum Profession { 196 | case developer(favoriteLanguageName: String) 197 | case lawyer 198 | } 199 | 200 | struct Person { 201 | let profession = Profession.developer(favoriteLanguageName: "Swift") 202 | let hobbyProfession = Profession.lawyer 203 | } 204 | ``` 205 | 206 | Encodes into: 207 | 208 | ```json 209 | { 210 | "profession": { 211 | "developer": "Swift" 212 | }, 213 | "hobbyProfession": "lawyer" 214 | } 215 | ``` 216 | 217 | ## Contextual objects 218 | 219 | To be able to easily encode any dependencies that you might want to use during the encoding process, Wrap provides the ability to supply a contextual object when initiating the wrapping process (by calling `wrap(object, context: myContext`). 220 | 221 | A context can be of `Any` type and is accessible in all `WrapCustomizable` wrapping methods. Here is an example, where we send in a prefix to add to a `Book`’s `title`: 222 | 223 | ```swift 224 | struct Book: WrapCustomizable { 225 | let title: String 226 | 227 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 228 | guard let prefix = context as? String else { 229 | return nil 230 | } 231 | 232 | return [ 233 | "title" : prefix + self.title 234 | ] 235 | } 236 | } 237 | ``` 238 | 239 | ## String serialization 240 | 241 | ```swift 242 | let data = try wrap(object) as Data 243 | let string = String(data: data, encoding: .utf8) 244 | ``` 245 | 246 | ## Compatibility 247 | 248 | Wrap supports the following platforms: 249 | 250 | - 📱 iOS 8+ 251 | - 🖥 macOS 10.9+ 252 | - ⌚️ watchOS 2+ 253 | - 📺 tvOS 9+ 254 | - 🐧 Linux 255 | 256 | ## Usage 257 | 258 | Wrap can be easily used in either a Swift script, command line tool or in an app for iOS, macOS, watchOS, tvOS or Linux. 259 | 260 | ### In an application 261 | 262 | Either 263 | 264 | - Drag the file `Wrap.swift` into your application's Xcode project. 265 | 266 | or 267 | 268 | - Use [CocoaPods](cocoapods.org), [Carthage](https://github.com/Carthage/Carthage) or the [Swift Package Manager](https://github.com/apple/swift-package-manager) to include Wrap as a dependency in your project. 269 | 270 | ### In a script 271 | 272 | - Install [Marathon](https://github.com/johnsundell/marathon). 273 | - Add Wrap using `$ marathon add https://github.com/JohnSundell/Wrap.git`. 274 | - Run your script using `$ marathon run `. 275 | 276 | ### In a command line tool 277 | 278 | - Drag the file `Wrap.swift` into your command line tool's Xcode project. 279 | 280 | ### Hope you enjoy wrapping your objects! 281 | 282 | For more updates on Wrap, and my other open source projects, follow me on Twitter: [@johnsundell](http://www.twitter.com/johnsundell) 283 | 284 | Also make sure to check out [Unbox](http://github.com/johnsundell/unbox) that let’s you easily **decode** JSON. 285 | -------------------------------------------------------------------------------- /Sources/Wrap.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrap - the easy to use Swift JSON encoder 3 | * 4 | * For usage, see documentation of the classes/symbols listed in this file, as well 5 | * as the guide available at: github.com/johnsundell/wrap 6 | * 7 | * Copyright (c) 2015 - 2017 John Sundell. Licensed under the MIT license, as follows: 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | import Foundation 29 | 30 | /// Type alias defining what type of Dictionary that Wrap produces 31 | public typealias WrappedDictionary = [String : Any] 32 | 33 | /** 34 | * Wrap any object or value, encoding it into a JSON compatible Dictionary 35 | * 36 | * - Parameter object: The object to encode 37 | * - Parameter context: An optional contextual object that will be available throughout 38 | * the wrapping process. Can be used to inject extra information or objects needed to 39 | * perform the wrapping. 40 | * - Parameter dateFormatter: Optionally pass in a date formatter to use to encode any 41 | * `NSDate` values found while encoding the object. If this is `nil`, any found date 42 | * values will be encoded using the "yyyy-MM-dd HH:mm:ss" format. 43 | * 44 | * All the type's stored properties (both public & private) will be recursively 45 | * encoded with their property names as the key. For example, given the following 46 | * Struct as input: 47 | * 48 | * ``` 49 | * struct User { 50 | * let name = "John" 51 | * let age = 28 52 | * } 53 | * ``` 54 | * 55 | * This function will produce the following output: 56 | * 57 | * ``` 58 | * [ 59 | * "name" : "John", 60 | * "age" : 28 61 | * ] 62 | * ``` 63 | * 64 | * The object passed to this function must be an instance of a Class, or a value 65 | * based on a Struct. Standard library values, such as Ints, Strings, etc are not 66 | * valid input. 67 | * 68 | * Throws a WrapError if the operation could not be completed. 69 | * 70 | * For more customization options, make your type conform to `WrapCustomizable`, 71 | * that lets you override encoding keys and/or the whole wrapping process. 72 | * 73 | * See also `WrappableKey` (for dictionary keys) and `WrappableEnum` for Enum values. 74 | */ 75 | public func wrap(_ object: T, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> WrappedDictionary { 76 | return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, enableCustomizedWrapping: true) 77 | } 78 | 79 | /** 80 | * Alternative `wrap()` overload that returns JSON-based `Data` 81 | * 82 | * See the documentation for the dictionary-based `wrap()` function for more information 83 | */ 84 | public func wrap(_ object: T, writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { 85 | return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, writingOptions: writingOptions ?? []) 86 | } 87 | 88 | /** 89 | * Alternative `wrap()` overload that encodes an array of objects into an array of dictionaries 90 | * 91 | * See the documentation for the dictionary-based `wrap()` function for more information 92 | */ 93 | public func wrap(_ objects: [T], context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> [WrappedDictionary] { 94 | return try objects.map { try wrap($0, context: context, dateFormatter: dateFormatter) } 95 | } 96 | 97 | /** 98 | * Alternative `wrap()` overload that encodes an array of objects into JSON-based `Data` 99 | * 100 | * See the documentation for the dictionary-based `wrap()` function for more information 101 | */ 102 | public func wrap(_ objects: [T], writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { 103 | let dictionaries: [WrappedDictionary] = try wrap(objects, context: context, dateFormatter: dateFormatter) 104 | return try JSONSerialization.data(withJSONObject: dictionaries, options: writingOptions ?? []) 105 | } 106 | 107 | // Enum describing various styles of keys in a wrapped dictionary 108 | public enum WrapKeyStyle { 109 | /// The keys in a dictionary produced by Wrap should match their property name (default) 110 | case matchPropertyName 111 | /// The keys in a dictionary produced by Wrap should be converted to snake_case. 112 | /// For example, "myProperty" will be converted to "my_property". All keys will be lowercased. 113 | case convertToSnakeCase 114 | } 115 | 116 | /** 117 | * Protocol providing the main customization point for Wrap 118 | * 119 | * It's optional to implement all of the methods in this protocol, as Wrap 120 | * supplies default implementations of them. 121 | */ 122 | public protocol WrapCustomizable { 123 | /** 124 | * The style that wrap should apply to the keys of a wrapped dictionary 125 | * 126 | * The value of this property is ignored if a type provides a custom 127 | * implementation of the `keyForWrapping(propertyNamed:)` method. 128 | */ 129 | var wrapKeyStyle: WrapKeyStyle { get } 130 | /** 131 | * Override the wrapping process for this type 132 | * 133 | * All top-level types should return a `WrappedDictionary` from this method. 134 | * 135 | * You may use the default wrapping implementation by using a `Wrapper`, but 136 | * never call `wrap()` from an implementation of this method, since that might 137 | * cause an infinite recursion. 138 | * 139 | * The context & dateFormatter passed to this method is any formatter that you 140 | * supplied when initiating the wrapping process by calling `wrap()`. 141 | * 142 | * Returning nil from this method will be treated as an error, and cause 143 | * a `WrapError.wrappingFailedForObject()` error to be thrown. 144 | */ 145 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? 146 | /** 147 | * Override the key that will be used when encoding a certain property 148 | * 149 | * Returning nil from this method will cause Wrap to skip the property 150 | */ 151 | func keyForWrapping(propertyNamed propertyName: String) -> String? 152 | /** 153 | * Override the wrapping of any property of this type 154 | * 155 | * The original value passed to this method will be the original value that the 156 | * type is currently storing for the property. You can choose to either use this, 157 | * or just access the property in question directly. 158 | * 159 | * The dateFormatter passed to this method is any formatter that you supplied 160 | * when initiating the wrapping process by calling `wrap()`. 161 | * 162 | * Returning nil from this method will cause Wrap to use the default 163 | * wrapping mechanism for the property, so you can choose which properties 164 | * you want to customize the wrapping for. 165 | * 166 | * If you encounter an error while attempting to wrap the property in question, 167 | * you can choose to throw. This will cause a WrapError.WrappingFailedForObject 168 | * to be thrown from the main `wrap()` call that started the process. 169 | */ 170 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? 171 | } 172 | 173 | /// Protocol implemented by types that may be used as keys in a wrapped Dictionary 174 | public protocol WrappableKey { 175 | /// Convert this type into a key that can be used in a wrapped Dictionary 176 | func toWrappedKey() -> String 177 | } 178 | 179 | /** 180 | * Protocol implemented by Enums to enable them to be directly wrapped 181 | * 182 | * If an Enum implementing this protocol conforms to `RawRepresentable` (it's based 183 | * on a raw type), no further implementation is required. If you wish to customize 184 | * how the Enum is wrapped, you can use the APIs in `WrapCustomizable`. 185 | */ 186 | public protocol WrappableEnum: WrapCustomizable {} 187 | 188 | /// Protocol implemented by Date types to enable them to be wrapped 189 | public protocol WrappableDate { 190 | /// Wrap the date using a date formatter, generating a string representation 191 | func wrap(dateFormatter: DateFormatter) -> String 192 | } 193 | 194 | /** 195 | * Class used to wrap an object or value. Use this in any custom `wrap()` implementations 196 | * in case you only want to add on top of the default implementation. 197 | * 198 | * You normally don't have to interact with this API. Use the `wrap()` function instead 199 | * to wrap an object from top-level code. 200 | */ 201 | public class Wrapper { 202 | fileprivate let context: Any? 203 | fileprivate var dateFormatter: DateFormatter? 204 | 205 | /** 206 | * Initialize an instance of this class 207 | * 208 | * - Parameter context: An optional contextual object that will be available throughout the 209 | * wrapping process. Can be used to inject extra information or objects needed to perform 210 | * the wrapping. 211 | * - Parameter dateFormatter: Any specific date formatter to use to encode any found `NSDate` 212 | * values. If this is `nil`, any found date values will be encoded using the "yyyy-MM-dd 213 | * HH:mm:ss" format. 214 | */ 215 | public init(context: Any? = nil, dateFormatter: DateFormatter? = nil) { 216 | self.context = context 217 | self.dateFormatter = dateFormatter 218 | } 219 | 220 | /// Perform automatic wrapping of an object or value. For more information, see `Wrap()`. 221 | public func wrap(object: Any) throws -> WrappedDictionary { 222 | return try self.wrap(object: object, enableCustomizedWrapping: false) 223 | } 224 | } 225 | 226 | /// Error type used by Wrap 227 | public enum WrapError: Error { 228 | /// Thrown when an invalid top level object (such as a String or Int) was passed to `Wrap()` 229 | case invalidTopLevelObject(Any) 230 | /// Thrown when an object couldn't be wrapped. This is a last resort error. 231 | case wrappingFailedForObject(Any) 232 | } 233 | 234 | // MARK: - Default protocol implementations 235 | 236 | /// Extension containing default implementations of `WrapCustomizable`. Override as you see fit. 237 | public extension WrapCustomizable { 238 | var wrapKeyStyle: WrapKeyStyle { 239 | return .matchPropertyName 240 | } 241 | 242 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 243 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) 244 | } 245 | 246 | func keyForWrapping(propertyNamed propertyName: String) -> String? { 247 | switch self.wrapKeyStyle { 248 | case .matchPropertyName: 249 | return propertyName 250 | case .convertToSnakeCase: 251 | return self.convertPropertyNameToSnakeCase(propertyName: propertyName) 252 | } 253 | } 254 | 255 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { 256 | return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(value: originalValue, propertyName: propertyName) 257 | } 258 | } 259 | 260 | /// Extension adding convenience APIs to `WrapCustomizable` types 261 | public extension WrapCustomizable { 262 | /// Convert a given property name (assumed to be camelCased) to snake_case 263 | func convertPropertyNameToSnakeCase(propertyName: String) -> String { 264 | let regex = try! NSRegularExpression(pattern: "(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])", options: []) 265 | let range = NSRange(location: 0, length: propertyName.count) 266 | let camelCasePropertyName = regex.stringByReplacingMatches(in: propertyName, options: [], range: range, withTemplate: "_$1$2") 267 | return camelCasePropertyName.lowercased() 268 | } 269 | } 270 | 271 | /// Extension providing a default wrapping implementation for `RawRepresentable` Enums 272 | public extension WrappableEnum where Self: RawRepresentable { 273 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 274 | return self.rawValue 275 | } 276 | } 277 | 278 | /// Extension customizing how Arrays are wrapped 279 | extension Array: WrapCustomizable { 280 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 281 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) 282 | } 283 | } 284 | 285 | /// Extension customizing how Dictionaries are wrapped 286 | extension Dictionary: WrapCustomizable { 287 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 288 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self) 289 | } 290 | } 291 | 292 | /// Extension customizing how Sets are wrapped 293 | extension Set: WrapCustomizable { 294 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 295 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) 296 | } 297 | } 298 | 299 | /// Extension customizing how Int64s are wrapped, ensuring compatbility with 32 bit systems 300 | extension Int64: WrapCustomizable { 301 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 302 | return NSNumber(value: self) 303 | } 304 | } 305 | 306 | /// Extension customizing how UInt64s are wrapped, ensuring compatbility with 32 bit systems 307 | extension UInt64: WrapCustomizable { 308 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 309 | return NSNumber(value: self) 310 | } 311 | } 312 | 313 | /// Extension customizing how NSStrings are wrapped 314 | extension NSString: WrapCustomizable { 315 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 316 | return self 317 | } 318 | } 319 | 320 | /// Extension customizing how NSURLs are wrapped 321 | extension NSURL: WrapCustomizable { 322 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 323 | return self.absoluteString 324 | } 325 | } 326 | 327 | /// Extension customizing how URLs are wrapped 328 | extension URL: WrapCustomizable { 329 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 330 | return self.absoluteString 331 | } 332 | } 333 | 334 | 335 | /// Extension customizing how NSArrays are wrapped 336 | extension NSArray: WrapCustomizable { 337 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 338 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: Array(self)) 339 | } 340 | } 341 | 342 | #if !os(Linux) 343 | /// Extension customizing how NSDictionaries are wrapped 344 | extension NSDictionary: WrapCustomizable { 345 | public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 346 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self as [NSObject : AnyObject]) 347 | } 348 | } 349 | #endif 350 | 351 | /// Extension making Int a WrappableKey 352 | extension Int: WrappableKey { 353 | public func toWrappedKey() -> String { 354 | return String(self) 355 | } 356 | } 357 | 358 | /// Extension making Date a WrappableDate 359 | extension Date: WrappableDate { 360 | public func wrap(dateFormatter: DateFormatter) -> String { 361 | return dateFormatter.string(from: self) 362 | } 363 | } 364 | 365 | #if !os(Linux) 366 | /// Extension making NSdate a WrappableDate 367 | extension NSDate: WrappableDate { 368 | public func wrap(dateFormatter: DateFormatter) -> String { 369 | return dateFormatter.string(from: self as Date) 370 | } 371 | } 372 | #endif 373 | 374 | // MARK: - Private 375 | 376 | private extension Wrapper { 377 | func wrap(object: T, enableCustomizedWrapping: Bool) throws -> WrappedDictionary { 378 | if enableCustomizedWrapping { 379 | if let customizable = object as? WrapCustomizable { 380 | let wrapped = try self.performCustomWrapping(object: customizable) 381 | 382 | guard let wrappedDictionary = wrapped as? WrappedDictionary else { 383 | throw WrapError.invalidTopLevelObject(object) 384 | } 385 | 386 | return wrappedDictionary 387 | } 388 | } 389 | 390 | var mirrors = [Mirror]() 391 | var currentMirror: Mirror? = Mirror(reflecting: object) 392 | 393 | while let mirror = currentMirror { 394 | mirrors.append(mirror) 395 | currentMirror = mirror.superclassMirror 396 | } 397 | 398 | return try self.performWrapping(object: object, mirrors: mirrors.reversed()) 399 | } 400 | 401 | func wrap(object: T, writingOptions: JSONSerialization.WritingOptions) throws -> Data { 402 | let dictionary = try self.wrap(object: object, enableCustomizedWrapping: true) 403 | return try JSONSerialization.data(withJSONObject: dictionary, options: writingOptions) 404 | } 405 | 406 | func wrap(value: T, propertyName: String? = nil) throws -> Any? { 407 | if let customizable = value as? WrapCustomizable { 408 | return try self.performCustomWrapping(object: customizable) 409 | } 410 | 411 | if let date = value as? WrappableDate { 412 | return self.wrap(date: date) 413 | } 414 | 415 | let mirror = Mirror(reflecting: value) 416 | 417 | if mirror.children.isEmpty { 418 | if let displayStyle = mirror.displayStyle { 419 | switch displayStyle { 420 | case .enum: 421 | if let wrappableEnum = value as? WrappableEnum { 422 | if let wrapped = wrappableEnum.wrap(context: self.context, dateFormatter: self.dateFormatter) { 423 | return wrapped 424 | } 425 | 426 | throw WrapError.wrappingFailedForObject(value) 427 | } 428 | 429 | return "\(value)" 430 | case .struct: 431 | return [:] 432 | default: 433 | return value 434 | } 435 | } 436 | 437 | if !(value is CustomStringConvertible) { 438 | if String(describing: value) == "(Function)" { 439 | return nil 440 | } 441 | } 442 | 443 | return value 444 | } else if value is ExpressibleByNilLiteral && mirror.children.count == 1 { 445 | if let firstMirrorChild = mirror.children.first { 446 | return try self.wrap(value: firstMirrorChild.value, propertyName: propertyName) 447 | } 448 | } 449 | 450 | return try self.wrap(object: value, enableCustomizedWrapping: false) 451 | } 452 | 453 | func wrap(collection: T) throws -> [Any] { 454 | var wrappedArray = [Any]() 455 | let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) 456 | 457 | for element in collection { 458 | if let wrapped = try wrapper.wrap(value: element) { 459 | wrappedArray.append(wrapped) 460 | } 461 | } 462 | 463 | return wrappedArray 464 | } 465 | 466 | func wrap(dictionary: [K : V]) throws -> WrappedDictionary { 467 | var wrappedDictionary = WrappedDictionary() 468 | let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) 469 | 470 | for (key, value) in dictionary { 471 | let wrappedKey: String? 472 | 473 | if let stringKey = key as? String { 474 | wrappedKey = stringKey 475 | } else if let wrappableKey = key as? WrappableKey { 476 | wrappedKey = wrappableKey.toWrappedKey() 477 | } else if let stringConvertible = key as? CustomStringConvertible { 478 | wrappedKey = stringConvertible.description 479 | } else { 480 | wrappedKey = nil 481 | } 482 | 483 | if let wrappedKey = wrappedKey { 484 | wrappedDictionary[wrappedKey] = try wrapper.wrap(value: value, propertyName: wrappedKey) 485 | } 486 | } 487 | 488 | return wrappedDictionary 489 | } 490 | 491 | func wrap(date: WrappableDate) -> String { 492 | let dateFormatter: DateFormatter 493 | 494 | if let existingFormatter = self.dateFormatter { 495 | dateFormatter = existingFormatter 496 | } else { 497 | dateFormatter = DateFormatter() 498 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 499 | self.dateFormatter = dateFormatter 500 | } 501 | 502 | return date.wrap(dateFormatter: dateFormatter) 503 | } 504 | 505 | func performWrapping(object: T, mirrors: [Mirror]) throws -> WrappedDictionary { 506 | let customizable = object as? WrapCustomizable 507 | var wrappedDictionary = WrappedDictionary() 508 | 509 | for mirror in mirrors { 510 | for property in mirror.children { 511 | 512 | if (property.value as? WrapOptional)?.isNil == true { 513 | continue 514 | } 515 | 516 | guard let propertyName = property.label else { 517 | continue 518 | } 519 | 520 | let wrappingKey: String? 521 | 522 | if let customizable = customizable { 523 | wrappingKey = customizable.keyForWrapping(propertyNamed: propertyName) 524 | } else { 525 | wrappingKey = propertyName 526 | } 527 | 528 | if let wrappingKey = wrappingKey { 529 | if let wrappedProperty = try customizable?.wrap(propertyNamed: propertyName, originalValue: property.value, context: self.context, dateFormatter: self.dateFormatter) { 530 | wrappedDictionary[wrappingKey] = wrappedProperty 531 | } else { 532 | wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) 533 | } 534 | } 535 | } 536 | } 537 | 538 | return wrappedDictionary 539 | } 540 | 541 | func performCustomWrapping(object: WrapCustomizable) throws -> Any { 542 | guard let wrapped = object.wrap(context: self.context, dateFormatter: self.dateFormatter) else { 543 | throw WrapError.wrappingFailedForObject(object) 544 | } 545 | 546 | return wrapped 547 | } 548 | } 549 | 550 | // MARK: - Nil Handling 551 | 552 | private protocol WrapOptional { 553 | var isNil: Bool { get } 554 | } 555 | 556 | extension Optional : WrapOptional { 557 | var isNil: Bool { 558 | switch self { 559 | case .none: 560 | return true 561 | case .some(let wrapped): 562 | if let nillable = wrapped as? WrapOptional { 563 | return nillable.isNil 564 | } 565 | return false 566 | } 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /Wrap.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 47; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 52D6D9871BEFF229002C0205 /* Wrap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* Wrap.framework */; }; 11 | 52D6D9981BEFF375002C0205 /* Wrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9961BEFF375002C0205 /* Wrap.swift */; }; 12 | 52D6D99B1BEFF375002C0205 /* WrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9971BEFF375002C0205 /* WrapTests.swift */; }; 13 | 52D6D9EA1BEFFFA4002C0205 /* Wrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9961BEFF375002C0205 /* Wrap.swift */; }; 14 | 52D6DA091BF00081002C0205 /* Wrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9961BEFF375002C0205 /* Wrap.swift */; }; 15 | 52D6DA261BF00118002C0205 /* Wrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9961BEFF375002C0205 /* Wrap.swift */; }; 16 | DD7502861C68FDDC006590AF /* WrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9971BEFF375002C0205 /* WrapTests.swift */; }; 17 | DD7502881C68FEDE006590AF /* Wrap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Wrap.framework */; }; 18 | DD7502921C690C7A006590AF /* Wrap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Wrap.framework */; }; 19 | DD75029A1C690CBE006590AF /* WrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D6D9971BEFF375002C0205 /* WrapTests.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 52D6D97B1BEFF229002C0205; 28 | remoteInfo = Wrap; 29 | }; 30 | DD7502801C68FCFC006590AF /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 52D6DA0E1BF000BD002C0205; 35 | remoteInfo = "Wrap-macOS"; 36 | }; 37 | DD7502931C690C7A006590AF /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 52D6D9EF1BEFFFBE002C0205; 42 | remoteInfo = "Wrap-tvOS"; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 52D6D97C1BEFF229002C0205 /* Wrap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Wrap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 52D6D9861BEFF229002C0205 /* Wrap-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Wrap-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 52D6D9961BEFF375002C0205 /* Wrap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Wrap.swift; path = Sources/Wrap.swift; sourceTree = ""; }; 50 | 52D6D9971BEFF375002C0205 /* WrapTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WrapTests.swift; path = Tests/WrapTests/WrapTests.swift; sourceTree = ""; }; 51 | 52D6D9E21BEFFF6E002C0205 /* Wrap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Wrap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 52D6D9F01BEFFFBE002C0205 /* Wrap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Wrap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 52D6DA0F1BF000BD002C0205 /* Wrap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Wrap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 52E48D2F1E60FA5C0053A668 /* WrapTests+Linux.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "WrapTests+Linux.swift"; path = "Tests/WrapTests/WrapTests+Linux.swift"; sourceTree = ""; }; 55 | AD2FAA261CD0B6D800659CF4 /* Wrap.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Wrap.plist; sourceTree = ""; }; 56 | AD2FAA281CD0B6E100659CF4 /* WrapTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = WrapTests.plist; sourceTree = ""; }; 57 | DD75027A1C68FCFC006590AF /* Wrap-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Wrap-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | DD75028D1C690C7A006590AF /* Wrap-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Wrap-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 52D6D9781BEFF229002C0205 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 52D6D9831BEFF229002C0205 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 52D6D9871BEFF229002C0205 /* Wrap.framework in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | 52D6D9DE1BEFFF6E002C0205 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | 52D6D9EC1BEFFFBE002C0205 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 52D6DA0B1BF000BD002C0205 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | DD7502771C68FCFC006590AF /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | DD7502881C68FEDE006590AF /* Wrap.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | DD75028A1C690C7A006590AF /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | DD7502921C690C7A006590AF /* Wrap.framework in Frameworks */, 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | /* End PBXFrameworksBuildPhase section */ 115 | 116 | /* Begin PBXGroup section */ 117 | 52D6D9721BEFF229002C0205 = { 118 | isa = PBXGroup; 119 | children = ( 120 | 52D6D9961BEFF375002C0205 /* Wrap.swift */, 121 | 52D6D9971BEFF375002C0205 /* WrapTests.swift */, 122 | 52E48D2F1E60FA5C0053A668 /* WrapTests+Linux.swift */, 123 | 52D6D99C1BEFF38C002C0205 /* Configs */, 124 | 52D6D97D1BEFF229002C0205 /* Products */, 125 | ); 126 | sourceTree = ""; 127 | }; 128 | 52D6D97D1BEFF229002C0205 /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 52D6D97C1BEFF229002C0205 /* Wrap.framework */, 132 | 52D6D9861BEFF229002C0205 /* Wrap-iOS Tests.xctest */, 133 | 52D6D9E21BEFFF6E002C0205 /* Wrap.framework */, 134 | 52D6D9F01BEFFFBE002C0205 /* Wrap.framework */, 135 | 52D6DA0F1BF000BD002C0205 /* Wrap.framework */, 136 | DD75027A1C68FCFC006590AF /* Wrap-macOS Tests.xctest */, 137 | DD75028D1C690C7A006590AF /* Wrap-tvOS Tests.xctest */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 52D6D99C1BEFF38C002C0205 /* Configs */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | DD7502721C68FC1B006590AF /* Frameworks */, 146 | DD7502731C68FC20006590AF /* Tests */, 147 | ); 148 | path = Configs; 149 | sourceTree = ""; 150 | }; 151 | DD7502721C68FC1B006590AF /* Frameworks */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | AD2FAA261CD0B6D800659CF4 /* Wrap.plist */, 155 | ); 156 | name = Frameworks; 157 | sourceTree = ""; 158 | }; 159 | DD7502731C68FC20006590AF /* Tests */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | AD2FAA281CD0B6E100659CF4 /* WrapTests.plist */, 163 | ); 164 | name = Tests; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXGroup section */ 168 | 169 | /* Begin PBXHeadersBuildPhase section */ 170 | 52D6D9791BEFF229002C0205 /* Headers */ = { 171 | isa = PBXHeadersBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | 52D6D9DF1BEFFF6E002C0205 /* Headers */ = { 178 | isa = PBXHeadersBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | 52D6D9ED1BEFFFBE002C0205 /* Headers */ = { 185 | isa = PBXHeadersBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | 52D6DA0C1BF000BD002C0205 /* Headers */ = { 192 | isa = PBXHeadersBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXHeadersBuildPhase section */ 199 | 200 | /* Begin PBXNativeTarget section */ 201 | 52D6D97B1BEFF229002C0205 /* Wrap-iOS */ = { 202 | isa = PBXNativeTarget; 203 | buildConfigurationList = 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Wrap-iOS" */; 204 | buildPhases = ( 205 | 52D6D9771BEFF229002C0205 /* Sources */, 206 | 52D6D9781BEFF229002C0205 /* Frameworks */, 207 | 52D6D9791BEFF229002C0205 /* Headers */, 208 | 52D6D97A1BEFF229002C0205 /* Resources */, 209 | ); 210 | buildRules = ( 211 | ); 212 | dependencies = ( 213 | ); 214 | name = "Wrap-iOS"; 215 | productName = Wrap; 216 | productReference = 52D6D97C1BEFF229002C0205 /* Wrap.framework */; 217 | productType = "com.apple.product-type.framework"; 218 | }; 219 | 52D6D9851BEFF229002C0205 /* Wrap-iOS Tests */ = { 220 | isa = PBXNativeTarget; 221 | buildConfigurationList = 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Wrap-iOS Tests" */; 222 | buildPhases = ( 223 | 52D6D9821BEFF229002C0205 /* Sources */, 224 | 52D6D9831BEFF229002C0205 /* Frameworks */, 225 | 52D6D9841BEFF229002C0205 /* Resources */, 226 | ); 227 | buildRules = ( 228 | ); 229 | dependencies = ( 230 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */, 231 | ); 232 | name = "Wrap-iOS Tests"; 233 | productName = WrapTests; 234 | productReference = 52D6D9861BEFF229002C0205 /* Wrap-iOS Tests.xctest */; 235 | productType = "com.apple.product-type.bundle.unit-test"; 236 | }; 237 | 52D6D9E11BEFFF6E002C0205 /* Wrap-watchOS */ = { 238 | isa = PBXNativeTarget; 239 | buildConfigurationList = 52D6D9E71BEFFF6E002C0205 /* Build configuration list for PBXNativeTarget "Wrap-watchOS" */; 240 | buildPhases = ( 241 | 52D6D9DD1BEFFF6E002C0205 /* Sources */, 242 | 52D6D9DE1BEFFF6E002C0205 /* Frameworks */, 243 | 52D6D9DF1BEFFF6E002C0205 /* Headers */, 244 | 52D6D9E01BEFFF6E002C0205 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | ); 250 | name = "Wrap-watchOS"; 251 | productName = "Wrap-watchOS"; 252 | productReference = 52D6D9E21BEFFF6E002C0205 /* Wrap.framework */; 253 | productType = "com.apple.product-type.framework"; 254 | }; 255 | 52D6D9EF1BEFFFBE002C0205 /* Wrap-tvOS */ = { 256 | isa = PBXNativeTarget; 257 | buildConfigurationList = 52D6DA011BEFFFBE002C0205 /* Build configuration list for PBXNativeTarget "Wrap-tvOS" */; 258 | buildPhases = ( 259 | 52D6D9EB1BEFFFBE002C0205 /* Sources */, 260 | 52D6D9EC1BEFFFBE002C0205 /* Frameworks */, 261 | 52D6D9ED1BEFFFBE002C0205 /* Headers */, 262 | 52D6D9EE1BEFFFBE002C0205 /* Resources */, 263 | ); 264 | buildRules = ( 265 | ); 266 | dependencies = ( 267 | ); 268 | name = "Wrap-tvOS"; 269 | productName = "Wrap-tvOS"; 270 | productReference = 52D6D9F01BEFFFBE002C0205 /* Wrap.framework */; 271 | productType = "com.apple.product-type.framework"; 272 | }; 273 | 52D6DA0E1BF000BD002C0205 /* Wrap-macOS */ = { 274 | isa = PBXNativeTarget; 275 | buildConfigurationList = 52D6DA201BF000BD002C0205 /* Build configuration list for PBXNativeTarget "Wrap-macOS" */; 276 | buildPhases = ( 277 | 52D6DA0A1BF000BD002C0205 /* Sources */, 278 | 52D6DA0B1BF000BD002C0205 /* Frameworks */, 279 | 52D6DA0C1BF000BD002C0205 /* Headers */, 280 | 52D6DA0D1BF000BD002C0205 /* Resources */, 281 | ); 282 | buildRules = ( 283 | ); 284 | dependencies = ( 285 | ); 286 | name = "Wrap-macOS"; 287 | productName = "Wrap-macOS"; 288 | productReference = 52D6DA0F1BF000BD002C0205 /* Wrap.framework */; 289 | productType = "com.apple.product-type.framework"; 290 | }; 291 | DD7502791C68FCFC006590AF /* Wrap-macOS Tests */ = { 292 | isa = PBXNativeTarget; 293 | buildConfigurationList = DD7502821C68FCFC006590AF /* Build configuration list for PBXNativeTarget "Wrap-macOS Tests" */; 294 | buildPhases = ( 295 | DD7502761C68FCFC006590AF /* Sources */, 296 | DD7502771C68FCFC006590AF /* Frameworks */, 297 | DD7502781C68FCFC006590AF /* Resources */, 298 | ); 299 | buildRules = ( 300 | ); 301 | dependencies = ( 302 | DD7502811C68FCFC006590AF /* PBXTargetDependency */, 303 | ); 304 | name = "Wrap-macOS Tests"; 305 | productName = "Wrap-OS Tests"; 306 | productReference = DD75027A1C68FCFC006590AF /* Wrap-macOS Tests.xctest */; 307 | productType = "com.apple.product-type.bundle.unit-test"; 308 | }; 309 | DD75028C1C690C7A006590AF /* Wrap-tvOS Tests */ = { 310 | isa = PBXNativeTarget; 311 | buildConfigurationList = DD7502951C690C7A006590AF /* Build configuration list for PBXNativeTarget "Wrap-tvOS Tests" */; 312 | buildPhases = ( 313 | DD7502891C690C7A006590AF /* Sources */, 314 | DD75028A1C690C7A006590AF /* Frameworks */, 315 | DD75028B1C690C7A006590AF /* Resources */, 316 | ); 317 | buildRules = ( 318 | ); 319 | dependencies = ( 320 | DD7502941C690C7A006590AF /* PBXTargetDependency */, 321 | ); 322 | name = "Wrap-tvOS Tests"; 323 | productName = "Wrap-tvOS Tests"; 324 | productReference = DD75028D1C690C7A006590AF /* Wrap-tvOS Tests.xctest */; 325 | productType = "com.apple.product-type.bundle.unit-test"; 326 | }; 327 | /* End PBXNativeTarget section */ 328 | 329 | /* Begin PBXProject section */ 330 | 52D6D9731BEFF229002C0205 /* Project object */ = { 331 | isa = PBXProject; 332 | attributes = { 333 | LastSwiftUpdateCheck = 0720; 334 | LastUpgradeCheck = 0910; 335 | ORGANIZATIONNAME = Wrap; 336 | TargetAttributes = { 337 | 52D6D97B1BEFF229002C0205 = { 338 | CreatedOnToolsVersion = 7.1; 339 | LastSwiftMigration = 0910; 340 | }; 341 | 52D6D9851BEFF229002C0205 = { 342 | CreatedOnToolsVersion = 7.1; 343 | LastSwiftMigration = 0910; 344 | }; 345 | 52D6D9E11BEFFF6E002C0205 = { 346 | CreatedOnToolsVersion = 7.1; 347 | LastSwiftMigration = 0800; 348 | }; 349 | 52D6D9EF1BEFFFBE002C0205 = { 350 | CreatedOnToolsVersion = 7.1; 351 | LastSwiftMigration = 0800; 352 | }; 353 | 52D6DA0E1BF000BD002C0205 = { 354 | CreatedOnToolsVersion = 7.1; 355 | LastSwiftMigration = 0800; 356 | }; 357 | DD7502791C68FCFC006590AF = { 358 | CreatedOnToolsVersion = 7.2.1; 359 | LastSwiftMigration = 0800; 360 | }; 361 | DD75028C1C690C7A006590AF = { 362 | CreatedOnToolsVersion = 7.2.1; 363 | LastSwiftMigration = 0800; 364 | }; 365 | }; 366 | }; 367 | buildConfigurationList = 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "Wrap" */; 368 | compatibilityVersion = "Xcode 6.3"; 369 | developmentRegion = English; 370 | hasScannedForEncodings = 0; 371 | knownRegions = ( 372 | en, 373 | ); 374 | mainGroup = 52D6D9721BEFF229002C0205; 375 | productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; 376 | projectDirPath = ""; 377 | projectRoot = ""; 378 | targets = ( 379 | 52D6D97B1BEFF229002C0205 /* Wrap-iOS */, 380 | 52D6DA0E1BF000BD002C0205 /* Wrap-macOS */, 381 | 52D6D9E11BEFFF6E002C0205 /* Wrap-watchOS */, 382 | 52D6D9EF1BEFFFBE002C0205 /* Wrap-tvOS */, 383 | 52D6D9851BEFF229002C0205 /* Wrap-iOS Tests */, 384 | DD7502791C68FCFC006590AF /* Wrap-macOS Tests */, 385 | DD75028C1C690C7A006590AF /* Wrap-tvOS Tests */, 386 | ); 387 | }; 388 | /* End PBXProject section */ 389 | 390 | /* Begin PBXResourcesBuildPhase section */ 391 | 52D6D97A1BEFF229002C0205 /* Resources */ = { 392 | isa = PBXResourcesBuildPhase; 393 | buildActionMask = 2147483647; 394 | files = ( 395 | ); 396 | runOnlyForDeploymentPostprocessing = 0; 397 | }; 398 | 52D6D9841BEFF229002C0205 /* Resources */ = { 399 | isa = PBXResourcesBuildPhase; 400 | buildActionMask = 2147483647; 401 | files = ( 402 | ); 403 | runOnlyForDeploymentPostprocessing = 0; 404 | }; 405 | 52D6D9E01BEFFF6E002C0205 /* Resources */ = { 406 | isa = PBXResourcesBuildPhase; 407 | buildActionMask = 2147483647; 408 | files = ( 409 | ); 410 | runOnlyForDeploymentPostprocessing = 0; 411 | }; 412 | 52D6D9EE1BEFFFBE002C0205 /* Resources */ = { 413 | isa = PBXResourcesBuildPhase; 414 | buildActionMask = 2147483647; 415 | files = ( 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | 52D6DA0D1BF000BD002C0205 /* Resources */ = { 420 | isa = PBXResourcesBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | ); 424 | runOnlyForDeploymentPostprocessing = 0; 425 | }; 426 | DD7502781C68FCFC006590AF /* Resources */ = { 427 | isa = PBXResourcesBuildPhase; 428 | buildActionMask = 2147483647; 429 | files = ( 430 | ); 431 | runOnlyForDeploymentPostprocessing = 0; 432 | }; 433 | DD75028B1C690C7A006590AF /* Resources */ = { 434 | isa = PBXResourcesBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | ); 438 | runOnlyForDeploymentPostprocessing = 0; 439 | }; 440 | /* End PBXResourcesBuildPhase section */ 441 | 442 | /* Begin PBXSourcesBuildPhase section */ 443 | 52D6D9771BEFF229002C0205 /* Sources */ = { 444 | isa = PBXSourcesBuildPhase; 445 | buildActionMask = 2147483647; 446 | files = ( 447 | 52D6D9981BEFF375002C0205 /* Wrap.swift in Sources */, 448 | ); 449 | runOnlyForDeploymentPostprocessing = 0; 450 | }; 451 | 52D6D9821BEFF229002C0205 /* Sources */ = { 452 | isa = PBXSourcesBuildPhase; 453 | buildActionMask = 2147483647; 454 | files = ( 455 | 52D6D99B1BEFF375002C0205 /* WrapTests.swift in Sources */, 456 | ); 457 | runOnlyForDeploymentPostprocessing = 0; 458 | }; 459 | 52D6D9DD1BEFFF6E002C0205 /* Sources */ = { 460 | isa = PBXSourcesBuildPhase; 461 | buildActionMask = 2147483647; 462 | files = ( 463 | 52D6D9EA1BEFFFA4002C0205 /* Wrap.swift in Sources */, 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | 52D6D9EB1BEFFFBE002C0205 /* Sources */ = { 468 | isa = PBXSourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | 52D6DA091BF00081002C0205 /* Wrap.swift in Sources */, 472 | ); 473 | runOnlyForDeploymentPostprocessing = 0; 474 | }; 475 | 52D6DA0A1BF000BD002C0205 /* Sources */ = { 476 | isa = PBXSourcesBuildPhase; 477 | buildActionMask = 2147483647; 478 | files = ( 479 | 52D6DA261BF00118002C0205 /* Wrap.swift in Sources */, 480 | ); 481 | runOnlyForDeploymentPostprocessing = 0; 482 | }; 483 | DD7502761C68FCFC006590AF /* Sources */ = { 484 | isa = PBXSourcesBuildPhase; 485 | buildActionMask = 2147483647; 486 | files = ( 487 | DD7502861C68FDDC006590AF /* WrapTests.swift in Sources */, 488 | ); 489 | runOnlyForDeploymentPostprocessing = 0; 490 | }; 491 | DD7502891C690C7A006590AF /* Sources */ = { 492 | isa = PBXSourcesBuildPhase; 493 | buildActionMask = 2147483647; 494 | files = ( 495 | DD75029A1C690CBE006590AF /* WrapTests.swift in Sources */, 496 | ); 497 | runOnlyForDeploymentPostprocessing = 0; 498 | }; 499 | /* End PBXSourcesBuildPhase section */ 500 | 501 | /* Begin PBXTargetDependency section */ 502 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */ = { 503 | isa = PBXTargetDependency; 504 | target = 52D6D97B1BEFF229002C0205 /* Wrap-iOS */; 505 | targetProxy = 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */; 506 | }; 507 | DD7502811C68FCFC006590AF /* PBXTargetDependency */ = { 508 | isa = PBXTargetDependency; 509 | target = 52D6DA0E1BF000BD002C0205 /* Wrap-macOS */; 510 | targetProxy = DD7502801C68FCFC006590AF /* PBXContainerItemProxy */; 511 | }; 512 | DD7502941C690C7A006590AF /* PBXTargetDependency */ = { 513 | isa = PBXTargetDependency; 514 | target = 52D6D9EF1BEFFFBE002C0205 /* Wrap-tvOS */; 515 | targetProxy = DD7502931C690C7A006590AF /* PBXContainerItemProxy */; 516 | }; 517 | /* End PBXTargetDependency section */ 518 | 519 | /* Begin XCBuildConfiguration section */ 520 | 52D6D98E1BEFF229002C0205 /* Debug */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ALWAYS_SEARCH_USER_PATHS = NO; 524 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 525 | CLANG_CXX_LIBRARY = "libc++"; 526 | CLANG_ENABLE_MODULES = YES; 527 | CLANG_ENABLE_OBJC_ARC = YES; 528 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 529 | CLANG_WARN_BOOL_CONVERSION = YES; 530 | CLANG_WARN_COMMA = YES; 531 | CLANG_WARN_CONSTANT_CONVERSION = YES; 532 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 533 | CLANG_WARN_EMPTY_BODY = YES; 534 | CLANG_WARN_ENUM_CONVERSION = YES; 535 | CLANG_WARN_INFINITE_RECURSION = YES; 536 | CLANG_WARN_INT_CONVERSION = YES; 537 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 538 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 539 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 540 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 541 | CLANG_WARN_STRICT_PROTOTYPES = YES; 542 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 543 | CLANG_WARN_UNREACHABLE_CODE = YES; 544 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 545 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 546 | COPY_PHASE_STRIP = NO; 547 | CURRENT_PROJECT_VERSION = 1; 548 | DEBUG_INFORMATION_FORMAT = dwarf; 549 | ENABLE_STRICT_OBJC_MSGSEND = YES; 550 | ENABLE_TESTABILITY = YES; 551 | GCC_C_LANGUAGE_STANDARD = gnu99; 552 | GCC_DYNAMIC_NO_PIC = NO; 553 | GCC_NO_COMMON_BLOCKS = YES; 554 | GCC_OPTIMIZATION_LEVEL = 0; 555 | GCC_PREPROCESSOR_DEFINITIONS = ( 556 | "DEBUG=1", 557 | "$(inherited)", 558 | ); 559 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 560 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 561 | GCC_WARN_UNDECLARED_SELECTOR = YES; 562 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 563 | GCC_WARN_UNUSED_FUNCTION = YES; 564 | GCC_WARN_UNUSED_VARIABLE = YES; 565 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 566 | MTL_ENABLE_DEBUG_INFO = YES; 567 | ONLY_ACTIVE_ARCH = YES; 568 | SDKROOT = iphoneos; 569 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 570 | SWIFT_VERSION = 3.0; 571 | TARGETED_DEVICE_FAMILY = "1,2"; 572 | VERSIONING_SYSTEM = "apple-generic"; 573 | VERSION_INFO_PREFIX = ""; 574 | }; 575 | name = Debug; 576 | }; 577 | 52D6D98F1BEFF229002C0205 /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | ALWAYS_SEARCH_USER_PATHS = NO; 581 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 582 | CLANG_CXX_LIBRARY = "libc++"; 583 | CLANG_ENABLE_MODULES = YES; 584 | CLANG_ENABLE_OBJC_ARC = YES; 585 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 586 | CLANG_WARN_BOOL_CONVERSION = YES; 587 | CLANG_WARN_COMMA = YES; 588 | CLANG_WARN_CONSTANT_CONVERSION = YES; 589 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 590 | CLANG_WARN_EMPTY_BODY = YES; 591 | CLANG_WARN_ENUM_CONVERSION = YES; 592 | CLANG_WARN_INFINITE_RECURSION = YES; 593 | CLANG_WARN_INT_CONVERSION = YES; 594 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 595 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 596 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 597 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 598 | CLANG_WARN_STRICT_PROTOTYPES = YES; 599 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 600 | CLANG_WARN_UNREACHABLE_CODE = YES; 601 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 602 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 603 | COPY_PHASE_STRIP = NO; 604 | CURRENT_PROJECT_VERSION = 1; 605 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 606 | ENABLE_NS_ASSERTIONS = NO; 607 | ENABLE_STRICT_OBJC_MSGSEND = YES; 608 | GCC_C_LANGUAGE_STANDARD = gnu99; 609 | GCC_NO_COMMON_BLOCKS = YES; 610 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 611 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 612 | GCC_WARN_UNDECLARED_SELECTOR = YES; 613 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 614 | GCC_WARN_UNUSED_FUNCTION = YES; 615 | GCC_WARN_UNUSED_VARIABLE = YES; 616 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 617 | MTL_ENABLE_DEBUG_INFO = NO; 618 | SDKROOT = iphoneos; 619 | SWIFT_VERSION = 3.0; 620 | TARGETED_DEVICE_FAMILY = "1,2"; 621 | VALIDATE_PRODUCT = YES; 622 | VERSIONING_SYSTEM = "apple-generic"; 623 | VERSION_INFO_PREFIX = ""; 624 | }; 625 | name = Release; 626 | }; 627 | 52D6D9911BEFF229002C0205 /* Debug */ = { 628 | isa = XCBuildConfiguration; 629 | buildSettings = { 630 | APPLICATION_EXTENSION_API_ONLY = YES; 631 | CLANG_ENABLE_MODULES = YES; 632 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 633 | DEFINES_MODULE = YES; 634 | DYLIB_COMPATIBILITY_VERSION = 1; 635 | DYLIB_CURRENT_VERSION = 1; 636 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 637 | INFOPLIST_FILE = Configs/Wrap.plist; 638 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 639 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 640 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 641 | ONLY_ACTIVE_ARCH = NO; 642 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-iOS"; 643 | PRODUCT_NAME = Wrap; 644 | SKIP_INSTALL = YES; 645 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 646 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 647 | SWIFT_VERSION = 4.0; 648 | }; 649 | name = Debug; 650 | }; 651 | 52D6D9921BEFF229002C0205 /* Release */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | APPLICATION_EXTENSION_API_ONLY = YES; 655 | CLANG_ENABLE_MODULES = YES; 656 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 657 | DEFINES_MODULE = YES; 658 | DYLIB_COMPATIBILITY_VERSION = 1; 659 | DYLIB_CURRENT_VERSION = 1; 660 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 661 | INFOPLIST_FILE = Configs/Wrap.plist; 662 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 663 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 664 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 665 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-iOS"; 666 | PRODUCT_NAME = Wrap; 667 | SKIP_INSTALL = YES; 668 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 669 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 670 | SWIFT_VERSION = 4.0; 671 | }; 672 | name = Release; 673 | }; 674 | 52D6D9941BEFF229002C0205 /* Debug */ = { 675 | isa = XCBuildConfiguration; 676 | buildSettings = { 677 | CLANG_ENABLE_MODULES = YES; 678 | INFOPLIST_FILE = Configs/WrapTests.plist; 679 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 680 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-iOS-Tests"; 681 | PRODUCT_NAME = "$(TARGET_NAME)"; 682 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 683 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 684 | SWIFT_VERSION = 4.0; 685 | }; 686 | name = Debug; 687 | }; 688 | 52D6D9951BEFF229002C0205 /* Release */ = { 689 | isa = XCBuildConfiguration; 690 | buildSettings = { 691 | CLANG_ENABLE_MODULES = YES; 692 | INFOPLIST_FILE = Configs/WrapTests.plist; 693 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 694 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-iOS-Tests"; 695 | PRODUCT_NAME = "$(TARGET_NAME)"; 696 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 697 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 698 | SWIFT_VERSION = 4.0; 699 | }; 700 | name = Release; 701 | }; 702 | 52D6D9E81BEFFF6E002C0205 /* Debug */ = { 703 | isa = XCBuildConfiguration; 704 | buildSettings = { 705 | APPLICATION_EXTENSION_API_ONLY = YES; 706 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 707 | DEFINES_MODULE = YES; 708 | DYLIB_COMPATIBILITY_VERSION = 1; 709 | DYLIB_CURRENT_VERSION = 1; 710 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 711 | INFOPLIST_FILE = Configs/Wrap.plist; 712 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 713 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 714 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-watchOS"; 715 | PRODUCT_NAME = Wrap; 716 | SDKROOT = watchos; 717 | SKIP_INSTALL = YES; 718 | SWIFT_VERSION = 3.0; 719 | TARGETED_DEVICE_FAMILY = 4; 720 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 721 | }; 722 | name = Debug; 723 | }; 724 | 52D6D9E91BEFFF6E002C0205 /* Release */ = { 725 | isa = XCBuildConfiguration; 726 | buildSettings = { 727 | APPLICATION_EXTENSION_API_ONLY = YES; 728 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 729 | DEFINES_MODULE = YES; 730 | DYLIB_COMPATIBILITY_VERSION = 1; 731 | DYLIB_CURRENT_VERSION = 1; 732 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 733 | INFOPLIST_FILE = Configs/Wrap.plist; 734 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 735 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 736 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-watchOS"; 737 | PRODUCT_NAME = Wrap; 738 | SDKROOT = watchos; 739 | SKIP_INSTALL = YES; 740 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 741 | SWIFT_VERSION = 3.0; 742 | TARGETED_DEVICE_FAMILY = 4; 743 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 744 | }; 745 | name = Release; 746 | }; 747 | 52D6DA021BEFFFBE002C0205 /* Debug */ = { 748 | isa = XCBuildConfiguration; 749 | buildSettings = { 750 | APPLICATION_EXTENSION_API_ONLY = YES; 751 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 752 | DEFINES_MODULE = YES; 753 | DYLIB_COMPATIBILITY_VERSION = 1; 754 | DYLIB_CURRENT_VERSION = 1; 755 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 756 | INFOPLIST_FILE = Configs/Wrap.plist; 757 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 758 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 759 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-tvOS"; 760 | PRODUCT_NAME = Wrap; 761 | SDKROOT = appletvos; 762 | SKIP_INSTALL = YES; 763 | SWIFT_VERSION = 3.0; 764 | TARGETED_DEVICE_FAMILY = 3; 765 | TVOS_DEPLOYMENT_TARGET = 9.0; 766 | }; 767 | name = Debug; 768 | }; 769 | 52D6DA031BEFFFBE002C0205 /* Release */ = { 770 | isa = XCBuildConfiguration; 771 | buildSettings = { 772 | APPLICATION_EXTENSION_API_ONLY = YES; 773 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 774 | DEFINES_MODULE = YES; 775 | DYLIB_COMPATIBILITY_VERSION = 1; 776 | DYLIB_CURRENT_VERSION = 1; 777 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 778 | INFOPLIST_FILE = Configs/Wrap.plist; 779 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 780 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 781 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-tvOS"; 782 | PRODUCT_NAME = Wrap; 783 | SDKROOT = appletvos; 784 | SKIP_INSTALL = YES; 785 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 786 | SWIFT_VERSION = 3.0; 787 | TARGETED_DEVICE_FAMILY = 3; 788 | TVOS_DEPLOYMENT_TARGET = 9.0; 789 | }; 790 | name = Release; 791 | }; 792 | 52D6DA211BF000BD002C0205 /* Debug */ = { 793 | isa = XCBuildConfiguration; 794 | buildSettings = { 795 | APPLICATION_EXTENSION_API_ONLY = YES; 796 | CODE_SIGN_IDENTITY = "-"; 797 | COMBINE_HIDPI_IMAGES = YES; 798 | DEFINES_MODULE = YES; 799 | DYLIB_COMPATIBILITY_VERSION = 1; 800 | DYLIB_CURRENT_VERSION = 1; 801 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 802 | FRAMEWORK_VERSION = A; 803 | INFOPLIST_FILE = Configs/Wrap.plist; 804 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 805 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 806 | MACOSX_DEPLOYMENT_TARGET = 10.10; 807 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-macOS"; 808 | PRODUCT_NAME = Wrap; 809 | SDKROOT = macosx; 810 | SKIP_INSTALL = YES; 811 | SWIFT_VERSION = 3.0; 812 | }; 813 | name = Debug; 814 | }; 815 | 52D6DA221BF000BD002C0205 /* Release */ = { 816 | isa = XCBuildConfiguration; 817 | buildSettings = { 818 | APPLICATION_EXTENSION_API_ONLY = YES; 819 | CODE_SIGN_IDENTITY = "-"; 820 | COMBINE_HIDPI_IMAGES = YES; 821 | DEFINES_MODULE = YES; 822 | DYLIB_COMPATIBILITY_VERSION = 1; 823 | DYLIB_CURRENT_VERSION = 1; 824 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 825 | FRAMEWORK_VERSION = A; 826 | INFOPLIST_FILE = Configs/Wrap.plist; 827 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 828 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 829 | MACOSX_DEPLOYMENT_TARGET = 10.10; 830 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-macOS"; 831 | PRODUCT_NAME = Wrap; 832 | SDKROOT = macosx; 833 | SKIP_INSTALL = YES; 834 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 835 | SWIFT_VERSION = 3.0; 836 | }; 837 | name = Release; 838 | }; 839 | DD7502831C68FCFC006590AF /* Debug */ = { 840 | isa = XCBuildConfiguration; 841 | buildSettings = { 842 | CODE_SIGN_IDENTITY = "-"; 843 | COMBINE_HIDPI_IMAGES = YES; 844 | INFOPLIST_FILE = Configs/WrapTests.plist; 845 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 846 | MACOSX_DEPLOYMENT_TARGET = 10.11; 847 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-macOS-Tests"; 848 | PRODUCT_NAME = "$(TARGET_NAME)"; 849 | SDKROOT = macosx; 850 | SWIFT_VERSION = 3.0; 851 | }; 852 | name = Debug; 853 | }; 854 | DD7502841C68FCFC006590AF /* Release */ = { 855 | isa = XCBuildConfiguration; 856 | buildSettings = { 857 | CODE_SIGN_IDENTITY = "-"; 858 | COMBINE_HIDPI_IMAGES = YES; 859 | INFOPLIST_FILE = Configs/WrapTests.plist; 860 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 861 | MACOSX_DEPLOYMENT_TARGET = 10.11; 862 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-macOS-Tests"; 863 | PRODUCT_NAME = "$(TARGET_NAME)"; 864 | SDKROOT = macosx; 865 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 866 | SWIFT_VERSION = 3.0; 867 | }; 868 | name = Release; 869 | }; 870 | DD7502961C690C7A006590AF /* Debug */ = { 871 | isa = XCBuildConfiguration; 872 | buildSettings = { 873 | INFOPLIST_FILE = Configs/WrapTests.plist; 874 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 875 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-tvOS-Tests"; 876 | PRODUCT_NAME = "$(TARGET_NAME)"; 877 | SDKROOT = appletvos; 878 | SWIFT_VERSION = 3.0; 879 | TVOS_DEPLOYMENT_TARGET = 9.1; 880 | }; 881 | name = Debug; 882 | }; 883 | DD7502971C690C7A006590AF /* Release */ = { 884 | isa = XCBuildConfiguration; 885 | buildSettings = { 886 | INFOPLIST_FILE = Configs/WrapTests.plist; 887 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 888 | PRODUCT_BUNDLE_IDENTIFIER = "com.Wrap.Wrap-tvOS-Tests"; 889 | PRODUCT_NAME = "$(TARGET_NAME)"; 890 | SDKROOT = appletvos; 891 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 892 | SWIFT_VERSION = 3.0; 893 | TVOS_DEPLOYMENT_TARGET = 9.1; 894 | }; 895 | name = Release; 896 | }; 897 | /* End XCBuildConfiguration section */ 898 | 899 | /* Begin XCConfigurationList section */ 900 | 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "Wrap" */ = { 901 | isa = XCConfigurationList; 902 | buildConfigurations = ( 903 | 52D6D98E1BEFF229002C0205 /* Debug */, 904 | 52D6D98F1BEFF229002C0205 /* Release */, 905 | ); 906 | defaultConfigurationIsVisible = 0; 907 | defaultConfigurationName = Release; 908 | }; 909 | 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Wrap-iOS" */ = { 910 | isa = XCConfigurationList; 911 | buildConfigurations = ( 912 | 52D6D9911BEFF229002C0205 /* Debug */, 913 | 52D6D9921BEFF229002C0205 /* Release */, 914 | ); 915 | defaultConfigurationIsVisible = 0; 916 | defaultConfigurationName = Release; 917 | }; 918 | 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Wrap-iOS Tests" */ = { 919 | isa = XCConfigurationList; 920 | buildConfigurations = ( 921 | 52D6D9941BEFF229002C0205 /* Debug */, 922 | 52D6D9951BEFF229002C0205 /* Release */, 923 | ); 924 | defaultConfigurationIsVisible = 0; 925 | defaultConfigurationName = Release; 926 | }; 927 | 52D6D9E71BEFFF6E002C0205 /* Build configuration list for PBXNativeTarget "Wrap-watchOS" */ = { 928 | isa = XCConfigurationList; 929 | buildConfigurations = ( 930 | 52D6D9E81BEFFF6E002C0205 /* Debug */, 931 | 52D6D9E91BEFFF6E002C0205 /* Release */, 932 | ); 933 | defaultConfigurationIsVisible = 0; 934 | defaultConfigurationName = Release; 935 | }; 936 | 52D6DA011BEFFFBE002C0205 /* Build configuration list for PBXNativeTarget "Wrap-tvOS" */ = { 937 | isa = XCConfigurationList; 938 | buildConfigurations = ( 939 | 52D6DA021BEFFFBE002C0205 /* Debug */, 940 | 52D6DA031BEFFFBE002C0205 /* Release */, 941 | ); 942 | defaultConfigurationIsVisible = 0; 943 | defaultConfigurationName = Release; 944 | }; 945 | 52D6DA201BF000BD002C0205 /* Build configuration list for PBXNativeTarget "Wrap-macOS" */ = { 946 | isa = XCConfigurationList; 947 | buildConfigurations = ( 948 | 52D6DA211BF000BD002C0205 /* Debug */, 949 | 52D6DA221BF000BD002C0205 /* Release */, 950 | ); 951 | defaultConfigurationIsVisible = 0; 952 | defaultConfigurationName = Release; 953 | }; 954 | DD7502821C68FCFC006590AF /* Build configuration list for PBXNativeTarget "Wrap-macOS Tests" */ = { 955 | isa = XCConfigurationList; 956 | buildConfigurations = ( 957 | DD7502831C68FCFC006590AF /* Debug */, 958 | DD7502841C68FCFC006590AF /* Release */, 959 | ); 960 | defaultConfigurationIsVisible = 0; 961 | defaultConfigurationName = Release; 962 | }; 963 | DD7502951C690C7A006590AF /* Build configuration list for PBXNativeTarget "Wrap-tvOS Tests" */ = { 964 | isa = XCConfigurationList; 965 | buildConfigurations = ( 966 | DD7502961C690C7A006590AF /* Debug */, 967 | DD7502971C690C7A006590AF /* Release */, 968 | ); 969 | defaultConfigurationIsVisible = 0; 970 | defaultConfigurationName = Release; 971 | }; 972 | /* End XCConfigurationList section */ 973 | }; 974 | rootObject = 52D6D9731BEFF229002C0205 /* Project object */; 975 | } 976 | -------------------------------------------------------------------------------- /Tests/WrapTests/WrapTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrap 3 | * 4 | * Copyright (c) 2015 - 2017 John Sundell. Licensed under the MIT license, as follows: 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | import Foundation 26 | import XCTest 27 | import Wrap 28 | 29 | // MARK: - Tests 30 | 31 | class WrapTests: XCTestCase { 32 | func testBasicStruct() { 33 | struct Model { 34 | let string = "A string" 35 | let int = 15 36 | let double = 7.6 37 | } 38 | 39 | do { 40 | try verify(dictionary: wrap(Model()), againstDictionary: [ 41 | "string" : "A string", 42 | "int" : 15, 43 | "double" : 7.6 44 | ]) 45 | } catch { 46 | XCTFail(error.toString()) 47 | } 48 | } 49 | 50 | func testOptionalProperties() { 51 | struct Model { 52 | let string: String? = "A string" 53 | let int: Int? = 5 54 | let missing: String? = nil 55 | let missingNestedOptional: Optional> = .some(.none) 56 | } 57 | 58 | do { 59 | try verify(dictionary: wrap(Model()), againstDictionary: [ 60 | "string" : "A string", 61 | "int" : 5 62 | ]) 63 | } catch { 64 | XCTFail(error.toString()) 65 | } 66 | } 67 | 68 | func testSpecificNonOptionalProperties() { 69 | struct Model { 70 | let some: String = "value" 71 | let Some: Int = 1 72 | } 73 | 74 | do { 75 | try verify(dictionary: wrap(Model()), againstDictionary: [ 76 | "some" : "value", 77 | "Some" : 1 78 | ]) 79 | } catch { 80 | XCTFail(error.toString()) 81 | } 82 | } 83 | 84 | func testSpecificNonOptionalValues() { 85 | struct Model { 86 | let string: String = "nil" 87 | } 88 | 89 | do { 90 | try verify(dictionary: wrap(Model()), againstDictionary: [ 91 | "string" : "nil" 92 | ]) 93 | } catch { 94 | XCTFail(error.toString()) 95 | } 96 | } 97 | 98 | 99 | func testProtocolProperties() { 100 | struct NestedModel: MockProtocol { 101 | let constantString = "Another string" 102 | var mutableInt = 27 103 | } 104 | 105 | struct Model: MockProtocol { 106 | let constantString = "A string" 107 | var mutableInt = 15 108 | let nested: MockProtocol = NestedModel() 109 | } 110 | 111 | do { 112 | try verify(dictionary: wrap(Model()), againstDictionary: [ 113 | "constantString" : "A string", 114 | "mutableInt" : 15, 115 | "nested": [ 116 | "constantString" : "Another string", 117 | "mutableInt" : 27 118 | ] 119 | ]) 120 | } catch { 121 | XCTFail(error.toString()) 122 | } 123 | } 124 | 125 | func testRootEnum() { 126 | enum Enum { 127 | case first 128 | case second(String) 129 | } 130 | 131 | do { 132 | try verify(dictionary: wrap(Enum.first), againstDictionary: [:]) 133 | 134 | try verify(dictionary: wrap(Enum.second("Hello")), againstDictionary: [ 135 | "second" : "Hello" 136 | ]) 137 | } catch { 138 | XCTFail(error.toString()) 139 | } 140 | } 141 | 142 | func testEnumProperties() { 143 | enum Enum { 144 | case first 145 | case second(String) 146 | case third(intValue: Int) 147 | } 148 | 149 | enum IntEnum: Int, WrappableEnum { 150 | case first 151 | case second = 17 152 | } 153 | 154 | enum StringEnum: String, WrappableEnum { 155 | case first = "First string" 156 | case second = "Second string" 157 | } 158 | 159 | struct Model { 160 | let first = Enum.first 161 | let second = Enum.second("Hello") 162 | let third = Enum.third(intValue: 15) 163 | let firstInt = IntEnum.first 164 | let secondInt = IntEnum.second 165 | let firstString = StringEnum.first 166 | let secondString = StringEnum.second 167 | } 168 | 169 | do { 170 | try verify(dictionary: wrap(Model()), againstDictionary: [ 171 | "first" : "first", 172 | "second" : [ 173 | "second" : "Hello" 174 | ], 175 | "third" : [ 176 | "third" : 15 177 | ], 178 | "firstInt" : 0, 179 | "secondInt" : 17, 180 | "firstString" : "First string", 181 | "secondString" : "Second string" 182 | ]) 183 | } catch { 184 | XCTFail(error.toString()) 185 | } 186 | } 187 | 188 | func testDateProperty() { 189 | let date = Date() 190 | 191 | struct Model { 192 | let date: Date 193 | } 194 | 195 | let dateFormatter = DateFormatter() 196 | 197 | do { 198 | let model = Model(date: date) 199 | 200 | try verify(dictionary: wrap(model, dateFormatter: dateFormatter), againstDictionary: [ 201 | "date" : dateFormatter.string(from: date) 202 | ]) 203 | } catch { 204 | XCTFail("\(try! wrap(Model(date: date), dateFormatter: dateFormatter) as WrappedDictionary)") 205 | XCTFail(error.toString()) 206 | } 207 | } 208 | 209 | #if !os(Linux) 210 | func testNSDateProperty() { 211 | let date = NSDate() 212 | 213 | struct Model { 214 | let date: NSDate 215 | } 216 | 217 | let dateFormatter = DateFormatter() 218 | 219 | do { 220 | let model = Model(date: date) 221 | 222 | try verify(dictionary: wrap(model, dateFormatter: dateFormatter), againstDictionary: [ 223 | "date" : dateFormatter.string(from: date as Date) 224 | ]) 225 | } catch { 226 | XCTFail("\(try! wrap(Model(date: date), dateFormatter: dateFormatter) as WrappedDictionary)") 227 | XCTFail(error.toString()) 228 | } 229 | } 230 | #endif 231 | 232 | func testDatePropertyWithCustomizableStruct() { 233 | let date = Date() 234 | 235 | struct Model: WrapCustomizable { 236 | let date: Date 237 | } 238 | 239 | let dateFormatter = DateFormatter() 240 | 241 | do { 242 | let model = Model(date: date) 243 | 244 | try verify(dictionary: wrap(model, dateFormatter: dateFormatter), againstDictionary: [ 245 | "date" : dateFormatter.string(from: date) 246 | ]) 247 | } catch { 248 | XCTFail(error.toString()) 249 | } 250 | } 251 | 252 | func testEmptyStruct() { 253 | struct Empty {} 254 | 255 | do { 256 | try verify(dictionary: wrap(Empty()), againstDictionary: [:]) 257 | } catch { 258 | XCTFail(error.toString()) 259 | } 260 | } 261 | 262 | func testNestedEmptyStruct() { 263 | struct Empty {} 264 | 265 | struct EmptyWithOptional { 266 | let optional: String? = nil 267 | } 268 | 269 | struct Model { 270 | let empty = Empty() 271 | let emptyWithOptional = EmptyWithOptional() 272 | } 273 | 274 | do { 275 | try verify(dictionary: wrap(Model()), againstDictionary: [ 276 | "empty" : [:], 277 | "emptyWithOptional" : [:] 278 | ]) 279 | } catch { 280 | XCTFail(error.toString()) 281 | } 282 | } 283 | 284 | func testArrayProperties() { 285 | struct Model { 286 | let homogeneous = ["Wrap", "Tests"] 287 | let mixed = ["Wrap", 15, 8.3] as [Any] 288 | } 289 | 290 | do { 291 | try verify(dictionary: wrap(Model()), againstDictionary: [ 292 | "homogeneous" : ["Wrap", "Tests"], 293 | "mixed" : ["Wrap", 15, 8.3] 294 | ]) 295 | } catch { 296 | XCTFail(error.toString()) 297 | } 298 | } 299 | 300 | func testDictionaryProperties() { 301 | struct Model { 302 | let homogeneous = [ 303 | "Key1" : "Value1", 304 | "Key2" : "Value2" 305 | ] 306 | 307 | let mixed: WrappedDictionary = [ 308 | "Key1" : 15, 309 | "Key2" : 19.2, 310 | "Key3" : "Value", 311 | "Key4" : ["Wrap", "Tests"], 312 | "Key5" : [ 313 | "NestedKey" : "NestedValue" 314 | ] 315 | ] 316 | } 317 | 318 | do { 319 | try verify(dictionary: wrap(Model()), againstDictionary: [ 320 | "homogeneous" : [ 321 | "Key1" : "Value1", 322 | "Key2" : "Value2" 323 | ], 324 | "mixed" : [ 325 | "Key1" : 15, 326 | "Key2" : 19.2, 327 | "Key3" : "Value", 328 | "Key4" : ["Wrap", "Tests"], 329 | "Key5" : [ 330 | "NestedKey" : "NestedValue" 331 | ] 332 | ] 333 | ]) 334 | } catch { 335 | XCTFail(error.toString()) 336 | } 337 | } 338 | 339 | func testHomogeneousSetProperty() { 340 | struct Model { 341 | let set: Set = ["Wrap", "Tests"] 342 | } 343 | 344 | do { 345 | let dictionary: WrappedDictionary = try wrap(Model()) 346 | XCTAssertEqual(dictionary.count, 1) 347 | 348 | guard let array = dictionary["set"] as? [String] else { 349 | return XCTFail("Expected array for key \"set\"") 350 | } 351 | 352 | XCTAssertEqual(Set(array), ["Wrap", "Tests"]) 353 | } catch { 354 | XCTFail(error.toString()) 355 | } 356 | } 357 | 358 | #if !os(Linux) 359 | func testMixedNSObjectSetProperty() { 360 | struct Model { 361 | let set: Set = ["Wrap" as NSObject, 15 as NSObject, 8.3 as NSObject] 362 | } 363 | 364 | do { 365 | try verify(dictionary: wrap(Model()), againstDictionary: [ 366 | "set" : ["Wrap", 15, 8.3] 367 | ]) 368 | } catch { 369 | XCTFail(error.toString()) 370 | } 371 | } 372 | #endif 373 | 374 | func testNSURLProperty() { 375 | struct Model { 376 | let optionalURL = NSURL(string: "http://github.com") 377 | let URL = NSURL(string: "http://google.com")! 378 | } 379 | 380 | do { 381 | try verify(dictionary: wrap(Model()), againstDictionary: [ 382 | "optionalURL" : "http://github.com", 383 | "URL" : "http://google.com" 384 | ]) 385 | } catch { 386 | XCTFail(error.toString()) 387 | } 388 | } 389 | 390 | func testURLProperty() { 391 | struct Model { 392 | let optionalUrl = URL(string: "http://github.com") 393 | let url = URL(string: "http://google.com")! 394 | } 395 | 396 | do { 397 | try verify(dictionary: wrap(Model()), againstDictionary: [ 398 | "optionalUrl" : "http://github.com", 399 | "url" : "http://google.com" 400 | ]) 401 | } catch { 402 | XCTFail(error.toString()) 403 | } 404 | } 405 | 406 | func test64BitIntegerProperties() { 407 | struct Model { 408 | let int = Int64.max 409 | let uint = UInt64.max 410 | } 411 | 412 | do { 413 | let dictionary = try JSONSerialization.jsonObject(with: wrap(Model()), options: []) as! WrappedDictionary 414 | 415 | try verify(dictionary: dictionary, againstDictionary: [ 416 | "int" : Int64.max, 417 | "uint" : UInt64.max 418 | ]) 419 | } catch { 420 | XCTFail(error.toString()) 421 | } 422 | } 423 | 424 | func testRootSubclass() { 425 | class Superclass { 426 | let string1 = "String1" 427 | let int1 = 1 428 | } 429 | 430 | class Subclass: Superclass { 431 | let string2 = "String2" 432 | let int2 = 2 433 | } 434 | 435 | do { 436 | try verify(dictionary: wrap(Subclass()), againstDictionary: [ 437 | "string1" : "String1", 438 | "string2" : "String2", 439 | "int1" : 1, 440 | "int2" : 2 441 | ]) 442 | } catch { 443 | XCTFail(error.toString()) 444 | } 445 | } 446 | 447 | func testRootNSObjectSubclass() { 448 | class Model: NSObject { 449 | let string = "String" 450 | let double = 7.14 451 | } 452 | 453 | do { 454 | try verify(dictionary: wrap(Model()), againstDictionary: [ 455 | "string" : "String", 456 | "double" : 7.14 457 | ]) 458 | } catch { 459 | XCTFail(error.toString()) 460 | } 461 | } 462 | 463 | func testRootDictionary() { 464 | struct Model { 465 | var string: String 466 | } 467 | 468 | let dictionary = [ 469 | "model1" : Model(string: "First"), 470 | "model2" : Model(string: "Second") 471 | ] 472 | 473 | do { 474 | try verify(dictionary: wrap(dictionary), againstDictionary: [ 475 | "model1" : [ 476 | "string" : "First" 477 | ], 478 | "model2" : [ 479 | "string" : "Second" 480 | ] 481 | ]) 482 | } catch { 483 | XCTFail(error.toString()) 484 | } 485 | } 486 | 487 | func testNestedStruct() { 488 | struct NestedModel { 489 | let string = "Nested model" 490 | } 491 | 492 | struct Model { 493 | let nested = NestedModel() 494 | } 495 | 496 | do { 497 | try verify(dictionary: wrap(Model()), againstDictionary: [ 498 | "nested" : [ 499 | "string" : "Nested model" 500 | ] 501 | ]) 502 | } catch { 503 | XCTFail(error.toString()) 504 | } 505 | } 506 | 507 | func testNestedArrayOfStructs() { 508 | struct NestedModel1 { 509 | let string1: String 510 | } 511 | 512 | struct NestedModel2 { 513 | let string2: String 514 | } 515 | 516 | struct Model { 517 | let nested: [Any] = [ 518 | NestedModel1(string1: "String1"), 519 | NestedModel2(string2: "String2"), 520 | ] 521 | } 522 | 523 | do { 524 | let wrapped: WrappedDictionary = try wrap(Model()) 525 | 526 | if let nested = wrapped["nested"] as? [WrappedDictionary] { 527 | XCTAssertEqual(nested.count, 2) 528 | 529 | if let firstDictionary = nested.first, let secondDictionary = nested.last { 530 | try verify(dictionary: firstDictionary, againstDictionary: [ 531 | "string1" : "String1" 532 | ]) 533 | 534 | try verify(dictionary: secondDictionary, againstDictionary: [ 535 | "string2" : "String2" 536 | ]) 537 | } else { 538 | XCTFail("Missing dictionaries") 539 | } 540 | } else { 541 | XCTFail("Unexpected type") 542 | } 543 | } catch { 544 | XCTFail(error.toString()) 545 | } 546 | } 547 | 548 | func testNestedDictionariesOfStructs() { 549 | struct NestedModel { 550 | let string = "Hello" 551 | } 552 | 553 | struct Model { 554 | let nested = [ 555 | "model" : NestedModel() 556 | ] 557 | } 558 | 559 | do { 560 | try verify(dictionary: wrap(Model()), againstDictionary: [ 561 | "nested" : [ 562 | "model" : [ 563 | "string" : "Hello" 564 | ] 565 | ] 566 | ]) 567 | } catch { 568 | XCTFail(error.toString()) 569 | } 570 | } 571 | 572 | func testNestedSubclass() { 573 | class Superclass { 574 | let string1 = "String1" 575 | } 576 | 577 | class Subclass: Superclass { 578 | let string2 = "String2" 579 | } 580 | 581 | struct Model { 582 | let superclass = Superclass() 583 | let subclass = Subclass() 584 | } 585 | 586 | do { 587 | try verify(dictionary: wrap(Model()), againstDictionary: [ 588 | "superclass" : [ 589 | "string1" : "String1" 590 | ], 591 | "subclass" : [ 592 | "string1" : "String1", 593 | "string2" : "String2" 594 | ] 595 | ]) 596 | } catch { 597 | XCTFail(error.toString()) 598 | } 599 | } 600 | 601 | func testDeepNesting() { 602 | struct ThirdModel { 603 | let string = "Third String" 604 | } 605 | 606 | struct SecondModel { 607 | let string = "Second String" 608 | let nestedArray = [ThirdModel()] 609 | } 610 | 611 | struct FirstModel { 612 | let string = "First String" 613 | let nestedDictionary = [ "nestedDictionary" : SecondModel()] 614 | } 615 | 616 | do { 617 | let wrappedDictionary :WrappedDictionary = try wrap(FirstModel()) 618 | try verify(dictionary: wrappedDictionary, againstDictionary: [ 619 | "string" : "First String", 620 | "nestedDictionary" : [ 621 | "nestedDictionary" : [ 622 | "string" : "Second String", 623 | "nestedArray" : [ 624 | ["string" : "Third String"] 625 | ] 626 | ] 627 | ] 628 | ]) 629 | } catch { 630 | XCTFail(error.toString()) 631 | } 632 | } 633 | 634 | #if !os(Linux) 635 | func testObjectiveCObjectProperties() { 636 | struct Model { 637 | let string = NSString(string: "Hello") 638 | let number = NSNumber(value: 17) 639 | let array = NSArray(object: NSString(string: "Unwrap")) 640 | } 641 | 642 | do { 643 | try verify(dictionary: wrap(Model()), againstDictionary: [ 644 | "string" : "Hello", 645 | "number" : 17, 646 | "array" : ["Unwrap"] 647 | ]) 648 | } catch { 649 | XCTFail(error.toString()) 650 | } 651 | } 652 | #endif 653 | 654 | func testWrappableKey() { 655 | enum Key: Int, WrappableKey { 656 | case first = 15 657 | case second = 19 658 | 659 | func toWrappedKey() -> String { 660 | return String(self.rawValue) 661 | } 662 | } 663 | 664 | struct Model { 665 | let dictionary = [ 666 | Key.first : "First value", 667 | Key.second : "Second value" 668 | ] 669 | } 670 | 671 | do { 672 | try verify(dictionary: wrap(Model()), againstDictionary: [ 673 | "dictionary" : [ 674 | "15" : "First value", 675 | "19" : "Second value" 676 | ] 677 | ]) 678 | } catch { 679 | XCTFail(error.toString()) 680 | } 681 | } 682 | 683 | func testKeyCustomization() { 684 | struct Model: WrapCustomizable { 685 | let string = "Default" 686 | let customized = "I'm customized" 687 | let skipThis = 15 688 | 689 | fileprivate func keyForWrapping(propertyNamed propertyName: String) -> String? { 690 | if propertyName == "customized" { 691 | return "totallyCustomized" 692 | } 693 | 694 | if propertyName == "skipThis" { 695 | return nil 696 | } 697 | 698 | return propertyName 699 | } 700 | } 701 | 702 | do { 703 | try verify(dictionary: wrap(Model()), againstDictionary: [ 704 | "string" : "Default", 705 | "totallyCustomized" : "I'm customized" 706 | ]) 707 | } catch { 708 | XCTFail(error.toString()) 709 | } 710 | } 711 | 712 | func testCustomWrapping() { 713 | struct Model: WrapCustomizable { 714 | let string = "A string" 715 | 716 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 717 | return [ 718 | "custom" : "A value" 719 | ] 720 | } 721 | } 722 | 723 | do { 724 | try verify(dictionary: wrap(Model()), againstDictionary: [ 725 | "custom" : "A value" 726 | ]) 727 | } catch { 728 | XCTFail(error.toString()) 729 | } 730 | } 731 | 732 | func testCustomWrappingCallingWrapFunction() { 733 | struct Model: WrapCustomizable { 734 | let int = 27 735 | 736 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 737 | do { 738 | var wrapped = try Wrapper().wrap(object: self) 739 | wrapped["custom"] = "A value" 740 | return wrapped 741 | } catch { 742 | return nil 743 | } 744 | } 745 | } 746 | 747 | do { 748 | try verify(dictionary: wrap(Model()), againstDictionary: [ 749 | "int" : 27, 750 | "custom" : "A value" 751 | ]) 752 | } catch { 753 | XCTFail(error.toString()) 754 | } 755 | } 756 | 757 | func testCustomWrappingForSingleProperty() { 758 | struct Model: WrapCustomizable { 759 | let string = "Hello" 760 | let int = 16 761 | 762 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { 763 | if propertyName == "int" { 764 | XCTAssertEqual((originalValue as? Int) ?? 0, self.int) 765 | return 27 766 | } 767 | 768 | return nil 769 | } 770 | } 771 | 772 | do { 773 | try verify(dictionary: wrap(Model()), againstDictionary: [ 774 | "string" : "Hello", 775 | "int" : 27 776 | ]) 777 | } catch { 778 | XCTFail(error.toString()) 779 | } 780 | } 781 | 782 | func testCustomWrappingFailureThrows() { 783 | struct Model: WrapCustomizable { 784 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 785 | return nil 786 | } 787 | } 788 | 789 | do { 790 | _ = try wrap(Model()) as WrappedDictionary 791 | XCTFail("Should have thrown") 792 | } catch WrapError.wrappingFailedForObject(let object) { 793 | XCTAssertTrue(object is Model) 794 | } catch { 795 | XCTFail("Invalid error type: " + error.toString()) 796 | } 797 | } 798 | 799 | func testCustomWrappingForSinglePropertyFailureThrows() { 800 | struct Model: WrapCustomizable { 801 | let string = "A string" 802 | 803 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { 804 | throw NSError(domain: "ERROR", code: 0, userInfo: nil) 805 | } 806 | } 807 | 808 | do { 809 | _ = try wrap(Model()) as WrappedDictionary 810 | XCTFail("Should have thrown") 811 | } catch WrapError.wrappingFailedForObject(let object) { 812 | XCTAssertTrue(object is Model) 813 | } catch { 814 | XCTFail("Invalid error type: " + error.toString()) 815 | } 816 | } 817 | 818 | func testInvalidRootObjectThrows() { 819 | do { 820 | _ = try wrap("A string") as WrappedDictionary 821 | } catch WrapError.invalidTopLevelObject(let object) { 822 | XCTAssertEqual((object as? String) ?? "", "A string") 823 | } catch { 824 | XCTFail("Invalid error type: " + error.toString()) 825 | } 826 | } 827 | 828 | func testDataWrapping() { 829 | struct Model { 830 | let string = "A string" 831 | let int = 42 832 | let array = [4, 1, 9] 833 | } 834 | 835 | do { 836 | let data: Data = try wrap(Model()) 837 | let object = try JSONSerialization.jsonObject(with: data, options: []) 838 | 839 | guard let dictionary = object as? WrappedDictionary else { 840 | return XCTFail("Invalid encoded type") 841 | } 842 | 843 | try verify(dictionary: dictionary, againstDictionary: [ 844 | "string" : "A string", 845 | "int" : 42, 846 | "array" : [4, 1, 9] 847 | ]) 848 | } catch { 849 | XCTFail(error.toString()) 850 | } 851 | } 852 | 853 | func testWrappingArray() { 854 | struct Model { 855 | let string: String 856 | } 857 | 858 | do { 859 | let models = [Model(string: "A"), Model(string: "B"), Model(string: "C")] 860 | let wrapped: [WrappedDictionary] = try wrap(models) 861 | XCTAssertEqual(wrapped.count, 3) 862 | 863 | try verify(dictionary: wrapped[0], againstDictionary: ["string" : "A"]) 864 | try verify(dictionary: wrapped[1], againstDictionary: ["string" : "B"]) 865 | try verify(dictionary: wrapped[2], againstDictionary: ["string" : "C"]) 866 | } catch { 867 | XCTFail(error.toString()) 868 | } 869 | } 870 | 871 | func testSnakeCasedKeyWrapping() { 872 | struct Model: WrapCustomizable { 873 | var wrapKeyStyle: WrapKeyStyle { return .convertToSnakeCase } 874 | 875 | let simple = "simple name" 876 | let camelCased = "camel cased name" 877 | let CAPITALIZED = "capitalized name" 878 | let _underscored = "underscored name" 879 | let center_underscored = "center underscored name" 880 | let double__underscored = "double underscored name" 881 | } 882 | 883 | do { 884 | try verify(dictionary: wrap(Model()), againstDictionary: [ 885 | "simple" : "simple name", 886 | "camel_cased" : "camel cased name", 887 | "capitalized" : "capitalized name", 888 | "_underscored" : "underscored name", 889 | "center_underscored" : "center underscored name", 890 | "double__underscored" : "double underscored name" 891 | ]) 892 | } catch { 893 | XCTFail(error.toString()) 894 | } 895 | } 896 | 897 | func testContext() { 898 | struct NestedModel: WrapCustomizable { 899 | let string = "String" 900 | 901 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 902 | XCTAssertEqual(context as! String, "Context") 903 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) 904 | } 905 | 906 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { 907 | XCTAssertEqual(context as! String, "Context") 908 | return context 909 | } 910 | } 911 | 912 | class Model: WrapCustomizable { 913 | let string = "String" 914 | let nestedArray = [NestedModel()] 915 | let nestedDictionary = ["nested" : NestedModel()] 916 | 917 | func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { 918 | XCTAssertEqual(context as! String, "Context") 919 | return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) 920 | } 921 | 922 | func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { 923 | XCTAssertEqual(context as! String, "Context") 924 | return nil 925 | } 926 | } 927 | 928 | do { 929 | try verify(dictionary: wrap(Model(), context: "Context"), againstDictionary: [ 930 | "string" : "String", 931 | "nestedArray" : [["string" : "Context"]], 932 | "nestedDictionary" : ["nested" : ["string" : "Context"]] 933 | ]) 934 | } catch { 935 | XCTFail(error.toString()) 936 | } 937 | } 938 | 939 | func testInheritance() { 940 | class Superclass { 941 | let string = "String" 942 | } 943 | 944 | class Subclass: Superclass { 945 | let int = 22 946 | } 947 | 948 | do { 949 | try verify(dictionary: wrap(Subclass()), againstDictionary: [ 950 | "string" : "String", 951 | "int" : 22 952 | ]) 953 | } catch { 954 | XCTFail(error.toString()) 955 | } 956 | } 957 | 958 | func testIgnoringClosureProperties() { 959 | struct StringConvertible: CustomStringConvertible { 960 | var description: String { return "(Function)" } 961 | } 962 | 963 | struct Model { 964 | let closure = {} 965 | let string = "(Function)" 966 | let stringConvertible = StringConvertible() 967 | } 968 | 969 | do { 970 | try verify(dictionary: wrap(Model()), againstDictionary: [ 971 | "string" : "(Function)", 972 | "stringConvertible" : [:] 973 | ]) 974 | } catch { 975 | XCTFail(error.toString()) 976 | } 977 | } 978 | } 979 | 980 | // MARK: - Mocks 981 | 982 | private protocol MockProtocol { 983 | var constantString: String { get } 984 | var mutableInt: Int { get set } 985 | } 986 | 987 | // MARK: - Utilities 988 | 989 | private enum VerificationError: Error { 990 | case arrayCountMismatch(Int, Int) 991 | case dictionaryKeyMismatch([String], [String]) 992 | case cannotVerifyValue(Any) 993 | case missingValueForKey(String) 994 | case valueMismatchBetween(Any, Any) 995 | } 996 | 997 | extension VerificationError: CustomStringConvertible { 998 | var description: String { 999 | switch self { 1000 | case .arrayCountMismatch(let countA, let countB): 1001 | return "Array count mismatch: \(countA) vs \(countB)" 1002 | case .dictionaryKeyMismatch(let keysA, let keysB): 1003 | return "Dictionary key count mismatch: \(keysA) vs \(keysB)" 1004 | case .cannotVerifyValue(let value): 1005 | return "Cannot verify value: \(value)" 1006 | case .missingValueForKey(let key): 1007 | return "Missing expected value for key: \(key)" 1008 | case .valueMismatchBetween(let valueA, let valueB): 1009 | return "Values don't match: \(valueA) vs \(valueB)" 1010 | } 1011 | } 1012 | } 1013 | 1014 | private protocol Verifiable { 1015 | static func convert(objectiveCObject: NSObject) -> Self? 1016 | var hashValue: Int { get } 1017 | } 1018 | 1019 | extension Int: Verifiable { 1020 | fileprivate static func convert(objectiveCObject object: NSObject) -> Int? { 1021 | guard let number = object as? NSNumber else { 1022 | return nil 1023 | } 1024 | 1025 | return number.intValue 1026 | } 1027 | } 1028 | 1029 | extension Int64: Verifiable { 1030 | fileprivate static func convert(objectiveCObject object: NSObject) -> Int64? { 1031 | guard let number = object as? NSNumber else { 1032 | return nil 1033 | } 1034 | 1035 | return number.int64Value 1036 | } 1037 | } 1038 | 1039 | extension UInt64: Verifiable { 1040 | fileprivate static func convert(objectiveCObject object: NSObject) -> UInt64? { 1041 | guard let number = object as? NSNumber else { 1042 | return nil 1043 | } 1044 | 1045 | return number.uint64Value 1046 | } 1047 | } 1048 | 1049 | extension Double: Verifiable { 1050 | fileprivate static func convert(objectiveCObject object: NSObject) -> Double? { 1051 | guard let number = object as? NSNumber else { 1052 | return nil 1053 | } 1054 | 1055 | return number.doubleValue 1056 | } 1057 | } 1058 | 1059 | extension String: Verifiable { 1060 | fileprivate static func convert(objectiveCObject object: NSObject) -> String? { 1061 | guard let string = object as? NSString else { 1062 | return nil 1063 | } 1064 | 1065 | #if os(Linux) 1066 | return nil 1067 | #else 1068 | return String(string) 1069 | #endif 1070 | } 1071 | } 1072 | 1073 | extension Date: Verifiable { 1074 | fileprivate static func convert(objectiveCObject object: NSObject) -> Date? { 1075 | guard let date = object as? NSDate else { 1076 | return nil 1077 | } 1078 | 1079 | return Date(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate) 1080 | } 1081 | } 1082 | 1083 | extension NSNumber: Verifiable { 1084 | fileprivate static func convert(objectiveCObject object: NSObject) -> Self? { 1085 | return nil 1086 | } 1087 | } 1088 | 1089 | extension NSString: Verifiable { 1090 | fileprivate static func convert(objectiveCObject object: NSObject) -> Self? { 1091 | return nil 1092 | } 1093 | } 1094 | 1095 | extension NSDate: Verifiable { 1096 | fileprivate static func convert(objectiveCObject object: NSObject) -> Self? { 1097 | return nil 1098 | } 1099 | } 1100 | 1101 | private func verify(dictionary: WrappedDictionary, againstDictionary expectedDictionary: WrappedDictionary) throws { 1102 | if dictionary.count != expectedDictionary.count { 1103 | throw VerificationError.dictionaryKeyMismatch(Array(dictionary.keys), Array(expectedDictionary.keys)) 1104 | } 1105 | 1106 | for (key, expectedValue) in expectedDictionary { 1107 | guard let actualValue = dictionary[key] else { 1108 | throw VerificationError.missingValueForKey(key) 1109 | } 1110 | 1111 | if let expectedNestedDictionary = expectedValue as? WrappedDictionary { 1112 | if let actualNestedDictionary = actualValue as? WrappedDictionary { 1113 | try verify(dictionary: actualNestedDictionary, againstDictionary: expectedNestedDictionary) 1114 | continue 1115 | } else { 1116 | throw VerificationError.valueMismatchBetween(actualValue, expectedValue) 1117 | } 1118 | } 1119 | 1120 | if let expectedNestedArray = expectedValue as? [Any] { 1121 | if let actualNestedArray = actualValue as? [Any] { 1122 | try verify(array: actualNestedArray, againstArray: expectedNestedArray) 1123 | continue 1124 | } else { 1125 | throw VerificationError.valueMismatchBetween(actualValue, expectedValue) 1126 | } 1127 | } 1128 | 1129 | try verify(value: actualValue, againstValue: expectedValue) 1130 | } 1131 | } 1132 | 1133 | private func verify(array: [Any], againstArray expectedArray: [Any]) throws { 1134 | if array.count != expectedArray.count { 1135 | throw VerificationError.arrayCountMismatch(array.count, expectedArray.count) 1136 | } 1137 | 1138 | for (index, expectedValue) in expectedArray.enumerated() { 1139 | let actualValue = array[index] 1140 | 1141 | if let expectedNestedDictionary = expectedValue as? WrappedDictionary { 1142 | if let actualNestedDictionary = actualValue as? WrappedDictionary { 1143 | try verify(dictionary: actualNestedDictionary, againstDictionary: expectedNestedDictionary) 1144 | continue 1145 | } else { 1146 | throw VerificationError.valueMismatchBetween(actualValue, expectedValue) 1147 | } 1148 | } 1149 | 1150 | if let expectedNestedArray = expectedValue as? [Any] { 1151 | if let actualNestedArray = actualValue as? [Any] { 1152 | try verify(array: actualNestedArray, againstArray: expectedNestedArray) 1153 | continue 1154 | } else { 1155 | throw VerificationError.valueMismatchBetween(actualValue, expectedValue) 1156 | } 1157 | } 1158 | 1159 | try verify(value: actualValue, againstValue: expectedValue) 1160 | } 1161 | } 1162 | 1163 | private func verify(value: Any, againstValue expectedValue: Any, convertToObjectiveCObjectIfNeeded: Bool = true) throws { 1164 | guard let expectedVerifiableValue = expectedValue as? Verifiable else { 1165 | throw VerificationError.cannotVerifyValue(expectedValue) 1166 | } 1167 | 1168 | guard let actualVerifiableValue = value as? Verifiable else { 1169 | throw VerificationError.cannotVerifyValue(value) 1170 | } 1171 | 1172 | if actualVerifiableValue.hashValue != expectedVerifiableValue.hashValue { 1173 | if convertToObjectiveCObjectIfNeeded { 1174 | if let objectiveCObject = value as? NSObject { 1175 | let expectedValueType = type(of: expectedVerifiableValue) 1176 | 1177 | guard let convertedObject = expectedValueType.convert(objectiveCObject: objectiveCObject) else { 1178 | throw VerificationError.cannotVerifyValue(value) 1179 | } 1180 | 1181 | return try verify(value: convertedObject, againstValue: expectedVerifiableValue, convertToObjectiveCObjectIfNeeded: false) 1182 | } 1183 | } 1184 | 1185 | throw VerificationError.valueMismatchBetween(value, expectedValue) 1186 | } 1187 | } 1188 | 1189 | private extension Error { 1190 | func toString() -> String { 1191 | return "\(self)" 1192 | } 1193 | } 1194 | --------------------------------------------------------------------------------