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:
Mathew Polzin
2019-01-24 00:47:24 -08:00
parent 7045373708
commit dc30cb3b9e
4 changed files with 226 additions and 4 deletions
@@ -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)