mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 491fe1fbea | |||
| 6b6f40c968 | |||
| 14bd29bf42 | |||
| cf4d8fc378 | |||
| 10b756f6db | |||
| 4cd697aae4 | |||
| db0f7329e4 | |||
| fb710c397b | |||
| b1adc60719 | |||
| 423e61b285 | |||
| 895d575cdb | |||
| cd75e9649b | |||
| c60dfc1584 | |||
| 16004051e9 | |||
| 864a66f1ea | |||
| 0c7bf9a92d | |||
| b294cec542 | |||
| 11e8d13f02 |
@@ -49,11 +49,15 @@ print("-----")
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
/*
|
||||
---- CRASHING IN XCODE 10.2 PLAYGROUND ----
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
|
||||
} else {
|
||||
print("no body data")
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<playground version='6.0' target-platform='macos'>
|
||||
<pages>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#
|
||||
# Be sure to run `pod spec lint JSONAPI.podspec' to ensure this is a
|
||||
# valid spec and to remove all comments including this before submitting the spec.
|
||||
#
|
||||
# To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html
|
||||
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
|
||||
#
|
||||
|
||||
Pod::Spec.new do |spec|
|
||||
|
||||
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# These will help people to find your library, and whilst it
|
||||
# can feel like a chore to fill in it's definitely to your advantage. The
|
||||
# summary should be tweet-length, and the description more in depth.
|
||||
#
|
||||
|
||||
spec.name = "JSONAPI"
|
||||
spec.version = "0.21.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
spec.description = <<-DESC
|
||||
A Swift package for encoding to- and decoding from JSON API compliant requests and responses.
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/mattpolzin/JSONAPI"
|
||||
# spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"
|
||||
|
||||
|
||||
# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Licensing your code is important. See https://choosealicense.com for more info.
|
||||
# CocoaPods will detect a license file if there is a named LICENSE*
|
||||
# Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
|
||||
#
|
||||
|
||||
# spec.license = "MIT"
|
||||
spec.license = { :type => "MIT", :file => "LICENSE.txt" }
|
||||
|
||||
|
||||
# ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Specify the authors of the library, with email addresses. Email addresses
|
||||
# of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
|
||||
# accepts just a name if you'd rather not provide an email address.
|
||||
#
|
||||
# Specify a social_media_url where others can refer to, for example a twitter
|
||||
# profile URL.
|
||||
#
|
||||
|
||||
spec.author = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||
# Or just: spec.author = "Mathew Polzin"
|
||||
# spec.authors = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||
# spec.social_media_url = "https://twitter.com/Mathew Polzin"
|
||||
|
||||
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# If this Pod runs only on iOS or OS X, then specify the platform and
|
||||
# the deployment target. You can optionally include the target after the platform.
|
||||
#
|
||||
|
||||
# spec.platform = :ios
|
||||
# spec.platform = :ios, "5.0"
|
||||
|
||||
# When using multiple platforms
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.osx.deployment_target = "10.9"
|
||||
# spec.watchos.deployment_target = "2.0"
|
||||
# spec.tvos.deployment_target = "9.0"
|
||||
|
||||
|
||||
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Specify the location from where the source should be retrieved.
|
||||
# Supports git, hg, bzr, svn and HTTP.
|
||||
#
|
||||
|
||||
spec.source = { :git => "https://github.com/mattpolzin/JSONAPI.git", :tag => "#{spec.version}" }
|
||||
|
||||
|
||||
# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# CocoaPods is smart about how it includes source code. For source files
|
||||
# giving a folder will include any swift, h, m, mm, c & cpp files.
|
||||
# For header files it will include any header in the folder.
|
||||
# Not including the public_header_files will make all headers public.
|
||||
#
|
||||
|
||||
spec.source_files = "Sources", "Sources/**/*.{swift}"
|
||||
# spec.exclude_files = "Classes/Exclude"
|
||||
|
||||
# spec.public_header_files = "Classes/**/*.h"
|
||||
|
||||
|
||||
# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# A list of resources included with the Pod. These are copied into the
|
||||
# target bundle with a build phase script. Anything else will be cleaned.
|
||||
# You can preserve files from being cleaned, please don't preserve
|
||||
# non-essential files like tests, examples and documentation.
|
||||
#
|
||||
|
||||
# spec.resource = "icon.png"
|
||||
# spec.resources = "Resources/*.png"
|
||||
|
||||
# spec.preserve_paths = "FilesToSave", "MoreFilesToSave"
|
||||
|
||||
|
||||
# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Link your library with frameworks, or libraries. Libraries do not include
|
||||
# the lib prefix of their name.
|
||||
#
|
||||
|
||||
spec.framework = "Poly"
|
||||
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||
|
||||
# spec.library = "iconv"
|
||||
# spec.libraries = "iconv", "xml2"
|
||||
|
||||
|
||||
# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# If your library depends on compiler flags you can set them in the xcconfig hash
|
||||
# where they will only apply to your library. If you depend on other Podspecs
|
||||
# you can include multiple dependencies to ensure it works.
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
# spec.requires_arc = true
|
||||
|
||||
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
|
||||
spec.dependency "Poly", "~> 2.0"
|
||||
|
||||
end
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
|
||||
"version": "1.0.0"
|
||||
"revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+7
-3
@@ -1,10 +1,14 @@
|
||||
// swift-tools-version:4.2
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "JSONAPI",
|
||||
platforms: [
|
||||
.macOS(.v10_10),
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "JSONAPI",
|
||||
@@ -14,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -30,5 +34,5 @@ let package = Package(
|
||||
name: "JSONAPITestingTests",
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# JSONAPI
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](https://app.bitrise.io/app/c8295b9589aa401e)
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](https://app.bitrise.io/app/c8295b9589aa401e)
|
||||
|
||||
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
|
||||
|
||||
@@ -16,6 +16,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [Caveat](#caveat)
|
||||
- [Dev Environment](#dev-environment)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Xcode project](#xcode-project)
|
||||
- [Running the Playground](#running-the-playground)
|
||||
- [Project Status](#project-status)
|
||||
@@ -33,8 +34,8 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [`JSONAPI.Entity`](#jsonapientity)
|
||||
- [`Meta`](#meta)
|
||||
- [`Links`](#links)
|
||||
- [`IdType`](#idtype)
|
||||
- [`MaybeRawId`](#mayberawid)
|
||||
- [`RawIdType`](#rawidtype)
|
||||
- [Convenient `typealiases`](#convenient-typealiases)
|
||||
- [`JSONAPI.Relationships`](#jsonapirelationships)
|
||||
- [`JSONAPI.Attributes`](#jsonapiattributes)
|
||||
@@ -82,7 +83,15 @@ If you find something wrong with this library and it isn't already mentioned und
|
||||
|
||||
## Dev Environment
|
||||
### Prerequisites
|
||||
1. Swift 4.2+ and Swift Package Manager
|
||||
1. Swift 4.2+
|
||||
2. Swift Package Manager *OR* Cocoapods
|
||||
|
||||
### CocoaPods
|
||||
To use this framework in your project via Cocoapods instead of Swift Package Manager, add the following dependencies to your Podfile.
|
||||
```
|
||||
pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git'
|
||||
pod 'JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
|
||||
```
|
||||
|
||||
### Xcode project
|
||||
To create an Xcode project for JSONAPI, run
|
||||
@@ -339,11 +348,6 @@ typealias Attributes = NoAttributes
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
NOTE: Because of support for computed properties that are not wrapped in `Attribute`, `TransformedAttribute`, or `ValidatedAttribute`, the compiler cannot always infer the type of thing you want back when using subscript attribute access. The following code is ambiguous about whether it should return a `String` or an `Attribute<String>`:
|
||||
```swift
|
||||
let favoriteColor = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
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`.
|
||||
@@ -382,7 +386,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
|
||||
|
||||
#### Computed `Attribute`
|
||||
|
||||
You can add computed properties to your `EntityDescription.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.
|
||||
You can add computed properties to your `EntityDescription.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.
|
||||
|
||||
```swift
|
||||
public var fullName: Attribute<String> {
|
||||
@@ -390,6 +394,8 @@ public var fullName: Attribute<String> {
|
||||
}
|
||||
```
|
||||
|
||||
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 `Entities`
|
||||
`Entity` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `Entity`:
|
||||
1. Assigning a new `Identifier` to the copy of an identified `Entity`.
|
||||
@@ -584,7 +590,7 @@ extension EntityDescription1.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 entities 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 entity. 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, this 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 an entity. 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: EntityDescription {
|
||||
|
||||
@@ -440,31 +440,34 @@ public extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
public extension EntityProxy {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
/// of `entity.attributes.propertyName`.
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
/// of `entity.attributes.propertyName`.
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
/// of `entity.attributes.propertyName`.
|
||||
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
|
||||
// Implementation Note: Handles Transform that returns optional
|
||||
// type.
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
|
||||
/// Access the computed attribute at the given keypath. This just
|
||||
/// Access the storage of the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<Description.Attributes, T>) -> T {
|
||||
/// of `entity.attributes.propertyName`.
|
||||
/// Most of the subscripts dig into an `AttributeType`. This subscript
|
||||
/// returns the `AttributeType` (or another type, if you are accessing
|
||||
/// an attribute that is not stored in an `AttributeType`).
|
||||
subscript<T>(direct path: KeyPath<Description.Attributes, T>) -> T {
|
||||
// Implementation Note: Handles attributes that are not
|
||||
// AttributeType. These should only exist as computed properties.
|
||||
return attributes[keyPath: path]
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// Result.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/5/18.
|
||||
//
|
||||
|
||||
enum Result<T, E: Swift.Error> {
|
||||
case success(T)
|
||||
case failure(E)
|
||||
|
||||
var value: T? {
|
||||
guard case .success(let val) = self else {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
var error: E? {
|
||||
guard case .failure(let err) = self else {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func map<U>(_ transform: (T) -> U) -> Result<U, E> {
|
||||
switch self {
|
||||
case .failure(let err):
|
||||
return .failure(err)
|
||||
case .success(let val):
|
||||
return .success(transform(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Result: CustomStringConvertible where T: CustomStringConvertible, E: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .success(let val):
|
||||
return String(describing: val)
|
||||
case .failure(let err):
|
||||
return String(describing: err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,13 @@ class ComputedPropertiesTests: XCTestCase {
|
||||
let entity = decoded(type: TestType.self, data: computed_property_attribute)
|
||||
|
||||
XCTAssertEqual(entity[\.computed], "Sarah2")
|
||||
XCTAssertEqual(entity[\.secretsOut], "shhhh")
|
||||
XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh")
|
||||
}
|
||||
|
||||
func test_ComputedNonAttributeAccess() {
|
||||
let entity = decoded(type: TestType.self, data: computed_property_attribute)
|
||||
|
||||
XCTAssertEqual(entity[\.computed2], "Sarah2")
|
||||
XCTAssertEqual(entity[direct: \.directComputed], "Sarah2")
|
||||
}
|
||||
|
||||
func test_ComputedRelationshipAccess() {
|
||||
@@ -56,11 +56,11 @@ extension ComputedPropertiesTests {
|
||||
return name.map { $0 + "2" }
|
||||
}
|
||||
|
||||
public var computed2: String {
|
||||
public var directComputed: String {
|
||||
return computed.value
|
||||
}
|
||||
|
||||
public var secretsOut: String {
|
||||
public var directSecretsOut: String {
|
||||
return secret.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class PolyProxyTests: XCTestCase {
|
||||
XCTAssertEqual(polyUserA[\.name], "Ken Moore")
|
||||
XCTAssertEqual(polyUserA.id, "1")
|
||||
XCTAssertEqual(polyUserA.relationships, .none)
|
||||
XCTAssertEqual(polyUserA[\.x], .init(x: "y"))
|
||||
XCTAssertEqual(polyUserA[direct: \.x], .init(x: "y"))
|
||||
}
|
||||
|
||||
func test_UserAAndBEncodeEquality() {
|
||||
@@ -59,7 +59,7 @@ public class PolyProxyTests: XCTestCase {
|
||||
XCTAssertEqual(polyUserB[\.name], "Ken Less")
|
||||
XCTAssertEqual(polyUserB.id, "2")
|
||||
XCTAssertEqual(polyUserB.relationships, .none)
|
||||
XCTAssertEqual(polyUserB[\.x], .init(x: "y"))
|
||||
XCTAssertEqual(polyUserB[direct: \.x], .init(x: "y"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ public extension PolyProxyTests {
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public typealias UserA = BasicEntity<UserDescription1>
|
||||
public typealias UserB = BasicEntity<UserDescription2>
|
||||
typealias UserA = BasicEntity<UserDescription1>
|
||||
typealias UserB = BasicEntity<UserDescription2>
|
||||
|
||||
public typealias User = Poly2<UserA, UserB>
|
||||
typealias User = Poly2<UserA, UserB>
|
||||
}
|
||||
|
||||
extension Poly2: EntityProxy, JSONTyped where A == PolyProxyTests.UserA, B == PolyProxyTests.UserB {
|
||||
|
||||
Reference in New Issue
Block a user