mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc30cb3b9e | |||
| 7045373708 | |||
| 952fe8ba7e | |||
| 951c04ad44 | |||
| 57df6b147e | |||
| a88844fe5e | |||
| 850a713dcb | |||
| e3c637a41e | |||
| 9cbc626410 | |||
| 6dd14daf53 | |||
| 744e08acf4 | |||
| 5da4a963d4 | |||
| 5433dddc81 | |||
| 95f9d8084d | |||
| d6911f170c | |||
| 9972d13a4e | |||
| 4dc63167d6 | |||
| 845d085455 | |||
| 45eb7d06bd | |||
| 4afe4cfb31 |
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import Poly
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
|
||||
@@ -10,4 +10,21 @@ encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let personSchemaData = try? encoder.encode(Person.openAPINode())
|
||||
|
||||
print("Person Schema")
|
||||
print("====")
|
||||
print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample(using: encoder))
|
||||
|
||||
print("Dog Document Schema")
|
||||
print("====")
|
||||
print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample(using: encoder))
|
||||
|
||||
print("Batch Person Document Schema")
|
||||
print("====")
|
||||
print(batchPersonSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
@@ -11,8 +11,6 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
@@ -34,8 +32,6 @@ let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner
|
||||
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
@@ -139,4 +139,6 @@ public enum HouseDescription: EntityDescription {
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
@@ -2,15 +2,70 @@ import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPITesting // for the convenience of literal initialization
|
||||
import JSONAPIOpenAPI
|
||||
import SwiftCheck
|
||||
import JSONAPIArbitrary
|
||||
|
||||
extension PersonDescription.Attributes: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<PersonDescription.Attributes> {
|
||||
return Gen.compose { c in
|
||||
return PersonDescription.Attributes(name: c.generate(),
|
||||
favoriteColor: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonDescription.Attributes: Sampleable {
|
||||
public static var sample: PersonDescription.Attributes {
|
||||
return .init(name: ["Abbie", "Eibba"], favoriteColor: "Blue")
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonDescription.Relationships: Sampleable {
|
||||
extension PersonDescription.Relationships: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<PersonDescription.Relationships> {
|
||||
return Gen.compose { c in
|
||||
return PersonDescription.Relationships(friends: c.generate(),
|
||||
dogs: c.generate(),
|
||||
home: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: PersonDescription.Relationships {
|
||||
return .init(friends: ["1", "2"], dogs: ["2"], home: "1")
|
||||
}
|
||||
}
|
||||
|
||||
extension DogDescription.Attributes: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<DogDescription.Attributes> {
|
||||
return Gen.compose { c in
|
||||
return DogDescription.Attributes(name: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: DogDescription.Attributes {
|
||||
return DogDescription.Attributes.arbitrary.generate
|
||||
}
|
||||
}
|
||||
|
||||
extension DogDescription.Relationships: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<DogDescription.Relationships> {
|
||||
return Gen.compose { c in
|
||||
return DogDescription.Relationships(owner: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: DogDescription.Relationships {
|
||||
return DogDescription.Relationships.arbitrary.generate
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: Sampleable where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary, APIDescription: Arbitrary {
|
||||
public static var sample: Document {
|
||||
return Document.arbitrary.generate
|
||||
}
|
||||
|
||||
public static var successSample: Document? {
|
||||
return Document.arbitraryData.generate
|
||||
}
|
||||
|
||||
public static var failureSample: Document? {
|
||||
return Document.arbitraryErrors.generate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='OpenAPI Documentation'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+2
-2
@@ -23,9 +23,9 @@
|
||||
"package": "Poly",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"branch": null,
|
||||
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
|
||||
"version": null
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ let package = Package(
|
||||
targets: ["JSONAPIOpenAPI"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0"),
|
||||
.package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.11.0")
|
||||
],
|
||||
|
||||
@@ -25,7 +25,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [Relationship Object](#relationship-object)
|
||||
- [Links Object](#links-object)
|
||||
- [Misc](#misc)
|
||||
- [JSONAPI+Testing](#jsonapitesting)
|
||||
- [Testing](#testing)
|
||||
- [Entity Validator](#entity-validator)
|
||||
- [Potential Improvements](#potential-improvements)
|
||||
- [Usage](#usage)
|
||||
@@ -100,11 +100,11 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- [x] OpenAPI
|
||||
- `included`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- [x] OpenAPI
|
||||
- `errors`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
@@ -175,7 +175,7 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
|
||||
### JSONAPI+Testing
|
||||
### Testing
|
||||
#### Entity Validator
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Error+Arbitrary.swift
|
||||
// JSONAPIArbitrary
|
||||
//
|
||||
// Created by Mathew Polzin on 1/21/19.
|
||||
//
|
||||
|
||||
import SwiftCheck
|
||||
import JSONAPI
|
||||
|
||||
extension UnknownJSONAPIError: Arbitrary {
|
||||
public static var arbitrary: Gen<UnknownJSONAPIError> {
|
||||
return Gen.pure(.unknownError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// JSONAPIInclude+OpenAPI.swift
|
||||
// JSONAPIOpenAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 1/22/19.
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
|
||||
extension Includes: OpenAPINodeType where I: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
let includeNode = try I.openAPINode()
|
||||
|
||||
return .array(.init(format: .generic,
|
||||
required: true),
|
||||
.init(items: includeNode,
|
||||
uniqueItems: true))
|
||||
}
|
||||
}
|
||||
|
||||
extension Include0: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
throw OpenAPITypeError.invalidNode
|
||||
}
|
||||
}
|
||||
|
||||
extension Include1: OpenAPINodeType where A: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [A.openAPINode()])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include2: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include3: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include4: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include5: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include6: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include7: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include8: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode(),
|
||||
H.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include9: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType, I: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode(),
|
||||
H.openAPINode(),
|
||||
I.openAPINode()
|
||||
])
|
||||
}
|
||||
}
|
||||
+79
-16
@@ -6,11 +6,17 @@
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
import AnyCodable
|
||||
|
||||
private protocol _Optional {}
|
||||
extension Optional: _Optional {}
|
||||
|
||||
private protocol Wrapper {
|
||||
associatedtype Wrapped
|
||||
}
|
||||
extension Optional: Wrapper {}
|
||||
|
||||
extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType {
|
||||
static public func openAPINode() throws -> JSONNode {
|
||||
// If the RawValue is not required, we actually consider it
|
||||
@@ -48,14 +54,14 @@ extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType {
|
||||
}
|
||||
|
||||
extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable {
|
||||
public static var allCases: [AnyCodable] {
|
||||
return (try? allCases(from: Array(RawValue.allCases))) ?? []
|
||||
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
|
||||
return (try? allCases(from: Array(RawValue.allCases), using: encoder)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable {
|
||||
public static var allCases: [AnyCodable] {
|
||||
return RawValue.allCases
|
||||
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
|
||||
return RawValue.allCases(using: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +77,17 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: conform TransformedAttribute to all of the above protocols that Attribute conforms to.
|
||||
|
||||
extension RelationshipType {
|
||||
static func relationshipNode(nullable: Bool) -> JSONNode {
|
||||
static func relationshipNode(nullable: Bool, jsonType: String) -> JSONNode {
|
||||
let propertiesDict: [String: JSONNode] = [
|
||||
"id": .string(.init(format: .generic,
|
||||
required: true),
|
||||
.init()),
|
||||
"type": .string(.init(format: .generic,
|
||||
required: true),
|
||||
required: true,
|
||||
allowedValues: [.init(jsonType)]),
|
||||
.init())
|
||||
]
|
||||
|
||||
@@ -90,20 +99,24 @@ extension RelationshipType {
|
||||
}
|
||||
|
||||
extension ToOneRelationship: OpenAPINodeType {
|
||||
// TODO: const for json `type`
|
||||
// NOTE: const for json `type` not supported by OpenAPI 3.0
|
||||
// Will use "enum" with one possible value for now.
|
||||
|
||||
// TODO: metadata & links
|
||||
static public func openAPINode() throws -> JSONNode {
|
||||
let nullable = Identifiable.self is _Optional.Type
|
||||
return .object(.init(format: .generic,
|
||||
required: true),
|
||||
.init(properties: [
|
||||
"data": ToOneRelationship.relationshipNode(nullable: nullable)
|
||||
"data": ToOneRelationship.relationshipNode(nullable: nullable, jsonType: Identifiable.jsonType)
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship: OpenAPINodeType {
|
||||
// TODO: const for json `type`
|
||||
// NOTE: const for json `type` not supported by OpenAPI 3.0
|
||||
// Will use "enum" with one possible value for now.
|
||||
|
||||
// TODO: metadata & links
|
||||
static public func openAPINode() throws -> JSONNode {
|
||||
return .object(.init(format: .generic,
|
||||
@@ -111,13 +124,16 @@ extension ToManyRelationship: OpenAPINodeType {
|
||||
.init(properties: [
|
||||
"data": .array(.init(format: .generic,
|
||||
required: true),
|
||||
.init(items: ToManyRelationship.relationshipNode(nullable: false)))
|
||||
.init(items: ToManyRelationship.relationshipNode(nullable: false, jsonType: Relatable.jsonType)))
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
// NOTE: const for json `type` not supported by OpenAPI 3.0
|
||||
// Will use "enum" with one possible value for now.
|
||||
|
||||
// TODO: metadata, links
|
||||
|
||||
let idNode = JSONNode.string(.init(format: .generic,
|
||||
@@ -126,19 +142,20 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc
|
||||
let idProperty = ("id", idNode)
|
||||
|
||||
let typeNode = JSONNode.string(.init(format: .generic,
|
||||
required: true),
|
||||
required: true,
|
||||
allowedValues: [.init(Entity.jsonType)]),
|
||||
.init())
|
||||
let typeProperty = ("type", typeNode)
|
||||
|
||||
let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self
|
||||
? nil
|
||||
: try Description.Attributes.genericObjectOpenAPINode()
|
||||
: try Description.Attributes.genericOpenAPINode(using: encoder)
|
||||
|
||||
let attributesProperty = attributesNode.map { ("attributes", $0) }
|
||||
|
||||
let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self
|
||||
? nil
|
||||
: try Description.Relationships.genericObjectOpenAPINode()
|
||||
: try Description.Relationships.genericOpenAPINode(using: encoder)
|
||||
|
||||
let relationshipsProperty = relationshipsNode.map { ("relationships", $0) }
|
||||
|
||||
@@ -147,7 +164,53 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc
|
||||
typeProperty,
|
||||
attributesProperty,
|
||||
relationshipsProperty
|
||||
].compactMap { $0 }) { _, value in value }
|
||||
].compactMap { $0 }) { _, value in value }
|
||||
|
||||
return .object(.init(format: .generic,
|
||||
required: true),
|
||||
.init(properties: propertiesDict))
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try Entity.openAPINode(using: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return .array(.init(format: .generic,
|
||||
required: true),
|
||||
.init(items: try Entity.openAPINode(using: encoder)))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
// TODO: metadata, links, api description, errors
|
||||
// TODO: represent data and errors as the two distinct possible outcomes
|
||||
|
||||
let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode(using: encoder)
|
||||
|
||||
let primaryDataProperty = primaryDataNode.map { ("data", $0) }
|
||||
|
||||
let includeNode: JSONNode?
|
||||
do {
|
||||
includeNode = try Includes<Include>.openAPINode()
|
||||
} catch let err as OpenAPITypeError {
|
||||
guard err == .invalidNode else {
|
||||
throw err
|
||||
}
|
||||
includeNode = nil
|
||||
}
|
||||
|
||||
let includeProperty = includeNode.map { ("included", $0) }
|
||||
|
||||
let propertiesDict = Dictionary([
|
||||
primaryDataProperty,
|
||||
includeProperty
|
||||
].compactMap { $0 }) { _, value in value }
|
||||
|
||||
return .object(.init(format: .generic,
|
||||
required: true),
|
||||
+36
-18
@@ -12,6 +12,8 @@ extension JSONNode.Context: Encodable {
|
||||
case format
|
||||
case allowedValues = "enum"
|
||||
case nullable
|
||||
case example
|
||||
// case constantValue = "const"
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -27,7 +29,15 @@ extension JSONNode.Context: Encodable {
|
||||
try container.encode(allowedValues, forKey: .allowedValues)
|
||||
}
|
||||
|
||||
// if constantValue != nil {
|
||||
// try container.encode(constantValue, forKey: .constantValue)
|
||||
// }
|
||||
|
||||
try container.encode(nullable, forKey: .nullable)
|
||||
|
||||
if example != nil {
|
||||
try container.encode(example, forKey: .example)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +120,7 @@ extension JSONNode.ArrayContext: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONNode.ObjectContext : Encodable{
|
||||
extension JSONNode.ObjectContext : Encodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case maxProperties
|
||||
case minProperties
|
||||
@@ -132,17 +142,21 @@ extension JSONNode.ObjectContext : Encodable{
|
||||
try container.encode(additionalProperties, forKey: .additionalProperties)
|
||||
}
|
||||
|
||||
let required = properties.filter { (name, node) in
|
||||
node.required
|
||||
}.keys
|
||||
try container.encode(requiredProperties, forKey: .required)
|
||||
|
||||
try container.encode(Array(required), forKey: .required)
|
||||
|
||||
try container.encode(max(minProperties, required.count), forKey: .minProperties)
|
||||
try container.encode(minProperties, forKey: .minProperties)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONNode: Encodable {
|
||||
|
||||
private enum SubschemaCodingKeys: String, CodingKey {
|
||||
case allOf
|
||||
case oneOf
|
||||
case anyOf
|
||||
case not
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .boolean(let context):
|
||||
@@ -156,21 +170,25 @@ extension JSONNode: Encodable {
|
||||
try contextA.encode(to: encoder)
|
||||
try contextB.encode(to: encoder)
|
||||
|
||||
case .allOf(let nodes):
|
||||
// TODO
|
||||
print("TODO")
|
||||
case .all(of: let nodes):
|
||||
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
|
||||
|
||||
case .oneOf(let nodes):
|
||||
// TODO
|
||||
print("TODO")
|
||||
try container.encode(nodes, forKey: .allOf)
|
||||
|
||||
case .anyOf(let nodes):
|
||||
// TODO
|
||||
print("TODO")
|
||||
case .one(of: let nodes):
|
||||
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
|
||||
|
||||
try container.encode(nodes, forKey: .oneOf)
|
||||
|
||||
case .any(of: let nodes):
|
||||
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
|
||||
|
||||
try container.encode(nodes, forKey: .anyOf)
|
||||
|
||||
case .not(let node):
|
||||
// TODO
|
||||
print("TODO")
|
||||
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
|
||||
|
||||
try container.encode(node, forKey: .not)
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
-19
@@ -16,6 +16,25 @@ public protocol OpenAPINodeType {
|
||||
static func openAPINode() throws -> JSONNode
|
||||
}
|
||||
|
||||
extension OpenAPINodeType where Self: Sampleable, Self: Encodable {
|
||||
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode {
|
||||
return try openAPINode().with(example: Self.successSample ?? Self.sample, using: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything conforming to `OpenAPIEncodedNodeType` can provide an
|
||||
/// OpenAPI schema representing itself but it may need an Encoder
|
||||
/// to do its job.
|
||||
public protocol OpenAPIEncodedNodeType: OpenAPINodeType {
|
||||
static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode
|
||||
}
|
||||
|
||||
extension OpenAPIEncodedNodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try openAPINode(using: JSONEncoder())
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything conforming to `RawOpenAPINodeType` can provide an
|
||||
/// OpenAPI schema representing itself. This second protocol is
|
||||
/// necessary so that one type can conditionally provide a
|
||||
@@ -49,15 +68,21 @@ public protocol DoubleWrappedRawOpenAPIType {
|
||||
static func wrappedOpenAPINode() throws -> JSONNode
|
||||
}
|
||||
|
||||
/// A GenericOpenAPINodeType can take a stab at
|
||||
/// determining its OpenAPINode because it is sampleable.
|
||||
public protocol GenericOpenAPINodeType {
|
||||
static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode
|
||||
}
|
||||
|
||||
/// Anything conforming to `AnyJSONCaseIterable` can provide a
|
||||
/// list of its possible values.
|
||||
public protocol AnyJSONCaseIterable {
|
||||
static var allCases: [AnyCodable] { get }
|
||||
static func allCases(using encoder: JSONEncoder) -> [AnyCodable]
|
||||
}
|
||||
|
||||
extension AnyJSONCaseIterable {
|
||||
/// Given an array of Codable values, retrieve an array of AnyCodables.
|
||||
static func allCases<T: Codable>(from input: [T]) throws -> [AnyCodable] {
|
||||
static func allCases<T: Codable>(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] {
|
||||
if let alreadyGoodToGo = input as? [AnyCodable] {
|
||||
return alreadyGoodToGo
|
||||
}
|
||||
@@ -70,7 +95,7 @@ extension AnyJSONCaseIterable {
|
||||
// by AnyCodable, but AnyCodable wants it to actually BE a String
|
||||
// upon initialization.
|
||||
|
||||
guard let arrayOfCodables = try JSONSerialization.jsonObject(with: JSONEncoder().encode(input), options: []) as? [Any] else {
|
||||
guard let arrayOfCodables = try JSONSerialization.jsonObject(with: encoder.encode(input), options: []) as? [Any] else {
|
||||
throw OpenAPICodableError.allCasesArrayNotCodable
|
||||
}
|
||||
return arrayOfCodables.map(AnyCodable.init)
|
||||
@@ -85,7 +110,7 @@ extension AnyJSONCaseIterable {
|
||||
/// The "different" conditions have to do
|
||||
/// with Optionality, hence the name of this protocol.
|
||||
public protocol AnyWrappedJSONCaseIterable {
|
||||
static var allCases: [AnyCodable] { get }
|
||||
static func allCases(using encoder: JSONEncoder) -> [AnyCodable]
|
||||
}
|
||||
|
||||
public protocol SwiftTyped {
|
||||
@@ -239,9 +264,9 @@ public enum JSONNode: Equatable {
|
||||
case number(Context<JSONTypeFormat.NumberFormat>, NumericContext)
|
||||
case integer(Context<JSONTypeFormat.IntegerFormat>, NumericContext)
|
||||
case string(Context<JSONTypeFormat.StringFormat>, StringContext)
|
||||
indirect case allOf([JSONNode])
|
||||
indirect case oneOf([JSONNode])
|
||||
indirect case anyOf([JSONNode])
|
||||
indirect case all(of: [JSONNode])
|
||||
indirect case one(of: [JSONNode])
|
||||
indirect case any(of: [JSONNode])
|
||||
indirect case not(JSONNode)
|
||||
|
||||
public struct Context<Format: OpenAPIFormat>: JSONNodeContext, Equatable {
|
||||
@@ -249,6 +274,10 @@ public enum JSONNode: Equatable {
|
||||
public let required: Bool
|
||||
public let nullable: Bool
|
||||
|
||||
// NOTE: "const" is supported by the newest JSON Schema spec but not
|
||||
// yet by OpenAPI. Instead, will use "enum" with one possible value for now.
|
||||
// public let constantValue: Format.SwiftType?
|
||||
|
||||
/// The OpenAPI spec calls this "enum"
|
||||
/// If not specified, it is assumed that any
|
||||
/// value of the given format is allowed.
|
||||
@@ -261,14 +290,26 @@ public enum JSONNode: Equatable {
|
||||
/// into an allowed value.
|
||||
public let allowedValues: [AnyCodable]?
|
||||
|
||||
// I wanted example to be AnyCodable, but alas that causes
|
||||
// runtime problems when encoding in a very strange way.
|
||||
// For now, a String (which is OK by the OpenAPI spec) will
|
||||
// have to do.
|
||||
public let example: String?
|
||||
|
||||
public init(format: Format,
|
||||
required: Bool,
|
||||
nullable: Bool = false,
|
||||
allowedValues: [AnyCodable]? = nil) {
|
||||
// constantValue: Format.SwiftType? = nil,
|
||||
allowedValues: [AnyCodable]? = nil,
|
||||
example: (codable: AnyCodable, encoder: JSONEncoder)? = nil) {
|
||||
self.format = format
|
||||
self.required = required
|
||||
self.nullable = nullable
|
||||
// self.constantValue = constantValue
|
||||
self.allowedValues = allowedValues
|
||||
self.example = example
|
||||
.flatMap { try? $0.encoder.encode($0.codable)}
|
||||
.flatMap { String(data: $0, encoding: .utf8) }
|
||||
}
|
||||
|
||||
/// Return the optional version of this Context
|
||||
@@ -276,6 +317,7 @@ public enum JSONNode: Equatable {
|
||||
return .init(format: format,
|
||||
required: false,
|
||||
nullable: nullable,
|
||||
// constantValue: constantValue,
|
||||
allowedValues: allowedValues)
|
||||
}
|
||||
|
||||
@@ -284,6 +326,7 @@ public enum JSONNode: Equatable {
|
||||
return .init(format: format,
|
||||
required: true,
|
||||
nullable: nullable,
|
||||
// constantValue: constantValue,
|
||||
allowedValues: allowedValues)
|
||||
}
|
||||
|
||||
@@ -292,16 +335,28 @@ public enum JSONNode: Equatable {
|
||||
return .init(format: format,
|
||||
required: required,
|
||||
nullable: true,
|
||||
// constantValue: constantValue,
|
||||
allowedValues: allowedValues)
|
||||
}
|
||||
|
||||
/// Return this context with the given list of possible values
|
||||
public func with(allowedValues: [AnyCodable]?) -> Context {
|
||||
public func with(allowedValues: [AnyCodable]) -> Context {
|
||||
return .init(format: format,
|
||||
required: required,
|
||||
nullable: nullable,
|
||||
// constantValue: constantValue,
|
||||
allowedValues: allowedValues)
|
||||
}
|
||||
|
||||
/// Return this context with the given example
|
||||
public func with(example: AnyCodable, using encoder: JSONEncoder) -> Context {
|
||||
return .init(format: format,
|
||||
required: required,
|
||||
nullable: nullable,
|
||||
// constantValue: constantValue,
|
||||
allowedValues: allowedValues,
|
||||
example: (codable: example, encoder: encoder))
|
||||
}
|
||||
}
|
||||
|
||||
public struct NumericContext: Equatable {
|
||||
@@ -371,7 +426,7 @@ public enum JSONNode: Equatable {
|
||||
|
||||
public struct ObjectContext: Equatable {
|
||||
public let maxProperties: Int?
|
||||
public let minProperties: Int
|
||||
let _minProperties: Int
|
||||
public let properties: [String: JSONNode]
|
||||
public let additionalProperties: [String: JSONNode]?
|
||||
|
||||
@@ -379,8 +434,16 @@ public enum JSONNode: Equatable {
|
||||
// NOTE that an object's required properties
|
||||
// array is determined by looking at its properties'
|
||||
// required Bool.
|
||||
public let required: [String]
|
||||
*/
|
||||
public var requiredProperties: [String] {
|
||||
return Array(properties.filter { (name, node) in
|
||||
node.required
|
||||
}.keys)
|
||||
}
|
||||
|
||||
public var minProperties: Int {
|
||||
return max(_minProperties, requiredProperties.count)
|
||||
}
|
||||
|
||||
public init(properties: [String: JSONNode],
|
||||
additionalProperties: [String: JSONNode]? = nil,
|
||||
@@ -389,7 +452,7 @@ public enum JSONNode: Equatable {
|
||||
self.properties = properties
|
||||
self.additionalProperties = additionalProperties
|
||||
self.maxProperties = maxProperties
|
||||
self.minProperties = minProperties
|
||||
self._minProperties = minProperties
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +470,7 @@ public enum JSONNode: Equatable {
|
||||
return .integer(context.format)
|
||||
case .string(let context, _):
|
||||
return .string(context.format)
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -421,7 +484,7 @@ public enum JSONNode: Equatable {
|
||||
.integer(let contextA as JSONNodeContext, _),
|
||||
.string(let contextA as JSONNodeContext, _):
|
||||
return contextA.required
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -441,7 +504,7 @@ public enum JSONNode: Equatable {
|
||||
return .integer(context.optionalContext(), contextB)
|
||||
case .string(let context, let contextB):
|
||||
return .string(context.optionalContext(), contextB)
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -461,7 +524,7 @@ public enum JSONNode: Equatable {
|
||||
return .integer(context.requiredContext(), contextB)
|
||||
case .string(let context, let contextB):
|
||||
return .string(context.requiredContext(), contextB)
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -481,7 +544,7 @@ public enum JSONNode: Equatable {
|
||||
return .integer(context.nullableContext(), contextB)
|
||||
case .string(let context, let contextB):
|
||||
return .string(context.nullableContext(), contextB)
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -501,12 +564,45 @@ public enum JSONNode: Equatable {
|
||||
return .integer(context.with(allowedValues: allowedValues), contextB)
|
||||
case .string(let context, let contextB):
|
||||
return .string(context.with(allowedValues: allowedValues), contextB)
|
||||
case .allOf, .oneOf, .anyOf, .not:
|
||||
case .all, .one, .any, .not:
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func with<T: Encodable>(example codableExample: T,
|
||||
using encoder: JSONEncoder) throws -> JSONNode {
|
||||
let example: AnyCodable
|
||||
if let goodToGo = codableExample as? AnyCodable {
|
||||
example = goodToGo
|
||||
} else {
|
||||
example = AnyCodable(try JSONSerialization.jsonObject(with: encoder.encode(codableExample), options: []))
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .boolean(let context):
|
||||
return .boolean(context.with(example: example, using: encoder))
|
||||
case .object(let contextA, let contextB):
|
||||
return .object(contextA.with(example: example, using: encoder), contextB)
|
||||
case .array(let contextA, let contextB):
|
||||
return .array(contextA.with(example: example, using: encoder), contextB)
|
||||
case .number(let context, let contextB):
|
||||
return .number(context.with(example: example, using: encoder), contextB)
|
||||
case .integer(let context, let contextB):
|
||||
return .integer(context.with(example: example, using: encoder), contextB)
|
||||
case .string(let context, let contextB):
|
||||
return .string(context.with(example: example, using: encoder), contextB)
|
||||
case .all, .one, .any, .not:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OpenAPICodableError: Swift.Error {
|
||||
public enum OpenAPICodableError: Swift.Error, Equatable {
|
||||
case allCasesArrayNotCodable
|
||||
case exampleNotCodable
|
||||
case primitiveGuessFailed
|
||||
}
|
||||
|
||||
public enum OpenAPITypeError: Swift.Error, Equatable {
|
||||
case invalidNode
|
||||
}
|
||||
+3
-2
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import AnyCodable
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,8 +57,8 @@ extension Optional: DoubleWrappedRawOpenAPIType where Wrapped: WrappedRawOpenAPI
|
||||
}
|
||||
|
||||
extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Codable {
|
||||
public static var allCases: [AnyCodable] {
|
||||
return (try? allCases(from: Array(Wrapped.allCases))) ?? []
|
||||
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
|
||||
return (try? allCases(from: Array(Wrapped.allCases), using: encoder)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Mathew Polzin on 1/19/19.
|
||||
//
|
||||
|
||||
/// Zip two optionals together with the given operation performed on
|
||||
/// the unwrapped contents. If either optional is nil, the zip
|
||||
/// yields nil.
|
||||
func zip<X, Y, Z>(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? {
|
||||
return left.flatMap { lft in right.map { rght in fn(lft, rght) }}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
//
|
||||
// Sampleable.swift
|
||||
// JSONAPIOpenAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 1/15/19.
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
import AnyCodable
|
||||
|
||||
/// A Sampleable type can provide a sample value.
|
||||
/// This is useful for reflection.
|
||||
public protocol Sampleable {
|
||||
/// Get a sample value of type Self. This can be the
|
||||
/// same value every time, or it can be an arbitrarily random
|
||||
/// value each time.
|
||||
static var sample: Self { get }
|
||||
}
|
||||
|
||||
extension Sampleable {
|
||||
public static func genericObjectOpenAPINode() throws -> JSONNode {
|
||||
let mirror = Mirror(reflecting: Self.sample)
|
||||
let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in
|
||||
|
||||
// see if we can enumerate the possible values
|
||||
let maybeAllCases: [AnyCodable]? = {
|
||||
switch type(of: child.value) {
|
||||
case let valType as AnyJSONCaseIterable.Type:
|
||||
return valType.allCases
|
||||
case let valType as AnyWrappedJSONCaseIterable.Type:
|
||||
return valType.allCases
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
// try to snag an OpenAPI Node
|
||||
let maybeOpenAPINode: JSONNode? = try {
|
||||
switch type(of: child.value) {
|
||||
case let valType as OpenAPINodeType.Type:
|
||||
return try valType.openAPINode()
|
||||
|
||||
case let valType as RawOpenAPINodeType.Type:
|
||||
return try valType.rawOpenAPINode()
|
||||
|
||||
case let valType as WrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
case let valType as DoubleWrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
// put it all together
|
||||
let newNode: JSONNode?
|
||||
if let allCases = maybeAllCases,
|
||||
let openAPINode = maybeOpenAPINode {
|
||||
newNode = try openAPINode.with(allowedValues: allCases)
|
||||
} else {
|
||||
newNode = maybeOpenAPINode
|
||||
}
|
||||
|
||||
return zip(child.label, newNode) { ($0, $1) }
|
||||
}
|
||||
|
||||
// There should not be any duplication of keys since these are
|
||||
// property names, but rather than risk runtime exception, we just
|
||||
// fail to the newer value arbitrarily
|
||||
let propertiesDict = Dictionary(properties) { _, value2 in value2 }
|
||||
|
||||
return .object(.init(format: .generic,
|
||||
required: true),
|
||||
.init(properties: propertiesDict))
|
||||
}
|
||||
}
|
||||
|
||||
extension NoAttributes: Sampleable {
|
||||
public static var sample: NoAttributes {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoRelationships: Sampleable {
|
||||
public static var sample: NoRelationships {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoMetadata: Sampleable {
|
||||
public static var sample: NoMetadata {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoLinks: Sampleable {
|
||||
public static var sample: NoLinks {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// Include+Sampleable.swift
|
||||
// JSONAPIOpenAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 1/23/19.
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
|
||||
extension Includes: Sampleable where I: Sampleable {
|
||||
public static var sample: Includes<I> {
|
||||
guard I.self != NoIncludes.self else {
|
||||
return .none
|
||||
}
|
||||
|
||||
return .init(values: I.samples)
|
||||
}
|
||||
}
|
||||
|
||||
extension NoIncludes: Sampleable {
|
||||
public static var sample: NoIncludes {
|
||||
return NoIncludes()
|
||||
}
|
||||
}
|
||||
|
||||
extension Include1: Sampleable where A: Sampleable {
|
||||
public static var sample: Include1<A> {
|
||||
return .init(A.sample)
|
||||
}
|
||||
|
||||
public static var samples: [Include1<A>] {
|
||||
return A.samples.map(Include1<A>.init)
|
||||
}
|
||||
}
|
||||
|
||||
extension Include2: Sampleable where A: Sampleable, B: Sampleable {
|
||||
public static var sample: Include2<A, B> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include2<A, B>] {
|
||||
return A.samples.map(Include2<A, B>.init)
|
||||
+ B.samples.map(Include2<A, B>.init)
|
||||
}
|
||||
}
|
||||
|
||||
extension Include3: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable {
|
||||
public static var sample: Include3<A, B, C> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include3<A, B, C>] {
|
||||
return A.samples.map(Include3<A, B, C>.init)
|
||||
+ B.samples.map(Include3<A, B, C>.init)
|
||||
+ C.samples.map(Include3<A, B, C>.init)
|
||||
}
|
||||
}
|
||||
|
||||
extension Include4: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable {
|
||||
public static var sample: Include4<A, B, C, D> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include4<A, B, C, D>] {
|
||||
return A.samples.map(Include4<A, B, C, D>.init)
|
||||
+ B.samples.map(Include4<A, B, C, D>.init)
|
||||
+ C.samples.map(Include4<A, B, C, D>.init)
|
||||
+ D.samples.map(Include4<A, B, C, D>.init)
|
||||
}
|
||||
}
|
||||
|
||||
extension Include5: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable {
|
||||
public static var sample: Include5<A, B, C, D, E> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include5<A, B, C, D, E>] {
|
||||
let set1: [Include5<A, B, C, D, E>] = A.samples.map(Include5<A, B, C, D, E>.init)
|
||||
+ B.samples.map(Include5<A, B, C, D, E>.init)
|
||||
+ C.samples.map(Include5<A, B, C, D, E>.init)
|
||||
|
||||
let set2: [Include5<A, B, C, D, E>] = D.samples.map(Include5<A, B, C, D, E>.init)
|
||||
+ E.samples.map(Include5<A, B, C, D, E>.init)
|
||||
|
||||
return set1 + set2
|
||||
}
|
||||
}
|
||||
|
||||
extension Include6: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable {
|
||||
public static var sample: Include6<A, B, C, D, E, F> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include6<A, B, C, D, E, F>] {
|
||||
let set1: [Include6<A, B, C, D, E, F>] = A.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
+ B.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
+ C.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
|
||||
let set2: [Include6<A, B, C, D, E, F>] = D.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
+ E.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
+ F.samples.map(Include6<A, B, C, D, E, F>.init)
|
||||
|
||||
return set1 + set2
|
||||
}
|
||||
}
|
||||
|
||||
extension Include7: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable {
|
||||
public static var sample: Include7<A, B, C, D, E, F, G> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include7<A, B, C, D, E, F, G>] {
|
||||
let set1: [Include7<A, B, C, D, E, F, G>] = A.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
+ B.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
+ C.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
|
||||
let set2: [Include7<A, B, C, D, E, F, G>] = D.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
+ E.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
+ F.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
|
||||
let set3: [Include7<A, B, C, D, E, F, G>] = G.samples.map(Include7<A, B, C, D, E, F, G>.init)
|
||||
|
||||
return set1 + set2 + set3
|
||||
}
|
||||
}
|
||||
|
||||
extension Include8: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable {
|
||||
public static var sample: Include8<A, B, C, D, E, F, G, H> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include8<A, B, C, D, E, F, G, H>] {
|
||||
let set1: [Include8<A, B, C, D, E, F, G, H>] = A.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
+ B.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
+ C.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
|
||||
let set2: [Include8<A, B, C, D, E, F, G, H>] = D.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
+ E.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
+ F.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
|
||||
let set3: [Include8<A, B, C, D, E, F, G, H>] = G.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
+ H.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
|
||||
|
||||
return set1 + set2 + set3
|
||||
}
|
||||
}
|
||||
|
||||
extension Include9: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable, I: Sampleable {
|
||||
public static var sample: Include9<A, B, C, D, E, F, G, H, I> {
|
||||
let randomChoice = Int.random(in: 0..<samples.count)
|
||||
|
||||
return samples[randomChoice]
|
||||
}
|
||||
|
||||
public static var samples: [Include9<A, B, C, D, E, F, G, H, I>] {
|
||||
let set1: [Include9<A, B, C, D, E, F, G, H, I>] = A.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ B.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ C.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
|
||||
let set2: [Include9<A, B, C, D, E, F, G, H, I>] = D.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ E.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ F.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
|
||||
let set3: [Include9<A, B, C, D, E, F, G, H, I>] = G.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ H.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
+ I.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
|
||||
|
||||
return set1 + set2 + set3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// Sampleable.swift
|
||||
// JSONAPIOpenAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 1/15/19.
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
import AnyCodable
|
||||
|
||||
/// A Sampleable type can provide a sample value.
|
||||
/// This is useful for reflection.
|
||||
public protocol Sampleable {
|
||||
/// Get a sample value of type Self. This can be the
|
||||
/// same value every time, or it can be an arbitrarily random
|
||||
/// value each time.
|
||||
static var sample: Self { get }
|
||||
|
||||
/// Get an example of success, if that is meaningful and
|
||||
/// available. If not, will be nil.
|
||||
///
|
||||
/// The default implementation returns `nil`.
|
||||
static var successSample: Self? { get }
|
||||
|
||||
/// Get an example of failure, if that is meaningful and
|
||||
/// available. If not, will be nil.
|
||||
///
|
||||
/// The default implementation returns `nil`.
|
||||
static var failureSample: Self? { get }
|
||||
|
||||
/// An array of samples of this Type.
|
||||
///
|
||||
/// The default implementation returns
|
||||
/// an array with just the result of
|
||||
/// `Self.sample` in it.
|
||||
static var samples: [Self] { get }
|
||||
}
|
||||
|
||||
public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType
|
||||
|
||||
public extension Sampleable {
|
||||
// default implementation:
|
||||
public static var successSample: Self? { return nil }
|
||||
|
||||
// default implementation:
|
||||
public static var failureSample: Self? { return nil }
|
||||
|
||||
// default implementation:
|
||||
public static var samples: [Self] { return [Self.sample] }
|
||||
}
|
||||
|
||||
extension Sampleable where Self: Encodable {
|
||||
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
let mirror = Mirror(reflecting: Self.sample)
|
||||
let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in
|
||||
|
||||
// see if we can enumerate the possible values
|
||||
let maybeAllCases: [AnyCodable]? = {
|
||||
switch type(of: child.value) {
|
||||
case let valType as AnyJSONCaseIterable.Type:
|
||||
return valType.allCases(using: encoder)
|
||||
case let valType as AnyWrappedJSONCaseIterable.Type:
|
||||
return valType.allCases(using: encoder)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
// try to snag an OpenAPI Node
|
||||
let maybeOpenAPINode: JSONNode? = try {
|
||||
switch type(of: child.value) {
|
||||
case let valType as OpenAPINodeType.Type:
|
||||
return try valType.openAPINode()
|
||||
|
||||
case let valType as RawOpenAPINodeType.Type:
|
||||
return try valType.rawOpenAPINode()
|
||||
|
||||
case let valType as WrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
case let valType as DoubleWrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
case let valType as GenericOpenAPINodeType.Type:
|
||||
return try valType.genericOpenAPINode(using: encoder)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
// put it all together
|
||||
let newNode: JSONNode?
|
||||
if let allCases = maybeAllCases,
|
||||
let openAPINode = maybeOpenAPINode {
|
||||
newNode = try openAPINode.with(allowedValues: allCases)
|
||||
} else {
|
||||
newNode = maybeOpenAPINode
|
||||
}
|
||||
|
||||
return zip(child.label, newNode) { ($0, $1) }
|
||||
}
|
||||
|
||||
// if there are no properties, let's see if we are dealing
|
||||
// with a primitive.
|
||||
if properties.count == 0,
|
||||
let primitive = try primitiveGuess(using: encoder) {
|
||||
return primitive
|
||||
}
|
||||
|
||||
// There should not be any duplication of keys since these are
|
||||
// property names, but rather than risk runtime exception, we just
|
||||
// fail to the newer value arbitrarily
|
||||
let propertiesDict = Dictionary(properties) { _, value2 in value2 }
|
||||
|
||||
return .object(.init(format: .generic,
|
||||
required: true),
|
||||
.init(properties: propertiesDict))
|
||||
}
|
||||
|
||||
private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? {
|
||||
let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample))
|
||||
let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments])
|
||||
|
||||
guard let wrapperDict = wrappedValue as? [String: Any],
|
||||
wrapperDict.contains(where: { $0.key == "primitive" }) else {
|
||||
throw OpenAPICodableError.primitiveGuessFailed
|
||||
}
|
||||
|
||||
let value = (wrappedValue as! [String: Any])["primitive"]!
|
||||
|
||||
return try {
|
||||
switch type(of: value) {
|
||||
case let valType as OpenAPINodeType.Type:
|
||||
return try valType.openAPINode()
|
||||
|
||||
case let valType as RawOpenAPINodeType.Type:
|
||||
return try valType.rawOpenAPINode()
|
||||
|
||||
case let valType as WrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
case let valType as DoubleWrappedRawOpenAPIType.Type:
|
||||
return try valType.wrappedOpenAPINode()
|
||||
|
||||
case let valType as GenericOpenAPINodeType.Type:
|
||||
return try valType.genericOpenAPINode(using: encoder)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}() ?? {
|
||||
switch value {
|
||||
case is String:
|
||||
return .string(.init(format: .generic,
|
||||
required: true),
|
||||
.init())
|
||||
|
||||
case is Int:
|
||||
return .integer(.init(format: .generic,
|
||||
required: true),
|
||||
.init())
|
||||
|
||||
case is Double:
|
||||
return .number(.init(format: .double,
|
||||
required: true),
|
||||
.init())
|
||||
|
||||
case is Bool:
|
||||
return .boolean(.init(format: .generic,
|
||||
required: true))
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// The following wrapper is only needed because JSONEncoder cannot yet encode
|
||||
// JSON fragments. It is a very unfortunate limitation that requires silly
|
||||
// workarounds in edge cases like this.
|
||||
private struct PrimitiveWrapper<Wrapped: Encodable>: Encodable {
|
||||
let primitive: Wrapped
|
||||
}
|
||||
|
||||
extension Sampleable {
|
||||
public static func samples<S1: Sampleable>(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] {
|
||||
return S1.samples.map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable>(using s1: S1.Type, _ s2: S2.Type, with constructor: (S1, S2) -> Self) -> [Self] {
|
||||
return zip(S1.samples, S2.samples).map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, with constructor: (S1, S2, S3) -> Self) -> [Self] {
|
||||
return zip3(S1.samples, S2.samples, S3.samples).map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, with constructor: (S1, S2, S3, S4) -> Self) -> [Self] {
|
||||
return zip4(S1.samples, S2.samples, S3.samples, S4.samples).map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, with constructor: (S1, S2, S3, S4, S5) -> Self) -> [Self] {
|
||||
return zip5(S1.samples, S2.samples, S3.samples, S4.samples, S5.samples).map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, with constructor: (S1, S2, S3, S4, S5, S6) -> Self) -> [Self] {
|
||||
// the compiler craps out at zip6. breaking it down makes the difference.
|
||||
let firstZip = zip3(S1.samples, S2.samples, S3.samples)
|
||||
let secondZip = zip3(S4.samples, S5.samples, S6.samples)
|
||||
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2) }.map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable, S7: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7) -> Self) -> [Self] {
|
||||
// the compiler craps out at zip6. breaking it down makes the difference.
|
||||
let firstZip = zip3(S1.samples, S2.samples, S3.samples)
|
||||
let secondZip = zip4(S4.samples, S5.samples, S6.samples, S7.samples)
|
||||
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor)
|
||||
}
|
||||
|
||||
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable, S7: Sampleable, S8: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, _ s8: S8.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7, S8) -> Self) -> [Self] {
|
||||
// the compiler craps out at zip6. breaking it down makes the difference.
|
||||
let firstZip = zip4(S1.samples, S2.samples, S3.samples, S4.samples)
|
||||
let secondZip = zip4(S5.samples, S6.samples, S7.samples, S8.samples)
|
||||
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.0.3, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor)
|
||||
}
|
||||
|
||||
@inlinable static func zip3<A: Sequence, B: Sequence, C: Sequence>(_ a: A, _ b: B, _ c: C) -> [(A.Element, B.Element, C.Element)] {
|
||||
return zip(a, zip(b, c)).map { arg in (arg.0, arg.1.0, arg.1.1) }
|
||||
}
|
||||
|
||||
@inlinable static func zip4<A: Sequence, B: Sequence, C: Sequence, D: Sequence>(_ a: A, _ b: B, _ c: C, _ d: D) -> [(A.Element, B.Element, C.Element, D.Element)] {
|
||||
return zip(a, zip(b, zip(c, d))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1) }
|
||||
}
|
||||
|
||||
@inlinable static func zip5<A: Sequence, B: Sequence, C: Sequence, D: Sequence, E: Sequence>(_ a: A, _ b: B, _ c: C, _ d: D, _ e: E) -> [(A.Element, B.Element, C.Element, D.Element, E.Element)] {
|
||||
return zip(a, zip(b, zip(c, zip(d, e)))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1.0, arg.1.1.1.1) }
|
||||
}
|
||||
}
|
||||
|
||||
extension NoAttributes: Sampleable {
|
||||
public static var sample: NoAttributes {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoRelationships: Sampleable {
|
||||
public static var sample: NoRelationships {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoMetadata: Sampleable {
|
||||
public static var sample: NoMetadata {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoLinks: Sampleable {
|
||||
public static var sample: NoLinks {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension NoAPIDescription: Sampleable {
|
||||
public static var sample: NoAPIDescription {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension UnknownJSONAPIError: Sampleable {
|
||||
public static var sample: UnknownJSONAPIError {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Sampleable where RawValue: Sampleable {
|
||||
public static var sample: Attribute<RawValue> {
|
||||
return .init(value: RawValue.sample)
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleResourceBody: Sampleable where Entity: Sampleable {
|
||||
public static var sample: SingleResourceBody<Entity> {
|
||||
return .init(entity: Entity.sample)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: Sampleable where Entity: Sampleable {
|
||||
public static var sample: ManyResourceBody<Entity> {
|
||||
return .init(entities: Entity.samples)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
import XCTest
|
||||
import JSONAPI
|
||||
import JSONAPIOpenAPI
|
||||
import SwiftCheck
|
||||
import AnyCodable
|
||||
|
||||
class JSONAPIAttributeOpenAPITests: XCTestCase {
|
||||
@@ -429,7 +430,7 @@ extension JSONAPIAttributeOpenAPITests {
|
||||
extension JSONAPIAttributeOpenAPITests {
|
||||
func test_EnumAttribute() {
|
||||
let node = try! Attribute<EnumAttribute>.rawOpenAPINode()
|
||||
print(EnumAttribute.allCases)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
|
||||
|
||||
@@ -504,6 +505,124 @@ extension JSONAPIAttributeOpenAPITests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Date
|
||||
extension JSONAPIAttributeOpenAPITests {
|
||||
func test_DateStringAttribute() {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
encoder.dateEncodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
|
||||
|
||||
guard case .string(let contextA, let stringContext) = node else {
|
||||
XCTFail("Expected string Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(contextA, .init(format: .generic,
|
||||
required: true,
|
||||
nullable: false,
|
||||
allowedValues: nil))
|
||||
|
||||
XCTAssertEqual(stringContext, .init())
|
||||
}
|
||||
|
||||
func test_DateNumberAttribute() {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
encoder.dateEncodingStrategy = .secondsSince1970
|
||||
|
||||
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .number(.double))
|
||||
|
||||
guard case .number(let contextA, let numberContext) = node else {
|
||||
XCTFail("Expected string Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(contextA, .init(format: .double,
|
||||
required: true,
|
||||
nullable: false,
|
||||
allowedValues: nil))
|
||||
|
||||
XCTAssertEqual(numberContext, .init())
|
||||
}
|
||||
|
||||
// func test_NullableEnumAttribute() {
|
||||
// let node = try! Attribute<EnumAttribute?>.wrappedOpenAPINode()
|
||||
//
|
||||
// XCTAssertTrue(node.required)
|
||||
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
|
||||
//
|
||||
// guard case .string(let contextA, let stringContext) = node else {
|
||||
// XCTFail("Expected string Node")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// XCTAssertEqual(contextA, .init(format: .generic,
|
||||
// required: true,
|
||||
// nullable: true,
|
||||
// allowedValues: nil))
|
||||
//
|
||||
// XCTAssertEqual(stringContext, .init())
|
||||
// }
|
||||
//
|
||||
// func test_OptionalEnumAttribute() {
|
||||
// let node = try! Attribute<EnumAttribute>?.wrappedOpenAPINode()
|
||||
//
|
||||
// XCTAssertFalse(node.required)
|
||||
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
|
||||
//
|
||||
// guard case .string(let contextA, let stringContext) = node else {
|
||||
// XCTFail("Expected string Node")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// XCTAssertEqual(contextA, .init(format: .generic,
|
||||
// required: false,
|
||||
// nullable: false,
|
||||
// allowedValues: nil))
|
||||
//
|
||||
// XCTAssertEqual(stringContext, .init())
|
||||
// }
|
||||
//
|
||||
// func test_OptionalNullableEnumAttribute() {
|
||||
// let node = try! Attribute<EnumAttribute?>?.wrappedOpenAPINode()
|
||||
//
|
||||
// XCTAssertFalse(node.required)
|
||||
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
|
||||
//
|
||||
// guard case .string(let contextA, let stringContext) = node else {
|
||||
// XCTFail("Expected string Node")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// XCTAssertEqual(contextA, .init(format: .generic,
|
||||
// required: false,
|
||||
// nullable: true,
|
||||
// allowedValues: nil))
|
||||
//
|
||||
// XCTAssertEqual(stringContext, .init())
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
extension JSONAPIAttributeOpenAPITests {
|
||||
enum EnumAttribute: String, Codable, CaseIterable {
|
||||
@@ -511,3 +630,9 @@ extension JSONAPIAttributeOpenAPITests {
|
||||
case two
|
||||
}
|
||||
}
|
||||
|
||||
extension Date: Sampleable {
|
||||
public static var sample: Date {
|
||||
return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user