diff --git a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift index a0691d7..412a409 100644 --- a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift @@ -3,6 +3,7 @@ import Foundation import JSONAPI import JSONAPIOpenAPI +import Poly // print Entity Schema let encoder = JSONEncoder() @@ -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: OpenAPIComponents.SchemasDict.RefType] = [ + "BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample() +] + +let components = OpenAPIComponents(schemas: tmp) + +let batchPeopleRef = JSONReference(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)!) diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes+Codable.swift index 9322035..91a71cd 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes+Codable.swift @@ -155,6 +155,7 @@ extension JSONNode: Encodable { case oneOf case anyOf case not + case reference = "$ref" } public func encode(to encoder: Encoder) throws { @@ -189,6 +190,172 @@ extension JSONNode: Encodable { var container = encoder.container(keyedBy: SubschemaCodingKeys.self) try container.encode(node, forKey: .not) + + case .reference(let reference): + var container = encoder.container(keyedBy: SubschemaCodingKeys.self) + + try container.encode(reference, forKey: .reference) + } + } +} + +extension JSONReference: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + 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) + } +} + +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 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) + + try container.encode(responses, 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 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) } } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index 580fdb3..faad090 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -7,6 +7,7 @@ import AnyCodable import Foundation +import Poly // MARK: Node (i.e. schema) Protocols @@ -268,6 +269,7 @@ public enum JSONNode: Equatable { indirect case one(of: [JSONNode]) indirect case any(of: [JSONNode]) indirect case not(JSONNode) + case reference(JSONReference) public struct Context: JSONNodeContext, Equatable { public let format: Format @@ -470,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 } } @@ -484,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 } } @@ -504,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 } } @@ -524,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 } } @@ -544,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 } } @@ -564,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 } } @@ -591,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 } } @@ -607,3 +609,316 @@ 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: 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: Equatable { + + case node(InternalReference) + case file(FileReference) + + public typealias FileReference = String + + public struct InternalReference: Equatable { + public let path: PartialKeyPath + 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(type: KeyPath, + 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) + 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>] + + 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: + 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, + responses: ResponseMap, + deprecated: Bool = false) { + self.tags = tags + self.summary = summary + self.description = description + self.operationId = operationId + self.parameters = parameters + self.responses = responses + self.deprecated = deprecated + } + + public typealias ResponseMap = [OpenAPIResponse.Code: Either>] + } + } +} + +public struct OpenAPIResponse: Encodable, Equatable { + public let description: String +// public let headers: + public let content: ContentMap +// public let links: + + public init(description: String, + content: ContentMap) { + self.description = description + self.content = content + } + + public typealias ContentMap = [ContentType: Content] + + public enum Code: Equatable, Hashable { + case `default` + case status(code: Int) + } + + public enum ContentType: String, Encodable, Equatable, Hashable { + case json = "application/json" + } + + public struct Content: Encodable, Equatable { + public let schema: Either> +// public let example: +// public let examples: +// public let encoding: + + public init(schema: Either>) { + 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 + + public enum ParametersName: RefName { + public static var refName: String { return "parameters" } + } + + public typealias ParametersDict = RefDict +} + +/// 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 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: Encodable, Equatable, Hashable { + public let components: [String] + + public init(_ components: [String]) { + self.components = components + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(components.joined(separator: "/")) + } + } +} diff --git a/Tests/JSONAPIOpenAPITests/OpenAPI/OpenAPITests.swift b/Tests/JSONAPIOpenAPITests/OpenAPI/OpenAPITests.swift new file mode 100644 index 0000000..62faad8 --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/OpenAPI/OpenAPITests.swift @@ -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)!) + } + +}