Compare commits

...

11 Commits

Author SHA1 Message Date
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 2b59f54067 Fix bug causing a supplied encoder to be used for generating an example but not for helping determine the correct Date formatting. 2019-01-24 17:47:44 -08:00
Mathew Polzin 58a7c82436 Restructure files a bit. Make Date handling relatively robust compared to my first pass at it. Make the failure to construct a generic open API node type throw an error rather than silently omit the node. 2019-01-24 17:25:34 -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
11 changed files with 1119 additions and 225 deletions
@@ -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)!)
@@ -65,6 +65,18 @@ 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
@@ -129,7 +141,7 @@ 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.
@@ -172,13 +184,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),
@@ -186,7 +198,7 @@ extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity
}
}
extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType {
extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType {
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
@@ -199,7 +211,7 @@ extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourc
do {
includeNode = try Includes<Include>.openAPINode()
} catch let err as OpenAPITypeError {
guard err == .invalidNode else {
guard case .invalidNode = err else {
throw err
}
includeNode = nil
@@ -0,0 +1,40 @@
//
// Date+OpenAPI.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/24/19.
//
import Foundation
extension Date: DateOpenAPINodeType {
public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
switch encoder.dateEncodingStrategy {
case .deferredToDate, .custom:
// I don't know if we can say anything about this case without
// encoding the Date and looking at it, which is what `primitiveGuess()`
// does.
return nil
case .secondsSince1970,
.millisecondsSince1970:
return .number(.init(format: .double,
required: true),
.init())
case .iso8601:
return .string(.init(format: .dateTime,
required: true),
.init())
case .formatted(let formatter):
let hasTime = formatter.timeStyle != .none
let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date
return .string(.init(format: format,
required: true),
.init())
}
}
}
@@ -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)
}
}
}
+334 -18
View File
@@ -7,6 +7,7 @@
import AnyCodable
import Foundation
import Poly
// MARK: Node (i.e. schema) Protocols
@@ -16,22 +17,16 @@ public protocol OpenAPINodeType {
static func openAPINode() throws -> JSONNode
}
extension OpenAPINodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode {
return try openAPINode().with(example: Self.successSample ?? Self.sample, using: encoder)
}
}
/// 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
}
extension OpenAPIEncodedNodeType {
public static func openAPINode() throws -> JSONNode {
return try openAPINode(using: JSONEncoder())
extension OpenAPIEncodedNodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode {
return try openAPINode(using: encoder).with(example: Self.successSample ?? Self.sample, using: encoder)
}
}
@@ -74,6 +69,12 @@ public protocol GenericOpenAPINodeType {
static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode
}
/// Anything conforming to `DateOpenAPINodeType` is
/// able to attempt to represent itself as a date OpenAPINode
public protocol DateOpenAPINodeType {
static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode?
}
/// Anything conforming to `AnyJSONCaseIterable` can provide a
/// list of its possible values.
public protocol AnyJSONCaseIterable {
@@ -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<OpenAPIComponents, JSONNode>)
public struct Context<Format: OpenAPIFormat>: 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
}
}
@@ -603,6 +605,320 @@ public enum OpenAPICodableError: Swift.Error, Equatable {
case primitiveGuessFailed
}
public enum OpenAPITypeError: Swift.Error, Equatable {
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:
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<OpenAPIResponse, JSONReference<OpenAPIComponents, OpenAPIResponse>>]
}
}
}
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<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: 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: "/"))
}
}
}
@@ -0,0 +1,68 @@
//
// JSONAPI+Sampleable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/24/19.
//
import JSONAPI
extension NoAttributes: Sampleable {
public static var sample: NoAttributes {
return .none
}
}
extension NoRelationships: Sampleable {
public static var sample: NoRelationships {
return .none
}
}
extension NoMetadata: Sampleable {
public static var sample: NoMetadata {
return .none
}
}
extension NoLinks: Sampleable {
public static var sample: NoLinks {
return .none
}
}
extension NoAPIDescription: Sampleable {
public static var sample: NoAPIDescription {
return .none
}
}
extension UnknownJSONAPIError: Sampleable {
public static var sample: UnknownJSONAPIError {
return .unknownError
}
}
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)
}
}
extension SingleResourceBody: Sampleable where Entity: Sampleable {
public static var sample: SingleResourceBody<Entity> {
return .init(entity: Entity.sample)
}
}
extension ManyResourceBody: Sampleable where Entity: Sampleable {
public static var sample: ManyResourceBody<Entity> {
return .init(entities: Entity.samples)
}
}
@@ -0,0 +1,161 @@
//
// Sampleable+OpenAPI.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/24/19.
//
import Foundation
import AnyCodable
public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType
extension Sampleable where Self: Encodable {
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// short circuit for dates
if let dateType = self as? Date.Type,
let node = try dateType.dateOpenAPINodeGuess(using: encoder) ?? primitiveGuess(using: encoder) {
return node
}
let mirror = Mirror(reflecting: Self.sample)
let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in
// see if we can enumerate the possible values
let maybeAllCases: [AnyCodable]? = {
switch type(of: child.value) {
case let valType as AnyJSONCaseIterable.Type:
return valType.allCases(using: encoder)
case let valType as AnyWrappedJSONCaseIterable.Type:
return valType.allCases(using: encoder)
default:
return nil
}
}()
// try to snag an OpenAPI Node
let maybeOpenAPINode: JSONNode? = try {
switch type(of: child.value) {
case let valType as OpenAPINodeType.Type:
return try valType.openAPINode()
case let valType as RawOpenAPINodeType.Type:
return try valType.rawOpenAPINode()
case let valType as WrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as DoubleWrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as GenericOpenAPINodeType.Type:
return try valType.genericOpenAPINode(using: encoder)
case let valType as DateOpenAPINodeType.Type:
return valType.dateOpenAPINodeGuess(using: encoder)
default:
throw OpenAPITypeError.unknownNodeType(self)
// return nil
}
}()
// put it all together
let newNode: JSONNode?
if let allCases = maybeAllCases,
let openAPINode = maybeOpenAPINode {
newNode = try openAPINode.with(allowedValues: allCases)
} else {
newNode = maybeOpenAPINode
}
return zip(child.label, newNode) { ($0, $1) }
}
// if there are no properties, let's see if we are dealing
// with a primitive.
if properties.count == 0,
let primitive = try primitiveGuess(using: encoder) {
return primitive
}
// There should not be any duplication of keys since these are
// property names, but rather than risk runtime exception, we just
// fail to the newer value arbitrarily
let propertiesDict = Dictionary(properties) { _, value2 in value2 }
return .object(.init(format: .generic,
required: true),
.init(properties: propertiesDict))
}
private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? {
let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample))
let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments])
guard let wrapperDict = wrappedValue as? [String: Any],
wrapperDict.contains(where: { $0.key == "primitive" }) else {
throw OpenAPICodableError.primitiveGuessFailed
}
let value = (wrappedValue as! [String: Any])["primitive"]!
return try {
switch type(of: value) {
case let valType as OpenAPINodeType.Type:
return try valType.openAPINode()
case let valType as RawOpenAPINodeType.Type:
return try valType.rawOpenAPINode()
case let valType as WrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as DoubleWrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as GenericOpenAPINodeType.Type:
return try valType.genericOpenAPINode(using: encoder)
case let valType as DateOpenAPINodeType.Type:
return valType.dateOpenAPINodeGuess(using: encoder)
default:
return nil
}
}() ?? {
switch value {
case is String:
return .string(.init(format: .generic,
required: true),
.init())
case is Int:
return .integer(.init(format: .generic,
required: true),
.init())
case is Double:
return .number(.init(format: .double,
required: true),
.init())
case is Bool:
return .boolean(.init(format: .generic,
required: true))
default:
return nil
}
}()
}
}
// The following wrapper is only needed because JSONEncoder cannot yet encode
// JSON fragments. It is a very unfortunate limitation that requires silly
// workarounds in edge cases like this.
private struct PrimitiveWrapper<Wrapped: Encodable>: Encodable {
let primitive: Wrapped
}
@@ -5,9 +5,7 @@
// Created by Mathew Polzin on 1/15/19.
//
import JSONAPI
import Foundation
import AnyCodable
/// A Sampleable type can provide a sample value.
/// This is useful for reflection.
@@ -37,8 +35,6 @@ public protocol Sampleable {
static var samples: [Self] { get }
}
public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType
public extension Sampleable {
// default implementation:
public static var successSample: Self? { return nil }
@@ -50,141 +46,6 @@ public extension Sampleable {
public static var samples: [Self] { return [Self.sample] }
}
extension Sampleable where Self: Encodable {
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
let mirror = Mirror(reflecting: Self.sample)
let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in
// see if we can enumerate the possible values
let maybeAllCases: [AnyCodable]? = {
switch type(of: child.value) {
case let valType as AnyJSONCaseIterable.Type:
return valType.allCases(using: encoder)
case let valType as AnyWrappedJSONCaseIterable.Type:
return valType.allCases(using: encoder)
default:
return nil
}
}()
// try to snag an OpenAPI Node
let maybeOpenAPINode: JSONNode? = try {
switch type(of: child.value) {
case let valType as OpenAPINodeType.Type:
return try valType.openAPINode()
case let valType as RawOpenAPINodeType.Type:
return try valType.rawOpenAPINode()
case let valType as WrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as DoubleWrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as GenericOpenAPINodeType.Type:
return try valType.genericOpenAPINode(using: encoder)
default:
return nil
}
}()
// put it all together
let newNode: JSONNode?
if let allCases = maybeAllCases,
let openAPINode = maybeOpenAPINode {
newNode = try openAPINode.with(allowedValues: allCases)
} else {
newNode = maybeOpenAPINode
}
return zip(child.label, newNode) { ($0, $1) }
}
// if there are no properties, let's see if we are dealing
// with a primitive.
if properties.count == 0,
let primitive = try primitiveGuess(using: encoder) {
return primitive
}
// There should not be any duplication of keys since these are
// property names, but rather than risk runtime exception, we just
// fail to the newer value arbitrarily
let propertiesDict = Dictionary(properties) { _, value2 in value2 }
return .object(.init(format: .generic,
required: true),
.init(properties: propertiesDict))
}
private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? {
let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample))
let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments])
guard let wrapperDict = wrappedValue as? [String: Any],
wrapperDict.contains(where: { $0.key == "primitive" }) else {
throw OpenAPICodableError.primitiveGuessFailed
}
let value = (wrappedValue as! [String: Any])["primitive"]!
return try {
switch type(of: value) {
case let valType as OpenAPINodeType.Type:
return try valType.openAPINode()
case let valType as RawOpenAPINodeType.Type:
return try valType.rawOpenAPINode()
case let valType as WrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as DoubleWrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as GenericOpenAPINodeType.Type:
return try valType.genericOpenAPINode(using: encoder)
default:
return nil
}
}() ?? {
switch value {
case is String:
return .string(.init(format: .generic,
required: true),
.init())
case is Int:
return .integer(.init(format: .generic,
required: true),
.init())
case is Double:
return .number(.init(format: .double,
required: true),
.init())
case is Bool:
return .boolean(.init(format: .generic,
required: true))
default:
return nil
}
}()
}
}
// The following wrapper is only needed because JSONEncoder cannot yet encode
// JSON fragments. It is a very unfortunate limitation that requires silly
// workarounds in edge cases like this.
private struct PrimitiveWrapper<Wrapped: Encodable>: Encodable {
let primitive: Wrapped
}
extension Sampleable {
public static func samples<S1: Sampleable>(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] {
return S1.samples.map(constructor)
@@ -239,57 +100,3 @@ extension Sampleable {
return zip(a, zip(b, zip(c, zip(d, e)))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1.0, arg.1.1.1.1) }
}
}
extension NoAttributes: Sampleable {
public static var sample: NoAttributes {
return .none
}
}
extension NoRelationships: Sampleable {
public static var sample: NoRelationships {
return .none
}
}
extension NoMetadata: Sampleable {
public static var sample: NoMetadata {
return .none
}
}
extension NoLinks: Sampleable {
public static var sample: NoLinks {
return .none
}
}
extension NoAPIDescription: Sampleable {
public static var sample: NoAPIDescription {
return .none
}
}
extension UnknownJSONAPIError: Sampleable {
public static var sample: UnknownJSONAPIError {
return .unknownError
}
}
extension Attribute: Sampleable where RawValue: Sampleable {
public static var sample: Attribute<RawValue> {
return .init(value: RawValue.sample)
}
}
extension SingleResourceBody: Sampleable where Entity: Sampleable {
public static var sample: SingleResourceBody<Entity> {
return .init(entity: Entity.sample)
}
}
extension ManyResourceBody: Sampleable where Entity: Sampleable {
public static var sample: ManyResourceBody<Entity> {
return .init(entities: Entity.samples)
}
}
@@ -508,6 +508,45 @@ extension JSONAPIAttributeOpenAPITests {
// MARK: - Date
extension JSONAPIAttributeOpenAPITests {
func test_DateStringAttribute() {
// TEST:
// Encoder is set to use
// formatter with date
// with no time.
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 = Attribute<Date>.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertTrue(node?.required ?? false)
XCTAssertEqual(node?.jsonTypeFormat, .string(.date))
guard case .string(let contextA, let stringContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .date,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
func test_DateStringAttribute_Sampleable() {
// TEST:
// Encoder is set to use
// formatter with date
// with no time.
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
@@ -521,14 +560,14 @@ extension JSONAPIAttributeOpenAPITests {
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
XCTAssertEqual(node.jsonTypeFormat, .string(.date))
guard case .string(let contextA, let stringContext) = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .generic,
XCTAssertEqual(contextA, .init(format: .date,
required: true,
nullable: false,
allowedValues: nil))
@@ -536,17 +575,215 @@ extension JSONAPIAttributeOpenAPITests {
XCTAssertEqual(stringContext, .init())
}
func test_DateNumberAttribute() {
func test_DateTimeStringAttribute() {
// TEST:
// Encoder is set to use
// formatter with date
// with time.
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let node = Attribute<Date>.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertTrue(node?.required ?? false)
XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime))
guard case .string(let contextA, let stringContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .dateTime,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
func test_DateTimeStringAttribute_Sampleable() {
// TEST:
// Encoder is set to use
// formatter with date
// with time.
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime))
guard case .string(let contextA, let stringContext) = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .dateTime,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
func test_8601DateStringAttribute() {
if #available(OSX 10.12, *) {
// TEST:
// Encoder is set to use
// iso8601 date format
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
let node = Attribute<Date>.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertTrue(node?.required ?? false)
XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime))
guard case .string(let contextA, let stringContext)? = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .dateTime,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
}
func test_8601DateStringAttribute_Sampleable() {
if #available(OSX 10.12, *) {
// TEST:
// Encoder is set to use
// iso8601 date format
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime))
guard case .string(let contextA, let stringContext) = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .dateTime,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
}
func test_DateNumberAttribute() {
// TEST:
// Encoder is set to use
// seconds since 1970 as
// date format
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: false,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_DateNumberAttribute_Sampleable() {
// TEST:
// Encoder is set to use
// seconds since 1970 as
// date format
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
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: false,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_DateDeferredAttribute() {
// TEST:
// Encoder is set to use
// Date default encoding
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .deferredToDate
let node = Attribute<Date>.dateOpenAPINodeGuess(using: encoder)
XCTAssertNil(node)
}
func test_DateDeferredAttribute_Sampleable() {
// TEST:
// Encoder is set to use
// Date default encoding
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .deferredToDate
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
@@ -631,7 +868,7 @@ extension JSONAPIAttributeOpenAPITests {
}
}
extension Date: Sampleable {
extension Date: SampleableOpenAPIType {
public static var sample: Date {
return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate
}
@@ -40,7 +40,16 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
}
func test_AttributesEntity() {
let node = try! TestType2.openAPINode(using: JSONEncoder())
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let node = try! TestType2.openAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -83,9 +92,9 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
nullable: false,
allowedValues: nil))
XCTAssertEqual(attributesContext.minProperties, 3)
XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "nullableProperty"]))
XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"]))
XCTAssertEqual(attributesContext.minProperties, 4)
XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "dateProperty", "nullableProperty"]))
XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "dateProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"]))
XCTAssertEqual(attributesContext.properties["stringProperty"],
.string(.init(format: .generic,
@@ -99,6 +108,13 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
allowedValues: ["one", "two"].map(AnyCodable.init)),
.init()))
XCTAssertEqual(attributesContext.properties["dateProperty"],
.string(.init(format: .dateTime,
required: true,
nullable: false,
allowedValues: nil),
.init()))
XCTAssertEqual(attributesContext.properties["optionalProperty"],
.string(.init(format: .generic,
required: false,
@@ -268,6 +284,7 @@ extension JSONAPIEntityOpenAPITests {
public struct Attributes: JSONAPI.Attributes, Sampleable {
let stringProperty: Attribute<String>
let enumProperty: Attribute<EnumType>
let dateProperty: Attribute<Date>
let optionalProperty: Attribute<String>?
let nullableProperty: Attribute<String?>
let nullableOptionalProperty: Attribute<String?>?
@@ -278,6 +295,7 @@ extension JSONAPIEntityOpenAPITests {
public static var sample: Attributes {
return Attributes(stringProperty: .init(value: "hello"),
enumProperty: .init(value: .one),
dateProperty: .init(value: Date()),
optionalProperty: nil,
nullableProperty: .init(value: nil),
nullableOptionalProperty: nil)
@@ -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)!)
}
}