Compare commits

...

8 Commits

8 changed files with 587 additions and 171 deletions
+1 -1
View File
@@ -843,4 +843,4 @@ This Framework is currently undocumented, but familiarity with `SwiftCheck` will
# JSONAPI+OpenAPI
The `JSONAPIOpenAPI` framework adds the ability to generate OpenAPI compliant JSON documentation of a JSONAPI Document.
This library is in its infancy. The documentation will grow as the framework becomes more complete.
*This library is in its infancy. The documentation will grow as the framework becomes more complete.*
@@ -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)
])
}
}
@@ -53,6 +53,31 @@ extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType {
}
}
extension Attribute: GenericOpenAPINodeType where RawValue: GenericOpenAPINodeType {
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if try !RawValue.genericOpenAPINode(using: encoder).required {
return try RawValue.genericOpenAPINode(using: encoder).requiredNode().nullableNode()
}
return try RawValue.genericOpenAPINode(using: encoder)
}
}
extension Attribute: DateOpenAPINodeType where RawValue: DateOpenAPINodeType {
public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if
!(RawValue.dateOpenAPINodeGuess(using: encoder)?.required ?? true) {
return RawValue.dateOpenAPINodeGuess(using: encoder)?.requiredNode().nullableNode()
}
return RawValue.dateOpenAPINodeGuess(using: encoder)
}
}
extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable {
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
return (try? allCases(from: Array(RawValue.allCases), using: encoder)) ?? []
@@ -65,18 +90,6 @@ extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseItera
}
}
extension Attribute: GenericOpenAPINodeType where RawValue: GenericOpenAPINodeType {
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try RawValue.genericOpenAPINode(using: encoder)
}
}
extension Attribute: DateOpenAPINodeType where RawValue: DateOpenAPINodeType {
public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
return RawValue.dateOpenAPINodeGuess(using: encoder)
}
}
extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType {
static public func openAPINode() throws -> JSONNode {
// If the RawValue is not required, we actually consider it
@@ -200,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
@@ -211,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
@@ -155,7 +155,6 @@ extension JSONNode: Encodable {
case oneOf
case anyOf
case not
case reference = "$ref"
}
public func encode(to encoder: Encoder) throws {
@@ -192,16 +191,20 @@ extension JSONNode: Encodable {
try container.encode(node, forKey: .not)
case .reference(let reference):
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
var container = encoder.singleValueContainer()
try container.encode(reference, forKey: .reference)
try container.encode(reference)
}
}
}
extension JSONReference: Encodable {
private enum CodingKeys: String, CodingKey {
case ref = "$ref"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
var container = encoder.container(keyedBy: CodingKeys.self)
let referenceString: String = {
switch self {
@@ -212,7 +215,7 @@ extension JSONReference: Encodable {
}
}()
try container.encode(referenceString)
try container.encode(referenceString, forKey: .ref)
}
}
@@ -241,6 +244,32 @@ extension OpenAPIResponse.Code: Encodable {
}
}
extension OpenAPIRequestBody: Encodable {
private enum CodingKeys: String, CodingKey {
case description
case content
case required
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if description != nil {
try container.encode(description, forKey: .description)
}
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
content.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .content)
try container.encode(required, forKey: .required)
}
}
extension OpenAPIPathItem.PathProperties.Operation: Encodable {
private enum CodingKeys: String, CodingKey {
case tags
@@ -276,6 +305,10 @@ extension OpenAPIPathItem.PathProperties.Operation: Encodable {
try container.encode(parameters, forKey: .parameters)
if requestBody != nil {
try container.encode(requestBody, forKey: .requestBody)
}
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
@@ -388,3 +421,34 @@ extension OpenAPIPathItem: Encodable {
}
}
}
extension OpenAPISchema: Encodable {
private enum CodingKeys: String, CodingKey {
case openAPIVersion = "openapi"
case info
case servers
case paths
case components
case security
case tags
case externalDocs
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(openAPIVersion, forKey: .openAPIVersion)
try container.encode(info, forKey: .info)
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
paths.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .paths)
try container.encode(components, forKey: .components)
}
}
@@ -759,7 +759,7 @@ public enum OpenAPIPathItem: Equatable {
// public let externalDocs:
public let operationId: String
public let parameters: ParameterArray
// public let requestBody:
public let requestBody: OpenAPIRequestBody?
public let responses: ResponseMap
// public let callbacks:
public let deprecated: Bool // default is false
@@ -771,6 +771,7 @@ public enum OpenAPIPathItem: Equatable {
description: String? = nil,
operationId: String,
parameters: ParameterArray,
requestBody: OpenAPIRequestBody? = nil,
responses: ResponseMap,
deprecated: Bool = false) {
self.tags = tags
@@ -778,29 +779,44 @@ public enum OpenAPIPathItem: Equatable {
self.description = description
self.operationId = operationId
self.parameters = parameters
self.requestBody = requestBody
self.responses = responses
self.deprecated = deprecated
}
public typealias ResponseMap = [OpenAPIResponse.Code: Either<OpenAPIResponse, JSONReference<OpenAPIComponents, OpenAPIResponse>>]
public typealias ContentMap = [OpenAPIContentType: OpenAPIContent]
}
}
}
public struct OpenAPIRequestBody: Equatable {
public let description: String?
public let content: OpenAPIPathItem.PathProperties.Operation.ContentMap
public let required: Bool
public init(description: String? = nil,
content: OpenAPIPathItem.PathProperties.Operation.ContentMap,
required: Bool = true) {
self.description = description
self.content = content
self.required = required
}
}
public struct OpenAPIResponse: Equatable {
public let description: String
// public let headers:
public let content: ContentMap
public let content: OpenAPIPathItem.PathProperties.Operation.ContentMap
// public let links:
public init(description: String,
content: ContentMap) {
content: OpenAPIPathItem.PathProperties.Operation.ContentMap) {
self.description = description
self.content = content
}
public typealias ContentMap = [ContentType: Content]
public enum Code: RawRepresentable, Equatable, Hashable {
public typealias RawValue = String
@@ -825,20 +841,20 @@ public struct OpenAPIResponse: Equatable {
}
}
}
}
public enum ContentType: String, Encodable, Equatable, Hashable {
case json = "application/json"
}
public enum OpenAPIContentType: String, Encodable, Equatable, Hashable {
case json = "application/json"
}
public struct Content: Encodable, Equatable {
public let schema: Either<JSONNode, JSONReference<OpenAPIComponents, JSONNode>>
// public let example:
// public let examples:
// public let encoding:
public struct OpenAPIContent: Encodable, Equatable {
public let schema: Either<JSONNode, JSONReference<OpenAPIComponents, JSONNode>>
// public let example:
// public let examples:
// public let encoding:
public init(schema: Either<JSONNode, JSONReference<OpenAPIComponents, JSONNode>>) {
self.schema = schema
}
public init(schema: Either<JSONNode, JSONReference<OpenAPIComponents, JSONNode>>) {
self.schema = schema
}
}
@@ -878,14 +894,7 @@ public struct OpenAPIComponents: Equatable, Encodable, ReferenceRoot {
}
/// The root of an OpenAPI 3.0 document.
public struct OpenAPISchema: Encodable {
private enum CodingKeys: String, CodingKey {
case openAPIVersion = "openapi"
case info
case paths
case components
}
public struct OpenAPISchema {
public let openAPIVersion: Version
public let info: Info
// public let servers:
@@ -928,17 +937,25 @@ public struct OpenAPISchema: Encodable {
}
}
public struct PathComponents: Encodable, Equatable, Hashable {
public struct PathComponents: RawRepresentable, Encodable, Equatable, Hashable {
public let components: [String]
public init(_ components: [String]) {
self.components = components
}
public init?(rawValue: String) {
components = rawValue.split(separator: "/").map(String.init)
}
public var rawValue: String {
return "/\(components.joined(separator: "/"))"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(components.joined(separator: "/"))
try container.encode(rawValue)
}
}
}
@@ -62,6 +62,12 @@ extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Co
}
}
extension Optional: DateOpenAPINodeType where Wrapped: DateOpenAPINodeType {
static public func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
return Wrapped.dateOpenAPINodeGuess(using: encoder)?.optionalNode()
}
}
extension String: OpenAPINodeType {
static public func openAPINode() throws -> JSONNode {
return .string(.init(format: .generic,
@@ -802,62 +802,80 @@ extension JSONAPIAttributeOpenAPITests {
XCTAssertEqual(numberContext, .init())
}
// func test_NullableEnumAttribute() {
// let node = try! Attribute<EnumAttribute?>.wrappedOpenAPINode()
//
// XCTAssertTrue(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: true,
// nullable: true,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
//
// func test_OptionalEnumAttribute() {
// let node = try! Attribute<EnumAttribute>?.wrappedOpenAPINode()
//
// XCTAssertFalse(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: false,
// nullable: false,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
//
// func test_OptionalNullableEnumAttribute() {
// let node = try! Attribute<EnumAttribute?>?.wrappedOpenAPINode()
//
// XCTAssertFalse(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: false,
// nullable: true,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
func test_NullableDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date?>.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertTrue(node?.required ?? false)
XCTAssertEqual(node?.jsonTypeFormat, .number(.double))
guard case .number(let contextA, let numberContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .double,
required: true,
nullable: true,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_OptionalDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date>?.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertFalse(node?.required ?? true)
XCTAssertEqual(node?.jsonTypeFormat, .number(.double))
guard case .number(let contextA, let numberContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .double,
required: false,
nullable: false,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_OptionalNullableDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date?>?.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertFalse(node?.required ?? true)
XCTAssertEqual(node?.jsonTypeFormat, .number(.double))
guard case .number(let contextA, let numberContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .double,
required: false,
nullable: true,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
}
// MARK: - Test Types
@@ -24,7 +24,289 @@ 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))
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, 1)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data"]))
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()))
}
func test_ManyResourceDocument() {
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! ManyEntityDocument.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, 1)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data"]))
guard case let .array(contextB, arrayContext)? = objectContext1.properties["data"] else {
XCTFail("Expected Data field of JSON Document to be an Array Node")
return
}
XCTAssertFalse(contextB.nullable)
XCTAssertEqual(contextB.format, .generic)
XCTAssertTrue(contextB.required)
XCTAssertFalse(arrayContext.uniqueItems)
XCTAssertEqual(arrayContext.minItems, 0)
guard case let .object(contextC, objectContext2) = arrayContext.items else {
XCTFail("Expected Items of Array under Data to be an Object Node")
return
}
XCTAssertFalse(contextC.nullable)
XCTAssertEqual(contextC.format, .generic)
XCTAssertTrue(contextC.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()))
}
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()))
}
}
@@ -49,6 +331,22 @@ extension JSONAPIDocumentOpenAPITests {
typealias TestEntity = BasicEntity<TestEntityDescription>
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 {