├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── EchoExtensions.swift ├── JSON.swift ├── JSONCodable.swift ├── Jsum+Helpers.swift ├── Jsum.swift ├── NSNumber+Jsum.swift ├── NumericConversions.swift ├── PointerExtensions.swift ├── Transformer.swift └── UntilOpenExistentials.swift └── Tests ├── AssumptionTests.swift ├── Blank.swift ├── JsumTests.swift ├── SampleTypes.swift ├── TestHelpers.swift └── TransformerTests.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [NSExceptional] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | *.xcodeproj 63 | *.xcworkspace 64 | 65 | ### SPM ### 66 | .build/ 67 | .swiftpm/ 68 | 69 | ### Tuist derived files ### 70 | graph.dot 71 | Derived/ 72 | 73 | ### Tuist managed dependencies ### 74 | Tuist/.build 75 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Echo", 6 | "repositoryURL": "https://github.com/Azoy/Echo.git", 7 | "state": { 8 | "branch": "main", 9 | "revision": "c93e53d8c26830928283e8bf8c2158501841357d", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "swift-atomics", 15 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f", 19 | "version": "0.0.3" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Jsum", 7 | platforms: [.macOS(.v10_15), .iOS(.v13)], 8 | products: [ 9 | .library( 10 | name: "Jsum", 11 | targets: ["Jsum"] 12 | ) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/Azoy/Echo.git", .branch("main")) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Jsum", 20 | dependencies: ["Echo"], 21 | path: "Sources" 22 | ), 23 | .testTarget( 24 | name: "JsumTests", 25 | dependencies: ["Jsum"], 26 | path: "Tests" 27 | ) 28 | ] 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jsum 2 | 3 | Jsum is a JSON object-mapping framework that aims to replace Codable for JSON object mapping. It takes a lot of inspiration from [Mantle](https://github.com/Mantle/Mantle), if you've ever used it back in Objective-C land. 4 | 5 | The name Jsum comes from the `JSON` enum it provides, and the fact that enums are sum types. JSON + sum = Jsum(?) 6 | 7 | ## Installation 8 | 9 | This library is a Swift package, so add it to your `Package.swift` like so or add it to your Xcode project. 10 | 11 | ```swift 12 | .package(url: "https://github.com/NSExceptional/Jsum.git", .branch("master")) 13 | ``` 14 | 15 | **Jsum is still in early development**, so there are no releases yet. I recommend sticking to `master` for now; I won't commit any broken code going forward until the first release. 16 | 17 | ## Motivation 18 | 19 | Codable is often thought of as not being flexible enough. Many common problems with it are outlined in the replies to [this Swift Forums post](https://forums.swift.org/t/serialization-in-swift/46641). In my opinion, Codable requires you to give up its most valuable feature—synthesized initializers—too often, and this is why it feels so cumbersome to use. 20 | 21 | Codable and `JSONDecoder` don't offer a lot of up-front decoding customization, and miss a lot of common use cases. All of these missed use cases mean you have to implement `init(decoder:)` and manually decode every single property for that type, even if you only needed to adjust a single property's behavior. 22 | 23 | Let's look at a not-quite-worst-case example. Say we have a JSON payload like this that we want to decode into a `Post` struct: 24 | 25 | ```json 26 | { 27 | "title": "my code won't compile", 28 | "author": "NoobMaster69", 29 | "score": "-5", 30 | "bookmarked": null, 31 | "link": "https://imagehost/i/ad9f8yw.png", 32 | "upvoted": 0, 33 | ..., // A dozen other properties 34 | "comment_count": 24 35 | } 36 | ``` 37 | 38 | Say we want to make the following changes: 39 | 40 | - We want `score` to be a number, not a string 41 | - We want `bookmarked` and `upvoted` to be booleans 42 | - There is a missing `body` key we want to be a non-optional string, even if it is missing or null 43 | 44 | In a perfect world, this is all we should need to write: 45 | 46 | ```swift 47 | struct Post: Decodable { 48 | let title: String 49 | let body: String = "" 50 | let link: URL 51 | let author: String 52 | let score: Int 53 | let upvoted: Bool 54 | let bookmarked: Bool 55 | // A dozen other unmodified properties 56 | ... 57 | let commentCount: Int 58 | } 59 | ``` 60 | 61 | However, this won't work for a number of reasons. For starters, Swift takes `let` seriously: `body` will only ever be `""` once you assign it that initial value. `JSONDecoder` won't intelligently do conversions between numbers/bools and strings, either, so we have to do those by hand. Or numbers and bools, etc. Pretty much all it will do for us is handle snake case to camel case and the automatic decoding of other properties that are `Codable` and decode successfully with their input. We end up writing a ton of boilerplate: 62 | 63 | ```swift 64 | struct Post: Decodable { 65 | let title: String 66 | let body: String 67 | let link: URL 68 | let author: String 69 | let score: Int 70 | let upvoted: Bool 71 | let bookmarked: Bool 72 | // A dozen other unmodified properties 73 | ... 74 | let commentCount: Int 75 | 76 | required init(from decoder: Decoder) throws { 77 | let container = try decoder.container(keyedBy: CodingKeys.self) 78 | 79 | self.title = try container.decode(String.self, forKey: .title) 80 | self.body = (try? container.decode(String.self, forKey: .body)) ?? "" 81 | self.link = try container.decode(URL.self, forKey: .link) 82 | self.author = try container.decode(String.self, forKey: .author) 83 | self.score = Int(try container.decode(String.self, forKey: .score)) ?? 0 84 | self.upvoted = try container.decode(Int.self, forKey: .upvoted) != 0 85 | self.bookmarked = (try? container.decode(Bool.self, forKey: .bookmarked)) ?? false 86 | // A dozen other unmodified properties 87 | // self.foo = try container.decode(String.self, forKey: .foo) 88 | // self.foo = try container.decode(String.self, forKey: .foo) 89 | // self.foo = try container.decode(String.self, forKey: .foo) 90 | // self.foo = try container.decode(String.self, forKey: .foo) 91 | // self.foo = try container.decode(String.self, forKey: .foo) 92 | // self.foo = try container.decode(String.self, forKey: .foo) 93 | // self.foo = try container.decode(String.self, forKey: .foo) 94 | // self.foo = try container.decode(String.self, forKey: .foo) 95 | // self.foo = try container.decode(String.self, forKey: .foo) 96 | // self.foo = try container.decode(String.self, forKey: .foo) 97 | // self.foo = try container.decode(String.self, forKey: .foo) 98 | // self.foo = try container.decode(String.self, forKey: .foo) 99 | self.commentCount = try container.decode(Int.self, forKey: .commentCount) 100 | } 101 | } 102 | ``` 103 | 104 | We didn't even need to adjust half of the properties we needed to decode, but we more than doubled the number of lines of this type by adding the initializer. There is also a lot of code duplication here: property names appear at least 3 times across the entire implementation, the types of properties are duplicated at least once because `Decoder` does not use the power of generics to supply the type parameters automatically, and `try container.decode` appears once fore every property in the model. On top of that, we have to explicitly unwrap the keyed container before we can do any real decoding. 105 | 106 | We didn't need to rename any keys here, which is not an uncommon thing to do. If you need to rename or rearrange keys aside from the snake case conversion, you have to override `CodingKeys` too, even if it is only one key: 107 | 108 | ```swift 109 | enum CodingKeys: String, CodingKey { 110 | case title = "name" 111 | case body, link, author, score, upvoted, bookmarked, commentCount 112 | // A dozen other keys 113 | case ... 114 | } 115 | ``` 116 | 117 | I set out to make the "ideal" approach possible, and Jsum is what I came up with. 118 | 119 | ### Goals 120 | 121 | - No unnecessary duplication of property names or types, ever 122 | - Rarely need to opt out of automatic initialization 123 | - Perform sane conversions automatically (i.e. string → number) 124 | - A familiar API for customizing parts of decoding, like the date format 125 | - Support decoding nearly any type, such as tuples or complex enums 126 | - Must work seamlessly with classes and inheritance 127 | - Minimize boilerplate above all else 128 | 129 | ## Usage 130 | 131 | Let's continue with our example from above. Jsum is powerful enough to do everything for us without almost any intervention: 132 | 133 | ```swift 134 | struct Post { 135 | let title: String 136 | let body: String 137 | let link: URL 138 | let author: String 139 | let score: Int 140 | let upvoted: Bool 141 | let bookmarked: Bool 142 | // A dozen other unmodified properties 143 | ... 144 | let commentCount: Int 145 | } 146 | 147 | let jsonObject = try JSONSerialization.jsonObject( 148 | with: "{ \"title\": … }".data(using: .utf8)!, options: [] 149 | ) 150 | 151 | let decoder = Jsum().keyDecoding(strategy: .convertFromSnakeCase) 152 | let post: Post = try decoder.decode(from: jsonObject) 153 | ``` 154 | 155 | To summarize what exactly is going on here: 156 | 157 | 1. We did not explicitly conform to any protocols; decoding just works™ 158 | 1. `body` is detected by Jsum as non-optional, so it is given a default value of `""` when it is not found or decoded as `null` 159 | 2. Assuming `URL` conforms to `JSONCodable`—the protocol provided by Jsum to customize decoding of your own types or other types—`link` will be decoded just like it would in Codable 160 | 3. `score` is automatically converted from a `String` to an `Int` 161 | 4. `upvoted` is automatically converted from an `Int` to a `Bool` 162 | 5. `bookmarked` is automatically coerced from `null` to `Bool`'s default value of `false` 163 | 6. We used `.convertFromSnakeCase` so `commentCount` was decoded from `"comment_count"`, but if we forgot, it would have been silently initialized with `0` 164 | 165 | ### Progressive disclosure 166 | 167 | At this point you're probably thinking, "that's cool, but what if I want stricter type checking like Codable has?" 168 | 169 | At a minimum, Jsum will always convert between strings/numbers/booleans automatically if the types do not match up. If you want `"score": "5"` to be a `String`, declare it as such. As for missing keys and `null`, you can opt into stricter checks like this: 170 | 171 | ```swift 172 | // Throw when a key is missing and the property is non-optional 173 | _ = Jsum().failOnMissingKeys() 174 | 175 | // Throw when `null` is decoded and the property is non-optional 176 | _ = Jsum().failOnNullNonOptionals() 177 | 178 | // Throw for both of the above 179 | _ = Jsum().failOnMissingKeys().failOnNullNonOptionals() 180 | ``` 181 | 182 | By default, both of these are turned off, so most properties will be given sensible default values if they cannot be decoded. This means that if you mistype a few keys, you usually won't find yourself spending ages debugging cryptic decoding errors before you can look at your decoded model. 183 | 184 | I find that this allows me to iterate on features faster and more easily, and save the potential bugs for later. When you're trying to mock up a view, you don't necessarily want to have to deal with the types of problems I've outlined here right away; you might want to flatten those out later. 185 | 186 | ### Decode anything 187 | 188 | One of my favorite things about Jsum is that it works on obscure types Codable won't handle, like tuples: 189 | 190 | ```swift 191 | let person: (name: String, age: Int) = try Jsum.decode( 192 | from: ["name": "Bob", "age": 25] 193 | ) 194 | ``` 195 | 196 | It Just Works™ ^1 197 | 198 | ^1 *Decoding enums with raw values is pending unlocked existentials* 199 | 200 | ### Default values? Payload-restructuring? Value transformers? 201 | 202 | It's all there. Check out `JSONCodable.swift` for more information. 203 | 204 | Payload restructuring works just like Mantle's `JSONKeyPathsByPropertyKey`, except that you don't need to list out every key; only the ones you want to change. Just conform to `JSONCodable` and implement this property: 205 | 206 | ``` 207 | static var jsonKeyPathsByProperty: [String: String] 208 | ``` 209 | 210 | Value transformers work similarly; conform to `JSONCodable` and implement this property: 211 | 212 | ``` 213 | static var transformersByProperty: [String: AnyTransformer] 214 | ``` 215 | 216 | Unfortunately, neither of these APIs can use type-safe key paths because key paths do not expose any data to the programmer. Jsum cannot accept a key path and use it to look up the stringy-name of the property it refers to. If key paths ever provide a way to opt-into exposing the path information, I will update Jsum to make use of this. 217 | 218 | ### What about classes? 219 | 220 | Jsum also works well with classes _and_ inheritance; something Codable makes difficult. I recommend having your classes conform to `Codable` to work around the `Class X has no initializers` error so you don't have to do something gross like `init() { fatalError() }`. 221 | 222 | ### Synthesizing entire types 223 | 224 | If you look at `JSONCodable`, you'll see a static `synthesizesDefaultJSON` property. By default this property returns `false`. If you want entire objects of your model to be synthesized from nothing (useful during development when part of your model is incomplete) you can override this property to return `true` on any type, and if a non-optional property is missing from the payload or decoded as `null`, it will be constructed and synthesized from nothing. "JSON types" will be populated with sensible defaults (empty arrays, `0`/`false`/`""`) until somewhere a key path is reached where the type of the property a) doesn't implement `static var defaultJSON: JSON`, and b) doesn't override `synthesizesDefaultJSON` to return `true` 225 | 226 | ## Not production ready 227 | 228 | Use this library at your own descretion. It is still in early active development. I am currently using it to build a Swift Forums client and adjusting the API and behaviors as I go for real world needs. 229 | -------------------------------------------------------------------------------- /Sources/EchoExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EchoExtensions.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/12/21. 6 | // Copyright © 2021 Tanner Bennett. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @_implementationOnly import Echo 11 | 12 | typealias RawType = UnsafeRawPointer 13 | typealias Field = (name: String, type: Metadata) 14 | 15 | /// For some reason, breaking it all out into separate vars like this 16 | /// eliminated a bug where the pointers in the final set were not the 17 | /// same pointers that would appear if you manually reflected a type 18 | extension KnownMetadata.Builtin { 19 | static var jsumSupported: Set = Set(_typePtrs) 20 | 21 | private static var _types: [Any.Type] { 22 | return [ 23 | Int8.self, Int16.self, Int32.self, Int64.self, Int.self, 24 | UInt8.self, UInt16.self, UInt32.self, UInt64.self, UInt.self, 25 | Float32.self, Float64.self, Float.self, Double.self 26 | ] 27 | } 28 | 29 | private static var _typePtrs: [RawType] { 30 | return self._types.map({ type in 31 | let metadata = reflect(type) 32 | return metadata.ptr 33 | }) 34 | } 35 | } 36 | 37 | extension KnownMetadata { 38 | static var array: StructDescriptor = reflectStruct([Any].self)!.descriptor 39 | static var dictionary: StructDescriptor = reflectStruct([String:Any].self)!.descriptor 40 | static var date: StructDescriptor = reflectStruct(Date.self)!.descriptor 41 | static var data: StructDescriptor = reflectStruct(Data.self)!.descriptor 42 | static var url: StructDescriptor = reflectStruct(URL.self)!.descriptor 43 | } 44 | 45 | extension Metadata { 46 | /// This doesn't actually work very well since Double etc aren't opaque, 47 | /// but instead contain a single member that is itself opaque 48 | private var isBuiltin_alt: Bool { 49 | return self is OpaqueMetadata 50 | } 51 | 52 | var isBuiltin: Bool { 53 | guard self.vwt.flags.isPOD else { 54 | return false 55 | } 56 | 57 | return KnownMetadata.Builtin.jsumSupported.contains(self.ptr) 58 | } 59 | 60 | func attemptJSONCodableConversion(value: Any) throws -> Any? { 61 | if let src = value as? JSONCodable, let destType = self.type as? JSONCodable.Type { 62 | return try destType.decode(from: src.toJSON) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func dynamicCast(from variable: Any) throws -> Any { 69 | func cast(_: T.Type) throws -> T { 70 | guard let casted = variable as? T else { 71 | throw Jsum.Error.couldNotDecode("Failed dynamic cast") 72 | } 73 | 74 | return casted 75 | } 76 | 77 | return try _openExistential(self.type, do: cast(_:)) 78 | } 79 | } 80 | 81 | extension StructMetadata { 82 | var isDateOrData: Bool { 83 | return self.descriptor == KnownMetadata.date || 84 | self.descriptor == KnownMetadata.data 85 | } 86 | } 87 | 88 | protocol NominalType: TypeMetadata { 89 | var genericMetadata: [Metadata] { get } 90 | var fieldOffsets: [Int] { get } 91 | var fields: [Field] { get } 92 | } 93 | 94 | protocol ContextualNominalType: NominalType { 95 | associatedtype NominalTypeDescriptor: TypeContextDescriptor 96 | var nominalDescriptor: NominalTypeDescriptor? { get } 97 | var name: String { get } 98 | } 99 | 100 | extension ContextualNominalType { 101 | var name: String { 102 | return self.nominalDescriptor?.name ?? "\(self)" 103 | } 104 | } 105 | 106 | extension ClassMetadata: NominalType, ContextualNominalType { 107 | var nominalDescriptor: ClassDescriptor? { 108 | descriptor 109 | } 110 | } 111 | extension StructMetadata: NominalType, ContextualNominalType { 112 | var nominalDescriptor: StructDescriptor? { 113 | descriptor 114 | } 115 | } 116 | extension EnumMetadata: NominalType, ContextualNominalType { 117 | var nominalDescriptor: Echo.EnumDescriptor? { 118 | descriptor 119 | } 120 | } 121 | 122 | // MARK: JSONCodable 123 | extension NominalType { 124 | var jsonCodableInfoByProperty: JSONCodableInfo { 125 | if let jsoncodable = self.type as? JSONCodable.Type { 126 | return ( 127 | jsoncodable.transformersByProperty, 128 | jsoncodable.jsonKeyPathsByProperty, 129 | jsoncodable.defaultsByProperty 130 | ) 131 | } 132 | 133 | return ([:], [:], [:]) 134 | } 135 | } 136 | 137 | // MARK: KVC 138 | extension ContextualNominalType { 139 | func recordIndex(forKey key: String) -> Int? { 140 | return self.nominalDescriptor?.fields.records.firstIndex { $0.name == key } 141 | } 142 | 143 | func fieldOffset(for key: String) -> Int? { 144 | if let idx = self.recordIndex(forKey: key) { 145 | return self.fieldOffsets[idx] 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func fieldType(for key: String) -> Metadata? { 152 | return self.fields.first(where: { $0.name == key })?.type 153 | } 154 | 155 | var _shallowFields: [Field] { 156 | guard let r: [FieldRecord] = self.nominalDescriptor?.fields.records else { 157 | return [] 158 | } 159 | 160 | return r.filter(\.hasMangledTypeName).map { 161 | return ( 162 | $0.name, 163 | reflect(self.type(of: $0.mangledTypeName)!) 164 | ) 165 | } 166 | } 167 | } 168 | 169 | extension StructMetadata { 170 | func getValue(forKey key: String, from object: O) -> T { 171 | let offset = self.fieldOffset(for: key)! 172 | let ptr = object~ 173 | return ptr[offset] 174 | } 175 | 176 | func set(value: T, forKey key: String, on object: inout O) { 177 | self.set(value: value, forKey: key, pointer: object~) 178 | } 179 | 180 | func set(value: Any, forKey key: String, pointer ptr: RawPointer) { 181 | let offset = self.fieldOffset(for: key)! 182 | let type = self.fieldType(for: key)! 183 | ptr.storeBytes(of: value, type: type, offset: offset) 184 | } 185 | 186 | var fields: [Field] { self._shallowFields } 187 | } 188 | 189 | extension ClassMetadata { 190 | func getValue(forKey key: String, from object: O) -> T { 191 | guard let offset = self.fieldOffset(for: key) else { 192 | if let sup = self.superclassMetadata { 193 | return sup.getValue(forKey: key, from: object) 194 | } else { 195 | fatalError("Class '\(self.name)' has no member '\(key)'") 196 | } 197 | } 198 | 199 | let ptr = object~ 200 | return ptr[offset] 201 | } 202 | 203 | func set(value: T, forKey key: String, on object: inout O) { 204 | self.set(value: value, forKey: key, pointer: object~) 205 | } 206 | 207 | func set(value: Any, forKey key: String, pointer ptr: RawPointer) { 208 | guard let offset = self.fieldOffset(for: key) else { 209 | if let sup = self.superclassMetadata { 210 | return sup.set(value: value, forKey: key, pointer: ptr) 211 | } else { 212 | fatalError("Class '\(self.name)' has no member '\(key)'") 213 | } 214 | } 215 | 216 | let type = self.fieldType(for: key)! 217 | ptr.storeBytes(of: value, type: type, offset: offset) 218 | } 219 | 220 | /// Consolidate all fields in the class hierarchy 221 | var fields: [Field] { 222 | if let sup = self.superclassMetadata, sup.isSwiftClass { 223 | return self._shallowFields + sup.fields 224 | } 225 | 226 | return self._shallowFields 227 | } 228 | } 229 | 230 | extension EnumMetadata { 231 | var fields: [Field] { self._shallowFields } 232 | } 233 | 234 | // MARK: Protocol conformance checking 235 | extension TypeMetadata { 236 | func conforms(to _protocol: Any) -> Bool { 237 | let existential = reflect(_protocol) as! MetatypeMetadata 238 | let instance = existential.instanceMetadata as! ExistentialMetadata 239 | let desc = instance.protocols.first! 240 | 241 | return !self.conformances.filter({ $0.protocol == desc }).isEmpty 242 | } 243 | } 244 | 245 | // MARK: MetadataKind 246 | extension MetadataKind { 247 | var isObject: Bool { 248 | return self == .class || self == .objcClassWrapper 249 | } 250 | } 251 | 252 | // MARK: Object allocation 253 | extension ClassMetadata { 254 | func createInstance(props: [String: Any] = [:]) -> T { 255 | let obj = swift_allocObject( 256 | for: self, 257 | size: self.instanceSize, 258 | alignment: self.instanceAlignmentMask 259 | ) 260 | 261 | for (key, value) in props { 262 | // TODO: this shouldn't be inout for this case 263 | self.set(value: value, forKey: key, pointer: obj~) 264 | } 265 | 266 | return Unmanaged.fromOpaque(obj).takeRetainedValue() 267 | } 268 | } 269 | 270 | // MARK: Struct initialization 271 | extension StructMetadata { 272 | func createInstance(props: [String: Any] = [:]) -> Any { 273 | assert({ 274 | let givenKeys = Set(props.keys.map { $0 as String }) 275 | let allKeys = Set(self.descriptor.fields.records.map(\.name)) 276 | return givenKeys == allKeys 277 | }()) 278 | 279 | var box = AnyExistentialContainer(metadata: self) 280 | for (key, value) in props { 281 | self.set(value: value, forKey: key, pointer: box.getValueBuffer()~) 282 | } 283 | 284 | return box.toAny 285 | } 286 | } 287 | 288 | // MARK: Tuple initialization 289 | extension TupleMetadata { 290 | func createInstance(elements: [Any] = []) -> Any { 291 | var box = AnyExistentialContainer(metadata: self) 292 | let ptr = box.getValueBuffer() 293 | 294 | // Copy each element of the array to each tuple element at the specified offset 295 | for (e, value) in zip(self.elements, elements) { 296 | var valueBox = container(for: value) 297 | ptr.copyMemory(ofTupleElement: valueBox.projectValue(), layout: e) 298 | } 299 | 300 | return box.toAny 301 | } 302 | } 303 | 304 | // MARK: Populating AnyExistentialContainer 305 | extension AnyExistentialContainer { 306 | var toAny: Any { 307 | return unsafeBitCast(self, to: Any.self) 308 | } 309 | 310 | var isEmpty: Bool { 311 | return self.data == (0, 0, 0) 312 | } 313 | 314 | init(boxing valuePtr: RawPointer, type: Metadata) { 315 | self = .init(metadata: type) 316 | self.store(value: valuePtr) 317 | } 318 | 319 | init(nil optionalType: EnumMetadata) { 320 | self = .init(metadata: optionalType) 321 | 322 | // Zero memory 323 | let size = optionalType.vwt.size 324 | self.getValueBuffer().initializeMemory( 325 | as: Int8.self, repeating: 0, count: size 326 | ) 327 | } 328 | 329 | mutating func store(value newValuePtr: RawPointer) { 330 | self.metadata.vwt.initializeWithCopy(self.getValueBuffer(), newValuePtr) 331 | // self.getValueBuffer().copyMemory(from: newValuePtr, type: self.metadata) 332 | } 333 | 334 | /// Calls into `projectValue()` but will allocate a box 335 | /// first if needed for types that are not inline 336 | mutating func getValueBuffer() -> RawPointer { 337 | // Allocate a box if needed and return it 338 | if !self.metadata.vwt.flags.isValueInline && self.data.0 == 0 { 339 | return self.metadata.allocateBoxForExistential(in: &self)~ 340 | } 341 | 342 | // We don't need a box or already have one 343 | return self.projectValue()~ 344 | } 345 | } 346 | 347 | //internal extension FieldRecord: @retroactive CustomDebugStringConvertible { 348 | // var debugDescription: String { 349 | // let ptr = self.mangledTypeName.assumingMemoryBound(to: UInt8.self) 350 | // return self.name + ": \(String(cString: ptr)) ( \(self.referenceStorage) : \(self.flags))" 351 | // } 352 | //} 353 | 354 | extension EnumMetadata { 355 | func getTag(for instance: Any) -> UInt32 { 356 | var box = container(for: instance) 357 | return self.enumVwt.getEnumTag(for: box.projectValue()) 358 | } 359 | 360 | func copyPayload(from instance: Any) -> (value: Any, type: Any.Type)? { 361 | let tag = self.getTag(for: instance) 362 | let isPayloadCase = self.descriptor.numPayloadCases > tag 363 | if isPayloadCase { 364 | let caseRecord = self.descriptor.fields.records[Int(tag)] 365 | let type = self.type(of: caseRecord.mangledTypeName)! 366 | var caseBox = container(for: instance) 367 | // Copies in the value and allocates a box as needed 368 | let payload = AnyExistentialContainer( 369 | boxing: caseBox.projectValue()~, 370 | type: reflect(type) 371 | ) 372 | return (unsafeBitCast(payload, to: Any.self), type) 373 | } 374 | 375 | return nil 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /Sources/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum JSON: Equatable { 11 | case null 12 | case bool(Bool) 13 | case int(Int) 14 | case float(Double) 15 | case string(String) 16 | case array([JSON]) 17 | case object([String: JSON]) 18 | 19 | public var unwrapped: Any { 20 | switch self { 21 | case .null: return NSNull() 22 | case .bool(let v): return v 23 | case .int(let v): return v 24 | case .float(let v): return v 25 | case .string(let v): return v 26 | case .array(let v): return v.map(\.unwrapped) 27 | case .object(let v): return v.mapValues(\.unwrapped) 28 | } 29 | } 30 | 31 | public var toBool: Bool { 32 | switch self { 33 | case .null: return false 34 | case .bool(let v): return v 35 | case .int(let v): return v != 0 36 | case .float(let v): return v != 0.0 37 | case .string(let v): return !v.isEmpty 38 | case .array(let v): return !v.isEmpty 39 | case .object(_): return true 40 | } 41 | } 42 | 43 | public var toInt: Int? { 44 | switch self { 45 | case .null: return 0 46 | case .bool(let v): return v ? 1 : 0 47 | case .int(let v): return v 48 | case .float(let v): return Int(v) 49 | case .string(let v): return Int(v) ?? nil 50 | case .array(_): return nil 51 | case .object(_): return nil 52 | } 53 | } 54 | 55 | /// Arrays and objects return nil. 56 | public var toFloat: Double? { 57 | switch self { 58 | case .null: return 0 59 | case .bool(let v): return v ? 1 : 0 60 | case .int(let v): return Double(v) 61 | case .float(let v): return v 62 | case .string(let v): return Double(v) ?? nil 63 | case .array(_): return nil 64 | case .object(_): return nil 65 | } 66 | } 67 | 68 | public var toString: String { 69 | switch self { 70 | case .null: return "null" 71 | case .bool(let v): return String(v) 72 | case .int(let v): return String(v) 73 | case .float(let v): return String(v) 74 | case .string(let v): return v 75 | case .array(_): fallthrough 76 | case .object(_): 77 | let obj = self.unwrapped 78 | let data = try! JSONSerialization.data(withJSONObject: obj, options: []) 79 | return String(data: data, encoding: .utf8)! 80 | } 81 | } 82 | 83 | /// Attempts to decode numbers as UNIX timestamps and strings as 84 | /// ISO8601 dates on macOS 10.12 and iOS 10 and beyond; this will 85 | /// not decode dates from strings on earlier versions of the OSes. 86 | public var toDate: Date? { 87 | switch self { 88 | case .int(let v): return Date(timeIntervalSince1970: Double(v)) 89 | case .float(let v): return Date(timeIntervalSince1970: v) 90 | case .string(let v): 91 | if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { 92 | return ISO8601DateFormatter().date(from: v) 93 | } else { 94 | return nil 95 | } 96 | 97 | default: return nil 98 | } 99 | } 100 | 101 | /// Attempts to decode strings as base-64 encoded data, and 102 | /// arrays and objects as JSON strings converted to data. 103 | public var toData: Data? { 104 | switch self { 105 | case .string(let v): return Data(base64Encoded: v) 106 | case .array(_): fallthrough 107 | case .object(_): 108 | let obj = self.unwrapped 109 | return try! JSONSerialization.data(withJSONObject: obj, options: []) 110 | 111 | default: return nil 112 | } 113 | } 114 | 115 | /// Null is returned as an empty array. Arrays are returned 116 | /// unmodified. Dictionaries are returned as their values 117 | /// alone. Everything else is an empty array. 118 | public var asArray: [JSON] { 119 | switch self { 120 | case .null: return [] 121 | case .array(let a): return a 122 | case .object(let o): return Array(o.values) 123 | default: return [] 124 | } 125 | } 126 | 127 | /// Null is returned as an empty dictionary. Dictionaries are 128 | /// returned ummodified. Everything else is a fatal error. 129 | public var asObject: [String: JSON] { 130 | switch self { 131 | case .null: return [:] 132 | case .object(let o): return o 133 | default: fatalError("Cannot convert non-objects to object") 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/JSONCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONCodable.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import Foundation 9 | @_implementationOnly import Echo 10 | 11 | public enum JSONDecodableError: Error { 12 | case decodingNotImplemented 13 | } 14 | 15 | typealias JSONCodableInfo = ( 16 | transformers: [String: AnyTransformer], 17 | jsonKeyPaths: [String: String], 18 | defaults: [String: Any] 19 | ) 20 | 21 | public protocol JSONCodable { 22 | /// Encodes the conformer to JSON 23 | var toJSON: JSON { get } 24 | /// Whether or not to opt-into synthesizing `defaultJSON` for nominal types 25 | static var synthesizesDefaultJSON: Bool { get } 26 | /// Sensible default used to coalesce the conformer from nil 27 | static var defaultJSON: JSON { get } 28 | /// A key path to a computed property to use to initialize 29 | /// the conformer or throw an error if the result is nil 30 | static var jsonKeyPathForDecoding: PartialKeyPath { get } 31 | /// Transformers are never passed nil; use default values 32 | /// to coerce nil to something else. JSONCodable types 33 | /// provide default values automatically, too, so you 34 | /// only need to provide default values for those if you 35 | /// need a different default value than the one provided. 36 | static var transformersByProperty: [String: AnyTransformer] { get } 37 | /// A mapping of property names to JSON key paths. Useful for 38 | /// flattening the hierarchy of a particular JSON object. 39 | static var jsonKeyPathsByProperty: [String: String] { get } 40 | /// A mapping of property names to default values. Use this 41 | /// to supply a default value for a non-optional property. 42 | /// If the type of your property conforms to JSONCodable, 43 | /// that type may provide its own default value through 44 | /// `var defaultJSON`. This property will override that. 45 | static var defaultsByProperty: [String: Any] { get } 46 | /// Initialize an instance of the conformer from JSON 47 | static func decode(from json: JSON) throws -> Self 48 | } 49 | 50 | struct AnyJSONCodable { 51 | let wrapped: JSONCodable 52 | 53 | init(_ json: JSONCodable) { 54 | self.wrapped = json 55 | } 56 | } 57 | 58 | extension JSONCodable { 59 | private var existential: AnyExistentialContainer { container(for: self) } 60 | private var isClass: Bool { self.existential.metadata.kind.isObject } 61 | private static var existential: AnyExistentialContainer { container(for: self) } 62 | private static var instanceMetadata: Metadata { 63 | let metadata = self.existential.metadata as! MetatypeMetadata 64 | return metadata.instanceMetadata 65 | } 66 | static var isClass: Bool { 67 | return self.instanceMetadata.kind.isObject 68 | } 69 | static var isTuple: Bool { 70 | return self.instanceMetadata.kind == .tuple 71 | } 72 | 73 | public static var synthesizesDefaultJSON: Bool { false } 74 | 75 | public var toJSON: JSON { 76 | /// TODO: implement reverse decoding so that this works 77 | if self.isClass { 78 | return .object(["class": .string("\(type(of: self))")]) 79 | } 80 | 81 | return .null 82 | } 83 | 84 | public static var defaultJSON: JSON { 85 | if self.synthesizesDefaultJSON { 86 | // TODO: make this use values from defaultJSON 87 | return try! Jsum.synthesizeJSON(Self.self) as! JSON 88 | } 89 | 90 | fatalError("defaultJSON not implemented for type") 91 | } 92 | } 93 | 94 | extension JSONCodable { 95 | /// If neither this property nor decode() are implemented, 96 | /// `JSONDecodableError.decodingNotImplemented` will be thrown 97 | public static var jsonKeyPathForDecoding: PartialKeyPath { \JSON.self } 98 | public static var transformersByProperty: [String: AnyTransformer] { [:] } 99 | public static var jsonKeyPathsByProperty: [String: String] { [:] } 100 | public static var defaultsByProperty: [String: Any] { [:] } 101 | 102 | public static func decode(from json: JSON) throws -> Self { 103 | if let keyPath = self.jsonKeyPathForDecoding as? KeyPath { 104 | guard let value = json[keyPath: keyPath] else { 105 | throw TransformError.notConvertible 106 | } 107 | 108 | return value 109 | } 110 | 111 | if let keyPath = self.jsonKeyPathForDecoding as? KeyPath { 112 | return json[keyPath: keyPath] 113 | } 114 | 115 | throw JSONDecodableError.decodingNotImplemented 116 | } 117 | 118 | var asArray: [Any]? { 119 | return self as? [Any] 120 | } 121 | 122 | var asDictionary: [String: Any]? { 123 | return self as? [String: Any] 124 | } 125 | 126 | var any: AnyJSONCodable { AnyJSONCodable(self) } 127 | } 128 | 129 | extension NSNull: JSONCodable { 130 | public var toJSON: JSON { .null } 131 | public static var defaultJSON: JSON { .null } 132 | } 133 | 134 | extension Optional: JSONCodable where Wrapped: JSONCodable { 135 | public var toJSON: JSON { 136 | switch self { 137 | case .none: return .null 138 | case .some(let v): return v.toJSON 139 | } 140 | } 141 | 142 | public static var defaultJSON: JSON { .null } 143 | 144 | public static func decode(from json: JSON) throws -> Self { 145 | switch json { 146 | case .null: return nil 147 | case .bool(let v): return v as? Wrapped 148 | case .int(let v): return v as? Wrapped 149 | case .float(let v): return v as? Wrapped 150 | case .string(let v): return v as? Wrapped 151 | case .array(let v): return v as? Wrapped 152 | case .object(let v): return v as? Wrapped 153 | } 154 | } 155 | } 156 | 157 | extension Bool: JSONCodable { 158 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toBool 159 | public var toJSON: JSON { .bool(self) } 160 | public static var defaultJSON: JSON = .bool(false) 161 | } 162 | 163 | extension String: JSONCodable { 164 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toString 165 | public var toJSON: JSON { .string(self) } 166 | public static var defaultJSON: JSON = .string("") 167 | } 168 | 169 | extension NSString: JSONCodable { 170 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toString 171 | public var toJSON: JSON { .string(self as String) } 172 | public static var defaultJSON: JSON = .string("") 173 | } 174 | 175 | extension Int: JSONCodable { 176 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toInt 177 | public var toJSON: JSON { .int(Int(self)) } 178 | public static var defaultJSON: JSON = .int(0) 179 | } 180 | 181 | extension Double: JSONCodable { 182 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toFloat 183 | public var toJSON: JSON { .float(self) } 184 | public static var defaultJSON: JSON = .float(0) 185 | } 186 | 187 | extension NSNumber: JSONCodable { 188 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toFloat 189 | public var toJSON: JSON { 190 | if self.isBool { 191 | return .bool(self.boolValue) 192 | } 193 | if self.isFloat { 194 | return .float(self.doubleValue) 195 | } 196 | return .int(self.intValue) 197 | } 198 | 199 | public static var defaultJSON: JSON = .float(0) 200 | } 201 | 202 | extension Date: JSONCodable { 203 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toDate 204 | public var toJSON: JSON { return .float(self.timeIntervalSince1970) } 205 | public static var defaultJSON: JSON = .int(0) 206 | } 207 | 208 | extension Data: JSONCodable { 209 | public static var jsonKeyPathForDecoding: PartialKeyPath = \.toData 210 | public var toJSON: JSON { return .string(self.base64EncodedString()) } 211 | public static var defaultJSON: JSON = .string("") 212 | } 213 | 214 | extension Array: JSONCodable where Element: JSONCodable { 215 | public var toJSON: JSON { .array(self.map(\.toJSON)) } 216 | public static var defaultJSON: JSON { .array([]) } 217 | 218 | public static func decode(from json: JSON) throws -> Self { 219 | // return json.asArray.map(\.unwrapped) 220 | return try json.asArray.map(Element.decode(from:)) 221 | } 222 | } 223 | 224 | extension Dictionary: JSONCodable where Key == String, Value: JSONCodable { 225 | public var toJSON: JSON { .object(self.mapValues(\.toJSON)) } 226 | public static var defaultJSON: JSON { .object([:]) } 227 | 228 | public static func decode(from json: JSON) throws -> Self { 229 | // return json.asObject.mapValues(\.unwrapped) 230 | return try json.asObject.mapValues(Value.decode(from:)) 231 | } 232 | } 233 | 234 | extension Dictionary where Key == String { 235 | /// Given a string like "foo.bar", this assumes the key "foo" contains another 236 | /// dictionary, and attempts to retrieve and return its value for "bar" 237 | public func jsum_value(for jsonKeyPath: String) throws -> Any? { 238 | if jsonKeyPath.contains(".") { 239 | // Nested key paths must consist of at least "x.x" 240 | assert(jsonKeyPath.count > 2) 241 | 242 | // Get list of keys from key path, stop at the last key 243 | var keys = jsonKeyPath.split(separator: ".").map(String.init) 244 | let lastKey = keys.popLast()! 245 | 246 | // Iteratively get nested dictionaries until we reach the last key 247 | var dict = self 248 | for key in keys { 249 | if let subdict = dict[key] as? Self { 250 | dict = subdict 251 | } else { 252 | throw Jsum.Error.couldNotDecode("Invalid JSON key path '\(jsonKeyPath)'") 253 | } 254 | } 255 | 256 | return dict[lastKey] 257 | } 258 | 259 | return self[jsonKeyPath] 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Sources/Jsum+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Jsum+Helpers.swift 3 | // 4 | // 5 | // Created by Tanner Bennett on 5/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Jsum { 11 | // This function is part of the Swift.org open source project 12 | // 13 | // Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 14 | // Licensed under Apache License v2.0 with Runtime Library Exception 15 | // 16 | // See https://swift.org/LICENSE.txt for license information 17 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 18 | // 19 | // Taken from JSONEncoder.swift in the standard library 20 | /// Convert a given snake_case string to its camelCase equivalent. 21 | /// Exposed so you can use it as you see fit in a custom key decoder. 22 | public static func snakeCaseToCamelCase(_ stringKey: String) -> String { 23 | guard !stringKey.isEmpty else { return stringKey } 24 | 25 | // Find the first non-underscore character 26 | guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else { 27 | // Reached the end without finding an _ 28 | return stringKey 29 | } 30 | 31 | // Find the last non-underscore character 32 | var lastNonUnderscore = stringKey.index(before: stringKey.endIndex) 33 | while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" { 34 | stringKey.formIndex(before: &lastNonUnderscore) 35 | } 36 | 37 | let keyRange = firstNonUnderscore...lastNonUnderscore 38 | let leadingUnderscoreRange = stringKey.startIndex.. String { 79 | guard !stringKey.isEmpty else { return stringKey } 80 | 81 | var words : [Range] = [] 82 | // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase 83 | // 84 | // myProperty -> my_property 85 | // myURLProperty -> my_url_property 86 | // 87 | // We assume, per Swift naming conventions, that the first character of the key is lowercase. 88 | var wordStart = stringKey.startIndex 89 | var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. 112 | let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) 113 | words.append(upperCaseRange.lowerBound..) 93 | } 94 | 95 | /// The strategy to use for decoding `Date` values. 96 | public enum DateDecodingStrategy { 97 | 98 | /// Decode JSON numbers as UNIX timestamps, and strings 99 | /// as an ISO-8601-formatted strings. This is the default strategy. 100 | case bestGuess 101 | 102 | /// Decode the `Date` as a UNIX timestamp from a JSON number. 103 | case secondsSince1970 104 | 105 | /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. 106 | case millisecondsSince1970 107 | 108 | /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). 109 | @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 110 | case iso8601 111 | 112 | /// Decode the `Date` as a string parsed by the given formatter. 113 | /// `DateFormatter` is expensive to create; you should cache it. 114 | case formatter(DateFormatter) 115 | 116 | /// Decode the `Date` as a custom value decoded by the given closure. 117 | /// Cast the input value to `String` or `Int` etc. as needed. 118 | case custom((Any) throws -> Date) 119 | } 120 | 121 | /// The strategy to use for decoding `Data` values. 122 | public enum DataDecodingStrategy { 123 | 124 | /// Decode the `Data` from a Base64-encoded string. This is the default strategy. 125 | case base64 126 | 127 | /// Decode the `Data` as a custom value decoded by the given closure. 128 | /// Cast the input value to `String` or `Int` etc. as needed. 129 | case custom((Any) throws -> Data) 130 | } 131 | 132 | public init() { 133 | // Enable fractional seconds, which is the default for 134 | // JavaScript's Date.toJSON() 135 | // TODO: this actually only works if the date also has fractional seconds 💀 fix it 136 | self._iso8601Formatter.formatOptions = [ 137 | self._iso8601Formatter.formatOptions, 138 | .withFractionalSeconds 139 | ] 140 | } 141 | 142 | private var _failOnMissingKeys: Bool = false 143 | private var _failOnNullNonOptionals: Bool = false 144 | private var _keyDecoding: KeyDecodingStrategy = .usePropertyKeys 145 | private var _dateDecoding: DateDecodingStrategy = .bestGuess 146 | private var _dataDecoding: DataDecodingStrategy = .base64 147 | private let _iso8601Formatter = ISO8601DateFormatter() 148 | 149 | private static let specialCaseStructs: [UnsafeRawPointer: OpaqueTransformer] = [ 150 | KnownMetadata.url.ptr: OpaqueTransformer(forwardBlock: { (value) -> Any in 151 | guard let urlString = value! as? String else { 152 | throw Error.couldNotDecode("URL requires a string to be decoded") 153 | } 154 | 155 | guard let url = URL(string: urlString) else { 156 | throw Error.couldNotDecode("URL(string:) returned nil with string '\(urlString)'") 157 | } 158 | 159 | return url 160 | }), 161 | ] 162 | 163 | /// Set whether decoding should fail when no key is present in the 164 | /// JSON payload at all for a given property. If the key is just null, 165 | /// decoding will not fail. To fail on null, call `failOnNullNonOptionals()`. 166 | /// 167 | /// By default, Jsum will attempt to synthesize a default value if the 168 | /// property conforms to `JSONCodable`, and _then_ fail if it doesn't. 169 | /// Setting this flag will cause Jsum to skip that step. 170 | /// Additionally, default values are ignored if this flag is set. 171 | /// Transformers are never invoked for missing keys. 172 | /// 173 | /// - Note: The default behavior is the _opposite_ of the default 174 | /// parameter passed to this builder-function. 175 | public func failOnMissingKeys(_ flag: Bool = true) -> Self { 176 | self._failOnMissingKeys = flag 177 | return self 178 | } 179 | 180 | /// Set whether decoding should fail when null is decoded for a property 181 | /// with a non-optional type, iff no default value is supplied by the 182 | /// enclosing type. If the key is missing entirely, decoding will not fail. 183 | /// To fail on a missing key, call `failOnMissingKeys()`. 184 | /// 185 | /// By default, Jsum will attempt to synthesize a default value if the 186 | /// property conforms to `JSONCodable`, and _then_ fail if it doesn't. 187 | /// Setting this flag will cause Jsum to skip that step. 188 | /// Transformers are never invoked on null keys. 189 | /// 190 | /// - Note: The default behavior is the _opposite_ of the default 191 | /// parameter passed to this builder-function. 192 | public func failOnNullNonOptionals(_ flag: Bool = true) -> Self { 193 | self._failOnNullNonOptionals = flag 194 | return self 195 | } 196 | 197 | /// Change the key decoding strategy from the default `.useDefaultKeys` 198 | public func keyDecoding(strategy: KeyDecodingStrategy) -> Self { 199 | self._keyDecoding = strategy 200 | return self 201 | } 202 | 203 | /// Change the date decoding strategy from the default `.useDefaultKeys` 204 | public func dateDecoding(strategy: DateDecodingStrategy) -> Self { 205 | self._dateDecoding = strategy 206 | return self 207 | } 208 | 209 | /// Change the data decoding strategy from the default `.useDefaultKeys` 210 | public func dataDecoding(strategy: DataDecodingStrategy) -> Self { 211 | self._dataDecoding = strategy 212 | return self 213 | } 214 | 215 | /// Try to decode an instance of `T` from the given JSON object with the default options. 216 | public static func tryDecode(_ type: T.Type = T.self, from json: Any) -> Result { 217 | return Jsum().tryDecode(type, from: json) 218 | } 219 | 220 | /// Decode an instance of `T` from the given JSON object with the default options. 221 | public static func decode(_ type: T.Type = T.self, from json: Any) throws -> T { 222 | return try Jsum().decode(type, from: json) 223 | } 224 | 225 | /// Try to decode an instance of `T` from the given JSON object. 226 | public func tryDecode(_ type: T.Type = T.self, from json: Any) -> Result { 227 | do { 228 | let value: T = try self.decode(type, from: json) 229 | return .success(value) 230 | } catch { 231 | if let error = error as? Jsum.Error { 232 | return .failure(error) 233 | } 234 | 235 | return .failure(.other(error)) 236 | } 237 | } 238 | 239 | /// Decode an instance of `T` from the given JSON object. 240 | public func decode(_ type: T.Type = T.self, from json: Any) throws -> T { 241 | let metadata = reflect(type) 242 | let box = try self.decode(type: metadata, from: json) 243 | return box as! T 244 | } 245 | 246 | /// Decode the 247 | public static func synthesize(_ type: T.Type = T.self) throws -> T { 248 | let metadata = reflect(type) 249 | return try self.synthesize(type: metadata, asJSON: false) as! T 250 | } 251 | 252 | public static func synthesizeJSON(_ type: Any.Type) throws -> Any { 253 | let metadata = reflect(type) 254 | return try self.synthesize(type: metadata, asJSON: true) 255 | } 256 | 257 | // MARK: Private: decoding 258 | 259 | private func decode(type metadata: Metadata, from json: Any) throws -> Any { 260 | // Case: NSNull and not optional 261 | if json is NSNull && metadata.kind != .optional { 262 | throw Error.couldNotDecode( 263 | "Type '\(metadata.type)' cannot be converted from null" 264 | ) 265 | } 266 | 267 | // Case: Strings, arrays of exact type, etc... 268 | guard metadata.type != type(of: json) else { 269 | return json 270 | } 271 | 272 | switch metadata.kind { 273 | case .struct: 274 | let structure = metadata as! StructMetadata 275 | if structure.isBuiltin { 276 | return try Self.decodeBuiltinStruct(structure, from: json) 277 | } else { 278 | return try self.decodeStruct(structure, from: json) 279 | } 280 | case .class: 281 | return try self.decodeClass(metadata as! ClassMetadata, from: json) 282 | case .enum: 283 | 284 | /// This is needed because we cannot write `as? enumType.RawValue` below, 285 | /// because the compiler expects a type literal, not a type variable 286 | func initRawRepresentable(_ _: R.Type, with value: Any) -> R? { 287 | guard let value = value as? R.RawValue else { 288 | return nil 289 | } 290 | 291 | return R.init(rawValue: value) 292 | } 293 | 294 | // Currently, we cannot initialize enums with raw values 295 | // by hand, so we have to call the decode method. In the 296 | // future, we will cast to RawRepresentable and call then 297 | // `init(rawValue:)` with `.defaultJSON.unwrapped` that way 298 | guard let enumType = metadata.type as? any RawRepresentable.Type else { 299 | throw Error.decodingNotSupported("Enum must be RawRepresentable") 300 | } 301 | 302 | return initRawRepresentable(enumType, with: json) as Any 303 | case .optional: 304 | let optional = metadata as! EnumMetadata 305 | if json is NSNull { 306 | let none = AnyExistentialContainer(nil: optional) 307 | return none.toAny 308 | } else { 309 | // let wrapped = optional.genericMetadata.first! 310 | let value = try self.decode(type: optional.genericMetadata.first!, from: json) 311 | if let emptiable = value as? Emptyable { 312 | // if let codable = wrapped as? JSONCodable.Type, !codable.synthesizesDefaultJSON { 313 | // TODO: runtime equatable check once existentials are unlocked 314 | // For now, explicitly check for String, Int, and Array 315 | if emptiable.isEmpty { 316 | let none = AnyExistentialContainer(nil: optional) 317 | return none.toAny 318 | } 319 | } 320 | 321 | return value 322 | } 323 | case .tuple: 324 | return try self.decodeTuple(metadata as! TupleMetadata, from: json) 325 | default: 326 | throw Error.decodingNotSupported( 327 | "Cannot decode kind \(metadata.kind) (\(metadata.type)" 328 | ) 329 | } 330 | } 331 | 332 | private static func synthesize(type metadata: Metadata, asJSON: Bool = false) throws -> Any { 333 | if let value = self.defaultJSONValue(for: metadata, synthesize: false) { 334 | return asJSON ? value : value.unwrapped 335 | } 336 | 337 | switch metadata.kind { 338 | case .struct: 339 | let structure = metadata as! StructMetadata 340 | if structure.isBuiltin { 341 | assert(!asJSON) // Built-ins should all have a defaultJSON 342 | return try self.decodeBuiltinStruct(structure, from: 0) 343 | } else { 344 | let properties = try structure.fields.reduce(into: [String: Any]()) { (props, field) in 345 | props[field.name] = try self.synthesize(type: field.type, asJSON: asJSON) 346 | } 347 | if asJSON { 348 | return JSON.object(properties as! [String: JSON]) 349 | } else { 350 | return structure.createInstance(props: properties) 351 | } 352 | } 353 | case .class: 354 | let cls = metadata as! ClassMetadata 355 | let properties = try cls.fields.reduce(into: [String: Any]()) { (props, field) in 356 | props[field.name] = try self.synthesize(type: field.type, asJSON: asJSON) 357 | } 358 | if asJSON { 359 | return JSON.object(properties as! [String: JSON]) 360 | } else { 361 | return cls.createInstance(props: properties) as AnyObject 362 | } 363 | case .enum: fallthrough 364 | case .optional: 365 | // Currently, we cannot synthesize enums with raw values 366 | // by hand, so we have to call the decode method. In the 367 | // future, we will cast to RawRepresentable and call then 368 | // `init(rawValue:)` with `.defaultJSON.unwrapped` that way 369 | guard let codable = metadata.type as? JSONCodable.Type else { 370 | throw Error.notYetImplemented 371 | } 372 | 373 | if asJSON { 374 | return codable.defaultJSON 375 | } else { 376 | return try codable.decode(from: codable.defaultJSON) 377 | } 378 | case .tuple: 379 | let tuple = metadata as! TupleMetadata 380 | return tuple.createInstance(elements: try tuple.elements.map { 381 | try self.synthesize(type: $0.metadata, asJSON: asJSON) 382 | }) 383 | default: 384 | throw Error.decodingNotSupported( 385 | "Cannot decode kind \(metadata.kind) (\(metadata.type))" 386 | ) 387 | } 388 | } 389 | 390 | /// Returns an error if null is encountered with `failOnNullNonOptionals` enabled, 391 | /// or if null is encountered and no default value can be generated at all. 392 | /// Or, if the type is optional, the value is returned whether or not it is null. 393 | private func unboxField(_ value: Any, type metadata: Metadata) -> Result { 394 | if value is NSNull && metadata.kind != .optional { 395 | // Null found, user wants error thrown 396 | if self._failOnNullNonOptionals { 397 | return .failure(.nullFoundOnNonOptional) 398 | } 399 | // If the type we're given is JSONCodable, use the type's default value 400 | else if let codable = metadata.type as? JSONCodable.Type { 401 | // Results in fatalError if the type doesn't override `defaultJSON` 402 | return .success(codable.defaultJSON.unwrapped) 403 | } 404 | // It is not JSONCodable so there is no default value; throw an error 405 | else { 406 | return .failure(.nullFoundWithNoDefaultValue) 407 | } 408 | } else { 409 | // Value may be NSNull here, but if it is, the field type is optional 410 | return .success(value) 411 | } 412 | } 413 | 414 | /// Encode a propery key to its corresponding JSON key 415 | private func encodeKey(_ propertyKey: String) throws -> String { 416 | switch self._keyDecoding { 417 | case .usePropertyKeys: 418 | return propertyKey 419 | case .convertFromSnakeCase: 420 | return Jsum.camelCaseToSnakeCase(propertyKey) 421 | case .custom(let transformer): 422 | /// Reverse because we are ENCODING the property 423 | /// key to its JSON key counterpart 424 | return try transformer.reverse(propertyKey) 425 | } 426 | } 427 | 428 | /// Note that these will need to be decoded before assignment 429 | private static func defaultJSONValue(for type: Metadata, synthesize: Bool = true) -> JSON? { 430 | if let codable = type.type as? JSONCodable.Type { 431 | // Only unwrap if we aren't opting-out of synthesizing or 432 | // if the type does not synthesize the default JSON 433 | if synthesize || !codable.synthesizesDefaultJSON { 434 | return codable.defaultJSON 435 | } 436 | } 437 | 438 | return nil 439 | } 440 | 441 | private func decode(properties: [String: Any], forType metadata: M) throws -> [String: Any] { 442 | let (transformers, jsonMap, defaults) = metadata.jsonCodableInfoByProperty 443 | var decodedProps: [String: Any] = [:] 444 | 445 | /// This cannot be moved because it relies on local variable captures 446 | /// 447 | /// Throws on invalid JSON key path (i.e. key path goes to a non-object 448 | /// somewhere before the end). The behavior for a missing key is 449 | /// defined by _failOnMissingKeys. If true, it will throw an error. 450 | func valueForProperty(_ propertyKey: String, _ type: Metadata) throws -> Any? { 451 | // Did the user specify a new key path for this property? 452 | if let jsonKeyPathForProperty = jsonMap[propertyKey] { 453 | if let optionalValue = try properties.jsum_value(for: jsonKeyPathForProperty) { 454 | let unboxResult = unboxField(optionalValue, type: type) 455 | 456 | // Null found and property is non-optional, user wants error thrown 457 | if case .failure(let unboxError) = unboxResult { 458 | // Did we encounter null with `failOnNullNonOptionals` enabled? 459 | if case .nullFoundOnNonOptional = unboxError { 460 | // Yes, check if a default value was supplied 461 | if defaults[propertyKey] != nil { 462 | // Return nil to use default value later on 463 | return nil 464 | } 465 | } 466 | 467 | // No, we encountered null after c 468 | // TODO defaults? 469 | throw unboxError 470 | } 471 | 472 | // Value found; coerce NSNull to nil 473 | return optionalValue is NSNull ? nil : optionalValue 474 | } else if self._failOnMissingKeys { 475 | // Value not found, user wants error thrown 476 | throw Error.missingKey(propertyKey, jsonKeyPathForProperty) 477 | } else { 478 | // Value not found, user wants decoding to continue; 479 | // if no default value is ever supplied, .missingKey 480 | // will be thrown later on 481 | return nil 482 | } 483 | } 484 | 485 | // User did not override the key path for this property; 486 | // Transform the key if needed before accessing JSON with it 487 | let encodedPropertyKey = try self.encodeKey(propertyKey) 488 | let value = properties[encodedPropertyKey] 489 | if value == nil { 490 | if self._failOnMissingKeys { 491 | // Value not found, user wants error thrown 492 | throw Error.missingKey(propertyKey, encodedPropertyKey) 493 | } 494 | // Value not found, user wants decoding to continue 495 | return nil 496 | } 497 | 498 | // Null found and property is non-optional, user wants error thrown 499 | if value is NSNull && self._failOnNullNonOptionals && type.kind != .optional { 500 | throw Error.nullFoundOnNonOptional 501 | } 502 | 503 | // value is not nil here; coerce NSNull to nil 504 | return value! is NSNull ? nil : value 505 | } 506 | 507 | for (key, type) in metadata.fields { 508 | // Throws on missing keys if _failOnMissingKeys = true. 509 | // Throws on null keys if _failOnNullNonOptionals = true. 510 | if var value = try valueForProperty(key, type) { 511 | assert(!(value is NSNull)) 512 | 513 | // Transform value first, if desired 514 | if let transform = transformers[key] { 515 | value = try transform.transform(forward: value) 516 | } 517 | 518 | // Perform decoding; if the transformed value already 519 | // matches the desired type, this should be a no-op 520 | decodedProps[key] = try self.decode(type: type, from: value) 521 | } else { 522 | // Check if a default value was supplied 523 | if let defaultValue = defaults[key] { 524 | decodedProps[key] = defaultValue 525 | } 526 | // If the type we're given is JSONCodable, use the type's default value 527 | // TODO: should we just try to synthesize it instead? 528 | else if let defaultValue = Self.defaultJSONValue(for: type)?.unwrapped { 529 | // Decode the default value to the expected type 530 | decodedProps[key] = try self.decode(type: type, from: defaultValue) 531 | } 532 | // User didn't opt-into missing key errors early on, so we expected 533 | // them to supply a default value or use a JSONCodable type with 534 | // `defaultJSON` implemented, and we got neither, so here we are 535 | else { 536 | throw Error.missingKey(key, jsonMap[key] ?? key) 537 | } 538 | } 539 | } 540 | 541 | return decodedProps 542 | } 543 | 544 | // MARK: Class decoding 545 | 546 | private func decodeClass(_ metadata: ClassMetadata, from json: Any) throws -> AnyObject { 547 | guard let json = json as? [String: Any] else { 548 | throw Error.couldNotDecode("Cannot decode classes and most structs without a dictionary") 549 | } 550 | 551 | let decodedProps = try self.decode(properties: json, forType: metadata) 552 | return metadata.createInstance(props: decodedProps) 553 | } 554 | 555 | // MARK: Struct decoding 556 | 557 | private func decodeStruct(_ metadata: StructMetadata, from data: Any) throws -> Any { 558 | assert(!metadata.isBuiltin) 559 | 560 | guard let json = data as? [String: Any] else { 561 | // Case: decoding an array 562 | if let array = data as? [Any], metadata.descriptor == KnownMetadata.array { 563 | let elementType = metadata.genericMetadata.first! 564 | let mapped = try array.map { try self.decode(type: elementType, from: $0) } 565 | return try metadata.dynamicCast(from: mapped) 566 | } 567 | 568 | // Case: decoding a Date or Data 569 | if metadata.isDateOrData { 570 | if metadata.descriptor == KnownMetadata.date { 571 | return try self.decodeDate(from: data, strategy: _dateDecoding) 572 | } else { 573 | return try self.decodeData(from: data, strategy: _dataDecoding) 574 | } 575 | } 576 | 577 | // Case: decoding a string/bool/number from an foundation type 578 | if let cast = try? metadata.dynamicCast(from: data) { 579 | return cast 580 | } 581 | 582 | // Case: decoding another special-cased struct, i.e. URL 583 | if let transformer = Jsum.specialCaseStructs[metadata.descriptor.ptr] { 584 | return try transformer.transform(forward: data) 585 | } 586 | 587 | // Case: decoding a JSONCodable from another JSONCodable without a transformer 588 | if let convertedValue = try metadata.attemptJSONCodableConversion(value: data) { 589 | return convertedValue 590 | } 591 | 592 | throw Error.couldNotDecode("Cannot decode classes and most structs without a dictionary") 593 | } 594 | 595 | // Case: decoding a dictionary 596 | // TODO: Allow decoding from more complex dictionary types 597 | if metadata.descriptor == KnownMetadata.dictionary { 598 | let elementType = metadata.genericMetadata[1] 599 | let mapped = try json.mapValues { try self.decode(type: elementType, from: $0) } 600 | return try metadata.dynamicCast(from: mapped) 601 | } 602 | 603 | let decodedProps = try self.decode(properties: json, forType: metadata) 604 | return metadata.createInstance(props: decodedProps) 605 | } 606 | 607 | // MARK: Specialized decoding 608 | 609 | private static func decodeBuiltinStruct(_ metadata: StructMetadata, from json: Any) throws -> Any { 610 | assert(metadata.isBuiltin) 611 | 612 | // Types are identical: return the value itself 613 | if type(of: json) == metadata.type { 614 | return json 615 | } else { 616 | guard let nsnumber = json as AnyObject as? NSNumber else { 617 | // We are trying to decode a number from something other than a number; 618 | // There is no transformer for whatever we're decoding, so try to 619 | // implicitly convert it if both types conform to JSONCodable 620 | if let convertedValue = try metadata.attemptJSONCodableConversion(value: json) { 621 | return convertedValue 622 | } 623 | 624 | throw Error.couldNotDecode( 625 | "Cannot convert non-JSONCodable type '\(metadata.type)' to a number" 626 | ) 627 | } 628 | 629 | return self.convert(number: nsnumber, to: metadata.type) 630 | } 631 | } 632 | 633 | private func decodeDate(from json: Any, strategy: DateDecodingStrategy) throws -> Any { 634 | switch strategy { 635 | case .bestGuess: 636 | if let stringyDate = json as? String { 637 | return try self.decodeDate(from: stringyDate, strategy: .iso8601) 638 | } 639 | if let number = json as? NSNumber { 640 | return try self.decodeDate(from: number, strategy: .secondsSince1970) 641 | } 642 | 643 | throw Error.couldNotDecode("Tried decoding Date but neither string nor number found") 644 | 645 | case .secondsSince1970: 646 | guard let number = json as? NSNumber else { 647 | throw Error.couldNotDecode("Cannot decode non-number as UNIX timestamp Date") 648 | } 649 | 650 | return Date(timeIntervalSince1970: number.doubleValue) 651 | 652 | case .millisecondsSince1970: 653 | guard let number = json as? NSNumber else { 654 | throw Error.couldNotDecode("Cannot decode non-number as UNIX timestamp Date") 655 | } 656 | 657 | return Date(timeIntervalSince1970: number.doubleValue / 1000.0) 658 | 659 | case .iso8601: 660 | if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { 661 | guard let stringyDate = json as? String else { 662 | throw Error.couldNotDecode("DateDecodingStrategy.iso8601 requires a string") 663 | } 664 | guard let date = self._iso8601Formatter.date(from: stringyDate) else { 665 | throw Error.couldNotDecode("Expected date string to be ISO8601-formatted") 666 | } 667 | 668 | return date 669 | } 670 | else { 671 | fatalError("ISO8601DateFormatter is unavailable on this platform") 672 | } 673 | 674 | case .formatter(let formatter): 675 | guard let stringyDate = json as? String else { 676 | throw Error.couldNotDecode("DateDecodingStrategy.formatter requires a string") 677 | } 678 | guard let date = formatter.date(from: stringyDate) else { 679 | throw Error.couldNotDecode("Date string does not match expected format") 680 | } 681 | 682 | return date 683 | 684 | case .custom(let closure): 685 | return try closure(json) 686 | } 687 | } 688 | 689 | private func decodeData(from json: Any, strategy: DataDecodingStrategy) throws -> Any { 690 | switch strategy { 691 | case .base64: 692 | guard let base64EncodedData = json as? String else { 693 | throw Error.couldNotDecode("DataDecodingStrategy.base64 expects a string") 694 | } 695 | guard let data = Data(base64Encoded: base64EncodedData) else { 696 | throw Error.couldNotDecode("String was expected to be base 64 encoded") 697 | } 698 | 699 | return data 700 | 701 | case .custom(let closure): 702 | return try closure(json) 703 | } 704 | } 705 | 706 | // MARK: Tuple decoding 707 | 708 | private func decodeTuple(_ tupleMetadata: TupleMetadata, from json: Any) throws -> Any { 709 | // Allocate space for the tuple 710 | var box = AnyExistentialContainer(metadata: tupleMetadata) 711 | let boxBuffer = box.getValueBuffer() 712 | 713 | // Populate the tuple from an array or dictionary and return a copy of it 714 | if let array = json as? [Any] { 715 | try self.populate(tuple: boxBuffer, from: array, tupleMetadata) 716 | return box.toAny 717 | } 718 | if let dictionary = json as? [String: Any] { 719 | try self.populate(tuple: boxBuffer, from: dictionary, tupleMetadata) 720 | return box.toAny 721 | } 722 | 723 | // TODO: support converting structs / classes to tuples 724 | 725 | // Error: we were not given an array or dictionary 726 | throw Error.decodingNotSupported("Tuples can only be decoded from arrays or dictionaries") 727 | } 728 | 729 | private func populate(tuple: RawPointer, from array: [Any], _ metadata: TupleMetadata) throws { 730 | guard array.count == metadata.elements.count else { 731 | throw Error.couldNotDecode("Array size must match number of elements in tuple type") 732 | } 733 | 734 | // Copy each element of the array to each tuple element at the specified offset 735 | for (e, value) in zip(metadata.elements, array) { 736 | try self.populate(element: e, ofTuple: tuple, with: value) 737 | } 738 | } 739 | 740 | private func populate(tuple: RawPointer, from dict: [String: Any], _ metadata: TupleMetadata) throws { 741 | // Transform the keys if needed before accessing JSON with it 742 | let keys = try metadata.labels.map { try self.encodeKey($0) } 743 | 744 | // Copy each value of the dictionary to each tuple element with the same name at the specified offset 745 | for (e,key) in zip(metadata.elements, keys) { 746 | guard let value = dict[key] else { 747 | throw Error.couldNotDecode("Missing tuple element value for expected payload key '\(key)'") 748 | } 749 | 750 | try self.populate(element: e, ofTuple: tuple, with: value) 751 | } 752 | } 753 | 754 | private func populate(element e: TupleMetadata.Element, ofTuple tuple: RawPointer, with value: Any) throws { 755 | // Assert types match and perform nullability checks before decoding 756 | let unboxedValue = try self.unboxField(value, type: e.metadata).get() 757 | let decodedValue = try self.decode(type: e.metadata, from: unboxedValue) 758 | 759 | var valueBox = container(for: decodedValue) 760 | tuple.copyMemory(ofTupleElement: valueBox.getValueBuffer(), layout: e) 761 | } 762 | } 763 | 764 | -------------------------------------------------------------------------------- /Sources/NSNumber+Jsum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSNumber+Jsum.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 5/13/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSNumber { 11 | private func _objcTypeIsAny(of types: String) -> Bool { 12 | return types.contains(self._objcTypeChar) 13 | } 14 | 15 | private func _objcType(is type: String) -> Bool { 16 | return self.objCType.pointee == type.utf8CString.first 17 | } 18 | 19 | private var _objcTypeChar: Character { 20 | return Character(Unicode.Scalar(UInt8(self.objCType.pointee))) 21 | } 22 | 23 | var isInt: Bool { 24 | return self._objcTypeIsAny(of: "liscqLISCQ") 25 | } 26 | 27 | var isFloat: Bool { 28 | return self._objcTypeIsAny(of: "dfD") 29 | } 30 | 31 | var isBool: Bool { 32 | return self._objcType(is: "B") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/NumericConversions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumericConversions.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/26/21. 6 | // 7 | 8 | import Foundation 9 | 10 | prefix operator ~ 11 | private prefix func ~(target: Any) -> T { 12 | return target as! T 13 | } 14 | 15 | extension Jsum { 16 | static func convert(number: NSNumber, to desiredType: Any.Type) -> Any { 17 | switch desiredType { 18 | case is Int8.Type: return number.int8Value 19 | case is Int16.Type: return number.int16Value 20 | case is Int32.Type: return number.int32Value 21 | case is Int64.Type: return number.int64Value 22 | case is Int.Type: return number.intValue 23 | 24 | case is UInt8.Type: return number.uint8Value 25 | case is UInt16.Type: return number.uint16Value 26 | case is UInt32.Type: return number.uint32Value 27 | case is UInt64.Type: return number.uint64Value 28 | case is UInt.Type: return number.uintValue 29 | 30 | case is Float32.Type: return Float32(number.floatValue) 31 | case is Float64.Type: return Float64(number.doubleValue) 32 | case is Float.Type: return number.floatValue 33 | case is Double.Type: return number.doubleValue 34 | 35 | default: fatalError("Unexpected type '\(desiredType)'") 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PointerExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // ReflexTests 4 | // 5 | // Created by Tanner Bennett on 4/12/21. 6 | // Copyright © 2021 Tanner Bennett. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @_implementationOnly import Echo 11 | 12 | typealias RawPointer = UnsafeMutableRawPointer 13 | 14 | extension UnsafeRawPointer { 15 | subscript(offset: Int) -> T { 16 | get { 17 | return self.load(fromByteOffset: offset, as: T.self) 18 | } 19 | } 20 | } 21 | 22 | extension RawPointer { 23 | /// Generic subscript. Do not use when T = Any unless you mean it... 24 | subscript(offset: Int) -> T { 25 | get { 26 | return self.load(fromByteOffset: offset, as: T.self) 27 | } 28 | 29 | set { 30 | self.storeBytes(of: newValue, toByteOffset: offset, as: T.self) 31 | } 32 | } 33 | 34 | /// Allocates space for a structure (or enum?) without an initial value 35 | static func allocateBuffer(for type: Metadata) -> Self { 36 | return RawPointer.allocate( 37 | byteCount: type.vwt.size, 38 | alignment: type.vwt.flags.alignment 39 | ) 40 | } 41 | 42 | /// Allocates space for and stores a value. 43 | /// You should probably use AnyExistentialContainer instead. 44 | init(wrapping value: Any, withType metadata: Metadata) { 45 | self = RawPointer.allocateBuffer(for: metadata) 46 | self.storeBytes(of: value, type: metadata) 47 | } 48 | 49 | /// For storing a value from an Any container 50 | func storeBytes(of value: Any, type: Metadata, offset: Int = 0) { 51 | var box = container(for: value) 52 | type.vwt.initializeWithCopy((self + offset), box.projectValue()~) 53 | // (self + offset).copyMemory(from: box.projectValue(), byteCount: type.vwt.size) 54 | } 55 | 56 | /// For copying a tuple element instance from a pointer 57 | func copyMemory(ofTupleElement valuePtr: UnsafeRawPointer, layout e: TupleMetadata.Element) { 58 | e.metadata.vwt.initializeWithCopy((self + e.offset), valuePtr~) 59 | // (self + e.offset).copyMemory(from: valuePtr, byteCount: e.metadata.vwt.size) 60 | } 61 | 62 | /// For copying a type instance from a pointer 63 | func copyMemory(from pointer: RawPointer, type: Metadata, offset: Int = 0) { 64 | type.vwt.initializeWithCopy((self + offset), pointer) 65 | // (self + offset).copyMemory(from: pointer, byteCount: type.vwt.size) 66 | } 67 | } 68 | 69 | extension Unmanaged where Instance == AnyObject { 70 | /// Quickly retain an object before you write its address to memory or something 71 | @discardableResult 72 | static func retainIfObject(_ thing: Any) -> Bool { 73 | if container(for: thing).metadata.kind.isObject { 74 | _ = self.passRetained(thing as AnyObject).retain() 75 | return true 76 | } 77 | 78 | return false 79 | } 80 | } 81 | 82 | postfix operator ~ 83 | postfix func ~(target: T) -> RawPointer { 84 | return unsafeBitCast(target, to: RawPointer.self) 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Transformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transformer.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // Copyright © 2021 Tanner Bennett. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum TransformError: Error { 12 | case notConvertible 13 | } 14 | 15 | /// A non-generic transformer class. You should use `Transform` first if you can. 16 | public class OpaqueTransformer { 17 | public typealias Transformation = (Any?) throws -> Any 18 | fileprivate var forwardBlock: Transformation? = nil 19 | fileprivate var reverseBlock: Transformation? = nil 20 | 21 | internal init() { } 22 | 23 | public init(forwardBlock: @escaping Transformation) { 24 | self.forwardBlock = forwardBlock 25 | self.reverseBlock = nil 26 | } 27 | 28 | public init(forwardBlock: @escaping Transformation, reverseBlock: Transformation? = nil) { 29 | self.forwardBlock = forwardBlock 30 | self.reverseBlock = reverseBlock 31 | } 32 | 33 | public func transform(forward value: Any?) throws -> Any { 34 | return try self.forwardBlock!(value) 35 | } 36 | 37 | public func transform(reverse value: Any?) throws -> Any { 38 | return try self.reverseBlock!(value) 39 | } 40 | } 41 | 42 | public typealias AnyTransformer = OpaqueTransformer 43 | 44 | public class Transform: OpaqueTransformer { 45 | public typealias ForwardTransformation = (T?) throws -> U 46 | public typealias ReverseTransformation = (U?) throws -> T 47 | 48 | private var _forwardBlock: ForwardTransformation? { self.forwardBlock as! ForwardTransformation? } 49 | private var _reverseBlock: ReverseTransformation? { self.reverseBlock as! ReverseTransformation? } 50 | 51 | public enum Error: Swift.Error { 52 | case typeMismatch(given: Any.Type, expected: Any.Type) 53 | } 54 | 55 | static var snakeCaseToCamelCase: Transform { 56 | .init(forwardBlock: { 57 | return Jsum.snakeCaseToCamelCase($0!) 58 | }, reverseBlock: { 59 | return Jsum.camelCaseToSnakeCase($0!) 60 | }) 61 | } 62 | 63 | static var camelCaseToSnakeCase: Transform { 64 | self.snakeCaseToCamelCase.reversed() 65 | } 66 | 67 | func reversed() -> Transform { 68 | guard let reverse = _reverseBlock else { 69 | fatalError("Cannot reverse one-way transformer") 70 | } 71 | 72 | return .init(forwardBlock: reverse, reverseBlock: _forwardBlock!) 73 | } 74 | 75 | public static func transform(_ t: T?) throws -> U { 76 | return try U.decode(from: t?.toJSON ?? T.defaultJSON) 77 | } 78 | 79 | public static func reverse(_ u: U?) throws -> T { 80 | return try T.decode(from: u?.toJSON ?? U.defaultJSON) 81 | } 82 | 83 | public override init() { super.init() } 84 | 85 | public init(forwardBlock: @escaping ForwardTransformation) { 86 | super.init(forwardBlock: { try forwardBlock(($0 as! T)) }) 87 | } 88 | 89 | public init(forwardBlock: @escaping ForwardTransformation, reverseBlock: ReverseTransformation?) { 90 | super.init( 91 | forwardBlock: { try forwardBlock(($0 as! T)) }, 92 | reverseBlock: reverseBlock == nil ? nil : { try reverseBlock!(($0 as! U)) } 93 | ) 94 | } 95 | 96 | public func transform(_ t: T?) throws -> U { 97 | return try Self.transform(t) 98 | } 99 | 100 | public func reverse(_ u: U?) throws -> T { 101 | return try Self.reverse(u) 102 | } 103 | 104 | override public func transform(forward value: Any?) throws -> Any { 105 | guard let t = value as? T? else { 106 | throw Error.typeMismatch(given: type(of: value), expected: T.self) 107 | } 108 | 109 | // If initialized with explicit blocks, use those instead 110 | if self.forwardBlock != nil { 111 | return try super.transform(forward: value) 112 | } 113 | 114 | return try self.transform(t) as Any 115 | } 116 | 117 | override public func transform(reverse value: Any?) throws -> Any { 118 | guard let u = value as? U? else { 119 | throw Error.typeMismatch(given: type(of: value), expected: U.self) 120 | } 121 | 122 | // If initialized with explicit blocks, use those instead 123 | if self.reverseBlock != nil { 124 | return try super.transform(reverse: value) 125 | } 126 | 127 | return try self.reverse(u) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/UntilOpenExistentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tanner Bennett on 5/15/21. 6 | // 7 | 8 | import Foundation 9 | @_implementationOnly import Echo 10 | 11 | extension StructMetadata { 12 | var isSimpleType: Bool { 13 | if self.isBuiltin { return true } 14 | 15 | let desc = self.descriptor 16 | if self.type == String.self || 17 | desc == KnownMetadata.array || 18 | desc == KnownMetadata.dictionary { 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | } 25 | 26 | protocol Emptyable { 27 | var isEmpty: Bool { get } 28 | } 29 | 30 | extension Array: Emptyable { } 31 | extension String: Emptyable { } 32 | extension Dictionary: Emptyable { } 33 | extension Int: Emptyable { 34 | var isEmpty: Bool { self == 0 } 35 | } 36 | extension Double: Emptyable { 37 | var isEmpty: Bool { self == 0 } 38 | } 39 | extension UInt: Emptyable { 40 | var isEmpty: Bool { self == 0 } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/AssumptionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsumTests.swift 3 | // JsumTests 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import Jsum 11 | @testable import Echo 12 | 13 | class Assumptions: XCTestCase { 14 | 15 | func testBuiltinMetadata() { 16 | let intm = reflect(Int.self) 17 | XCTAssert(intm.isBuiltin) 18 | 19 | let stringm = reflect(String.self) 20 | XCTAssert(!stringm.isBuiltin) 21 | } 22 | 23 | func testEmptyStruct() { 24 | struct Nothing { } 25 | 26 | let metadata = reflectStruct(Nothing.self)! 27 | XCTAssert(metadata.fields.isEmpty) 28 | XCTAssert(metadata.vwt.size == 0) 29 | } 30 | 31 | func testNSNumber() { 32 | // Just needs to not crash 33 | let _ = -1234 as AnyObject as! NSNumber 34 | } 35 | 36 | func testGenericMetadataRootTypeEquality() { 37 | let anyarray = reflectStruct([Any].self)! 38 | let intarray = reflectStruct([Int].self)! 39 | 40 | XCTAssert(anyarray.descriptor == intarray.descriptor) 41 | } 42 | 43 | func testFieldedTypeHasNoComputedFields() { 44 | let metadata = reflectClass(Person.self)! 45 | guard let fields = metadata.descriptor?.fields else { 46 | return XCTFail() 47 | } 48 | 49 | XCTAssertEqual(fields.records.count, metadata.fieldOffsets.count) 50 | XCTAssert(fields.records.filter({ $0.name == "tuple" }).isEmpty) 51 | } 52 | 53 | func test_verifyPODs() { 54 | struct JustPrimitives { let i: Int; let d: Double } 55 | struct HasString { let s: String } 56 | struct HasArray { let a: [Int] } 57 | struct HasObject { let a: AnyObject } 58 | enum OneCaseNoPayload { case foo } 59 | enum TwoCasesNoPayloads { case foo, bar } 60 | enum OneCaseIntPayload { case foo(Int) } 61 | enum OneCaseObjPayload { case foo(AnyObject) } 62 | enum OneCaseDuoPayload { case foo(Int, AnyObject) } 63 | 64 | 65 | XCTAssert(reflect(Int.self).vwt.flags.isPOD) 66 | XCTAssert(reflect(Double.self).vwt.flags.isPOD) 67 | XCTAssert(reflect(UnsafeRawPointer.self).vwt.flags.isPOD) 68 | XCTAssert(reflect(JustPrimitives.self).vwt.flags.isPOD) 69 | XCTAssert(reflect(OneCaseNoPayload.self).vwt.flags.isPOD) 70 | XCTAssert(reflect(TwoCasesNoPayloads.self).vwt.flags.isPOD) 71 | XCTAssert(reflect(OneCaseIntPayload.self).vwt.flags.isPOD) 72 | 73 | XCTAssert(!reflect(String.self).vwt.flags.isPOD) 74 | XCTAssert(!reflect([Int].self).vwt.flags.isPOD) 75 | XCTAssert(!reflect(HasString.self).vwt.flags.isPOD) 76 | XCTAssert(!reflect(HasArray.self).vwt.flags.isPOD) 77 | XCTAssert(!reflect(HasObject.self).vwt.flags.isPOD) 78 | XCTAssert(!reflect(AnyObject.self).vwt.flags.isPOD) 79 | XCTAssert(!reflect(OneCaseObjPayload.self).vwt.flags.isPOD) 80 | XCTAssert(!reflect(OneCaseDuoPayload.self).vwt.flags.isPOD) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/Blank.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsumTests.swift 3 | // JsumTests 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import Jsum 11 | @testable import Echo 12 | 13 | class Tests: XCTestCase { 14 | 15 | func test() throws { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/JsumTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsumTests.swift 3 | // JsumTests 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import Jsum 11 | @testable import Echo 12 | 13 | class JsumTests: XCTestCase { 14 | 15 | func testGenericsDecodeCorrectType() throws { 16 | class Base: Codable { } 17 | class Child: Base { 18 | var name: String 19 | required init(from decoder: any Decoder) throws { fatalError() } 20 | } 21 | 22 | let opaqueType: Base.Type = Child.self 23 | let data = ["name": "Hailey"] 24 | 25 | let obj: Base = try Jsum.decode(opaqueType, from: data) 26 | 27 | XCTAssert(type(of: obj) === opaqueType) 28 | } 29 | 30 | func testOpenExistentials() throws { 31 | 32 | enum Colors: String { 33 | case red = "red" 34 | case blue = "blue" 35 | case green = "green" 36 | } 37 | 38 | struct Marker { 39 | let color: Colors 40 | } 41 | 42 | let marker: Marker = try Jsum.decode(from: ["color": "red"]) 43 | let color: Colors = try Jsum.decode(from: "red") 44 | 45 | XCTAssertEqual(marker.color, .red) 46 | XCTAssertEqual(marker.color, color) 47 | 48 | // let inputValue = "red" 49 | // func createColor(_: T.Type) -> T { 50 | // return T.init(rawValue: inputValue as! T.RawValue)! 51 | // } 52 | 53 | // let type: any RawRepresentable.Type = Colors.self 54 | // let i = _openExistential(type, do: createColor(_:)) 55 | // 56 | // XCTAssertEqual(i as? Colors, .red) 57 | } 58 | 59 | func testDecodeTuple() throws { 60 | let person: (name: String, age: Int) = try Jsum.decode( 61 | from: ["name": "Bob", "age": 25] 62 | ) 63 | XCTAssertEqual(person.name, "Bob") 64 | XCTAssertEqual(person.age, 25) 65 | } 66 | 67 | /// Just needs to compile 68 | func testGenericConstraints() throws { 69 | _ = try Transform.transform(5) 70 | _ = try Transform<[Int], [String]>.transform([5]) 71 | 72 | let _: (name: String, age: Int) = try Jsum.decode( 73 | from: ["name": "Bob", "age": 25] 74 | ) 75 | } 76 | 77 | func testProtocolConformances() { 78 | let person = reflectClass(Person.self)! 79 | XCTAssert(person.conforms(to: Conformable.self)) 80 | } 81 | 82 | func testBuiltinDecodeProtocolMethod() throws { 83 | let int = reflectStruct(Int.self)! 84 | XCTAssert(int.conforms(to: JSONCodable.self)) 85 | 86 | XCTAssertEqual(try Jsum.decode(from: 5), 5) 87 | XCTAssertEqual(try Jsum.decode(from: 3.14159), 3.14159) 88 | } 89 | 90 | func testAllocObject() { 91 | let expect = XCTestExpectation(description: "deinit") 92 | let cls = reflectClass(JustDeallocateMe.self)! 93 | 94 | DispatchQueue.global().async { 95 | var _: JustDeallocateMe = cls.createInstance(props: ["expectation": expect]) 96 | } 97 | 98 | self.wait(for: [expect], timeout: 20) 99 | } 100 | 101 | func testJSONCodableExistentials() { 102 | let type: JSONCodable.Type = Employee.self as JSONCodable.Type 103 | XCTAssert(type.isClass) 104 | 105 | XCTAssertEqual(Employee.bob.toJSON.asObject, ["class": JSON.string("Employee")]) 106 | } 107 | 108 | func testDecodeEmployee() throws { 109 | let data: [String : Any] = [ 110 | "position": "Programmer", 111 | "salary": 120_000, 112 | "cubicleSize": ["width": 9, "height": 5], 113 | "name": "Janice", 114 | "age": 30 115 | ] 116 | 117 | let employee: Employee = try Jsum.decode(from: data) 118 | 119 | XCTAssertEqual(employee.name, data["name"] as! String) 120 | XCTAssertEqual(employee.age, data["age"] as! Int) 121 | XCTAssertEqual(employee.cubicleSize, Size(width: 9, height: 5)) 122 | XCTAssertEqual(employee.position, data["position"] as! String) 123 | XCTAssertEqual(employee.salary, Double(data["salary"] as! Int)) 124 | } 125 | 126 | func testDecodePostWithCustomTransformers() throws { 127 | let data: [String: Any] = [ 128 | "title": "My cat is so cute", 129 | "body_markdown": NSNull(), 130 | "details": [ 131 | "score": "-25034", 132 | "upvoted": NSNull() 133 | ] 134 | ] 135 | 136 | let post: Post = try Jsum.decode(from: data) 137 | XCTAssertEqual(post.score, -25034) 138 | XCTAssertEqual(post.saved, false) 139 | XCTAssertEqual(post.upvoted, false) 140 | XCTAssertEqual(post.body, nil) 141 | } 142 | 143 | func testOptionals() { 144 | // Just needs to not crash 145 | let anyOptional: Any? = nil 146 | let _: Int? = anyOptional as! Int? 147 | let any = anyOptional as Any 148 | 149 | // Assumptions about types 150 | let type = reflect(any) 151 | XCTAssertEqual(type.kind, .optional) 152 | XCTAssert((type as! EnumMetadata).genericMetadata.first!.type == Any.self) 153 | } 154 | 155 | func testDefaultValues() { 156 | var json: [String: Any] = [:] 157 | 158 | var instance: JustDecodeMe = try! Jsum.decode(from: json) 159 | XCTAssertEqual(instance.truth, true) 160 | XCTAssertEqual(instance.five, 5) 161 | XCTAssertEqual(instance.pie, 3.14) 162 | 163 | json = ["truth": false, "five": 1.168, "pie": 10] 164 | instance = try! Jsum.decode(from: json) 165 | 166 | XCTAssertEqual(instance.truth, false) 167 | XCTAssertEqual(instance.five, 1) 168 | XCTAssertEqual(instance.pie, 10.0) 169 | } 170 | 171 | func testDecodeCollections() { 172 | let strings = ["1", "2", "3"] 173 | let nums: [Int] = try! Jsum.decode(from: strings) 174 | XCTAssertEqual(nums, [1, 2, 3]) 175 | 176 | let numMap = ["a": 1, "b": 2, "c": 3] 177 | let stringMap: [String: String] = try! Jsum.decode(from: numMap) 178 | XCTAssertEqual(stringMap, ["a": "1", "b": "2", "c": "3"]) 179 | } 180 | 181 | func testDecodeDates() throws { 182 | let formatter = ISO8601DateFormatter() 183 | formatter.formatOptions = [formatter.formatOptions, .withFractionalSeconds] 184 | 185 | let now = Date().ignoringTime 186 | let j = Jsum() 187 | let dateString = formatter.string(from: now) 188 | 189 | var date: Date = try j.decode(from: dateString) 190 | XCTAssertEqual(date, now) 191 | 192 | date = try j.decode(from: now.timeIntervalSince1970) 193 | XCTAssertEqual(date, now) 194 | } 195 | 196 | func testDecodeData() throws { 197 | let string = "Hello, world!" 198 | let base64 = string.data(using: .utf8)!.base64EncodedString() 199 | 200 | let data: Data = try Jsum.decode(from: base64) 201 | XCTAssertEqual(String(data: data, encoding: .utf8), string) 202 | } 203 | 204 | func testDecodeURL() throws { 205 | struct HasURL: Equatable { 206 | var url: URL 207 | } 208 | let urlString = "https://google.com/" 209 | let obj = ["url": urlString] 210 | 211 | let hasurl: HasURL = try Jsum.decode(from: obj) 212 | XCTAssertEqual(URL(string: urlString)!, hasurl.url) 213 | } 214 | 215 | func testFailOnNullNonOptionals() { 216 | var json: [String: Any] = ["x": 5] 217 | // y is missing; we want it to default to 0 218 | var result = Jsum.tryDecode(Point.self, from: json) 219 | XCTAssert(result.succeeded) 220 | 221 | // y is missing and non-optional; we want it to default to 0 222 | result = Jsum().failOnNullNonOptionals().tryDecode(Point.self, from: json) 223 | XCTAssert(result.succeeded) 224 | 225 | json = ["x": 5, "y": NSNull()] 226 | // y is not missing but is nil/null; we want it to fail decoding 227 | result = Jsum().failOnNullNonOptionals().tryDecode(Point.self, from: json) 228 | XCTAssert(result.failed) 229 | 230 | // TODO: tests for tuples and enums with payloads, and default values 231 | } 232 | 233 | func testFailOnMissingKeys() { 234 | var json: [String: Any] = ["x": 5] 235 | // y is missing; we want it to default to 0 236 | var result = Jsum.tryDecode(Point.self, from: json) 237 | XCTAssert(result.succeeded) 238 | 239 | // y is missing; we want it to fail decoding 240 | result = Jsum().failOnMissingKeys().tryDecode(Point.self, from: json) 241 | XCTAssert(result.failed) 242 | 243 | json = ["x": 5, "y": NSNull()] 244 | // y is null; we want it to default to 0 245 | result = Jsum.tryDecode(Point.self, from: json) 246 | XCTAssert(result.succeeded) 247 | 248 | // y is not missing, just null; we want it to default to 0 249 | result = Jsum().failOnMissingKeys().tryDecode(Point.self, from: json) 250 | XCTAssert(result.succeeded) 251 | 252 | // TODO: tests for tuples and enums with payloads, and default values 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Tests/SampleTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleTypes.swift 3 | // ReflexTests 4 | // 5 | // Created by Tanner Bennett on 4/12/21. 6 | // Copyright © 2021 Tanner Bennett. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Jsum 11 | import XCTest 12 | 13 | protocol Conformable { } 14 | 15 | struct Counter { 16 | var count: T = 5 17 | } 18 | 19 | struct Point: Equatable { 20 | var x: Int = 0 21 | var y: Int = 0 22 | } 23 | 24 | struct Size: Equatable { 25 | var width: Int = 0 26 | var height: Int = 0 27 | } 28 | 29 | class Person: Equatable, Conformable { 30 | var name: String 31 | var age: Int 32 | 33 | var tuple: (String, Int) { 34 | return (self.name, self.age) 35 | } 36 | 37 | internal init(name: String, age: Int) { 38 | self.name = name 39 | self.age = age 40 | } 41 | 42 | static func == (lhs: Person, rhs: Person) -> Bool { 43 | return lhs.name == rhs.name && lhs.age == rhs.age 44 | } 45 | 46 | func sayHello() { 47 | print("Hello!") 48 | } 49 | } 50 | 51 | class Employee: Person, JSONCodable { 52 | private(set) var position: String 53 | private(set) var salary: Double 54 | let cubicleSize = Size(width: 5, height: 7) 55 | 56 | var job: (position: String, salary: Double) { 57 | return (self.position, self.salary) 58 | } 59 | 60 | static var bob = Employee(name: "Bob", age: 52, position: "Programmer") 61 | 62 | internal init(name: String, age: Int, position: String, salary: Double = 60_000) { 63 | self.position = position 64 | self.salary = salary 65 | super.init(name: name, age: age) 66 | } 67 | 68 | func promote() -> (position: String, salary: Double) { 69 | self.position += "+" 70 | self.salary *= 1.05 71 | 72 | return self.job 73 | } 74 | } 75 | 76 | /// Example data: 77 | /// ``` 78 | /// { 79 | /// "title": "…", 80 | /// "body_markdown": "…", 81 | /// "saved": null, 82 | /// "details": { 83 | /// "score": "-1234", 84 | /// "upvoted: null 85 | /// } 86 | /// } 87 | struct Post: JSONCodable { 88 | /// Never null, always there 89 | let title: String 90 | /// Comes from `body_markdown`, could be missing 91 | let body: String? 92 | /// Comes in as string 93 | let score: Int 94 | /// Comes in as "true"/"false"/null 95 | let saved: Bool 96 | /// Comes in as 1/0/null 97 | let upvoted: Bool 98 | 99 | static var transformersByProperty: [String: AnyTransformer] = [ 100 | "score": Transform(), 101 | "saved": Transform(), 102 | "upvoted": Transform(), 103 | ] 104 | 105 | static var jsonKeyPathsByProperty: [String : String] = [ 106 | "body": "body_markdown", 107 | "score": "details.score", 108 | "upvoted": "details.upvoted" 109 | ] 110 | } 111 | 112 | class JustDeallocateMe { 113 | var expectation: XCTestExpectation 114 | 115 | init(_ expectation: XCTestExpectation) { 116 | self.expectation = expectation 117 | } 118 | 119 | deinit { 120 | self.expectation.fulfill() 121 | } 122 | } 123 | 124 | struct JustDecodeMe: JSONCodable { 125 | static var defaultsByProperty: [String: Any] = [ 126 | "truth": true, 127 | "five": 5, 128 | "pie": 3.14 129 | ] 130 | 131 | let truth: Bool 132 | let five: Int 133 | let pie: Double 134 | } 135 | -------------------------------------------------------------------------------- /Tests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // JsumTests 4 | // 5 | // Created by Tanner Bennett on 5/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | var isToday: Bool { 12 | return Calendar.current.isDateInToday(self) 13 | } 14 | 15 | var ignoringTime: Date { 16 | let comps = Calendar.current.dateComponents([.day, .year], from: self) 17 | return Calendar.current.date(from: comps)! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/TransformerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformerTests.swift 3 | // Jsum 4 | // 5 | // Created by Tanner Bennett on 4/16/21. 6 | // 7 | 8 | import XCTest 9 | @testable import Jsum 10 | 11 | class TransformerTests: XCTestCase { 12 | 13 | func testBoolToT() { 14 | XCTAssertEqual(try! Transform.transform(true), true) 15 | XCTAssertEqual(try! Transform.transform(false), false) 16 | XCTAssertEqual(try! Transform.transform(nil), false) 17 | XCTAssertEqual(try! Transform.transform(true), 1) 18 | XCTAssertEqual(try! Transform.transform(false), 0) 19 | XCTAssertEqual(try! Transform.transform(nil), 0) 20 | XCTAssertEqual(try! Transform.transform(true), 1.0) 21 | XCTAssertEqual(try! Transform.transform(false), 0.0) 22 | XCTAssertEqual(try! Transform.transform(nil), 0.0) 23 | XCTAssertEqual(try! Transform.transform(true), "true") 24 | XCTAssertEqual(try! Transform.transform(false), "false") 25 | XCTAssertEqual(try! Transform.transform(nil), "false") 26 | 27 | XCTAssertEqual(try! Transform.transform(true), true) 28 | XCTAssertEqual(try! Transform.transform(false), false) 29 | XCTAssertEqual(try! Transform.transform(nil), false) 30 | XCTAssertEqual(try! Transform.transform(true), 1) 31 | XCTAssertEqual(try! Transform.transform(false), 0) 32 | XCTAssertEqual(try! Transform.transform(nil), 0) 33 | XCTAssertEqual(try! Transform.transform(true), 1.0) 34 | XCTAssertEqual(try! Transform.transform(false), 0.0) 35 | XCTAssertEqual(try! Transform.transform(nil), 0.0) 36 | XCTAssertEqual(try! Transform.transform(true), "true") 37 | XCTAssertEqual(try! Transform.transform(false), "false") 38 | XCTAssertEqual(try! Transform.transform(nil), "null") 39 | } 40 | 41 | func testIntToT() { 42 | let positive = 5 43 | let negative = -20 44 | let zero = 0 45 | 46 | XCTAssertEqual(try! Transform.transform(positive), true) 47 | XCTAssertEqual(try! Transform.transform(negative), true) 48 | XCTAssertEqual(try! Transform.transform(zero), false) 49 | XCTAssertEqual(try! Transform.transform(nil), false) 50 | XCTAssertEqual(try! Transform.transform(positive), positive) 51 | XCTAssertEqual(try! Transform.transform(negative), negative) 52 | XCTAssertEqual(try! Transform.transform(zero), zero) 53 | XCTAssertEqual(try! Transform.transform(nil), 0) 54 | XCTAssertEqual(try! Transform.transform(positive), Double(positive)) 55 | XCTAssertEqual(try! Transform.transform(negative), Double(negative)) 56 | XCTAssertEqual(try! Transform.transform(zero), Double(zero)) 57 | XCTAssertEqual(try! Transform.transform(nil), 0.0) 58 | XCTAssertEqual(try! Transform.transform(positive), String(positive)) 59 | XCTAssertEqual(try! Transform.transform(negative), String(negative)) 60 | XCTAssertEqual(try! Transform.transform(zero), String(zero)) 61 | XCTAssertEqual(try! Transform.transform(nil), String(0)) 62 | 63 | XCTAssertEqual(try! Transform.transform(positive), true) 64 | XCTAssertEqual(try! Transform.transform(negative), true) 65 | XCTAssertEqual(try! Transform.transform(zero), false) 66 | XCTAssertEqual(try! Transform.transform(nil), false) 67 | XCTAssertEqual(try! Transform.transform(positive), positive) 68 | XCTAssertEqual(try! Transform.transform(negative), negative) 69 | XCTAssertEqual(try! Transform.transform(zero), zero) 70 | XCTAssertEqual(try! Transform.transform(nil), 0) 71 | XCTAssertEqual(try! Transform.transform(positive), Double(positive)) 72 | XCTAssertEqual(try! Transform.transform(negative), Double(negative)) 73 | XCTAssertEqual(try! Transform.transform(zero), Double(zero)) 74 | XCTAssertEqual(try! Transform.transform(nil), 0.0) 75 | XCTAssertEqual(try! Transform.transform(positive), String(positive)) 76 | XCTAssertEqual(try! Transform.transform(negative), String(negative)) 77 | XCTAssertEqual(try! Transform.transform(zero), String(zero)) 78 | XCTAssertEqual(try! Transform.transform(nil), "null") 79 | } 80 | 81 | func testArrayToArray() { 82 | XCTAssertEqual(try! Transform<[String],[Int]> 83 | .transform(["234", "123"]), [234, 123]) 84 | 85 | XCTAssertEqual(try! Transform<[Bool?],[String]> 86 | .transform([true, false, nil]), ["true", "false", "null"]) 87 | } 88 | } 89 | 90 | --------------------------------------------------------------------------------