From 9e6e713ad260e863fb5cbb89efdfdfa3f4c81691 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 21:17:07 -0800 Subject: [PATCH] Add Codable for the parts of OpenAPI that are built so far. --- Package.resolved | 9 + Package.swift | 5 +- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 59 +++- .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 176 ++++++++++++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 253 ++++++++++++++---- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 69 +++-- .../JSONAPIRelationshipsOpenAPITests.swift | 47 ++++ 7 files changed, 534 insertions(+), 84 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift create mode 100644 Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift diff --git a/Package.resolved b/Package.resolved index 85b6784..0703061 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "AnyCodable", + "repositoryURL": "https://github.com/Flight-School/AnyCodable.git", + "state": { + "branch": null, + "revision": "396ccc3dba5bdee04c1e742e7fab40582861401e", + "version": "0.1.0" + } + }, { "package": "Poly", "repositoryURL": "https://github.com/mattpolzin/Poly.git", diff --git a/Package.swift b/Package.swift index 3278ecf..d1b92a5 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,8 @@ let package = Package( targets: ["JSONAPIOpenAPI"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")) + .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), + .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0") ], targets: [ .target( @@ -28,7 +29,7 @@ let package = Package( dependencies: ["JSONAPI"]), .target( name: "JSONAPIOpenAPI", - dependencies: ["JSONAPI"]), + dependencies: ["JSONAPI", "AnyCodable"]), .testTarget( name: "JSONAPITests", dependencies: ["JSONAPI", "JSONAPITesting"]), diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 8a27709..6cb2923 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -7,26 +7,61 @@ import JSONAPI -extension Attribute: OpenAPITyped where RawValue: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return value.openAPIType +extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return RawValue.openAPINode } } -extension TransformedAttribute: OpenAPITyped where RawValue: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return rawValue.openAPIType +extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return RawValue.openAPINode } } -extension ToOneRelationship: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .object(.generic) +private protocol _Optional {} +extension Optional: _Optional {} + +extension ToOneRelationship: OpenAPINodeType { + // TODO: const for json `type` + static public var openAPINode: OpenAPI.JSONNode { + let nullable = Identifiable.self is _Optional.Type + return .object(.init(format: .generic, + required: true), + .init(properties: [ + "data": .object(.init(format: .generic, + required: true, + nullable: nullable), + .init(properties: [ + "id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init()) + ])) + ])) } } -extension ToManyRelationship: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .object(.generic) +extension ToManyRelationship: OpenAPINodeType { + // TODO: const for json `type` + static public var openAPINode: OpenAPI.JSONNode { + return .object(.init(format: .generic, + required: true), + .init(properties: [ + "data": .array(.init(format: .generic, + required: true), + .init(items: .object(.init(format: .generic, + required: true), + .init(properties: [ + "id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init()) + ])))) + ])) } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift new file mode 100644 index 0000000..4212ccc --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -0,0 +1,176 @@ +// +// OpenAPITypes+Codable.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/14/19. +// + +extension OpenAPI.JSONNode.Context: Encodable { + + private enum CodingKeys: String, CodingKey { + case type + case format + case allowedValues = "enum" + case nullable + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(format.jsonType, forKey: .type) + + if format != Format.unspecified { + try container.encode(format, forKey: .format) + } + + if allowedValues != nil { + try container.encode(allowedValues, forKey: .allowedValues) + } + + try container.encode(nullable, forKey: .nullable) + } +} + +extension OpenAPI.JSONNode.NumericContext: Encodable { + private enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if multipleOf != nil { + try container.encode(multipleOf, forKey: .multipleOf) + } + + if maximum != nil { + try container.encode(maximum, forKey: .maximum) + } + + if exclusiveMaximum != nil { + try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum) + } + + if minimum != nil { + try container.encode(minimum, forKey: .minimum) + } + + if exclusiveMinimum != nil { + try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum) + } + } +} + +extension OpenAPI.JSONNode.StringContext: Encodable { + private enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if maxLength != nil { + try container.encode(maxLength, forKey: .maxLength) + } + + try container.encode(minLength, forKey: .minLength) + + if pattern != nil { + try container.encode(pattern, forKey: .pattern) + } + } +} + +extension OpenAPI.JSONNode.ArrayContext: Encodable { + private enum CodingKeys: String, CodingKey { + case items + case maxItems + case minItems + case uniqueItems + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(items, forKey: .items) + + if maxItems != nil { + try container.encode(maxItems, forKey: .maxItems) + } + + try container.encode(minItems, forKey: .minItems) + + try container.encode(uniqueItems, forKey: .uniqueItems) + } +} + +extension OpenAPI.JSONNode.ObjectContext : Encodable{ + private enum CodingKeys: String, CodingKey { + case maxProperties + case minProperties + case properties + case additionalProperties + case required + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if maxProperties != nil { + try container.encode(maxProperties, forKey: .maxProperties) + } + + try container.encode(properties, forKey: .properties) + + if additionalProperties != nil { + try container.encode(additionalProperties, forKey: .additionalProperties) + } + + let required = properties.filter { (name, node) in + node.required + }.keys + + try container.encode(Array(required), forKey: .required) + + try container.encode(max(minProperties, required.count), forKey: .minProperties) + } +} + +extension OpenAPI.JSONNode: Encodable { + public func encode(to encoder: Encoder) throws { + switch self { + case .boolean(let context): + try context.encode(to: encoder) + + case .object(let contextA as Encodable, let contextB as Encodable), + .array(let contextA as Encodable, let contextB as Encodable), + .number(let contextA as Encodable, let contextB as Encodable), + .integer(let contextA as Encodable, let contextB as Encodable), + .string(let contextA as Encodable, let contextB as Encodable): + try contextA.encode(to: encoder) + try contextB.encode(to: encoder) + + case .allOf(let nodes): + // TODO + print("TODO") + + case .oneOf(let nodes): + // TODO + print("TODO") + + case .anyOf(let nodes): + // TODO + print("TODO") + + case .not(let node): + // TODO + print("TODO") + } + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 24c0587..2ccb7c6 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -5,18 +5,28 @@ // Created by Mathew Polzin on 1/13/19. // -public protocol OpenAPITyped { - var openAPIType: OpenAPI.JSONTypeFormat { get } +import AnyCodable + +public protocol OpenAPINodeType { + static var openAPINode: OpenAPI.JSONNode { get } } public protocol SwiftTyped { - associatedtype SwiftType + associatedtype SwiftType: Codable } -public protocol OpenAPIFormat: SwiftTyped {} +public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable { + static var unspecified: Self { get } + + var jsonType: OpenAPI.JSONType { get } +} + +public protocol JSONNodeContext { + var required: Bool { get } +} public extension OpenAPI { - enum JSONType: String { + enum JSONType: String, Codable { case boolean = "boolean" case object = "object" case array = "array" @@ -34,75 +44,98 @@ public extension OpenAPI { case string(StringFormat) } - enum JSONTypeNode { + /// A JSON Node is what OpenAPI calls a + /// "Schema Object" + enum JSONNode { case boolean(Context) indirect case object(Context, ObjectContext) indirect case array(Context, ArrayContext) case number(Context, NumericContext) case integer(Context, NumericContext) case string(Context, StringContext) - indirect case allOf([JSONTypeNode]) - indirect case oneOf([JSONTypeNode]) - indirect case anyOf([JSONTypeNode]) - indirect case not(JSONTypeNode) - } -} - -extension OpenAPI.JSONType { - public var swiftType: Any.Type { - switch self { - case .boolean: - return Bool.self - case .object: - return Any.self - case .array: - return [Any].self - case .number: - return Double.self - case .integer: - return Int.self - case .string: - return String.self - } + indirect case allOf([JSONNode]) + indirect case oneOf([JSONNode]) + indirect case anyOf([JSONNode]) + indirect case not(JSONNode) } } public extension OpenAPI.JSONTypeFormat { - public enum BooleanFormat: String, Equatable, Codable, OpenAPIFormat { + public enum BooleanFormat: String, Equatable, OpenAPIFormat { case generic = "" public typealias SwiftType = Bool + + public static var unspecified: BooleanFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .boolean + } } - public enum ObjectFormat: String, Equatable, Codable, OpenAPIFormat { + public enum ObjectFormat: String, Equatable, OpenAPIFormat { case generic = "" - public typealias SwiftType = Any + public typealias SwiftType = AnyCodable + + public static var unspecified: ObjectFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .object + } } - public enum ArrayFormat: String, Equatable, Codable, OpenAPIFormat { + public enum ArrayFormat: String, Equatable, OpenAPIFormat { case generic = "" - public typealias SwiftType = [Any] + public typealias SwiftType = [AnyCodable] + + public static var unspecified: ArrayFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .array + } } - public enum NumberFormat: String, Equatable, Codable, OpenAPIFormat { + public enum NumberFormat: String, Equatable, OpenAPIFormat { case generic = "" case float = "float" case double = "double" public typealias SwiftType = Double + + public static var unspecified: NumberFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .number + } } - public enum IntegerFormat: String, Equatable, Codable, OpenAPIFormat { + public enum IntegerFormat: String, Equatable, OpenAPIFormat { case generic = "" case int32 = "int32" case int64 = "int64" public typealias SwiftType = Int + + public static var unspecified: IntegerFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .integer + } } - public enum StringFormat: String, Equatable, Codable, OpenAPIFormat { + public enum StringFormat: String, Equatable, OpenAPIFormat { case generic = "" case byte = "byte" case binary = "binary" @@ -111,6 +144,14 @@ public extension OpenAPI.JSONTypeFormat { case password = "password" public typealias SwiftType = String + + public static var unspecified: StringFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .string + } } public var jsonType: OpenAPI.JSONType { @@ -131,10 +172,11 @@ public extension OpenAPI.JSONTypeFormat { } } -extension OpenAPI.JSONTypeNode { - public struct Context { +extension OpenAPI.JSONNode { + public struct Context: JSONNodeContext { public let format: Format public let required: Bool + public let nullable: Bool /// The OpenAPI spec calls this "enum" /// If not specified, it is assumed that any @@ -143,44 +185,99 @@ extension OpenAPI.JSONTypeNode { public init(format: Format, required: Bool, + nullable: Bool = false, allowedValues: [Format.SwiftType]? = nil) { self.format = format self.required = required + self.nullable = nullable self.allowedValues = allowedValues } + + /// Return the optional version of this Context + public func optionalContext() -> Context { + return .init(format: format, + required: false, + allowedValues: allowedValues) + } + + /// Return the required version of this context + public func requiredContext() -> Context { + return .init(format: format, + required: true, + allowedValues: allowedValues) + } } public struct NumericContext { + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. public let multipleOf: Double? public let maximum: Double? public let exclusiveMaximum: Double? public let minimum: Double? public let exclusiveMinimum: Double? + + public init(multipleOf: Double? = nil, + maximum: Double? = nil, + exclusiveMaximum: Double? = nil, + minimum: Double? = nil, + exclusiveMinimum: Double? = nil) { + self.multipleOf = multipleOf + self.maximum = maximum + self.exclusiveMaximum = exclusiveMaximum + self.minimum = minimum + self.exclusiveMinimum = exclusiveMinimum + } } public struct StringContext { public let maxLength: Int? - public let minLength: Int? + public let minLength: Int /// Regular expression public let pattern: String? + + public init(maxLength: Int? = nil, + minLength: Int = 0, + pattern: String? = nil) { + self.maxLength = maxLength + self.minLength = minLength + self.pattern = pattern + } } public struct ArrayContext { /// A JSON Type Node that describes /// the type of each element in the array. - public let items: OpenAPI.JSONTypeNode + public let items: OpenAPI.JSONNode + /// Maximum number of items in array. public let maxItems: Int? - public let minItems: Int? - public let uniqueItems: Bool? + + /// Minimum number of items in array. + /// Defaults to 0. + public let minItems: Int + + /// Setting to true indicates all + /// elements of the array are expected + /// to be unique. Defaults to false. + public let uniqueItems: Bool + + public init(items: OpenAPI.JSONNode, + maxItems: Int? = nil, + minItems: Int = 0, + uniqueItems: Bool = false) { + self.items = items + self.maxItems = maxItems + self.minItems = minItems + self.uniqueItems = uniqueItems + } } public struct ObjectContext { public let maxProperties: Int? - public let minProperties: Int? - public let properties: [String: OpenAPI.JSONTypeNode] - public let additionalProperties: [String: OpenAPI.JSONTypeNode] + public let minProperties: Int + public let properties: [String: OpenAPI.JSONNode] + public let additionalProperties: [String: OpenAPI.JSONNode]? /* // NOTE that an object's required properties @@ -188,6 +285,16 @@ extension OpenAPI.JSONTypeNode { // required Bool. public let required: [String] */ + + public init(properties: [String: OpenAPI.JSONNode], + additionalProperties: [String: OpenAPI.JSONNode]? = nil, + maxProperties: Int? = nil, + minProperties: Int = 0) { + self.properties = properties + self.additionalProperties = additionalProperties + self.maxProperties = maxProperties + self.minProperties = minProperties + } } public var jsonTypeFormat: OpenAPI.JSONTypeFormat? { @@ -208,4 +315,58 @@ extension OpenAPI.JSONTypeNode { return nil } } + + public var required: Bool { + switch self { + case .boolean(let contextA as JSONNodeContext), + .object(let contextA as JSONNodeContext, _), + .array(let contextA as JSONNodeContext, _), + .number(let contextA as JSONNodeContext, _), + .integer(let contextA as JSONNodeContext, _), + .string(let contextA as JSONNodeContext, _): + return contextA.required + case .allOf, .oneOf, .anyOf, .not: + return true + } + } + + /// Return the optional version of this JSONNode + public func optionalNode() -> OpenAPI.JSONNode { + switch self { + case .boolean(let context): + return .boolean(context.optionalContext()) + case .object(let contextA, let contextB): + return .object(contextA.optionalContext(), contextB) + case .array(let contextA, let contextB): + return .array(contextA.optionalContext(), contextB) + case .number(let context, let contextB): + return .number(context.optionalContext(), contextB) + case .integer(let context, let contextB): + return .integer(context.optionalContext(), contextB) + case .string(let context, let contextB): + return .string(context.optionalContext(), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } + + /// Return the required version of this JSONNode + public func requiredNode() -> OpenAPI.JSONNode { + switch self { + case .boolean(let context): + return .boolean(context.requiredContext()) + case .object(let contextA, let contextB): + return .object(contextA.requiredContext(), contextB) + case .array(let contextA, let contextB): + return .array(contextA.requiredContext(), contextB) + case .number(let context, let contextB): + return .number(context.requiredContext(), contextB) + case .integer(let context, let contextB): + return .integer(context.requiredContext(), contextB) + case .string(let context, let contextB): + return .string(context.requiredContext(), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } } diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 7c91a49..236ef95 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -26,50 +26,71 @@ A hint to UIs to obscure input: **/ -extension String: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .string(.generic) +extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return Wrapped.openAPINode.optionalNode() } } -extension Bool: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .boolean(.generic) +extension String: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .string(.init(format: .generic, + required: true), + .init()) } } -extension Array: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .array(.generic) +extension Bool: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .boolean(.init(format: .generic, + required: true)) } } -extension Double: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .number(.double) +extension Array: OpenAPINodeType where Element: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .array(.init(format: .generic, + required: true), + .init(items: Element.openAPINode)) } } -extension Float: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .number(.float) +extension Double: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .number(.init(format: .double, + required: true), + .init()) } } -extension Int: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.generic) +extension Float: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .number(.init(format: .float, + required: true), + .init()) } } -extension Int32: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.int32) +extension Int: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .generic, + required: true), + .init()) } } -extension Int64: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.int64) +extension Int32: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .int32, + required: true), + .init()) + } +} + +extension Int64: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .int64, + required: true), + .init()) } } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift new file mode 100644 index 0000000..44d699c --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift @@ -0,0 +1,47 @@ +// +// JSONAPIRelationshipsOpenAPITests.swift +// JSONAPI +// +// Created by Mathew Polzin on 1/14/19. +// + +import Foundation +import XCTest +import JSONAPI +import JSONAPITesting +import JSONAPIOpenAPI + +class JSONAPIRelationshipsOpenAPITests: XCTestCase { + + func test_ToOne() { + let node = ToOneRelationship.openAPINode + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + print(String(data: (try? encoder.encode(node))!, encoding: .utf8)!) + } +} + +// MARK: Test Types +extension JSONAPIRelationshipsOpenAPITests { + enum TestEntityType1: EntityDescription { + static var jsonType: String { return "test_entities"} + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias TestEntity1 = BasicEntity + + enum TestEntityType2: EntityDescription { + static var jsonType: String { return "second_test_entities"} + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + let other: ToOneRelationship + } + } + + typealias TestEntity2 = BasicEntity +}