Compare commits

..

15 Commits

Author SHA1 Message Date
Mathew Polzin 04bf96a0cf update linuxmain 2018-11-29 20:13:21 -08:00
Mathew Polzin 8668311307 better poly proxy tests 2018-11-29 20:10:56 -08:00
Mathew Polzin 689517c633 Add ability to differentiate between things that _act_ like Entities and things that _are_ entities in the sense that the latter are persisted between client and server but the former is just a clientside convenience. 2018-11-29 20:03:44 -08:00
Mathew Polzin 65b80ee0eb Add ReversibleTransformer and tests. remove unneeded check in Entity.check(). 2018-11-29 18:39:59 -08:00
Mathew Polzin e91a03b396 Expose initializer for Document.Body.Data 2018-11-29 13:37:06 -08:00
Mathew Polzin 82088c7852 Remove BodyData which just aliased Body.Data 2018-11-29 13:15:07 -08:00
Mathew Polzin 34a4c8e7fc Add TestLib ability to represent to-one relationships with the values of their IDs. 2018-11-29 11:31:17 -08:00
Mathew Polzin 6aeb859c24 Remove now deleted test from linuxmain 2018-11-28 21:13:47 -08:00
Mathew Polzin 163ac94c51 I did some more type wrangling to finally get the Id type to specialize on Entity rather than EntityDescription. The compiler gets into trouble depending on which of a few semantically identical routes are taken, but I finally stumbled upon the correct combination of protocols and extensions to get the job done. this was always the ideal outcome, but I was not sure the Swift compiler would allow it. 2018-11-28 21:13:07 -08:00
Mathew Polzin e67b9fc142 Update playground and README for computed attributes. 2018-11-28 09:20:50 -08:00
Mathew Polzin a628992fcb Make Attribute a Functor to make computed attributes easier to write. 2018-11-28 09:09:23 -08:00
Mathew Polzin cf47f88a61 Add tests to confirm that Entities can safely have computed attributes or relationships. 2018-11-28 08:15:57 -08:00
Mathew Polzin 0425e2adcb rename folder in tests 2018-11-28 07:50:47 -08:00
Mathew Polzin d3763ba713 Add array literal expressibility for ToManyRelationship 2018-11-27 23:59:12 -08:00
Mathew Polzin fcc1796731 Expose Entity properties on EntityType protocol. 2018-11-27 18:58:48 -08:00
30 changed files with 685 additions and 101 deletions
@@ -40,10 +40,14 @@ let peopleFromData = peopleResponse.body.primaryData?.values
let dogsFromData = peopleResponse.body.includes?[Dog.self]
let housesFromData = peopleResponse.body.includes?[House.self]
print("-----")
print(peopleResponse)
print("-----")
// MARK: - Pass successfully parsed body to other parts of the code
if case let .data(bodyData) = peopleResponse.body {
print(bodyData)
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
} else {
print("no body data")
}
+10 -6
View File
@@ -24,7 +24,7 @@ extension String: CreatableRawIdType {
}
// MARK: - Entity typealias for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
@@ -35,6 +35,10 @@ public enum PersonDescription: EntityDescription {
public let name: Attribute<[String]>
public let favoriteColor: Attribute<String>
public var fullName: Attribute<String> {
return name.map { $0.joined(separator: " ") }
}
public init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
self.name = name
self.favoriteColor = favoriteColor
@@ -56,9 +60,9 @@ public enum PersonDescription: EntityDescription {
public typealias Person = ExampleEntity<PersonDescription>
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
public extension Entity where Description == PersonDescription, EntityRawIdType == String {
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
}
}
@@ -85,12 +89,12 @@ public enum DogDescription: EntityDescription {
public typealias Dog = ExampleEntity<DogDescription>
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
public extension Entity where Description == DogDescription, EntityRawIdType == String {
public init(name: String, owner: Person?) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
}
public init(name: String, owner: Person.Identifier) throws {
public init(name: String, owner: Person.Id) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
}
}
+13 -2
View File
@@ -100,6 +100,7 @@ To create an Xcode project for JSONAPI, run
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
- [x] Make it easier to construct `Attributes` and `Relationships` values in test cases (literal expressibility).
- [x] Make `TransformedAttribute` a Functor.
## Usage
@@ -169,13 +170,13 @@ An `Entity` needs to be specialized on two generic types. The first is the `Enti
#### `IdType`
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `EntityDescription` of the type of entity the Id identifies. Having the `EntityDescription` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `EntityDescription`; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `Entity` type that the Id identifies. Having the `Entity` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `Entity` type; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
#### Convenient `typealiases`
Often you can use one `RawIdType` for many if not all of your `Entities`. That means you can save yourself some boilerplate by using `typealias`es like the following:
```
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, String>
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
```
@@ -256,6 +257,16 @@ Note that the first generic parameter of `TransformAttribute` is the type you ex
You can also creator `Validator`s and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
#### 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 should still have the type `Attribute` because that way you can take advantage of the quick access provided by `Entity`'s subscript operator. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
```
public var fullName: Attribute<String> {
return name.map { $0.joined(separator: " ") }
}
```
### `JSONAPIDocument`
The entirety of a JSON API request or response is encoded or decoded from- or to a `JSONAPIDocument`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
+25 -2
View File
@@ -13,7 +13,6 @@ public protocol JSONAPIDocument: Codable, Equatable {
associatedtype Error: JSONAPIError
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
typealias BodyData = Body.Data
var body: Body { get }
}
@@ -40,6 +39,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
public let includes: Includes<Include>
public let meta: MetaType
public let links: LinksType
public init(primary: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.primary = primary
self.includes = includes
self.meta = meta
self.links = links
}
}
public var isError: Bool {
@@ -243,6 +249,23 @@ extension Document {
extension Document: CustomStringConvertible {
public var description: String {
return "Document(body: \(String(describing: body))"
return "Document(\(String(describing: body)))"
}
}
extension Document.Body: CustomStringConvertible {
public var description: String {
switch self {
case .errors(let errors, meta: let meta, links: let links):
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
case .data(let data):
return String(describing: data)
}
}
}
extension Document.Body.Data: CustomStringConvertible {
public var description: String {
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Decodable
// MARK: Codable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
+3 -1
View File
@@ -9,9 +9,11 @@
public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links {
public struct NoLinks: Links, CustomStringConvertible {
public static var none: NoLinks { return NoLinks() }
public init() {}
public var description: String { return "No Links" }
}
public protocol JSONAPIURL: Codable, Equatable {}
+3 -1
View File
@@ -19,8 +19,10 @@ public protocol Meta: Codable, Equatable {
// nullable.
extension Optional: Meta where Wrapped: Meta {}
public struct NoMetadata: Meta {
public struct NoMetadata: Meta, CustomStringConvertible {
public static var none: NoMetadata { return NoMetadata() }
public init() { }
public var description: String { return "No Metadata" }
}
@@ -0,0 +1,21 @@
//
// Attribute+Functor.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/28/18.
//
public extension TransformedAttribute {
/// Map an Attribute to a new wrapped type.
/// Note that the resulting Attribute will have no transformer, even if the
/// source Attribute has a transformer.
/// You are mapping the output of the source transform into
/// the RawValue of a new transformerless Attribute.
///
/// Generally, this is the most useful operation. The transformer gives you
/// control over the decoding of the Attribute, but once the Attribute exists,
/// mapping on it is most useful for creating computed Attribute properties.
public func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
return Attribute<T>(value: try transform(value))
}
}
+7
View File
@@ -19,6 +19,13 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
extension TransformedAttribute where Transformer: ReversibleTransformer {
public init(transformedValue: Transformer.To) throws {
self.value = transformedValue
rawValue = try Transformer.reverse(value)
}
}
extension TransformedAttribute: CustomStringConvertible {
public var description: String {
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
+65 -32
View File
@@ -16,11 +16,15 @@ public typealias Attributes = Codable & Equatable
/// Can be used as `Relationships` Type for Entities that do not
/// have any Relationships.
public struct NoRelationships: Relationships {}
public struct NoRelationships: Relationships {
public static var none: NoRelationships { return .init() }
}
/// Can be used as `Attributes` Type for Entities that do not
/// have any Attributes.
public struct NoAttributes: Attributes {}
public struct NoAttributes: Attributes {
public static var none: NoAttributes { return .init() }
}
/// An `EntityDescription` describes a JSON API
/// Resource Object. The Resource Object
@@ -34,31 +38,54 @@ public protocol EntityDescription {
static var type: String { get }
}
/// EntityType is the protocol that Entity conforms to. This
/// protocol lets other types accept any Entity as a generic
/// specialization.
public protocol EntityType: PrimaryResource {
/// EntityProxy is a protocol that can be used to create
/// types that _act_ like Entities but cannot be encoded
/// or decoded as Entities.
public protocol EntityProxy: Equatable {
associatedtype Description: EntityDescription
associatedtype Identifier: Equatable & Codable
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
typealias Attributes = Description.Attributes
typealias Relationships = Description.Relationships
/// The `Entity`'s Id. This can be of type `Unidentified` if
/// the entity is being created clientside and the
/// server is being asked to create a unique Id. Otherwise,
/// this should be of a type conforming to `IdType`.
var id: Id { get }
/// The JSON API compliant attributes of this `Entity`.
var attributes: Attributes { get }
/// The JSON API compliant relationships of this `Entity`.
var relationships: Relationships { get }
}
extension EntityProxy {
/// The JSON API compliant "type" of this `Entity`.
public static var type: String { return Description.type }
}
/// EntityType is the protocol that Entity conforms to. This
/// protocol lets other types accept any Entity as a generic
/// specialization.
public protocol EntityType: EntityProxy, PrimaryResource {
}
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
/// An `Entity` is a single model type that can be
/// encoded to or decoded from a JSON API
/// "Resource Object."
/// See https://jsonapi.org/format/#document-resource-objects
public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI.Identifier>: EntityType {
/// The JSON API compliant "type" of this `Entity`.
public static var type: String { return Description.type }
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
/// The `Entity`'s Id. This can be of type `Unidentified` if
/// the entity is being created clientside and the
/// server is being asked to create a unique Id. Otherwise,
/// this should be of a type conforming to `IdType`.
public let id: Identifier
public let id: Entity.Id
/// The JSON API compliant attributes of this `Entity`.
public let attributes: Description.Attributes
@@ -66,13 +93,18 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
/// The JSON API compliant relationships of this `Entity`.
public let relationships: Description.Relationships
public init(id: Identifier, attributes: Description.Attributes, relationships: Description.Relationships) {
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
self.id = id
self.attributes = attributes
self.relationships = relationships
}
}
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
public typealias Identifier = Entity.Id
public typealias WrappedIdentifier = Identifier
}
extension Entity: CustomStringConvertible {
public var description: String {
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
@@ -80,52 +112,52 @@ extension Entity: CustomStringConvertible {
}
// MARK: Convenience initializers
extension Entity where Identifier: CreatableIdType {
extension Entity where EntityRawIdType: CreatableRawIdType {
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
self.id = Identifier()
self.id = Entity.Id()
self.attributes = attributes
self.relationships = relationships
}
}
extension Entity where Description.Attributes == NoAttributes {
public init(id: Identifier, relationships: Description.Relationships) {
public init(id: Entity.Id, relationships: Description.Relationships) {
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
}
}
extension Entity where Description.Attributes == NoAttributes, Identifier: CreatableIdType {
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType {
public init(relationships: Description.Relationships) {
self.init(attributes: NoAttributes(), relationships: relationships)
}
}
extension Entity where Description.Relationships == NoRelationships {
public init(id: Identifier, attributes: Description.Attributes) {
public init(id: Entity.Id, attributes: Description.Attributes) {
self.init(id: id, attributes: attributes, relationships: NoRelationships())
}
}
extension Entity where Description.Relationships == NoRelationships, Identifier: CreatableIdType {
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
public init(attributes: Description.Attributes) {
self.init(attributes: attributes, relationships: NoRelationships())
}
}
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
public init(id: Identifier) {
public init(id: Entity.Id) {
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
}
}
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, Identifier: CreatableIdType {
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
public init() {
self.init(attributes: NoAttributes(), relationships: NoRelationships())
}
}
// MARK: Pointer for Relationships use.
public extension Entity where Identifier: IdType {
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
/// Get a pointer to this entity that can be used as a
/// relationship to another entity.
public var pointer: ToOneRelationship<Entity> {
@@ -134,21 +166,21 @@ public extension Entity where Identifier: IdType {
}
// MARK: Attribute Access
public extension Entity {
public extension EntityProxy {
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
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`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
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`.
@@ -158,18 +190,18 @@ public extension Entity {
}
// MARK: Relationship Access
public extension Entity {
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other.id`.
public static func ~><OtherEntity: OptionalRelatable>(entity: Entity, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
public static func ~><OtherEntity: OptionalRelatable>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
return entity.relationships[keyPath: path].id
}
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
public static func ~><OtherEntity: Relatable>(entity: Entity, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
public static func ~><OtherEntity: Relatable>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
return entity.relationships[keyPath: path].ids
}
}
@@ -190,7 +222,7 @@ public extension Entity {
try container.encode(Entity.type, forKey: .type)
if Identifier.self != Unidentified<Description>.self {
if EntityRawIdType.self != Unidentified.self {
try container.encode(id, forKey: .id)
}
@@ -212,8 +244,9 @@ public extension Entity {
guard Entity.type == type else {
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
}
id = try (Unidentified<Description>() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
let maybeUnidentified = Unidentified() as? EntityRawIdType
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
+24 -13
View File
@@ -5,10 +5,16 @@
// Created by Mathew Polzin on 7/24/18.
//
/// All types that are RawIdType and additionally
/// Unidentified conform to this protocol. You
/// should not add conformance to this protocol
/// directly.
public protocol MaybeRawId: Codable, Equatable {}
/// Any type that you would like to be encoded to and
/// decoded from JSON API ids should conform to this
/// protocol. Conformance for `String` is given.
public protocol RawIdType: Codable, Hashable {}
public protocol RawIdType: MaybeRawId, Hashable {}
/// If you would like to be able to create new
/// Entities with Ids backed by a RawIdType then
@@ -22,19 +28,18 @@ public protocol CreatableRawIdType: RawIdType {
extension String: RawIdType {}
public protocol Identifier: Codable, Equatable {
associatedtype EntityDescription: JSONAPI.EntityDescription
}
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: Identifier, CustomStringConvertible {
public struct Unidentified: MaybeRawId, CustomStringConvertible {
public init() {}
public var description: String { return "Id(Unidentified)" }
public var description: String { return "Unidentified" }
}
public protocol IdType: Identifier, Hashable, CustomStringConvertible {
associatedtype RawType: RawIdType
public protocol MaybeId: Codable {
associatedtype EntityType: JSONAPI.EntityProxy
associatedtype RawType: MaybeRawId
}
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
var rawValue: RawType { get }
}
@@ -48,27 +53,33 @@ public protocol CreatableIdType: IdType {
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: RawIdType, EntityDescription: JSONAPI.EntityDescription>: IdType {
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
public let rawValue: RawType
public init(rawValue: RawType) {
self.rawValue = rawValue
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(RawType.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
extension Id: CreatableIdType where RawType: CreatableRawIdType {
public init() {
rawValue = .unique()
}
}
extension Id where RawType == Unidentified {
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
}
+6 -6
View File
@@ -98,7 +98,7 @@ extension Poly1: CustomStringConvertible {
case .a(let a):
str = String(describing: a)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
@@ -177,7 +177,7 @@ extension Poly2: CustomStringConvertible {
case .b(let b):
str = String(describing: b)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
@@ -271,7 +271,7 @@ extension Poly3: CustomStringConvertible {
case .c(let c):
str = String(describing: c)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
@@ -380,7 +380,7 @@ extension Poly4: CustomStringConvertible {
case .d(let d):
str = String(describing: d)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
@@ -504,7 +504,7 @@ extension Poly5: CustomStringConvertible {
case .e(let e):
str = String(describing: e)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
@@ -643,6 +643,6 @@ extension Poly6: CustomStringConvertible {
case .f(let f):
str = String(describing: f)
}
return "Include(\(str))"
return "Poly(\(str))"
}
}
+9 -9
View File
@@ -21,13 +21,13 @@ public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Relations
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
public init(entity: Entity<Relatable.Description, Relatable.Identifier>) {
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity.id
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relatable.Identifier> {
public init(entity: Entity<Relatable.Description, Relatable.Identifier>?) {
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity?.id
}
}
@@ -40,6 +40,10 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType
public let ids: [Relatable.Identifier]
public init(ids: [Relatable.Identifier]) {
self.ids = ids
}
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
ids = relationships.map { $0.id }
}
@@ -54,7 +58,7 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType
}
extension ToManyRelationship {
public init(entities: [Entity<Relatable.Description, Relatable.Identifier>]) {
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
ids = entities.map { $0.id }
}
}
@@ -72,10 +76,6 @@ public typealias OptionalRelatable = WrappedRelatable
/// has an IdType Identifier
public protocol Relatable: WrappedRelatable {}
extension Entity: Relatable, WrappedRelatable where Identifier: JSONAPI.IdType {
public typealias WrappedIdentifier = Identifier
}
extension Optional: OptionalRelatable where Wrapped: Relatable {
public typealias Description = Wrapped.Description
public typealias Identifier = Wrapped.Identifier
@@ -180,5 +180,5 @@ extension ToOneRelationship: CustomStringConvertible {
}
extension ToManyRelationship: CustomStringConvertible {
public var description: String { return "Relationship(\(String(describing: ids)))" }
public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" }
}
+19 -5
View File
@@ -9,11 +9,16 @@ public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
static func transform(_ value: From) throws -> To
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
public protocol ReversibleTransformer: Transformer {
static func reverse(_ value: To) throws -> From
}
public enum IdentityTransformer<T>: ReversibleTransformer {
public static func transform(_ value: T) throws -> T { return value }
public static func reverse(_ value: T) throws -> T { return value }
}
// MARK: - Validator
@@ -22,10 +27,19 @@ public enum IdentityTransformer<T>: Transformer {
/// is passed to it but it does not change the type of the value. Any
/// Transformer will perform validation in one sense so a Validator is
/// really just semantic sugar (it can provide clarity in its use).
public protocol Validator: Transformer where From == To {}
/// To enforce the semantics, any change of the value in your implementation
/// of `Validator.transform()` will be ignored.
public protocol Validator: ReversibleTransformer where From == To {
}
extension Validator {
public static func reverse(_ value: To) throws -> To {
let _ = try transform(value)
return value
}
public static func validate(_ value: To) throws -> To {
return try transform(value)
let _ = try transform(value)
return value
}
}
+14 -5
View File
@@ -8,12 +8,25 @@
import JSONAPI
public enum EntityCheckError: Swift.Error {
/// The attributes should live in a struct, not
/// another type class.
case attributesNotStruct
/// The relationships should live in a struct, not
/// another type class.
case relationshipsNotStruct
/// All stored properties on an Attributes struct should
/// be one of the supplied Attribute types.
case nonAttribute(named: String)
/// All stored properties on a Relationships struct should
/// be one of the supplied Relationship types.
case nonRelationship(named: String)
/// It is explicitly stated by the JSON spec
/// a "none" value for arrays is an empty array, not `nil`.
case nullArray(named: String)
case badId
}
public struct EntityCheckErrors: Swift.Error {
@@ -36,10 +49,6 @@ public extension Entity {
public static func check(_ entity: Entity) throws {
var problems = [EntityCheckError]()
if Swift.type(of: entity.id).EntityDescription.self != Description.self {
problems.append(.badId)
}
let attributesMirror = Mirror(reflecting: entity.attributes)
if attributesMirror.displayStyle != .`struct` {
@@ -0,0 +1,30 @@
//
// Optional+Literal.swift
// JSONAPITestLib
//
// Created by Mathew Polzin on 11/29/18.
//
extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral {
public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self = .some(Wrapped(unicodeScalarLiteral: value))
}
}
extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self = .some(Wrapped(extendedGraphemeClusterLiteral: value))
}
}
extension Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral {
public typealias StringLiteralType = Wrapped.StringLiteralType
public init(stringLiteral value: StringLiteralType) {
self = .some(Wrapped(stringLiteral: value))
}
}
@@ -9,6 +9,39 @@ import JSONAPI
extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIdentifier: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
self.init(id: Relatable.WrappedIdentifier(nilLiteral: ()))
}
}
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Relatable.WrappedIdentifier: ExpressibleByUnicodeScalarLiteral {
public typealias UnicodeScalarLiteralType = Relatable.WrappedIdentifier.UnicodeScalarLiteralType
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self.init(id: Relatable.WrappedIdentifier(unicodeScalarLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Relatable.WrappedIdentifier: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Relatable.WrappedIdentifier.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self.init(id: Relatable.WrappedIdentifier(extendedGraphemeClusterLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByStringLiteral where Relatable.WrappedIdentifier: ExpressibleByStringLiteral {
public typealias StringLiteralType = Relatable.WrappedIdentifier.StringLiteralType
public init(stringLiteral value: StringLiteralType) {
self.init(id: Relatable.WrappedIdentifier(stringLiteral: value))
}
}
extension ToManyRelationship: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = Relatable.Identifier
public init(arrayLiteral elements: ArrayLiteralElement...) {
self.init(ids: elements)
}
}
@@ -0,0 +1,70 @@
//
// Attribute+FunctorTests.swift
// JSONAPITests
//
// Created by Mathew Polzin on 11/28/18.
//
import XCTest
import JSONAPI
import JSONAPITestLib
class Attribute_FunctorTests: XCTestCase {
func test_mapGuaranteed() {
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)))
XCTAssertNotNil(entity)
XCTAssertEqual(entity?[\.computedString], "Frankie2")
}
func test_mapOptionalSuccess() {
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)))
XCTAssertNotNil(entity)
XCTAssertEqual(entity?[\.computedNumber], 22)
}
func test_mapOptionalFailure() {
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)))
XCTAssertNotNil(entity)
XCTAssertNil(entity?[\.computedNumber])
}
}
// MARK: Test types
extension Attribute_FunctorTests {
enum TestTypeDescription: EntityDescription {
public static var type: String { return "test" }
public struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
let number: TransformedAttribute<Double, DoubleToString>
var computedString: Attribute<String> {
return name.map { $0 + "2" }
}
var computedNumber: Attribute<Int>? {
return try? number.map { string in
let num = Double(string).flatMap { Int(exactly: $0) }
guard let ret = num else {
throw DecodingError.typeMismatch(Int.self, .init(codingPath: [], debugDescription: "String was not an Int."))
}
return ret
}
}
}
public typealias Relationships = NoRelationships
}
typealias TestType = Entity<TestTypeDescription>
enum DoubleToString: Transformer {
public static func transform(_ from: Double) -> String {
return String(from)
}
}
}
@@ -18,4 +18,31 @@ class AttributeTests: XCTestCase {
XCTAssertEqual(try Attribute<String>(rawValue: "hello"), Attribute<String>(value: "hello"))
}
func test_TransformedAttributeNoThrow() {
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(rawValue: "10"))
}
func test_TransformedAttributeThrows() {
XCTAssertThrowsError(try TransformedAttribute<String, TestTransformer>(rawValue: "10.3"))
}
func test_TransformedAttributeReversNoThrow() {
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(transformedValue: 10))
}
}
// MARK: Test types
extension AttributeTests {
enum TestTransformer: ReversibleTransformer {
public static func transform(_ value: String) throws -> Int {
guard let ret = Int(value) else {
throw DecodingError.typeMismatch(Int.self, .init(codingPath: [], debugDescription: "Expected Int from String."))
}
return ret
}
public static func reverse(_ value: Int) throws -> String {
return String(value)
}
}
}
@@ -0,0 +1,61 @@
//
// ComputedPropertiesTests.swift
// JSONAPITests
//
// Created by Mathew Polzin on 11/28/18.
//
import XCTest
import JSONAPI
import JSONAPITestLib
class ComputedPropertiesTests: XCTestCase {
func test_DecodeIgnoresComputed() {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity.id, "1234")
XCTAssertEqual(entity[\.name], "Sarah")
XCTAssertEqual(entity ~> \.other, "5678")
XCTAssertNoThrow(try TestType.check(entity))
}
func test_EncodeIgnoresComputed() {
test_DecodeEncodeEquality(type: TestType.self, data: computed_property_attribute)
}
func test_ComputedAttributeAccess() {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity[\.computed], "Sarah2")
}
func test_ComputedRelationshipAccess() {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity ~> \.computed, "5678")
}
}
// MARK: Test types
extension ComputedPropertiesTests {
public enum TestTypeDescription: EntityDescription {
public static var type: String { return "test" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
public var computed: Attribute<String> {
return name.map { $0 + "2" }
}
}
public struct Relationships: JSONAPI.Relationships {
public let other: ToOneRelationship<TestType>
public var computed: ToOneRelationship<TestType> {
return other
}
}
}
public typealias TestType = Entity<TestTypeDescription>
}

Some files were not shown because too many files have changed in this diff Show More