currently in a pretty broken state with support for enumerations being turned into allowed values via reflection. I think I am going to have to give up type safety if I want to use reflection and keep things open ended

This commit is contained in:
Mathew Polzin
2019-01-19 15:30:09 -08:00
parent 52d2e9819d
commit cf746e182f
7 changed files with 422 additions and 49 deletions
@@ -11,48 +11,78 @@ private protocol _Optional {}
extension Optional: _Optional {}
extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if !RawValue.openAPINode.required {
return RawValue.openAPINode.requiredNode().nullableNode()
if try !RawValue.openAPINode().required {
return try RawValue.openAPINode().requiredNode().nullableNode()
}
return RawValue.openAPINode
return try RawValue.openAPINode()
}
}
extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawValue.RawValue: OpenAPINodeType {
static public func openAPINode() throws -> JSONNode {
if try !RawValue.RawValue.openAPINode().required {
return try RawValue.RawValue.openAPINode().requiredNode().nullableNode()
}
return try RawValue.RawValue.openAPINode()
}
}
extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable {
public static var allCases: [Any] {
return Array(RawValue.allCases)
}
}
extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable {
public static var allCases: [Any] {
return RawValue.allCases
}
}
extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if !RawValue.openAPINode.required {
return RawValue.openAPINode.requiredNode().nullableNode()
if try !RawValue.openAPINode().required {
return try RawValue.openAPINode().requiredNode().nullableNode()
}
return RawValue.openAPINode
return try RawValue.openAPINode()
}
}
extension RelationshipType {
static func relationshipNode(nullable: Bool) -> JSONNode {
let propertiesDict: [String: JSONNode] = [
"id": .string(.init(format: .generic,
required: true),
.init()),
"type": .string(.init(format: .generic,
required: true),
.init())
]
return .object(.init(format: .generic,
required: true,
nullable: nullable),
.init(properties: propertiesDict))
}
}
extension ToOneRelationship: OpenAPINodeType {
// TODO: const for json `type`
// TODO: metadata & links
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
let nullable = Identifiable.self is _Optional.Type
return .object(.init(format: .generic,
required: true),
.init(properties: [
"data": .object(.init(format: .generic,
required: true,
nullable: nullable),
.init(properties: [
"id": .string(.init(format: .generic,
required: true),
.init()),
"type": .string(.init(format: .generic,
required: true),
.init())
]))
"data": ToOneRelationship.relationshipNode(nullable: nullable)
]))
}
}
@@ -60,22 +90,38 @@ extension ToOneRelationship: OpenAPINodeType {
extension ToManyRelationship: OpenAPINodeType {
// TODO: const for json `type`
// TODO: metadata & links
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .object(.init(format: .generic,
required: true),
.init(properties: [
"data": .array(.init(format: .generic,
required: true),
.init(items: .object(.init(format: .generic,
required: true),
.init(properties: [
"id": .string(.init(format: .generic,
required: true),
.init()),
"type": .string(.init(format: .generic,
required: true),
.init())
]))))
.init(items: ToManyRelationship.relationshipNode(nullable: false)))
]))
}
}
extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
public static func openAPINode() throws -> JSONNode {
let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self
? nil
: try Description.Attributes.genericObjectOpenAPINode()
let attributesProperty = attributesNode.map { ("attributes", $0) }
let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self
? nil
: try Description.Relationships.genericObjectOpenAPINode()
let relationshipsProperty = relationshipsNode.map { ("relationships", $0) }
let propertiesDict = Dictionary([
attributesProperty,
relationshipsProperty
].compactMap { $0 }) { _, value in value }
return .object(.init(format: .generic,
required: true),
.init(properties: propertiesDict))
}
}
+100 -1
View File
@@ -7,8 +7,39 @@
import AnyCodable
// MARK: Node (i.e. schema) Protocols
/// Anything conforming to `OpenAPINodeType` can provide an
/// OpenAPI schema representing itself.
public protocol OpenAPINodeType {
static var openAPINode: JSONNode { get }
static func openAPINode() throws -> JSONNode
}
/// Anything conforming to `RawOpenAPINodeType` can provide an
/// OpenAPI schema representing itself. This second protocol is
/// necessary so that one type can conditionally provide a
/// schema and then (under different conditions) provide a
/// different schema. The "different" conditions have to do
/// with Raw Representability, hence the name of this protocol.
public protocol RawOpenAPINodeType {
static func openAPINode() throws -> JSONNode
}
/// Anything conforming to `AnyJSONCaseIterable` can provide a
/// list of its possible values.
public protocol AnyJSONCaseIterable {
static var allCases: [Any] { get }
}
/// Anything conforming to `AnyJSONCaseIterable` can provide a
/// list of its possible values. This second protocol is
/// necessary so that one type can conditionally provide a
/// list of possible values and then (under different conditions)
/// provide a different list of possible values.
/// The "different" conditions have to do
/// with Optionality, hence the name of this protocol.
public protocol AnyWrappedJSONCaseIterable {
static var allCases: [Any] { get }
}
public protocol SwiftTyped {
@@ -210,6 +241,14 @@ public enum JSONNode {
nullable: true,
allowedValues: allowedValues)
}
/// Return this context with the given list of possible values
public func with(allowedValues: [Format.SwiftType]?) -> Context {
return .init(format: format,
required: required,
nullable: nullable,
allowedValues: allowedValues)
}
}
public struct NumericContext {
@@ -393,4 +432,64 @@ public enum JSONNode {
return self
}
}
public func with<T>(allowedValues: [T]) throws -> JSONNode where T: RawRepresentable, T.RawValue == String {
return try with(allowedValues: allowedValues.map { $0.rawValue })
}
public func with(allowedValues: [JSONTypeFormat.BooleanFormat.SwiftType]) throws -> JSONNode {
guard case let .boolean(contextA) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.BooleanFormat.SwiftType.self)
}
return .boolean(contextA.with(allowedValues: allowedValues))
}
public func with(allowedValues: [JSONTypeFormat.ObjectFormat.SwiftType]) throws -> JSONNode {
guard case let .object(contextA, contextB) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ObjectFormat.SwiftType.self)
}
return .object(contextA.with(allowedValues: allowedValues), contextB)
}
public func with(allowedValues: [JSONTypeFormat.ArrayFormat.SwiftType]) throws -> JSONNode {
guard case let .array(contextA, contextB) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ArrayFormat.SwiftType.self)
}
return .array(contextA.with(allowedValues: allowedValues), contextB)
}
public func with(allowedValues: [JSONTypeFormat.NumberFormat.SwiftType]) throws -> JSONNode {
guard case let .number(contextA, contextB) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.NumberFormat.SwiftType.self)
}
return .number(contextA.with(allowedValues: allowedValues), contextB)
}
public func with(allowedValues: [JSONTypeFormat.IntegerFormat.SwiftType]) throws -> JSONNode {
guard case let .integer(contextA, contextB) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.IntegerFormat.SwiftType.self)
}
return .integer(contextA.with(allowedValues: allowedValues), contextB)
}
public func with(allowedValues: [JSONTypeFormat.StringFormat.SwiftType]) throws -> JSONNode {
guard case let .string(contextA, contextB) = self else {
throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.StringFormat.SwiftType.self)
}
return .string(contextA.with(allowedValues: allowedValues), contextB)
}
}
public struct AllowedValueError: Swift.Error, CustomStringConvertible {
public let expectation: JSONType?
public let receivedType: Any.Type
public init(expectation: JSONType?, receivedType: Any.Type) {
self.expectation = expectation
self.receivedType = receivedType
}
public var description: String {
return "Expected type compatible with JSON Type \(String(describing: expectation)) but found \(receivedType)"
}
}
@@ -0,0 +1,10 @@
//
// Optional+ZipWith.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/19/19.
//
func zip<X, Y, Z>(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? {
return left.flatMap { lft in right.map { rght in fn(lft, rght) }}
}
+122
View File
@@ -0,0 +1,122 @@
//
// Sampleable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/15/19.
//
import JSONAPI
import AnyCodable
/// A Sampleable type can provide a sample value.
/// This is useful for reflection.
public protocol Sampleable {
static var sample: Self { get }
}
extension Sampleable {
public static func genericObjectOpenAPINode() 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: [Any]? = {
switch type(of: child.value) {
case let valType as AnyJSONCaseIterable.Type:
return valType.allCases
case let valType as AnyWrappedJSONCaseIterable.Type:
return valType.allCases
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.openAPINode()
default:
return nil
}
}()
// put it all together
let newNode: JSONNode?
if let allCases = maybeAllCases,
let openAPINode = maybeOpenAPINode {
newNode = try {
if let cases = allCases as? [JSONTypeFormat.BooleanFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if let cases = allCases as? [JSONTypeFormat.ArrayFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if let cases = allCases as? [JSONTypeFormat.ObjectFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if let cases = allCases as? [JSONTypeFormat.NumberFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if let cases = allCases as? [JSONTypeFormat.IntegerFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if let cases = allCases as? [JSONTypeFormat.StringFormat.SwiftType] {
return try openAPINode.with(allowedValues: cases)
} else if allCases.compactMap({ $0 as? RawStringRepresentable }).count == allCases.count {
return try openAPINode.with(allowedValues: allCases.compactMap { ($0 as? RawStringRepresentable)?.rawValue })
} else {
throw SampleableError.allowedValuesNotOfExpectedType(forNode: openAPINode, allowedValues: allCases)
}
}()
} else {
newNode = maybeOpenAPINode
}
return zip(child.label, newNode) { ($0, $1) }
}
// 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))
}
}
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
}
}
public enum SampleableError: Swift.Error {
case allowedValuesNotOfExpectedType(forNode: JSONNode, allowedValues: [Any])
}
@@ -30,13 +30,25 @@ Any object:
**/
extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType {
static public var openAPINode: JSONNode {
return Wrapped.openAPINode.optionalNode()
static public func openAPINode() throws -> JSONNode {
return try Wrapped.openAPINode().optionalNode()
}
}
extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped.RawValue: OpenAPINodeType {
static public func openAPINode() throws -> JSONNode {
return try Wrapped.RawValue.openAPINode().optionalNode()
}
}
extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable {
public static var allCases: [Any] {
return Array(Wrapped.allCases)
}
}
extension String: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .string(.init(format: .generic,
required: true),
.init())
@@ -44,22 +56,22 @@ extension String: OpenAPINodeType {
}
extension Bool: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .boolean(.init(format: .generic,
required: true))
}
}
extension Array: OpenAPINodeType where Element: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .array(.init(format: .generic,
required: true),
.init(items: Element.openAPINode))
.init(items: try Element.openAPINode()))
}
}
extension Double: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .number(.init(format: .double,
required: true),
.init())
@@ -67,7 +79,7 @@ extension Double: OpenAPINodeType {
}
extension Float: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .number(.init(format: .float,
required: true),
.init())
@@ -75,7 +87,7 @@ extension Float: OpenAPINodeType {
}
extension Int: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .integer(.init(format: .generic,
required: true),
.init())
@@ -83,7 +95,7 @@ extension Int: OpenAPINodeType {
}
extension Int32: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .integer(.init(format: .int32,
required: true),
.init())
@@ -91,7 +103,7 @@ extension Int32: OpenAPINodeType {
}
extension Int64: OpenAPINodeType {
static public var openAPINode: JSONNode {
static public func openAPINode() throws -> JSONNode {
return .integer(.init(format: .int64,
required: true),
.init())
@@ -0,0 +1,84 @@
//
// JSONAPIEntityOpenAPITests.swift
// JSONAPIOpenAPITests
//
// Created by Mathew Polzin on 1/15/19.
//
import XCTest
import JSONAPI
import JSONAPIOpenAPI
class JSONAPIEntityOpenAPITests: XCTestCase {
func test_EmptyEntity() {
let node = try! TestType1.openAPINode()
// TODO: Write test
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let string = String(data: try! encoder.encode(node), encoding: .utf8)!
print(string)
}
func test_AttributesEntity() {
let tmp = ["hello"] as [Any]
let tmp2 = tmp as! [String]
let tmp3 = tmp as? RawStringArrayRepresentable
let tmp4 = tmp2 as? RawStringArrayRepresentable
let y = TestType2Description.EnumType.one
let z = y as Any
let x = [y as? TestType2Description.EnumType]
let node = try! TestType2.openAPINode()
// TODO: Write test
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let string = String(data: try! encoder.encode(node), encoding: .utf8)!
print(string)
}
}
// MARK: Test Types
extension JSONAPIEntityOpenAPITests {
enum TestType1Description: EntityDescription {
public static var jsonType: String { return "test1" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
}
typealias TestType1 = BasicEntity<TestType1Description>
enum TestType2Description: EntityDescription {
public static var jsonType: String { return "test1" }
public enum EnumType: String, CaseIterable, Codable, Equatable {
case one
case two
}
public struct Attributes: JSONAPI.Attributes, Sampleable {
let stringProperty: Attribute<String>
let enumProperty: Attribute<EnumType>
var computedProperty: Attribute<EnumType> {
return enumProperty
}
public static var sample: Attributes {
return Attributes(stringProperty: .init(value: "hello"),
enumProperty: .init(value: .one))
}
}
public typealias Relationships = NoRelationships
}
typealias TestType2 = BasicEntity<TestType2Description>
}
@@ -14,7 +14,7 @@ import JSONAPIOpenAPI
class JSONAPIRelationshipsOpenAPITests: XCTestCase {
func test_ToOne() {
let node = ToOneRelationship<TestEntity1, NoMetadata, NoLinks>.openAPINode
let node = try! ToOneRelationship<TestEntity1, NoMetadata, NoLinks>.openAPINode()
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -47,7 +47,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase {
}
func test_OptionalToOne() {
let node = ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?.openAPINode
let node = try! ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?.openAPINode()
XCTAssertFalse(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -80,7 +80,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase {
}
func test_NullableToOne() {
let node = ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>.openAPINode
let node = try! ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>.openAPINode()
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -113,7 +113,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase {
}
func test_OptionalNullableToOne() {
let node = ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?.openAPINode
let node = try! ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?.openAPINode()
XCTAssertFalse(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -146,7 +146,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase {
}
func test_ToMany() {
let node = ToManyRelationship<TestEntity1, NoMetadata, NoLinks>.openAPINode
let node = try! ToManyRelationship<TestEntity1, NoMetadata, NoLinks>.openAPINode()
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -189,7 +189,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase {
}
func test_OptionalToMany() {
let node = ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?.openAPINode
let node = try! ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?.openAPINode()
XCTAssertFalse(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))