mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Merge pull request #12 from mattpolzin/feature/OpenAPISchema
Testing, Documentation, more Arbitrary conformance, "example" support.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='OpenAPI Documentation'/>
|
||||
</pages>
|
||||
</playground>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+50
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user