Compare commits

..

12 Commits

Author SHA1 Message Date
Mathew Polzin 1e85e1d2a3 fix open API playground. 2019-01-27 22:55:32 -08:00
Mathew Polzin 11a7727ac9 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 2019-01-27 22:47:01 -08:00
Mathew Polzin e8bfbc881b Added a couple of test cases for JSONDocuments 2019-01-27 22:24:13 -08:00
Mathew Polzin af757a2fac Add emphasis to README line. 2019-01-27 21:46:22 -08:00
Mathew Polzin 84955872f8 Finish writing Date Attribute test cases and add support for Optional Date OpenAPI Node guesses. 2019-01-27 21:39:18 -08:00
Mathew Polzin cb2800abd4 Add support for RequestBody on an OpenAPI Operation. 2019-01-27 13:57:04 -08:00
Mathew Polzin 85d5fef3c8 Fix bug where some references were correctly encoded as objects and others were just encoded as strings 2019-01-27 13:22:18 -08:00
Mathew Polzin e4ef61fd56 bugfix: OpenAPI path components should begin with a slash. 2019-01-27 12:46:13 -08:00
Mathew Polzin 75ec4f156e Fix a third dictionary-as-array bug 2019-01-26 20:04:11 -08:00
Mathew Polzin b45fc73188 Merge pull request #15 from mattpolzin/feature/OpenAPISchema
Feature/open api schema
2019-01-26 19:52:15 -08:00
Mathew Polzin 3b73dc9989 Fix super shitty bug caused by Apples implementation of Dictionary's conformance to Encodable sometimes encoding the dictionary as an array. 2019-01-26 19:50:20 -08:00
Mathew Polzin b2c81026f4 remove 'id' from Unidentified Entity OpenAPI node. 2019-01-26 18:55:47 -08:00
10 changed files with 676 additions and 183 deletions
@@ -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)
+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
@@ -148,10 +161,12 @@ extension Entity: OpenAPIEncodedNodeType where Description.Attributes: Sampleabl
// TODO: metadata, links
let idNode = JSONNode.string(.init(format: .generic,
required: true),
.init())
let idProperty = ("id", idNode)
let idNode: JSONNode? = Id.RawType.self != Unidentified.self
? JSONNode.string(.init(format: .generic,
required: true),
.init())
: nil
let idProperty = idNode.map { ("id", $0) }
let typeNode = JSONNode.string(.init(format: .generic,
required: true,
@@ -198,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
@@ -209,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,7 +305,17 @@ extension OpenAPIPathItem.PathProperties.Operation: Encodable {
try container.encode(parameters, forKey: .parameters)
try container.encode(responses, forKey: .responses)
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(
responses.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .responses)
try container.encode(deprecated, forKey: .deprecated)
}
@@ -346,6 +385,29 @@ extension OpenAPIPathItem.PathProperties: Encodable {
}
}
extension OpenAPIResponse: Encodable {
private enum CodingKeys: String, CodingKey {
case description
case headers
case content
case links
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
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)
}
}
extension OpenAPIPathItem: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
@@ -359,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,47 +779,82 @@ 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 OpenAPIResponse: Encodable, Equatable {
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
public enum Code: Equatable, Hashable {
case `default`
case status(code: Int)
}
public enum ContentType: String, Encodable, Equatable, Hashable {
case json = "application/json"
}
public var rawValue: String {
switch self {
case .default:
return "default"
public struct Content: 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
case .status(code: let code):
return String(code)
}
}
public init?(rawValue: String) {
if let val = Int(rawValue) {
self = .status(code: val)
} else {
self = .default
}
}
}
}
public enum OpenAPIContentType: String, Encodable, Equatable, Hashable {
case json = "application/json"
}
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
}
}
@@ -858,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:
@@ -908,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 {
@@ -39,6 +39,31 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
.init()))
}
func test_UnidentifiedEmptyEntity() {
let node = try! UnidentifiedTestType1.openAPINode(using: JSONEncoder())
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, 1)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["type"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["type"]))
XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic,
required: true,
allowedValues: [.init(TestType1.jsonType)]),
.init()))
}
func test_AttributesEntity() {
let dateFormatter = DateFormatter()
@@ -272,6 +297,7 @@ extension JSONAPIEntityOpenAPITests {
}
typealias TestType1 = BasicEntity<TestType1Description>
typealias UnidentifiedTestType1 = JSONAPI.Entity<TestType1Description, NoMetadata, NoLinks, Unidentified>
enum TestType2Description: EntityDescription {
public static var jsonType: String { return "test2" }