mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Fix Include OpenAPI support. Fix bug with single include being considered 'OneOf' even though that is an overcomplication of the schema. Add tests for single include type and two include types on document
This commit is contained in:
@@ -8,9 +8,9 @@
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
|
||||
extension Includes: OpenAPINodeType where I: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
let includeNode = try I.openAPINode()
|
||||
extension Includes: OpenAPIEncodedNodeType where I: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
let includeNode = try I.openAPINode(using: encoder)
|
||||
|
||||
return .array(.init(format: .generic,
|
||||
required: true),
|
||||
@@ -19,114 +19,114 @@ extension Includes: OpenAPINodeType where I: OpenAPINodeType {
|
||||
}
|
||||
}
|
||||
|
||||
extension Include0: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include0: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
throw OpenAPITypeError.invalidNode
|
||||
}
|
||||
}
|
||||
|
||||
extension Include1: OpenAPINodeType where A: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
return try .one(of: [A.openAPINode()])
|
||||
extension Include1: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try A.openAPINode(using: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Include2: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include2: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include3: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include3: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include4: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include4: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include5: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include5: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder),
|
||||
E.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include6: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include6: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder),
|
||||
E.openAPINode(using: encoder),
|
||||
F.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include7: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include7: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder),
|
||||
E.openAPINode(using: encoder),
|
||||
F.openAPINode(using: encoder),
|
||||
G.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include8: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include8: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode(),
|
||||
H.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder),
|
||||
E.openAPINode(using: encoder),
|
||||
F.openAPINode(using: encoder),
|
||||
G.openAPINode(using: encoder),
|
||||
H.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension Include9: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType, I: OpenAPINodeType {
|
||||
public static func openAPINode() throws -> JSONNode {
|
||||
extension Include9: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType, I: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
return try .one(of: [
|
||||
A.openAPINode(),
|
||||
B.openAPINode(),
|
||||
C.openAPINode(),
|
||||
D.openAPINode(),
|
||||
E.openAPINode(),
|
||||
F.openAPINode(),
|
||||
G.openAPINode(),
|
||||
H.openAPINode(),
|
||||
I.openAPINode()
|
||||
A.openAPINode(using: encoder),
|
||||
B.openAPINode(using: encoder),
|
||||
C.openAPINode(using: encoder),
|
||||
D.openAPINode(using: encoder),
|
||||
E.openAPINode(using: encoder),
|
||||
F.openAPINode(using: encoder),
|
||||
G.openAPINode(using: encoder),
|
||||
H.openAPINode(using: encoder),
|
||||
I.openAPINode(using: encoder)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ extension ManyResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedN
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType {
|
||||
extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPIEncodedNodeType {
|
||||
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
// TODO: metadata, links, api description, errors
|
||||
// TODO: represent data and errors as the two distinct possible outcomes
|
||||
@@ -224,7 +224,7 @@ extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEnc
|
||||
|
||||
let includeNode: JSONNode?
|
||||
do {
|
||||
includeNode = try Includes<Include>.openAPINode()
|
||||
includeNode = try Includes<Include>.openAPINode(using: encoder)
|
||||
} catch let err as OpenAPITypeError {
|
||||
guard case .invalidNode = err else {
|
||||
throw err
|
||||
|
||||
@@ -24,8 +24,6 @@ class JSONAPIDocumentOpenAPITests: XCTestCase {
|
||||
|
||||
let node = try! SingleEntityDocument.openAPINodeWithExample(using: encoder)
|
||||
|
||||
print(String(data: try! encoder.encode(node), encoding: .utf8)!)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
|
||||
|
||||
@@ -76,8 +74,6 @@ class JSONAPIDocumentOpenAPITests: XCTestCase {
|
||||
|
||||
let node = try! ManyEntityDocument.openAPINodeWithExample(using: encoder)
|
||||
|
||||
print(String(data: try! encoder.encode(node), encoding: .utf8)!)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
|
||||
|
||||
@@ -126,6 +122,192 @@ class JSONAPIDocumentOpenAPITests: XCTestCase {
|
||||
allowedValues: [.init("test")]),
|
||||
.init()))
|
||||
}
|
||||
|
||||
func test_DocumentWithOneIncludeType() {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
encoder.dateEncodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let node = try! DocumentWithIncludes.openAPINodeWithExample(using: encoder)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
|
||||
|
||||
guard case let .object(contextA, objectContext1) = node else {
|
||||
XCTFail("Expected JSON Document to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(contextA.example)
|
||||
XCTAssertFalse(contextA.nullable)
|
||||
XCTAssertEqual(contextA.format, .generic)
|
||||
XCTAssertTrue(contextA.required)
|
||||
|
||||
XCTAssertEqual(objectContext1.minProperties, 2)
|
||||
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"]))
|
||||
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"]))
|
||||
|
||||
guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else {
|
||||
XCTFail("Expected Data field of JSON Document to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextB.nullable)
|
||||
XCTAssertEqual(contextB.format, .generic)
|
||||
XCTAssertTrue(contextB.required)
|
||||
|
||||
XCTAssertEqual(objectContext2.minProperties, 3)
|
||||
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
|
||||
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
|
||||
|
||||
XCTAssertEqual(objectContext2.properties["type"],
|
||||
JSONNode.string(.init(format: .generic,
|
||||
required: true,
|
||||
allowedValues: [.init("test")]),
|
||||
.init()))
|
||||
|
||||
guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else {
|
||||
XCTFail("Expected Includes field of JSON Document to be an Array Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextC.nullable)
|
||||
XCTAssertEqual(contextC.format, .generic)
|
||||
XCTAssertTrue(contextC.required)
|
||||
|
||||
XCTAssertTrue(arrayContext.uniqueItems)
|
||||
XCTAssertEqual(arrayContext.minItems, 0)
|
||||
|
||||
guard case let .object(contextD, objectContext3) = arrayContext.items else {
|
||||
XCTFail("Expected Items of Array under Data to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextD.nullable)
|
||||
XCTAssertEqual(contextD.format, .generic)
|
||||
XCTAssertTrue(contextD.required)
|
||||
|
||||
XCTAssertEqual(objectContext3.minProperties, 3)
|
||||
XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"]))
|
||||
XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"]))
|
||||
|
||||
XCTAssertEqual(objectContext3.properties["type"],
|
||||
JSONNode.string(.init(format: .generic,
|
||||
required: true,
|
||||
allowedValues: [.init("test")]),
|
||||
.init()))
|
||||
}
|
||||
|
||||
func test_DocumentWithTwoIncludeTypes() {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
encoder.dateEncodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let node = try! DocumentWithMultipleTypesOfIncludes.openAPINodeWithExample(using: encoder)
|
||||
|
||||
XCTAssertTrue(node.required)
|
||||
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
|
||||
|
||||
guard case let .object(contextA, objectContext1) = node else {
|
||||
XCTFail("Expected JSON Document to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(contextA.example)
|
||||
XCTAssertFalse(contextA.nullable)
|
||||
XCTAssertEqual(contextA.format, .generic)
|
||||
XCTAssertTrue(contextA.required)
|
||||
|
||||
XCTAssertEqual(objectContext1.minProperties, 2)
|
||||
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"]))
|
||||
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"]))
|
||||
|
||||
guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else {
|
||||
XCTFail("Expected Data field of JSON Document to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextB.nullable)
|
||||
XCTAssertEqual(contextB.format, .generic)
|
||||
XCTAssertTrue(contextB.required)
|
||||
|
||||
XCTAssertEqual(objectContext2.minProperties, 3)
|
||||
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
|
||||
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
|
||||
|
||||
XCTAssertEqual(objectContext2.properties["type"],
|
||||
JSONNode.string(.init(format: .generic,
|
||||
required: true,
|
||||
allowedValues: [.init("test")]),
|
||||
.init()))
|
||||
|
||||
guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else {
|
||||
XCTFail("Expected Includes field of JSON Document to be an Array Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextC.nullable)
|
||||
XCTAssertEqual(contextC.format, .generic)
|
||||
XCTAssertTrue(contextC.required)
|
||||
|
||||
XCTAssertTrue(arrayContext.uniqueItems)
|
||||
XCTAssertEqual(arrayContext.minItems, 0)
|
||||
|
||||
guard case let .one(of: includeNodes) = arrayContext.items else {
|
||||
XCTFail("Expected Included to contain multiple types of items.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(includeNodes.count, 2)
|
||||
|
||||
guard case let .object(contextD, objectContext3) = includeNodes[0] else {
|
||||
XCTFail("Expected Items of OneOf under Array under Data to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextD.nullable)
|
||||
XCTAssertEqual(contextD.format, .generic)
|
||||
XCTAssertTrue(contextD.required)
|
||||
|
||||
XCTAssertEqual(objectContext3.minProperties, 3)
|
||||
XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"]))
|
||||
XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"]))
|
||||
|
||||
XCTAssertEqual(objectContext3.properties["type"],
|
||||
JSONNode.string(.init(format: .generic,
|
||||
required: true,
|
||||
allowedValues: [.init("test")]),
|
||||
.init()))
|
||||
|
||||
guard case let .object(contextE, objectContext4) = includeNodes[1] else {
|
||||
XCTFail("Expected Items of OneOf under Array under Data to be an Object Node")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(contextE.nullable)
|
||||
XCTAssertEqual(contextE.format, .generic)
|
||||
XCTAssertTrue(contextE.required)
|
||||
|
||||
XCTAssertEqual(objectContext4.minProperties, 2)
|
||||
XCTAssertEqual(Set(objectContext4.requiredProperties), Set(["id", "type"]))
|
||||
XCTAssertEqual(Set(objectContext4.properties.keys), Set(["id", "type"]))
|
||||
|
||||
XCTAssertEqual(objectContext4.properties["type"],
|
||||
JSONNode.string(.init(format: .generic,
|
||||
required: true,
|
||||
allowedValues: [.init("test2")]),
|
||||
.init()))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
@@ -151,6 +333,20 @@ extension JSONAPIDocumentOpenAPITests {
|
||||
typealias SingleEntityDocument = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
typealias ManyEntityDocument = Document<ManyResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
typealias DocumentWithIncludes = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, Include1<TestEntity>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
enum TestEntityDescription2: EntityDescription {
|
||||
static var jsonType: String { return "test2" }
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias TestEntity2 = BasicEntity<TestEntityDescription2>
|
||||
|
||||
typealias DocumentWithMultipleTypesOfIncludes = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, Include2<TestEntity, TestEntity2>, NoAPIDescription, UnknownJSONAPIError>
|
||||
}
|
||||
|
||||
extension Id: Sampleable where RawType == String {
|
||||
|
||||
Reference in New Issue
Block a user