Compare commits

...

17 Commits

Author SHA1 Message Date
Mathew Polzin a88844fe5e Switch to Poly 1.0.0 (same commit as previously used but by version rather than branch) 2019-01-23 00:34:14 -08:00
Mathew Polzin 850a713dcb Merge pull request #13 from mattpolzin/feature/OpenAPISchema
Add Include support to OpenAPI schema of JSONAPI Document.
2019-01-22 12:01:40 -08:00
Mathew Polzin e3c637a41e update README and add associated type labels to patterns for clarity 2019-01-22 11:57:18 -08:00
Mathew Polzin 9cbc626410 Add Include support to OpenAPI schema of JSONAPI Document. 2019-01-22 11:53:32 -08:00
Mathew Polzin 6dd14daf53 Merge pull request #12 from mattpolzin/feature/OpenAPISchema
Testing, Documentation, more Arbitrary conformance, "example" support.
2019-01-22 08:21:22 -08:00
Mathew Polzin 744e08acf4 Update linuxmain 2019-01-21 23:00:13 -08:00
Mathew Polzin 5da4a963d4 Update README and code documentation slightly 2019-01-21 22:50:19 -08:00
Mathew Polzin 5433dddc81 Add support for 'const' via OpenAPI 3.0 workaround suggested by others: Use 'enum' with one value. Add Sampleable support for requesting samples representing 'success' and 'failure' if available. 2019-01-21 22:47:03 -08:00
Mathew Polzin 95f9d8084d Add Arbitrary conformance for UnknownJSONAPIError. Fix weird encoding exception that I cannot quite figure out or explain at the moment. 2019-01-21 21:56:33 -08:00
Mathew Polzin d6911f170c Add example to JSONNode 2019-01-21 20:57:54 -08:00
Mathew Polzin 9972d13a4e Document zip(with:) on optionals 2019-01-21 15:37:41 -08:00
Mathew Polzin 4dc63167d6 Rename a couple of files 2019-01-21 15:35:54 -08:00
Mathew Polzin 845d085455 Fill out a fair bit of OpenAPI Entity testing 2019-01-21 15:32:38 -08:00
Mathew Polzin 45eb7d06bd Merge branch 'master' into feature/OpenAPISchema 2019-01-21 13:18:59 -08:00
Mathew Polzin 6ce962cec9 Merge pull request #11 from mattpolzin/feature/Arbitrary
Add Arbitrary support for the rest of JSONAPI's key types.
2019-01-21 13:16:46 -08:00
Mathew Polzin 477dae0a73 Add Arbitrary support for the rest of JSONAPI's key types. 2019-01-21 13:13:09 -08:00
Mathew Polzin 4afe4cfb31 Rename first JSONAPI+Testing header to Testing to remove conflict in GitHub Markdown links. 2019-01-21 11:52:02 -08:00
25 changed files with 1077 additions and 72 deletions
@@ -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())
print("Dog Document Schema")
print("====")
print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
print("====")
let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample())
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
}
}
-1
View File
@@ -5,6 +5,5 @@
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='OpenAPI Documentation'/>
</pages>
</playground>
+2 -2
View File
@@ -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
View File
@@ -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")
],
+11 -11
View File
@@ -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)
@@ -99,27 +99,27 @@ Note that Playground support for importing non-system Frameworks is still a bit
#### Document
- `data`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
- [x] Arbitrary
- [x] OpenAPI
- `included`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
- [x] Arbitrary
- [x] OpenAPI
- `errors`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [x] Arbitrary
- [ ] OpenAPI
- `meta`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [x] Arbitrary
- [ ] OpenAPI
- `jsonapi` (i.e. API Information)
- [x] Encoding/Decoding
- [ ] Arbitrary
- [x] Arbitrary
- [ ] OpenAPI
- `links`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [x] Arbitrary
- [ ] OpenAPI
#### Resource Object
@@ -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.
@@ -838,7 +838,7 @@ The `JSONAPI` framework is packaged with a test library to help you test your `J
# JSONAPI+Arbitrary
The `JSONAPIArbitrary` framework adds `Arbitrary` support via `SwiftCheck`. With a little extra work on your part, this framework will allow you to create "arbitrary" (i.e. randomly generated) instances of your JSONAPI entities, includes, documents, etc.
This library does not offer full support of all `JSONAPI` types yet. The documentation will grow as the framework becomes more complete.
This Framework is currently undocumented, but familiarity with `SwiftCheck` will likely afford the user enough information to use this library to aid in the generation of arbitrary JSONAPI Documents for testing purposes.
# JSONAPI+OpenAPI
The `JSONAPIOpenAPI` framework adds the ability to generate OpenAPI compliant JSON documentation of a JSONAPI Document.
@@ -0,0 +1,24 @@
//
// APIDescription+Arbitrary.swift
// JSONAPIArbitrary
//
// Created by Mathew Polzin on 1/21/19.
//
import SwiftCheck
import JSONAPI
extension APIDescription: Arbitrary where Meta: Arbitrary {
public static var arbitrary: Gen<APIDescription<Meta>> {
return Gen.compose { c in
APIDescription(version: c.generate(),
meta: c.generate())
}
}
}
extension NoAPIDescription: Arbitrary {
public static var arbitrary: Gen<NoAPIDescription> {
return Gen.pure(.none)
}
}
@@ -14,7 +14,7 @@ extension Attribute: Arbitrary where RawValue: Arbitrary {
}
}
// Cannot extend TransformedAttribute here
// Cannot conform TransformedAttribute to Arbitrary here
// because there is no way to guarantee that an arbitrary
// RawValue will successfully transform or that an
// arbitrary Value will successfully reverse-transform.
@@ -0,0 +1,110 @@
//
// Document+Arbitrary.swift
// JSONAPIArbitrary
//
// Created by Mathew Polzin on 1/21/19.
//
import SwiftCheck
import JSONAPI
extension Document.Body.Data: Arbitrary where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary {
public static var arbitrary: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body.Data> {
return Gen.compose { c in
Document.Body.Data(primary: c.generate(),
includes: c.generate(),
meta: c.generate(),
links: c.generate())
}
}
}
extension Document.Body: Arbitrary where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary {
public static var arbitrary: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body> {
return Gen.one(of: [
arbitraryData,
arbitraryErrors
])
}
}
extension Document.Body where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary {
/// Arbitrary Document.Body with data (guaranteed to not
/// be an error body).
public static var arbitraryData: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body> {
return Document.Body.Data.arbitrary.map(Document.Body.data)
}
}
extension Document.Body where MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary {
/// Arbitrary Document.Body with errors (guaranteed to not
/// be a data body).
public static var arbitraryErrors: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body> {
return Gen.compose { c in
Document.Body.errors(c.generate(),
meta: c.generate(),
links: c.generate())
}
}
}
extension Document.Body where Error: Arbitrary {
/// Arbitrary Document.Body with errors but no
/// metadata or links (also guaranteed to not
/// be a data body).
public static var arbitraryErrorsWithoutMetaOrLinks: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body> {
return Gen.compose { c in
Document.Body.errors(c.generate(),
meta: nil,
links: nil)
}
}
}
extension Document: Arbitrary where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary, APIDescription: Arbitrary {
public static var arbitrary: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>> {
return Gen.one(of: [
arbitraryData,
arbitraryErrors
])
}
}
extension Document where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, APIDescription: Arbitrary {
/// Arbitrary Document with data (guaranteed to not
/// be an error body).
public static var arbitraryData: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>> {
return Gen.compose { c in
Document(apiDescription: c.generate(),
body: c.generate(),
includes: c.generate(),
meta: c.generate(),
links: c.generate())
}
}
}
extension Document where MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary, APIDescription: Arbitrary {
/// Arbitrary Document with errors (guaranteed to not
/// be a data body).
public static var arbitraryErrors: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>> {
return Gen.compose { c in
Document(apiDescription: c.generate(),
errors: c.generate(),
meta: c.generate(),
links: c.generate())
}
}
}
extension Document where Error: Arbitrary, APIDescription: Arbitrary {
/// Arbitrary Document with errors but no
/// metadata or links (also guaranteed to not
/// be a data body).
public static var arbitraryErrors: Gen<Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>> {
return Gen.compose { c in
Document(apiDescription: c.generate(),
errors: c.generate())
}
}
}
@@ -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,166 @@
//
// Includes+Arbitrary.swift
// JSONAPIArbitrary
//
// Created by Mathew Polzin on 1/21/19.
//
import SwiftCheck
import JSONAPI
extension Includes: Arbitrary where I: Arbitrary {
public static var arbitrary: Gen<Includes<I>> {
return I
.arbitrary
.proliferate
.map(Includes.init(values:))
}
}
extension NoIncludes: Arbitrary {
public static var arbitrary: Gen<NoIncludes> {
return Gen.pure(NoIncludes())
}
}
extension Include1: Arbitrary where A: Arbitrary {
public static var arbitrary: Gen<Include1<A>> {
return Gen.one(of: [
A.arbitrary.map(Include1.init)
])
}
}
extension Include2: Arbitrary where A: Arbitrary, B: Arbitrary {
public static var arbitrary: Gen<Include2<A, B>> {
return Gen.one(of: [
A.arbitrary.map(Include2.init),
B.arbitrary.map(Include2.init)
])
}
}
extension Include3: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary {
public static var arbitrary: Gen<Include3<A, B, C>> {
return Gen.one(of: [
A.arbitrary.map(Include3.init),
B.arbitrary.map(Include3.init),
C.arbitrary.map(Include3.init)
])
}
}
extension Include4: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary {
public static var arbitrary: Gen<Include4<A, B, C, D>> {
return Gen.one(of: [
A.arbitrary.map(Include4.init),
B.arbitrary.map(Include4.init),
C.arbitrary.map(Include4.init),
D.arbitrary.map(Include4.init)
])
}
}
extension Include5: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary {
public static var arbitrary: Gen<Include5<A, B, C, D, E>> {
return Gen.one(of: [
A.arbitrary.map(Include5.init),
B.arbitrary.map(Include5.init),
C.arbitrary.map(Include5.init),
D.arbitrary.map(Include5.init),
E.arbitrary.map(Include5.init)
])
}
}
extension Include6: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, F: Arbitrary {
public static var arbitrary: Gen<Include6<A, B, C, D, E, F>> {
// Note broken up because compiler cannot typecheck entire array
// before it times out
let set1: [Gen<Include6<A, B, C, D, E, F>>] = [
A.arbitrary.map(Include6.init),
B.arbitrary.map(Include6.init),
C.arbitrary.map(Include6.init)
]
let set2: [Gen<Include6<A, B, C, D, E, F>>] = [
D.arbitrary.map(Include6.init),
E.arbitrary.map(Include6.init),
F.arbitrary.map(Include6.init)
]
return Gen.one(of: set1 + set2)
}
}
extension Include7: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, F: Arbitrary, G: Arbitrary {
public static var arbitrary: Gen<Include7<A, B, C, D, E, F, G>> {
// Note broken up because compiler cannot typecheck entire array
// before it times out
let set1: [Gen<Include7<A, B, C, D, E, F, G>>] = [
A.arbitrary.map(Include7.init),
B.arbitrary.map(Include7.init),
C.arbitrary.map(Include7.init)
]
let set2: [Gen<Include7<A, B, C, D, E, F, G>>] = [
D.arbitrary.map(Include7.init),
E.arbitrary.map(Include7.init),
F.arbitrary.map(Include7.init),
G.arbitrary.map(Include7.init)
]
return Gen.one(of: set1 + set2)
}
}
extension Include8: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, F: Arbitrary, G: Arbitrary, H: Arbitrary {
public static var arbitrary: Gen<Include8<A, B, C, D, E, F, G, H>> {
// Note broken up because compiler cannot typecheck entire array
// before it times out
let set1: [Gen<Include8<A, B, C, D, E, F, G, H>>] = [
A.arbitrary.map(Include8.init),
B.arbitrary.map(Include8.init),
C.arbitrary.map(Include8.init)
]
let set2: [Gen<Include8<A, B, C, D, E, F, G, H>>] = [
D.arbitrary.map(Include8.init),
E.arbitrary.map(Include8.init),
F.arbitrary.map(Include8.init)
]
let set3: [Gen<Include8<A, B, C, D, E, F, G, H>>] = [
G.arbitrary.map(Include8.init),
H.arbitrary.map(Include8.init)
]
return Gen.one(of: set1 + set2 + set3)
}
}
extension Include9: Arbitrary where A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, F: Arbitrary, G: Arbitrary, H: Arbitrary, I: Arbitrary {
public static var arbitrary: Gen<Include9<A, B, C, D, E, F, G, H, I>> {
// Note broken up because compiler cannot typecheck entire array
// before it times out
let set1: [Gen<Include9<A, B, C, D, E, F, G, H, I>>] = [
A.arbitrary.map(Include9.init),
B.arbitrary.map(Include9.init),
C.arbitrary.map(Include9.init)
]
let set2: [Gen<Include9<A, B, C, D, E, F, G, H, I>>] = [
D.arbitrary.map(Include9.init),
E.arbitrary.map(Include9.init),
F.arbitrary.map(Include9.init)
]
let set3: [Gen<Include9<A, B, C, D, E, F, G, H, I>>] = [
G.arbitrary.map(Include9.init),
H.arbitrary.map(Include9.init),
I.arbitrary.map(Include9.init)
]
return Gen.one(of: set1 + set2 + set3)
}
}
@@ -0,0 +1,27 @@
//
// ResourceBody+Arbitrary.swift
// JSONAPIArbitrary
//
// Created by Mathew Polzin on 1/21/19.
//
import SwiftCheck
import JSONAPI
extension SingleResourceBody: Arbitrary where Entity: Arbitrary {
public static var arbitrary: Gen<SingleResourceBody<Entity>> {
return Entity.arbitrary.map(SingleResourceBody.init(entity:))
}
}
extension ManyResourceBody: Arbitrary where Entity: Arbitrary {
public static var arbitrary: Gen<ManyResourceBody<Entity>> {
return Entity.arbitrary.proliferate.map(ManyResourceBody.init(entities:))
}
}
extension NoResourceBody: Arbitrary {
public static var arbitrary: Gen<NoResourceBody> {
return Gen.pure(.none)
}
}
@@ -0,0 +1,131 @@
//
// JSONAPIInclude+OpenAPI.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/22/19.
//
import JSONAPI
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()
])
}
}
@@ -72,13 +72,14 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType
}
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 +91,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 +116,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 {
// 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,7 +134,8 @@ 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)
@@ -147,7 +156,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: OpenAPINodeType where Entity: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
return try Entity.openAPINode()
}
}
extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
return .array(.init(format: .generic,
required: true),
.init(items: try Entity.openAPINode()))
}
}
extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType, IncludeType: OpenAPINodeType {
public static func openAPINode() 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()
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),
@@ -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)
}
}
}
@@ -16,6 +16,12 @@ public protocol OpenAPINodeType {
static func openAPINode() throws -> JSONNode
}
extension OpenAPINodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample() throws -> JSONNode {
return try openAPINode().with(example: Self.successSample ?? Self.sample)
}
}
/// Anything conforming to `RawOpenAPINodeType` can provide an
/// OpenAPI schema representing itself. This second protocol is
/// necessary so that one type can conditionally provide a
@@ -239,9 +245,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 +255,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 +271,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: AnyCodable? = nil) {
self.format = format
self.required = required
self.nullable = nullable
// self.constantValue = constantValue
self.allowedValues = allowedValues
self.example = example
.flatMap { try? JSONEncoder().encode($0)}
.flatMap { String(data: $0, encoding: .utf8) }
}
/// Return the optional version of this Context
@@ -276,6 +298,7 @@ public enum JSONNode: Equatable {
return .init(format: format,
required: false,
nullable: nullable,
// constantValue: constantValue,
allowedValues: allowedValues)
}
@@ -284,6 +307,7 @@ public enum JSONNode: Equatable {
return .init(format: format,
required: true,
nullable: nullable,
// constantValue: constantValue,
allowedValues: allowedValues)
}
@@ -292,16 +316,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) -> Context {
return .init(format: format,
required: required,
nullable: nullable,
// constantValue: constantValue,
allowedValues: allowedValues,
example: example)
}
}
public struct NumericContext: Equatable {
@@ -371,7 +407,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 +415,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 +433,7 @@ public enum JSONNode: Equatable {
self.properties = properties
self.additionalProperties = additionalProperties
self.maxProperties = maxProperties
self.minProperties = minProperties
self._minProperties = minProperties
}
}
@@ -407,7 +451,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 +465,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 +485,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 +505,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 +525,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 +545,43 @@ 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) throws -> JSONNode {
let example: AnyCodable
if let goodToGo = codableExample as? AnyCodable {
example = goodToGo
} else {
example = AnyCodable(try JSONSerialization.jsonObject(with: JSONEncoder().encode(codableExample), options: []))
}
switch self {
case .boolean(let context):
return .boolean(context.with(example: example))
case .object(let contextA, let contextB):
return .object(contextA.with(example: example), contextB)
case .array(let contextA, let contextB):
return .array(contextA.with(example: example), contextB)
case .number(let context, let contextB):
return .number(context.with(example: example), contextB)
case .integer(let context, let contextB):
return .integer(context.with(example: example), contextB)
case .string(let context, let contextB):
return .string(context.with(example: example), contextB)
case .all, .one, .any, .not:
return self
}
}
}
public enum OpenAPICodableError: Swift.Error {
public enum OpenAPICodableError: Swift.Error, Equatable {
case allCasesArrayNotCodable
case exampleNotCodable
}
public enum OpenAPITypeError: Swift.Error, Equatable {
case invalidNode
}
@@ -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) }}
}

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