Merge pull request #12 from mattpolzin/feature/OpenAPISchema

Testing, Documentation, more Arbitrary conformance, "example" support.
This commit is contained in:
Mathew Polzin
2019-01-22 08:21:22 -08:00
committed by GitHub
17 changed files with 533 additions and 37 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>
+3 -3
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)
@@ -100,7 +100,7 @@ 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
@@ -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)
}
}
@@ -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>
}
@@ -8,28 +8,240 @@
import XCTest
import JSONAPI
import JSONAPIOpenAPI
import AnyCodable
class JSONAPIEntityOpenAPITests: XCTestCase {
func test_EmptyEntity() {
let node = try! TestType1.openAPINode()
// TODO: Write test
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let string = String(data: try! encoder.encode(node), encoding: .utf8)!
print(string)
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected Object node")
return
}
XCTAssertEqual(contextA, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(objectContext1.minProperties, 2)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type"]))
XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic,
required: true),
.init()))
XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic,
required: true,
allowedValues: [.init(TestType1.jsonType)]),
.init()))
}
func test_AttributesEntity() {
let node = try! TestType2.openAPINode()
// TODO: Write test
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected Object node")
return
}
XCTAssertEqual(contextA, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(objectContext1.minProperties, 3)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type", "attributes"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type", "attributes"]))
XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic,
required: true),
.init()))
XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic,
required: true,
allowedValues: [.init(TestType2.jsonType)]),
.init()))
let attributesNode = objectContext1.properties["attributes"]
XCTAssertNotNil(attributesNode)
XCTAssertTrue(attributesNode?.required ?? false)
XCTAssertEqual(attributesNode?.jsonTypeFormat, .object(.generic))
guard case let .object(contextB, attributesContext)? = attributesNode else {
XCTFail("Expected Object node for attributes")
return
}
XCTAssertEqual(contextB, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(attributesContext.minProperties, 3)
XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "nullableProperty"]))
XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"]))
XCTAssertEqual(attributesContext.properties["stringProperty"],
.string(.init(format: .generic,
required: true),
.init()))
XCTAssertEqual(attributesContext.properties["enumProperty"],
.string(.init(format: .generic,
required: true,
nullable: false,
allowedValues: ["one", "two"].map(AnyCodable.init)),
.init()))
XCTAssertEqual(attributesContext.properties["optionalProperty"],
.string(.init(format: .generic,
required: false,
nullable: false,
allowedValues: nil),
.init()))
XCTAssertEqual(attributesContext.properties["nullableProperty"],
.string(.init(format: .generic,
required: true,
nullable: true,
allowedValues: nil),
.init()))
XCTAssertEqual(attributesContext.properties["nullableOptionalProperty"],
.string(.init(format: .generic,
required: false,
nullable: true,
allowedValues: nil),
.init()))
}
func test_RelationshipsEntity() {
let node = try! TestType3.openAPINode()
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected Object node")
return
}
XCTAssertEqual(contextA, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(objectContext1.minProperties, 3)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type", "relationships"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type", "relationships"]))
XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic,
required: true),
.init()))
XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic,
required: true,
allowedValues: [.init(TestType3.jsonType)]),
.init()))
let relationshipsNode = objectContext1.properties["relationships"]
XCTAssertNotNil(relationshipsNode)
XCTAssertTrue(relationshipsNode?.required ?? false)
XCTAssertEqual(relationshipsNode?.jsonTypeFormat, .object(.generic))
guard case let .object(contextB, relationshipsContext)? = relationshipsNode else {
XCTFail("Expected Object node for relationships")
return
}
XCTAssertEqual(contextB, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(relationshipsContext.minProperties, 3)
XCTAssertEqual(Set(relationshipsContext.requiredProperties), Set(["toOne", "nullableToOne", "toMany"]))
XCTAssertEqual(Set(relationshipsContext.properties.keys), Set(["toOne", "optionalTooOne", "nullableToOne", "nullableOptionalToOne", "toMany", "optionalToMany"]))
let pointerDataContext = JSONNode.ObjectContext(properties: ["id": .string(.init(format: .generic,
required: true),
.init()),
"type": .string(.init(format: .generic,
required: true,
allowedValues: [.init(TestType1.jsonType)]),
.init())])
let pointerContext = JSONNode.ObjectContext(properties: ["data": .object(.init(format: .generic,
required: true),
pointerDataContext)])
let nullablePointerContext = JSONNode.ObjectContext(properties: ["data": .object(.init(format: .generic,
required: true,
nullable: true),
pointerDataContext)])
let manyPointerContext = JSONNode.ObjectContext(properties: ["data": .array(.init(format: .generic,
required: true),
.init(items: .object(.init(format: .generic,
required: true),
pointerDataContext)))])
XCTAssertEqual(relationshipsContext.properties["toOne"],
.object(.init(format: .generic,
required: true),
pointerContext))
XCTAssertEqual(relationshipsContext.properties["optionalTooOne"],
.object(.init(format: .generic,
required: false,
nullable: false,
allowedValues: nil),
pointerContext))
XCTAssertEqual(relationshipsContext.properties["nullableToOne"],
.object(.init(format: .generic,
required: true,
nullable: false,
allowedValues: nil),
nullablePointerContext))
XCTAssertEqual(relationshipsContext.properties["nullableOptionalToOne"],
.object(.init(format: .generic,
required: false,
nullable: false,
allowedValues: nil),
nullablePointerContext))
XCTAssertEqual(relationshipsContext.properties["toMany"],
.object(.init(format: .generic,
required: true),
manyPointerContext))
XCTAssertEqual(relationshipsContext.properties["optionalToMany"],
.object(.init(format: .generic,
required: false,
nullable: false,
allowedValues: nil),
manyPointerContext))
}
func test_AttributesAndRelationshipsEntity() {
// TODO: write test
/*
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let string = String(data: try! encoder.encode(node), encoding: .utf8)!
print(string)
*/
}
}
@@ -46,7 +258,7 @@ extension JSONAPIEntityOpenAPITests {
typealias TestType1 = BasicEntity<TestType1Description>
enum TestType2Description: EntityDescription {
public static var jsonType: String { return "test1" }
public static var jsonType: String { return "test2" }
public enum EnumType: String, CaseIterable, Codable, Equatable {
case one
@@ -56,13 +268,19 @@ extension JSONAPIEntityOpenAPITests {
public struct Attributes: JSONAPI.Attributes, Sampleable {
let stringProperty: Attribute<String>
let enumProperty: Attribute<EnumType>
let optionalProperty: Attribute<String>?
let nullableProperty: Attribute<String?>
let nullableOptionalProperty: Attribute<String?>?
var computedProperty: Attribute<EnumType> {
return enumProperty
}
public static var sample: Attributes {
return Attributes(stringProperty: .init(value: "hello"),
enumProperty: .init(value: .one))
enumProperty: .init(value: .one),
optionalProperty: nil,
nullableProperty: .init(value: nil),
nullableOptionalProperty: nil)
}
}
@@ -70,4 +288,33 @@ extension JSONAPIEntityOpenAPITests {
}
typealias TestType2 = BasicEntity<TestType2Description>
enum TestType3Description: EntityDescription {
public static var jsonType: String { return "test3" }
public typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships, Sampleable {
public let toOne: ToOneRelationship<TestType1, NoMetadata, NoLinks>
public let optionalTooOne: ToOneRelationship<TestType1, NoMetadata, NoLinks>?
public let nullableToOne: ToOneRelationship<TestType1?, NoMetadata, NoLinks>
public let nullableOptionalToOne: ToOneRelationship<TestType1?, NoMetadata, NoLinks>?
public let toMany: ToManyRelationship<TestType1, NoMetadata, NoLinks>
public let optionalToMany: ToManyRelationship<TestType1, NoMetadata, NoLinks>?
// Note there is no such thing as nullable to-many relationships (Just use
// an empty array)
public static var sample: Relationships {
return Relationships(toOne: .init(id: .init(rawValue: "1")),
optionalTooOne: nil,
nullableToOne: .init(id: nil),
nullableOptionalToOne: nil,
toMany: .init(ids: [.init(rawValue: "1")]),
optionalToMany: nil)
}
}
}
typealias TestType3 = BasicEntity<TestType3Description>
}
@@ -30,10 +30,18 @@ extension JSONAPIAttributeOpenAPITests {
]
}
extension JSONAPIDocumentOpenAPITests {
static let __allTests = [
("test_SingleResourceDocument", test_SingleResourceDocument),
]
}
extension JSONAPIEntityOpenAPITests {
static let __allTests = [
("test_AttributesAndRelationshipsEntity", test_AttributesAndRelationshipsEntity),
("test_AttributesEntity", test_AttributesEntity),
("test_EmptyEntity", test_EmptyEntity),
("test_RelationshipsEntity", test_RelationshipsEntity),
]
}
@@ -52,6 +60,7 @@ extension JSONAPIRelationshipsOpenAPITests {
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(JSONAPIAttributeOpenAPITests.__allTests),
testCase(JSONAPIDocumentOpenAPITests.__allTests),
testCase(JSONAPIEntityOpenAPITests.__allTests),
testCase(JSONAPIRelationshipsOpenAPITests.__allTests),
]