Skip to content

Commit 34dc351

Browse files
authored
Merge pull request #34 from mattpolzin/swift-5.1
Swift 5.1 tools dependency, access to attributes via dynamicMemberLookup
2 parents c142e93 + 3ff1b86 commit 34dc351

14 files changed

Lines changed: 274 additions & 165 deletions

File tree

JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,11 @@ print("-----")
5555

5656
// MARK: - Pass successfully parsed body to other parts of the code
5757

58-
/*
59-
---- CRASHING IN XCODE 10.2 PLAYGROUND ----
60-
6158
if case let .data(bodyData) = peopleResponse.body {
62-
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
59+
print("first person's name: \(bodyData.primary.values[0].fullName)")
6360
} else {
6461
print("no body data")
6562
}
66-
*/
6763

6864

6965
// MARK: - Work in the abstract

JSONAPI.playground/Sources/Entities.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
1515
********/
1616

1717
// MARK: - String as CreatableRawIdType
18-
var GlobalStringId: Int = 0
18+
var globalStringId: Int = 0
1919
extension String: CreatableRawIdType {
2020
public static func unique() -> String {
21-
GlobalStringId += 1
22-
return String(GlobalStringId)
21+
globalStringId += 1
22+
return String(globalStringId)
2323
}
2424
}
2525

JSONAPI.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
1616
#
1717

1818
spec.name = "MP-JSONAPI"
19-
spec.version = "1.0.0"
19+
spec.version = "2.0.0"
2020
spec.summary = "Swift Codable JSON API framework."
2121

2222
# This description is used to generate tags and improve search results.
@@ -131,7 +131,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
131131
# where they will only apply to your library. If you depend on other Podspecs
132132
# you can include multiple dependencies to ensure it works.
133133

134-
spec.swift_version = "5.0"
134+
spec.swift_version = "5.1"
135135
spec.module_name = "JSONAPI"
136136
# spec.requires_arc = true
137137

