mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e85e1d2a3 | |||
| 11a7727ac9 | |||
| e8bfbc881b | |||
| af757a2fac | |||
| 84955872f8 | |||
| cb2800abd4 | |||
| 85d5fef3c8 | |||
| e4ef61fd56 |
@@ -9,7 +9,7 @@ import Poly
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let personSchemaData = try? encoder.encode(Person.openAPINode())
|
||||
let personSchemaData = try? encoder.encode(Person.openAPINode(using: encoder))
|
||||
|
||||
print("Person Schema")
|
||||
print("====")
|
||||
@@ -30,13 +30,13 @@ print("====")
|
||||
print(batchPersonSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let tmp: [String: OpenAPIComponents.SchemasDict.RefType] = [
|
||||
"BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample()
|
||||
let tmp: [String: JSONNode] = [
|
||||
"BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample(using: encoder)
|
||||
]
|
||||
|
||||
let components = OpenAPIComponents(schemas: tmp)
|
||||
let components = OpenAPIComponents(schemas: tmp, parameters: [:])
|
||||
|
||||
let batchPeopleRef = JSONReference(type: \OpenAPIComponents.schemas, selector: "BatchPerson")
|
||||
let batchPeopleRef = JSONReference.node(.init(type: \OpenAPIComponents.schemas, selector: "BatchPerson"))
|
||||
|
||||
let tmp2 = JSONNode.reference(batchPeopleRef)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,7 +949,7 @@ public struct OpenAPISchema {
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
return components.joined(separator: "/")
|
||||
return "/\(components.joined(separator: "/"))"
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user