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.

This commit is contained in:
Mathew Polzin
2019-01-24 17:25:34 -08:00
parent dc30cb3b9e
commit 58a7c82436
7 changed files with 526 additions and 200 deletions
@@ -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
@@ -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())
}
}
}
@@ -74,6 +74,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 {
@@ -603,6 +609,7 @@ 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)
}
@@ -0,0 +1,62 @@
//
// Sampleable+JSONAPI.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 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)
}
}