merge w/ master

This commit is contained in:
Mathew Polzin
2019-01-25 12:01:01 -08:00
17 changed files with 1069 additions and 169 deletions
@@ -6,6 +6,7 @@
//
import JSONAPI
import Foundation
extension Includes: OpenAPINodeType where I: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
@@ -6,11 +6,17 @@
//
import JSONAPI
import Foundation
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
@@ -48,14 +54,26 @@ extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType {
}
extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable {
public static var allCases: [AnyCodable] {
return (try? allCases(from: Array(RawValue.allCases))) ?? []
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
return (try? allCases(from: Array(RawValue.allCases), using: encoder)) ?? []
}
}
extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable {
public static var allCases: [AnyCodable] {
return RawValue.allCases
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
return RawValue.allCases(using: encoder)
}
}
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)
}
}
@@ -71,6 +89,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] = [
@@ -121,8 +141,8 @@ extension ToManyRelationship: OpenAPINodeType {
}
}
extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
public static func openAPINode() throws -> JSONNode {
extension Entity: OpenAPIEncodedNodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// NOTE: const for json `type` not supported by OpenAPI 3.0
// Will use "enum" with one possible value for now.
@@ -141,13 +161,13 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc
let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self
? nil
: try Description.Attributes.genericObjectOpenAPINode()
: 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()
: try Description.Relationships.genericOpenAPINode(using: encoder)
let relationshipsProperty = relationshipsNode.map { ("relationships", $0) }
@@ -164,26 +184,26 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc
}
}
extension SingleResourceBody: OpenAPINodeType where Entity: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
return try Entity.openAPINode()
extension SingleResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try Entity.openAPINode(using: encoder)
}
}
extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
extension ManyResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return .array(.init(format: .generic,
required: true),
.init(items: try Entity.openAPINode()))
.init(items: try Entity.openAPINode(using: encoder)))
}
}
extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType, IncludeType: OpenAPINodeType {
public static func openAPINode() throws -> JSONNode {
extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// TODO: metadata, links, api description, errors
// TODO: represent data and errors as the two distinct possible outcomes
let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode()
let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode(using: encoder)
let primaryDataProperty = primaryDataNode.map { ("data", $0) }
@@ -191,7 +211,7 @@ extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType,
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())
}
}
}
@@ -17,9 +17,16 @@ public protocol OpenAPINodeType {
static func openAPINode() throws -> JSONNode
}
extension OpenAPINodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample() throws -> JSONNode {
return try openAPINode().with(example: Self.successSample ?? Self.sample)
/// Anything conforming to `OpenAPIEncodedNodeType` can provide an
/// OpenAPI schema representing itself but it may need an Encoder
/// to do its job.
public protocol OpenAPIEncodedNodeType {
static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode
}
extension OpenAPIEncodedNodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode {
return try openAPINode(using: encoder).with(example: Self.successSample ?? Self.sample, using: encoder)
}
}
@@ -56,15 +63,27 @@ 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 `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 {
static var allCases: [AnyCodable] { get }
static func allCases(using encoder: JSONEncoder) -> [AnyCodable]
}
extension AnyJSONCaseIterable {
/// Given an array of Codable values, retrieve an array of AnyCodables.
static func allCases<T: Codable>(from input: [T]) throws -> [AnyCodable] {
static func allCases<T: Codable>(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] {
if let alreadyGoodToGo = input as? [AnyCodable] {
return alreadyGoodToGo
}
@@ -77,7 +96,7 @@ extension AnyJSONCaseIterable {
// by AnyCodable, but AnyCodable wants it to actually BE a String
// upon initialization.
guard let arrayOfCodables = try JSONSerialization.jsonObject(with: JSONEncoder().encode(input), options: []) as? [Any] else {
guard let arrayOfCodables = try JSONSerialization.jsonObject(with: encoder.encode(input), options: []) as? [Any] else {
throw OpenAPICodableError.allCasesArrayNotCodable
}
return arrayOfCodables.map(AnyCodable.init)
@@ -92,7 +111,7 @@ extension AnyJSONCaseIterable {
/// The "different" conditions have to do
/// with Optionality, hence the name of this protocol.
public protocol AnyWrappedJSONCaseIterable {
static var allCases: [AnyCodable] { get }
static func allCases(using encoder: JSONEncoder) -> [AnyCodable]
}
public protocol SwiftTyped {
@@ -284,14 +303,14 @@ public enum JSONNode: Equatable {
nullable: Bool = false,
// constantValue: Format.SwiftType? = nil,
allowedValues: [AnyCodable]? = nil,
example: AnyCodable? = nil) {
example: (codable: AnyCodable, encoder: JSONEncoder)? = nil) {
self.format = format
self.required = required
self.nullable = nullable
// self.constantValue = constantValue
self.allowedValues = allowedValues
self.example = example
.flatMap { try? JSONEncoder().encode($0)}
.flatMap { try? $0.encoder.encode($0.codable)}
.flatMap { String(data: $0, encoding: .utf8) }
}
@@ -332,13 +351,13 @@ public enum JSONNode: Equatable {
}
/// Return this context with the given example
public func with(example: AnyCodable) -> Context {
public func with(example: AnyCodable, using encoder: JSONEncoder) -> Context {
return .init(format: format,
required: required,
nullable: nullable,
// constantValue: constantValue,
allowedValues: allowedValues,
example: example)
example: (codable: example, encoder: encoder))
}
}
@@ -552,27 +571,28 @@ public enum JSONNode: Equatable {
}
}
public func with<T: Encodable>(example codableExample: T) throws -> JSONNode {
public func with<T: Encodable>(example codableExample: T,
using encoder: JSONEncoder) throws -> JSONNode {
let example: AnyCodable
if let goodToGo = codableExample as? AnyCodable {
example = goodToGo
} else {
example = AnyCodable(try JSONSerialization.jsonObject(with: JSONEncoder().encode(codableExample), options: []))
example = AnyCodable(try JSONSerialization.jsonObject(with: encoder.encode(codableExample), options: []))
}
switch self {
case .boolean(let context):
return .boolean(context.with(example: example))
return .boolean(context.with(example: example, using: encoder))
case .object(let contextA, let contextB):
return .object(contextA.with(example: example), contextB)
return .object(contextA.with(example: example, using: encoder), contextB)
case .array(let contextA, let contextB):
return .array(contextA.with(example: example), contextB)
return .array(contextA.with(example: example, using: encoder), contextB)
case .number(let context, let contextB):
return .number(context.with(example: example), contextB)
return .number(context.with(example: example, using: encoder), contextB)
case .integer(let context, let contextB):
return .integer(context.with(example: example), contextB)
return .integer(context.with(example: example, using: encoder), contextB)
case .string(let context, let contextB):
return .string(context.with(example: example), contextB)
return .string(context.with(example: example, using: encoder), contextB)
case .all, .one, .any, .not, .reference:
return self
}
@@ -582,10 +602,12 @@ public enum JSONNode: Equatable {
public enum OpenAPICodableError: Swift.Error, Equatable {
case allCasesArrayNotCodable
case exampleNotCodable
case primitiveGuessFailed
}
public enum OpenAPITypeError: Swift.Error, Equatable {
public enum OpenAPITypeError: Swift.Error {
case invalidNode
case unknownNodeType(Any.Type)
}
/// Anything conforming to RefName knows what to call itself
@@ -6,6 +6,7 @@
//
import AnyCodable
import Foundation
/**
@@ -56,8 +57,8 @@ extension Optional: DoubleWrappedRawOpenAPIType where Wrapped: WrappedRawOpenAPI
}
extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Codable {
public static var allCases: [AnyCodable] {
return (try? allCases(from: Array(Wrapped.allCases))) ?? []
public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] {
return (try? allCases(from: Array(Wrapped.allCases), using: encoder)) ?? []
}
}
-116
View File
@@ -1,116 +0,0 @@
//
// 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 {
/// Get a sample value of type Self. This can be the
/// same value every time, or it can be an arbitrarily random
/// value each time.
static var sample: Self { get }
/// Get an example of success, if that is meaningful and
/// available. If not, will be nil.
static var successSample: Self? { get }
/// Get an example of failure, if that is meaningful and
/// available. If not, will be nil.
static var failureSample: Self? { get }
}
public extension Sampleable {
public static var successSample: Self? { return nil }
public static var failureSample: Self? { return nil }
}
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: [AnyCodable]? = {
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.rawOpenAPINode()
case let valType as WrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
case let valType as DoubleWrappedRawOpenAPIType.Type:
return try valType.wrappedOpenAPINode()
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) }
}
// 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
}
}
@@ -0,0 +1,184 @@
//
// Include+Sampleable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/23/19.
//
import JSONAPI
extension Includes: Sampleable where I: Sampleable {
public static var sample: Includes<I> {
guard I.self != NoIncludes.self else {
return .none
}
return .init(values: I.samples)
}
}
extension NoIncludes: Sampleable {
public static var sample: NoIncludes {
return NoIncludes()
}
}
extension Include1: Sampleable where A: Sampleable {
public static var sample: Include1<A> {
return .init(A.sample)
}
public static var samples: [Include1<A>] {
return A.samples.map(Include1<A>.init)
}
}
extension Include2: Sampleable where A: Sampleable, B: Sampleable {
public static var sample: Include2<A, B> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include2<A, B>] {
return A.samples.map(Include2<A, B>.init)
+ B.samples.map(Include2<A, B>.init)
}
}
extension Include3: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable {
public static var sample: Include3<A, B, C> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include3<A, B, C>] {
return A.samples.map(Include3<A, B, C>.init)
+ B.samples.map(Include3<A, B, C>.init)
+ C.samples.map(Include3<A, B, C>.init)
}
}
extension Include4: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable {
public static var sample: Include4<A, B, C, D> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include4<A, B, C, D>] {
return A.samples.map(Include4<A, B, C, D>.init)
+ B.samples.map(Include4<A, B, C, D>.init)
+ C.samples.map(Include4<A, B, C, D>.init)
+ D.samples.map(Include4<A, B, C, D>.init)
}
}
extension Include5: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable {
public static var sample: Include5<A, B, C, D, E> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include5<A, B, C, D, E>] {
let set1: [Include5<A, B, C, D, E>] = A.samples.map(Include5<A, B, C, D, E>.init)
+ B.samples.map(Include5<A, B, C, D, E>.init)
+ C.samples.map(Include5<A, B, C, D, E>.init)
let set2: [Include5<A, B, C, D, E>] = D.samples.map(Include5<A, B, C, D, E>.init)
+ E.samples.map(Include5<A, B, C, D, E>.init)
return set1 + set2
}
}
extension Include6: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable {
public static var sample: Include6<A, B, C, D, E, F> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include6<A, B, C, D, E, F>] {
let set1: [Include6<A, B, C, D, E, F>] = A.samples.map(Include6<A, B, C, D, E, F>.init)
+ B.samples.map(Include6<A, B, C, D, E, F>.init)
+ C.samples.map(Include6<A, B, C, D, E, F>.init)
let set2: [Include6<A, B, C, D, E, F>] = D.samples.map(Include6<A, B, C, D, E, F>.init)
+ E.samples.map(Include6<A, B, C, D, E, F>.init)
+ F.samples.map(Include6<A, B, C, D, E, F>.init)
return set1 + set2
}
}
extension Include7: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable {
public static var sample: Include7<A, B, C, D, E, F, G> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include7<A, B, C, D, E, F, G>] {
let set1: [Include7<A, B, C, D, E, F, G>] = A.samples.map(Include7<A, B, C, D, E, F, G>.init)
+ B.samples.map(Include7<A, B, C, D, E, F, G>.init)
+ C.samples.map(Include7<A, B, C, D, E, F, G>.init)
let set2: [Include7<A, B, C, D, E, F, G>] = D.samples.map(Include7<A, B, C, D, E, F, G>.init)
+ E.samples.map(Include7<A, B, C, D, E, F, G>.init)
+ F.samples.map(Include7<A, B, C, D, E, F, G>.init)
let set3: [Include7<A, B, C, D, E, F, G>] = G.samples.map(Include7<A, B, C, D, E, F, G>.init)
return set1 + set2 + set3
}
}
extension Include8: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable {
public static var sample: Include8<A, B, C, D, E, F, G, H> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include8<A, B, C, D, E, F, G, H>] {
let set1: [Include8<A, B, C, D, E, F, G, H>] = A.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
+ B.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
+ C.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
let set2: [Include8<A, B, C, D, E, F, G, H>] = D.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
+ E.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
+ F.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
let set3: [Include8<A, B, C, D, E, F, G, H>] = G.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
+ H.samples.map(Include8<A, B, C, D, E, F, G, H>.init)
return set1 + set2 + set3
}
}
extension Include9: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable, I: Sampleable {
public static var sample: Include9<A, B, C, D, E, F, G, H, I> {
let randomChoice = Int.random(in: 0..<samples.count)
return samples[randomChoice]
}
public static var samples: [Include9<A, B, C, D, E, F, G, H, I>] {
let set1: [Include9<A, B, C, D, E, F, G, H, I>] = A.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ B.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ C.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
let set2: [Include9<A, B, C, D, E, F, G, H, I>] = D.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ E.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ F.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
let set3: [Include9<A, B, C, D, E, F, G, H, I>] = G.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ H.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
+ I.samples.map(Include9<A, B, C, D, E, F, G, H, I>.init)
return set1 + set2 + set3
}
}
@@ -0,0 +1,68 @@
//
// JSONAPI+Sampleable.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 Unidentified: Sampleable {
public static var sample: Unidentified {
return Unidentified()
}
}
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
}
@@ -0,0 +1,102 @@
//
// Sampleable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/15/19.
//
import Foundation
/// A Sampleable type can provide a sample value.
/// This is useful for reflection.
public protocol Sampleable {
/// Get a sample value of type Self. This can be the
/// same value every time, or it can be an arbitrarily random
/// value each time.
static var sample: Self { get }
/// Get an example of success, if that is meaningful and
/// available. If not, will be nil.
///
/// The default implementation returns `nil`.
static var successSample: Self? { get }
/// Get an example of failure, if that is meaningful and
/// available. If not, will be nil.
///
/// The default implementation returns `nil`.
static var failureSample: Self? { get }
/// An array of samples of this Type.
///
/// The default implementation returns
/// an array with just the result of
/// `Self.sample` in it.
static var samples: [Self] { get }
}
public extension Sampleable {
// default implementation:
public static var successSample: Self? { return nil }
// default implementation:
public static var failureSample: Self? { return nil }
// default implementation:
public static var samples: [Self] { return [Self.sample] }
}
extension Sampleable {
public static func samples<S1: Sampleable>(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] {
return S1.samples.map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable>(using s1: S1.Type, _ s2: S2.Type, with constructor: (S1, S2) -> Self) -> [Self] {
return zip(S1.samples, S2.samples).map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, with constructor: (S1, S2, S3) -> Self) -> [Self] {
return zip3(S1.samples, S2.samples, S3.samples).map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, with constructor: (S1, S2, S3, S4) -> Self) -> [Self] {
return zip4(S1.samples, S2.samples, S3.samples, S4.samples).map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, with constructor: (S1, S2, S3, S4, S5) -> Self) -> [Self] {
return zip5(S1.samples, S2.samples, S3.samples, S4.samples, S5.samples).map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, with constructor: (S1, S2, S3, S4, S5, S6) -> Self) -> [Self] {
// the compiler craps out at zip6. breaking it down makes the difference.
let firstZip = zip3(S1.samples, S2.samples, S3.samples)
let secondZip = zip3(S4.samples, S5.samples, S6.samples)
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2) }.map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable, S7: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7) -> Self) -> [Self] {
// the compiler craps out at zip6. breaking it down makes the difference.
let firstZip = zip3(S1.samples, S2.samples, S3.samples)
let secondZip = zip4(S4.samples, S5.samples, S6.samples, S7.samples)
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor)
}
public static func samples<S1: Sampleable, S2: Sampleable, S3: Sampleable, S4: Sampleable, S5: Sampleable, S6: Sampleable, S7: Sampleable, S8: Sampleable>(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, _ s8: S8.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7, S8) -> Self) -> [Self] {
// the compiler craps out at zip6. breaking it down makes the difference.
let firstZip = zip4(S1.samples, S2.samples, S3.samples, S4.samples)
let secondZip = zip4(S5.samples, S6.samples, S7.samples, S8.samples)
return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.0.3, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor)
}
@inlinable static func zip3<A: Sequence, B: Sequence, C: Sequence>(_ a: A, _ b: B, _ c: C) -> [(A.Element, B.Element, C.Element)] {
return zip(a, zip(b, c)).map { arg in (arg.0, arg.1.0, arg.1.1) }
}
@inlinable static func zip4<A: Sequence, B: Sequence, C: Sequence, D: Sequence>(_ a: A, _ b: B, _ c: C, _ d: D) -> [(A.Element, B.Element, C.Element, D.Element)] {
return zip(a, zip(b, zip(c, d))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1) }
}
@inlinable static func zip5<A: Sequence, B: Sequence, C: Sequence, D: Sequence, E: Sequence>(_ a: A, _ b: B, _ c: C, _ d: D, _ e: E) -> [(A.Element, B.Element, C.Element, D.Element, E.Element)] {
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) }
}
}