Add Codable for the parts of OpenAPI that are built so far.

This commit is contained in:
Mathew Polzin
2019-01-14 21:17:07 -08:00
parent 308f168a8c
commit 9e6e713ad2
7 changed files with 534 additions and 84 deletions
+9
View File
@@ -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",
+3 -2
View File
@@ -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"]),
@@ -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())
]))))
]))
}
}
@@ -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")
}
}
}
+207 -46
View File
@@ -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<JSONTypeFormat.BooleanFormat>)
indirect case object(Context<JSONTypeFormat.ObjectFormat>, ObjectContext)
indirect case array(Context<JSONTypeFormat.ArrayFormat>, ArrayContext)
case number(Context<JSONTypeFormat.NumberFormat>, NumericContext)
case integer(Context<JSONTypeFormat.IntegerFormat>, NumericContext)
case string(Context<JSONTypeFormat.StringFormat>, 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<Format: OpenAPIFormat> {
extension OpenAPI.JSONNode {
public struct Context<Format: OpenAPIFormat>: 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
}
}
}
@@ -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())
}
}
@@ -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<TestEntity1?, NoMetadata, NoLinks>.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<TestEntityType1>
enum TestEntityType2: EntityDescription {
static var jsonType: String { return "second_test_entities"}
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let other: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>
}
}
typealias TestEntity2 = BasicEntity<TestEntityType2>
}