Package.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
// swift-tools-version:5.0
1+
// swift-tools-version:5.1
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "JSONAPI",
88
platforms: [
9-
.macOS(.v10_10),
10-
.iOS(.v10)
9+
.macOS(.v10_10),
10+
.iOS(.v10)
1111
],
1212
products: [
1313
.library(
1414
name: "JSONAPI",
1515
targets: ["JSONAPI"]),
16-
.library(
17-
name: "JSONAPITesting",
18-
targets: ["JSONAPITesting"])
16+
.library(
17+
name: "JSONAPITesting",
18+
targets: ["JSONAPITesting"])
1919
],
2020
dependencies: [
21-
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
21+
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
2222
],
2323
targets: [
2424
.target(
2525
name: "JSONAPI",
2626
dependencies: ["Poly"]),
27-
.target(
28-
name: "JSONAPITesting",
29-
dependencies: ["JSONAPI"]),
27+
.target(
28+
name: "JSONAPITesting",
29+
dependencies: ["JSONAPI"]),
3030
.testTarget(
3131
name: "JSONAPITests",
3232
dependencies: ["JSONAPI", "JSONAPITesting"]),
3333
.testTarget(
3434
name: "JSONAPITestingTests",
3535
dependencies: ["JSONAPI", "JSONAPITesting"])
3636
],
37-
swiftLanguageVersions: [.v5]
37+
swiftLanguageVersions: [.v5]
3838
)

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# JSONAPI
2-
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.0](http://img.shields.io/badge/Swift-5.0-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
2+
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
33

44
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
55

@@ -320,11 +320,18 @@ A resource object that does not have attributes can be described by adding the f
320320
typealias Attributes = NoAttributes
321321
```
322322

323-
`Attributes` can be accessed via the `subscript` operator of the `ResourceObject` type as follows:
323+
As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows:
324+
```swift
325+
let favoriteColor: String = person.favoriteColor
326+
```
327+
328+
🗒 `Attributes` can also be accessed via the older `subscript` operator as follows:
324329
```swift
325330
let favoriteColor: String = person[\.favoriteColor]
326331
```
327332

333+
In both cases you retain type-safety, although neither plays particularly nicely with code autocompletion. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated.
334+
328335
#### `Transformer`
329336

330337
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
@@ -363,15 +370,15 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
363370

364371
#### Computed `Attribute`
365372

366-
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person[\.name]` attribute from the example above and create a `fullName` computed property.
373+
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property.
367374

368375
```swift
369376
public var fullName: Attribute<String> {
370377
return name.map { $0.joined(separator: " ") }
371378
}
372379
```
373380

374-
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.
381+
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.
375382

376383
### Copying/Mutating `ResourceObjects`
377384
`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`:
@@ -633,7 +640,7 @@ typealias User = JSONAPI.ResourceObject<UserDescription, NoMetadata, NoLinks, St
633640
Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other:
634641

635642
```swift
636-
let createdAt = user[\.createdAt]
643+
let createdAt = user.createdAt
637644
```
638645

639646
This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute.
@@ -654,7 +661,7 @@ enum UserDescription: ResourceObjectDescription {
654661
struct Relationships: JSONAPI.Relationships {
655662
public var friend: (User) -> User.Identifier {
656663
return { user in
657-
return User.Identifier(rawValue: user[\.friend_id])
664+
return User.Identifier(rawValue: user.friend_id)
658665
}
659666
}
660667
}

Sources/JSONAPI/Resource/Attribute.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ public protocol AttributeType: Codable {
1717
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
1818
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
1919
public let rawValue: RawValue
20-
20+
2121
public let value: Transformer.To
22-
22+
2323
public init(rawValue: RawValue) throws {
2424
self.rawValue = rawValue
2525
value = try Transformer.transform(rawValue)

Sources/JSONAPI/Resource/ResourceObject.swift

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ public protocol ResourceObjectProxyDescription: JSONTyped {
6666
public protocol ResourceObjectDescription: ResourceObjectProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
6767

6868
/// ResourceObjectProxy is a protocol that can be used to create
69-
/// types that _act_ like Entities but cannot be encoded
70-
/// or decoded as Entities.
69+
/// types that _act_ like ResourceObject but cannot be encoded
70+
/// or decoded as ResourceObjects.
71+
@dynamicMemberLookup
7172
public protocol ResourceObjectProxy: Equatable, JSONTyped {
7273
associatedtype Description: ResourceObjectProxyDescription
7374
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
@@ -91,7 +92,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped {
9192
}
9293

9394
extension ResourceObjectProxy {
94-
/// The JSON API compliant "type" of this `Entity`.
95+
/// The JSON API compliant "type" of this `ResourceObject`.
9596
public static var jsonType: String { return Description.jsonType }
9697
}
9798

@@ -151,7 +152,7 @@ extension ResourceObject: CustomStringConvertible {
151152
}
152153
}
153154

154-
// MARK: Convenience initializers
155+
// MARK: - Convenience initializers
155156
extension ResourceObject where EntityRawIdType: CreatableRawIdType {
156157
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
157158
self.id = ResourceObject.Id()
@@ -402,7 +403,7 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent
402403
}
403404
*/
404405

405-
// MARK: Pointer for Relationships use.
406+
// MARK: - Pointer for Relationships use
406407
public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
407408

408409
/// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links.
@@ -428,7 +429,7 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
428429
}
429430
}
430431

431-
// MARK: Identifying Unidentified Entities
432+
// MARK: - Identifying Unidentified Entities
432433
public extension ResourceObject where EntityRawIdType == Unidentified {
433434
/// Create a new `ResourceObject` from this one with a newly created
434435
/// unique Id of the given type.
@@ -449,31 +450,55 @@ public extension ResourceObject where EntityRawIdType: CreatableRawIdType {
449450
}
450451
}
451452

452-
// MARK: Attribute Access
453+
// MARK: - Attribute Access
453454
public extension ResourceObjectProxy {
455+
// MARK: Keypath Subscript Lookup
454456
/// Access the attribute at the given keypath. This just
455457
/// allows you to write `resourceObject[\.propertyName]` instead
456-
/// of `resourceObject.attributes.propertyName`.
458+
/// of `resourceObject.attributes.propertyName.value`.
457459
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
458460
return attributes[keyPath: path].value
459461
}
460462

461463
/// Access the attribute at the given keypath. This just
462464
/// allows you to write `resourceObject[\.propertyName]` instead
463-
/// of `resourceObject.attributes.propertyName`.
465+
/// of `resourceObject.attributes.propertyName.value`.
464466
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
465467
return attributes[keyPath: path]?.value
466468
}
467469

468470
/// Access the attribute at the given keypath. This just
469471
/// allows you to write `resourceObject[\.propertyName]` instead
470-
/// of `resourceObject.attributes.propertyName`.
472+
/// of `resourceObject.attributes.propertyName.value`.
471473
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
472474
// Implementation Note: Handles Transform that returns optional
473475
// type.
474476
return attributes[keyPath: path].flatMap { $0.value }
475477
}
476478

479+
// MARK: Dynaminc Member Keypath Lookup
480+
/// Access the attribute at the given keypath. This just
481+
/// allows you to write `resourceObject[\.propertyName]` instead
482+
/// of `resourceObject.attributes.propertyName.value`.
483+
subscript<T: AttributeType>(dynamicMember path: KeyPath<Description.Attributes, T>) -> T.ValueType {
484+
return attributes[keyPath: path].value
485+
}
486+
487+
/// Access the attribute at the given keypath. This just
488+
/// allows you to write `resourceObject[\.propertyName]` instead
489+
/// of `resourceObject.attributes.propertyName.value`.
490+
subscript<T: AttributeType>(dynamicMember path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
491+
return attributes[keyPath: path]?.value
492+
}
493+
494+
/// Access the attribute at the given keypath. This just
495+
/// allows you to write `resourceObject[\.propertyName]` instead
496+
/// of `resourceObject.attributes.propertyName.value`.
497+
subscript<T: AttributeType, U>(dynamicMember path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
498+
return attributes[keyPath: path].flatMap { $0.value }
499+
}
500+
501+
// MARK: Direct Keypath Subscript Lookup
477502
/// Access the storage of the attribute at the given keypath. This just
478503
/// allows you to write `resourceObject[direct: \.propertyName]` instead
479504
/// of `resourceObject.attributes.propertyName`.
@@ -487,16 +512,24 @@ public extension ResourceObjectProxy {
487512
}
488513
}
489514

