mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Make Attribute Sampleable where its RawValue is Sampleable. Allow Sampleable things to provide a best guess for their node type based on the result of encoding and then deserializing them.
This commit is contained in:
@@ -12,6 +12,11 @@ import AnyCodable
|
||||
private protocol _Optional {}
|
||||
extension Optional: _Optional {}
|
||||
|
||||
private protocol Wrapper {
|
||||
associatedtype Wrapped
|
||||
}
|
||||
extension Optional: Wrapper {}
|
||||
|
||||
extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType {
|
||||
static public func openAPINode() throws -> JSONNode {
|
||||
// If the RawValue is not required, we actually consider it
|
||||
@@ -72,6 +77,8 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: conform TransformedAttribute to all of the above protocols that Attribute conforms to.
|
||||
|
||||
extension RelationshipType {
|
||||
static func relationshipNode(nullable: Bool, jsonType: String) -> JSONNode {
|
||||
let propertiesDict: [String: JSONNode] = [
|
||||
@@ -142,13 +149,13 @@ extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attr
|
||||
|
||||
let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self
|
||||
? nil
|
||||
: try Description.Attributes.genericObjectOpenAPINode(using: encoder)
|
||||
: try Description.Attributes.genericOpenAPINode(using: encoder)
|
||||
|
||||
let attributesProperty = attributesNode.map { ("attributes", $0) }
|
||||
|
||||
let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self
|
||||
? nil
|
||||
: try Description.Relationships.genericObjectOpenAPINode(using: encoder)
|
||||
: try Description.Relationships.genericOpenAPINode(using: encoder)
|
||||
|
||||
let relationshipsProperty = relationshipsNode.map { ("relationships", $0) }
|
||||
|
||||
|
||||
@@ -68,6 +68,12 @@ public protocol DoubleWrappedRawOpenAPIType {
|
||||
static func wrappedOpenAPINode() throws -> JSONNode
|
||||
}
|
||||
|
||||
/// A GenericOpenAPINodeType can take a stab at
|
||||
/// determining its OpenAPINode because it is sampleable.
|
||||
public protocol GenericOpenAPINodeType {
|
||||
static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode
|
||||
}
|
||||
|
||||
/// Anything conforming to `AnyJSONCaseIterable` can provide a
|
||||
/// list of its possible values.
|
||||
public protocol AnyJSONCaseIterable {
|
||||
@@ -594,6 +600,7 @@ public enum JSONNode: Equatable {
|
||||
public enum OpenAPICodableError: Swift.Error, Equatable {
|
||||
case allCasesArrayNotCodable
|
||||
case exampleNotCodable
|
||||
case primitiveGuessFailed
|
||||
}
|
||||
|
||||
public enum OpenAPITypeError: Swift.Error, Equatable {
|
||||
|
||||
@@ -37,6 +37,8 @@ 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 }
|
||||
@@ -48,8 +50,8 @@ public extension Sampleable {
|
||||
public static var samples: [Self] { return [Self.sample] }
|
||||
}
|
||||
|
||||
extension Sampleable {
|
||||
public static func genericObjectOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
|
||||
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
|
||||
|
||||
@@ -80,6 +82,9 @@ extension Sampleable {
|
||||
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
|
||||
}
|
||||
@@ -97,6 +102,13 @@ extension Sampleable {
|
||||
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
|
||||
@@ -106,6 +118,71 @@ extension Sampleable {
|
||||
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 {
|
||||
@@ -199,6 +276,12 @@ extension UnknownJSONAPIError: Sampleable {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user