mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user