Compare commits

..

13 Commits

Author SHA1 Message Date
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
22 changed files with 868 additions and 45 deletions
@@ -10,4 +10,14 @@ 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("====")
@@ -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,6 +2,8 @@ import Foundation
import JSONAPI
import JSONAPITesting // for the convenience of literal initialization
import JSONAPIOpenAPI
import SwiftCheck
import JSONAPIArbitrary
extension PersonDescription.Attributes: Sampleable {
public static var sample: PersonDescription.Attributes {
@@ -14,3 +16,41 @@ extension PersonDescription.Relationships: Sampleable {
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>
+10 -10
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
- [x] Arbitrary
- [ ] 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)
}
}
@@ -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,40 @@ 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 {
public static func openAPINode() throws -> JSONNode {
// TODO: metadata, links, api description, includes, 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 propertiesDict = Dictionary([
primaryDataProperty
].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,13 +142,9 @@ 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)
}
}
+76 -5
View File
@@ -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
@@ -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
}
}
@@ -505,8 +549,35 @@ public enum JSONNode: Equatable {
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 .allOf, .oneOf, .anyOf, .not:
return self
}
}
}
public enum OpenAPICodableError: Swift.Error {
case allCasesArrayNotCodable
case exampleNotCodable
}
@@ -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) }}
}
+14
View File
@@ -15,6 +15,20 @@ public protocol Sampleable {
/// 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.
static var successSample: Self? { get }
/// Get an example of failure, if that is meaningful and
/// available. If not, will be nil.
static var failureSample: Self? { get }
}
public extension Sampleable {
public static var successSample: Self? { return nil }
public static var failureSample: Self? { return nil }
}
extension Sampleable {
@@ -429,7 +429,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))
@@ -0,0 +1,42 @@
//
// JSONAPIDocumentOpenAPITests.swift
// JSONAPIOpenAPITests
//
// Created by Mathew Polzin on 1/21/19.
//
import XCTest
import JSONAPI
import JSONAPIOpenAPI
class JSONAPIDocumentOpenAPITests: XCTestCase {
func test_SingleResourceDocument() {
let node = try! SingleEntityDocument.openAPINode()
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
print(String(data: try! encoder.encode(node), encoding: .utf8)!)
}
}
// MARK: - Test Types
extension JSONAPIDocumentOpenAPITests {
enum TestEntityDescription: EntityDescription {
static var jsonType: String { return "test" }
struct Attributes: JSONAPI.Attributes, Sampleable {
let name: Attribute<String>
static var sample: Attributes {
return .init(name: "hello world")
}
}
typealias Relationships = NoRelationships
}
typealias TestEntity = BasicEntity<TestEntityDescription>
typealias SingleEntityDocument = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
}

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