490-
// MARK: Meta-Attribute Access
515+
// MARK: - Meta-Attribute Access
491516
public extension ResourceObjectProxy {
517+
// MARK: Keypath Subscript Lookup
492518
/// Access an attribute requiring a transformation on the RawValue _and_
493519
/// a secondary transformation on this entity (self).
494520
subscript<T>(_ path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
495521
return attributes[keyPath: path](self)
496522
}
523+
524+
// MARK: Dynamic Member Keypath Lookup
525+
/// Access an attribute requiring a transformation on the RawValue _and_
526+
/// a secondary transformation on this entity (self).
527+
subscript<T>(dynamicMember path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
528+
return attributes[keyPath: path](self)
529+
}
497530
}
498531

499-
// MARK: Relationship Access
532+
// MARK: - Relationship Access
500533
public extension ResourceObjectProxy {
501534
/// Access to an Id of a `ToOneRelationship`.
502535
/// This allows you to write `resourceObject ~> \.other` instead
@@ -538,7 +571,7 @@ public extension ResourceObjectProxy {
538571
}
539572
}
540573

541-
// MARK: Meta-Relationship Access
574+
// MARK: - Meta-Relationship Access
542575
public extension ResourceObjectProxy {
543576
/// Access to an Id of a `ToOneRelationship`.
544577
/// This allows you to write `resourceObject ~> \.other` instead

Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Attribute_FunctorTests: XCTestCase {
1616
XCTAssertNotNil(entity)
1717

1818
XCTAssertEqual(entity?[\.computedString], "Frankie2")
19+
XCTAssertEqual(entity?.computedString, "Frankie2")
1920
}
2021

2122
func test_mapOptionalSuccess() {
@@ -24,6 +25,7 @@ class Attribute_FunctorTests: XCTestCase {
2425
XCTAssertNotNil(entity)
2526

2627
XCTAssertEqual(entity?[\.computedNumber], 22)
28+
XCTAssertEqual(entity?.computedNumber, 22)
2729
}
2830

2931
func test_mapOptionalFailure() {
@@ -32,6 +34,7 @@ class Attribute_FunctorTests: XCTestCase {
3234
XCTAssertNotNil(entity)
3335

3436
XCTAssertNil(entity?[\.computedNumber])
37+
XCTAssertNil(entity?.computedNumber)
3538
}
3639
}
3740

Tests/JSONAPITests/Attribute/AttributeTests.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,37 @@ extension AttributeTests {
7777
}
7878
}
7979

80-
enum IntToString: Transformer {
80+
enum IntToString: ReversibleTransformer {
8181
public static func transform(_ from: Int) -> String {
8282
return String(from)
8383
}
84+
85+
public static func reverse(_ value: String) throws -> Int {
86+
guard let intValue = Int(value) else {
87+
fatalError("Reversed IntToString with invalid String value.")
88+
}
89+
return intValue
90+
}
8491
}
8592

93+
enum OptionalIntToOptionalString: ReversibleTransformer {
94+
public static func transform(_ from: Int?) -> String? {
95+
return from.map(String.init)
96+
}
97+
98+
public static func reverse(_ value: String?) throws -> Int? {
99+
guard let stringValue = value else {
100+
return nil
101+
}
102+
103+
guard let intValue = Int(stringValue) else {
104+
fatalError("Reversed IntToString with invalid String value.")
105+
}
106+
107+
return intValue
108+
}
109+
}
110+
86111
enum IntToInt: Transformer {
87112
public static func transform(_ from: Int) -> Int {
88113
return from + 100

Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class ComputedPropertiesTests: XCTestCase {
1515

1616
XCTAssertEqual(entity.id, "1234")
1717
XCTAssertEqual(entity[\.name], "Sarah")
18+
XCTAssertEqual(entity.name, "Sarah")
1819
XCTAssertEqual(entity ~> \.other, "5678")
1920
XCTAssertNoThrow(try TestType.check(entity))
2021
}
@@ -27,6 +28,7 @@ class ComputedPropertiesTests: XCTestCase {
2728
let entity = decoded(type: TestType.self, data: computed_property_attribute)
2829

2930
XCTAssertEqual(entity[\.computed], "Sarah2")
31+
XCTAssertEqual(entity.computed, "Sarah2")
3032
XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh")
3133
}
3234

0 commit comments

Comments
 (0)