From e9b9a2bd7840fbb2933914248cfdbe9976a60e52 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 2 Jul 2019 17:36:54 -0700 Subject: [PATCH 1/4] Add Empty Object Decoder to be used in upcoming release. --- Sources/JSONAPI/EmptyObjectDecoder.swift | 215 +++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Sources/JSONAPI/EmptyObjectDecoder.swift diff --git a/Sources/JSONAPI/EmptyObjectDecoder.swift b/Sources/JSONAPI/EmptyObjectDecoder.swift new file mode 100644 index 0000000..f5fd699 --- /dev/null +++ b/Sources/JSONAPI/EmptyObjectDecoder.swift @@ -0,0 +1,215 @@ +// +// EmptyObjectDecoder.swift +// JSONAPI +// +// Created by Mathew Polzin on 7/2/19. +// + +/// `EmptyObjectDecoder` exists internally for the sole purpose of +/// allowing certain fallback logic paths to attempt to create `Decodable` +/// types from empty containers (specifically in a way that is agnostic +/// of any given encoding). In other words, this serves the same purpose +/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)` +/// without needing to use a third party or `Foundation` library decoder. +struct EmptyObjectDecoder: Decoder { + + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey : Any] = [:] + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + return KeyedDecodingContainer(EmptyKeyedContainer()) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + return EmptyUnkeyedContainer() + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue + } +} + +enum EmptyObjectDecodingError: Swift.Error { + case emptyObjectCannotBeSingleValue + case emptyObjectCannotBeUnkeyedValues + case emptyObjectCannotHaveKeyedValues + case emptyObjectCannotHaveNestedContainers + case emptyObjectCannotHaveSuper +} + +struct EmptyUnkeyedContainer: UnkeyedDecodingContainer { + var codingPath: [CodingKey] { return [] } + + var count: Int? { return 0 } + + var isAtEnd: Bool { return true } + + var currentIndex: Int { return 0 } + + mutating func decodeNil() throws -> Bool { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: String.Type) throws -> String { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Double.Type) throws -> Double { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Float.Type) throws -> Float { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Int.Type) throws -> Int { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Int8.Type) throws -> Int8 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Int16.Type) throws -> Int16 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Int32.Type) throws -> Int32 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: Int64.Type) throws -> Int64 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: UInt.Type) throws -> UInt { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers + } + + mutating func superDecoder() throws -> Decoder { + throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper + } +} + +struct EmptyKeyedContainer: KeyedDecodingContainerProtocol { + var codingPath: [CodingKey] { return [] } + + var allKeys: [Key] { return [] } + + func contains(_ key: Key) -> Bool { + return false + } + + func decodeNil(forKey key: Key) throws -> Bool { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers + } + + func superDecoder() throws -> Decoder { + throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper + } + + func superDecoder(forKey key: Key) throws -> Decoder { + throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper + } +} From e4481c9e4f1f25ea35306c0b07a136ec3bdb0c2e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 2 Jul 2019 18:05:22 -0700 Subject: [PATCH 2/4] noticed the README had some typos I missed when I changed Entity to ResourceObject. --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b518724..56e4e13 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ In this documentation, in order to draw attention to the difference between the ### `JSONAPI.ResourceObjectDescription` -An `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: +A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: ```swift enum PersonDescription: IdentifiedResourceObjectDescription { @@ -220,7 +220,7 @@ enum PersonDescription: IdentifiedResourceObjectDescription { } ``` -The requirements of an `ResourceObjectDescription` are: +The requirements of a `ResourceObjectDescription` are: 1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". 2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` 3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` @@ -259,11 +259,11 @@ This readme doesn't go into detail on the **SPEC**, but the following *Resource ### `JSONAPI.ResourceObject` -Once you have an `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. +Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. -An `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. +A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. #### `Meta` @@ -275,7 +275,7 @@ The third generic specialization on `ResourceObject` is `Links`. This is describ #### `MaybeRawId` -The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate an `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create an `ResourceObject` and assign it a new `Id`). +The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). ##### `RawIdType` @@ -283,7 +283,7 @@ The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `Re Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal. -A `RawIdType` is the underlying type that uniquely identifies an `ResourceObject`. This is often a `String` or a `UUID`. +A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`. #### Convenient `typealiases` @@ -305,7 +305,7 @@ Note that I am assuming an unidentified person is a "new" person. I suspect that ### `JSONAPI.Relationships` -There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `ResourceObjectDescription`. +There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. @@ -314,7 +314,7 @@ To describe a relationship that may be omitted (i.e. the key is not even present let nullableRelative: ToOneRelationship ``` -An resource object that does not have relationships can be described by adding the following to an `ResourceObjectDescription`: +A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: ```swift typealias Relationships = NoRelationships ``` @@ -326,7 +326,7 @@ let friendIds: [Person.Identifier] = person ~> \.friends ### `JSONAPI.Attributes` -The `Attributes` of an `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. +The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional: ```swift @@ -338,7 +338,7 @@ To describe an attribute that is expected to exist but might have a `null` value let nullableAttribute: Attribute ``` -An resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: +A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: ```swift typealias Attributes = NoAttributes ``` @@ -396,8 +396,8 @@ public var fullName: Attribute { If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. -### Copying `ResourceObjects` -`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `ResourceObject`: +### Copying/Mutating `ResourceObjects` +`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: 1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. 2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. @@ -590,7 +590,7 @@ extension ResourceObjectDescription1.Attributes { ### Meta-Attributes This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with. -Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of an resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. +Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. ```swift enum UserDescription: ResourceObjectDescription { From c75912ab796699af63dfc733ba67c316080bccec Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 2 Jul 2019 18:09:06 -0700 Subject: [PATCH 3/4] switch Poly versioning to 'upToNextMajor' and updated the resolved version. --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index fac7812..4a251e7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645", - "version": "2.0.0" + "revision": "38051821d7ef49e590e26e819a2fe447e50be9ff", + "version": "2.0.1" } } ] diff --git a/Package.swift b/Package.swift index 0789b25..11fea30 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")), ], targets: [ .target( From e820f34253fa982c3dba1a3fba7207c8c1c560c7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 2 Jul 2019 18:12:29 -0700 Subject: [PATCH 4/4] change podspec version in anticipation of next release --- JSONAPI.podspec | 2 +- Tests/JSONAPITests/Entity/EntityTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 36a800c..0d8e633 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "JSONAPI" - spec.version = "0.30.0" + spec.version = "0.31.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 18e313e..0adfaf5 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -31,7 +31,7 @@ class EntityTests: XCTestCase { XCTAssertEqual(entity ~> \.optionalOne, entity1.id) } - + func test_toMany_relationship_operator_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)