Compare commits

...

21 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
Mathew Polzin 8d057b4398 Merge pull request #14 from mattpolzin/feature/OpenAPISchema
Just enough OpenAPI Schema stuff to be dangerous
2019-01-25 18:25:19 -08:00
Mathew Polzin c8421cdd58 just throw that json extension on the filename to make everything super real 2019-01-25 18:20:04 -08:00
Mathew Polzin cde10a8491 Finish implementing remaining first wave of encodable conformances for OpenAPI 2019-01-25 18:17:58 -08:00
Mathew Polzin ad05d3908a Add a simple test OpenAPISchema and start to tweak things to get it workable. 2019-01-25 12:49:59 -08:00
Mathew Polzin 23b2b2e04f merge w/ master 2019-01-25 12:01:01 -08:00
Mathew Polzin 2988503d7d Add Sampleable conformance to Unidentified. Rename file slightly. 2019-01-25 11:59:05 -08:00
Mathew Polzin 5ea83b07c1 Hopefully remove some ambiguity. 2019-01-24 19:09:46 -08:00
Mathew Polzin d1cf19f9fe Neck deep in stubbing out OpenAPI types 2019-01-22 22:14:36 -08:00
Mathew Polzin 59835fbe11 Add JSON Reference and OpenAPIComponents (initially just with schemas) 2019-01-22 16:08:47 -08:00
12 changed files with 1206 additions and 163 deletions
@@ -3,12 +3,13 @@
import Foundation
import JSONAPI
import JSONAPIOpenAPI
import Poly
// print Entity Schema
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("====")
@@ -28,3 +29,18 @@ print("Batch Person Document Schema")
print("====")
print(batchPersonSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
print("====")
let tmp: [String: JSONNode] = [
"BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample(using: encoder)
]
let components = OpenAPIComponents(schemas: tmp, parameters: [:])
let batchPeopleRef = JSONReference.node(.init(type: \OpenAPIComponents.schemas, selector: "BatchPerson"))
let tmp2 = JSONNode.reference(batchPeopleRef)
print("====")
print("====")
//print(String(data: try! encoder.encode(components), encoding: .utf8)!)
print(String(data: try! encoder.encode(tmp2), encoding: .utf8)!)
+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
@@ -141,17 +154,19 @@ extension ToManyRelationship: OpenAPINodeType {
}
}
extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
extension Entity: OpenAPIEncodedNodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// NOTE: const for json `type` not supported by OpenAPI 3.0
// Will use "enum" with one possible value for now.
// TODO: metadata, links
let idNode = JSONNode.string(.init(format: .generic,
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,
@@ -184,13 +199,13 @@ extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attr
}
}
extension SingleResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType {
extension SingleResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try Entity.openAPINode(using: encoder)
}
}
extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType {
extension ManyResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return .array(.init(format: .generic,
required: true),
@@ -198,7 +213,7 @@ extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity
}
}
extension Document: OpenAPIEncodedNodeType, OpenAPINodeType 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, OpenAPINodeType where PrimaryResourc
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
@@ -189,6 +189,266 @@ extension JSONNode: Encodable {
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
try container.encode(node, forKey: .not)
case .reference(let reference):
var container = encoder.singleValueContainer()
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.container(keyedBy: CodingKeys.self)
let referenceString: String = {
switch self {
case .file(let reference):
return reference
case .node(let reference):
return "#/\(Root.refName)/\(reference.refName)/\(reference.selector)"
}
}()
try container.encode(referenceString, forKey: .ref)
}
}
extension RefDict: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(dict)
}
}
extension OpenAPIResponse.Code: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let string: String
switch self {
case .`default`:
string = "default"
case .status(code: let code):
string = String(code)
}
try container.encode(string)
}
}
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
case summary
case description
case externalDocs
case operationId
case parameters
case requestBody
case responses
case callbacks
case deprecated
case security
case servers
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if tags != nil {
try container.encode(tags, forKey: .tags)
}
if summary != nil {
try container.encode(summary, forKey: .summary)
}
if description != nil {
try container.encode(description, forKey: .description)
}
try container.encode(operationId, forKey: .operationId)
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(
responses.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .responses)
try container.encode(deprecated, forKey: .deprecated)
}
}
extension OpenAPIPathItem.PathProperties: Encodable {
private enum CodingKeys: String, CodingKey {
case summary
case description
case servers
case parameters
case get
case put
case post
case delete
case options
case head
case patch
case trace
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if summary != nil {
try container.encode(summary, forKey: .summary)
}
if description != nil {
try container.encode(description, forKey: .description)
}
try container.encode(parameters, forKey: .parameters)
if get != nil {
try container.encode(get, forKey: .get)
}
if put != nil {
try container.encode(put, forKey: .put)
}
if post != nil {
try container.encode(post, forKey: .post)
}
if delete != nil {
try container.encode(delete, forKey: .delete)
}
if options != nil {
try container.encode(options, forKey: .options)
}
if head != nil {
try container.encode(head, forKey: .head)
}
if patch != nil {
try container.encode(patch, forKey: .patch)
}
if trace != nil {
try container.encode(trace, forKey: .trace)
}
}
}
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()
switch self {
case .reference(let reference):
try container.encode(reference)
case .operations(let operations):
try container.encode(operations)
}
}
}
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)
}
}
+360 -14
View File
@@ -7,6 +7,7 @@
import AnyCodable
import Foundation
import Poly
// MARK: Node (i.e. schema) Protocols
@@ -19,7 +20,7 @@ public protocol OpenAPINodeType {
/// Anything conforming to `OpenAPIEncodedNodeType` can provide an
/// OpenAPI schema representing itself but it may need an Encoder
/// to do its job.
public protocol OpenAPIEncodedNodeType: OpenAPINodeType {
public protocol OpenAPIEncodedNodeType {
static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode
}
@@ -29,12 +30,6 @@ extension OpenAPIEncodedNodeType where Self: Sampleable, Self: Encodable {
}
}
extension OpenAPIEncodedNodeType {
public static func openAPINode() throws -> JSONNode {
return try openAPINode(using: JSONEncoder())
}
}
/// Anything conforming to `RawOpenAPINodeType` can provide an
/// OpenAPI schema representing itself. This second protocol is
/// necessary so that one type can conditionally provide a
@@ -274,6 +269,7 @@ public enum JSONNode: Equatable {
indirect case one(of: [JSONNode])
indirect case any(of: [JSONNode])
indirect case not(JSONNode)
case reference(JSONReference<OpenAPIComponents, JSONNode>)
public struct Context<Format: OpenAPIFormat>: JSONNodeContext, Equatable {
public let format: Format
@@ -476,7 +472,7 @@ public enum JSONNode: Equatable {
return .integer(context.format)
case .string(let context, _):
return .string(context.format)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return nil
}
}
@@ -490,7 +486,7 @@ public enum JSONNode: Equatable {
.integer(let contextA as JSONNodeContext, _),
.string(let contextA as JSONNodeContext, _):
return contextA.required
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return true
}
}
@@ -510,7 +506,7 @@ public enum JSONNode: Equatable {
return .integer(context.optionalContext(), contextB)
case .string(let context, let contextB):
return .string(context.optionalContext(), contextB)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return self
}
}
@@ -530,7 +526,7 @@ public enum JSONNode: Equatable {
return .integer(context.requiredContext(), contextB)
case .string(let context, let contextB):
return .string(context.requiredContext(), contextB)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return self
}
}
@@ -550,7 +546,7 @@ public enum JSONNode: Equatable {
return .integer(context.nullableContext(), contextB)
case .string(let context, let contextB):
return .string(context.nullableContext(), contextB)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return self
}
}
@@ -570,7 +566,7 @@ public enum JSONNode: Equatable {
return .integer(context.with(allowedValues: allowedValues), contextB)
case .string(let context, let contextB):
return .string(context.with(allowedValues: allowedValues), contextB)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return self
}
}
@@ -597,7 +593,7 @@ public enum JSONNode: Equatable {
return .integer(context.with(example: example, using: encoder), contextB)
case .string(let context, let contextB):
return .string(context.with(example: example, using: encoder), contextB)
case .all, .one, .any, .not:
case .all, .one, .any, .not, .reference:
return self
}
}
@@ -613,3 +609,353 @@ public enum OpenAPITypeError: Swift.Error {
case invalidNode
case unknownNodeType(Any.Type)
}
/// Anything conforming to RefName knows what to call itself
/// in the context of JSON References.
public protocol RefName {
static var refName: String { get }
}
public protocol ReferenceRoot: RefName {}
public protocol ReferenceDict: RefName {
associatedtype Value
}
/// A RefDict knows what to call itself (Name) and where to
/// look for itself (Root) and it stores a dictionary of
/// JSONNodes (some of which might be other references).
public struct RefDict<Root: ReferenceRoot, Name: RefName, RefType: Equatable & Encodable>: ReferenceDict, Equatable {
public static var refName: String { return Name.refName }
public typealias Value = RefType
public typealias Key = String
let dict: [String: RefType]
public init(_ dict: [String: RefType]) {
self.dict = dict
}
public subscript(_ key: String) -> RefType? {
return dict[key]
}
}
/// A Reference is the combination of
/// a path to a reference dictionary
/// and a selector that the dictionary is keyed off of.
public enum JSONReference<Root: ReferenceRoot, RefType: Equatable>: Equatable {
case node(InternalReference)
case file(FileReference)
public typealias FileReference = String
public struct InternalReference: Equatable {
public let path: PartialKeyPath<Root>
public let selector: String
public var refName: String {
// we require RD be a RefName in the initializer
// so it is safe to force cast here.
return (type(of: path).valueType as! RefName.Type).refName
}
public init<RD: RefName & ReferenceDict>(type: KeyPath<Root, RD>,
selector: String) where RD.Value == RefType {
self.path = type
self.selector = selector
}
}
}
/// An OpenAPI Path Item
/// This type describes the endpoints a server has
/// bound to a particular path.
public enum OpenAPIPathItem: Equatable {
case reference(JSONReference<OpenAPIComponents, OpenAPIPathItem>)
case operations(PathProperties)
public struct PathProperties: Equatable {
public let summary: String?
public let description: String?
// public let servers:
public let parameters: ParameterArray
public let get: Operation?
public let put: Operation?
public let post: Operation?
public let delete: Operation?
public let options: Operation?
public let head: Operation?
public let patch: Operation?
public let trace: Operation?
public init(summary: String? = nil,
description: String? = nil,
parameters: ParameterArray,
get: Operation? = nil,
put: Operation? = nil,
post: Operation? = nil,
delete: Operation? = nil,
options: Operation? = nil,
head: Operation? = nil,
patch: Operation? = nil,
trace: Operation? = nil) {
self.summary = summary
self.description = description
self.parameters = parameters
self.get = get
self.put = put
self.post = post
self.delete = delete
self.options = options
self.head = head
self.patch = patch
self.trace = trace
}
public typealias ParameterArray = [Either<Parameter, JSONReference<OpenAPIComponents, Parameter>>]
public struct Parameter: Equatable, Encodable {
private enum CodingKeys: String, CodingKey {
case name
// case parameterLocation = "in"
case description
case deprecated
}
public let name: String
// public let parameterLocation: Location
public let description: String?
public let deprecated: Bool // default is false
// TODO: serialization rules
/*
Serialization Rules
*/
public init(name: String,
description: String? = nil,
deprecated: Bool = false) {
self.name = name
self.description = description
self.deprecated = deprecated
}
// public enum Location: Encodable {
// case query(required: Bool?)
// case header(required: Bool?)
// case path
// case cookie(required: Bool?)
// }
}
public struct Operation: Equatable {
public let tags: [String]?
public let summary: String?
public let description: String?
// public let externalDocs:
public let operationId: String
public let parameters: ParameterArray
public let requestBody: OpenAPIRequestBody?
public let responses: ResponseMap
// public let callbacks:
public let deprecated: Bool // default is false
// public let security:
// public let servers:
public init(tags: [String]? = nil,
summary: String? = nil,
description: String? = nil,
operationId: String,
parameters: ParameterArray,
requestBody: OpenAPIRequestBody? = nil,
responses: ResponseMap,
deprecated: Bool = false) {
self.tags = tags
self.summary = summary
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: OpenAPIPathItem.PathProperties.Operation.ContentMap
// public let links:
public init(description: String,
content: OpenAPIPathItem.PathProperties.Operation.ContentMap) {
self.description = description
self.content = content
}
public enum Code: RawRepresentable, Equatable, Hashable {
public typealias RawValue = String
case `default`
case status(code: Int)
public var rawValue: String {
switch self {
case .default:
return "default"
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
}
}
/// What the spec calls the "Components Object".
/// This is a place to put reusable components to
/// be referenced from other parts of the spec.
public struct OpenAPIComponents: Equatable, Encodable, ReferenceRoot {
public static var refName: String { return "components" }
public let schemas: SchemasDict
// public let responses:
public let parameters: ParametersDict
// public let examples:
// public let requestBodies:
// public let headers:
// public let headers:
// public let securitySchemas:
// public let links:
// public let callbacks:
public init(schemas: [String: SchemasDict.Value], parameters: [String: ParametersDict.Value]) {
self.schemas = SchemasDict(schemas)
self.parameters = ParametersDict(parameters)
}
public enum SchemasName: RefName {
public static var refName: String { return "schemas" }
}
public typealias SchemasDict = RefDict<OpenAPIComponents, SchemasName, JSONNode>
public enum ParametersName: RefName {
public static var refName: String { return "parameters" }
}
public typealias ParametersDict = RefDict<OpenAPIComponents, ParametersName, OpenAPIPathItem.PathProperties.Parameter>
}
/// The root of an OpenAPI 3.0 document.
public struct OpenAPISchema {
public let openAPIVersion: Version
public let info: Info
// public let servers:
public let paths: [PathComponents: OpenAPIPathItem]
public let components: OpenAPIComponents
// public let security:
// public let tags:
// public let externalDocs:
public init(openAPIVersion: Version = .v3_0_0,
info: Info,
paths: [PathComponents: OpenAPIPathItem],
components: OpenAPIComponents) {
self.openAPIVersion = openAPIVersion
self.info = info
self.paths = paths
self.components = components
}
public enum Version: String, Encodable {
case v3_0_0 = "3.0.0"
}
public struct Info: Encodable {
public let title: String
public let description: String?
public let termsOfService: URL?
// public let contact:
// public let license:
public let version: String
public init(title: String,
description: String? = nil,
termsOfService: URL? = nil,
version: String) {
self.title = title
self.description = description
self.termsOfService = termsOfService
self.version = version
}
}
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(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,
@@ -1,5 +1,5 @@
//
// Sampleable+JSONAPI.swift
// JSONAPI+Sampleable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/24/19.
@@ -43,6 +43,12 @@ extension UnknownJSONAPIError: Sampleable {
}
}
extension Unidentified: Sampleable {
public static var sample: Unidentified {
return Unidentified()
}
}
extension Attribute: Sampleable where RawValue: Sampleable {
public static var sample: Attribute<RawValue> {
return .init(value: RawValue.sample)
@@ -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" }
@@ -0,0 +1,52 @@
//
// OpenAPITests.swift
// JSONAPIOpenAPITests
//
// Created by Mathew Polzin on 1/25/19.
//
import XCTest
import JSONAPI
import JSONAPIOpenAPI
class OpenAPITests: XCTestCase {
func test_placeholder() {
let schemaInfo = OpenAPISchema.Info(title: "Cool API", version: "0.1.0")
let personResponse = OpenAPIResponse(description: "Successfully created a Person",
content: [
.json: .init(schema: .init(JSONReference.node(.init(type: \.schemas, selector: "person"))))
])
let schemaPaths: [OpenAPISchema.PathComponents: OpenAPIPathItem] = [
.init(["api","people"]):
.operations(
.init(parameters: [],
post: OpenAPIPathItem.PathProperties.Operation(
summary: "",
operationId: "createPerson",
parameters: [],
responses: [
.status(code: 200): .init(personResponse)
]
)
)
)
]
let schemaComponents = OpenAPIComponents(schemas: ["person": .reference(.file("person.json"))],
parameters: [:])
let openAPISchema = OpenAPISchema(info: schemaInfo,
paths: schemaPaths,
components: schemaComponents)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
print(String(data: try! encoder.encode(openAPISchema), encoding: .utf8)!)
}
}