Compare commits

..

4 Commits

10 changed files with 398 additions and 48 deletions
@@ -1,5 +1,6 @@
import Foundation
import JSONAPI
import Poly
// MARK: - Preamble (setup)
@@ -15,14 +15,14 @@ print("====")
print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
print("====")
let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample())
let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample(using: encoder))
print("Dog Document Schema")
print("====")
print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
print("====")
let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample())
let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample(using: encoder))
print("Batch Person Document Schema")
print("====")
@@ -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,14 @@ 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)
}
}
@@ -71,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] = [
@@ -121,8 +129,8 @@ extension ToManyRelationship: OpenAPINodeType {
}
}
extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable {
public static func openAPINode() throws -> JSONNode {
extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType 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 +149,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 +172,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, OpenAPINodeType 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, OpenAPINodeType 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, OpenAPINodeType 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) }
@@ -17,8 +17,21 @@ public protocol OpenAPINodeType {
}
extension OpenAPINodeType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample() throws -> JSONNode {
return try openAPINode().with(example: Self.successSample ?? Self.sample)
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode {
return try openAPINode().with(example: Self.successSample ?? Self.sample, using: encoder)
}
}
/// Anything conforming to `OpenAPIEncodedNodeType` can provide an
/// OpenAPI schema representing itself but it may need an Encoder
/// to do its job.
public protocol OpenAPIEncodedNodeType: OpenAPINodeType {
static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode
}
extension OpenAPIEncodedNodeType {
public static func openAPINode() throws -> JSONNode {
return try openAPINode(using: JSONEncoder())
}
}
@@ -55,15 +68,21 @@ 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 {
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
}
@@ -76,7 +95,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)
@@ -91,7 +110,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 {
@@ -282,14 +301,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) }
}
@@ -330,13 +349,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))
}
}
@@ -550,27 +569,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:
return self
}
@@ -580,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 {
@@ -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)) ?? []
}
}
@@ -6,6 +6,7 @@
//
import JSONAPI
import Foundation
import AnyCodable
/// A Sampleable type can provide a sample value.
@@ -36,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 }
@@ -47,8 +50,8 @@ public extension Sampleable {
public static var samples: [Self] { return [Self.sample] }
}
extension Sampleable {
public static func genericObjectOpenAPINode() 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
@@ -56,9 +59,9 @@ extension Sampleable {
let maybeAllCases: [AnyCodable]? = {
switch type(of: child.value) {
case let valType as AnyJSONCaseIterable.Type:
return valType.allCases
return valType.allCases(using: encoder)
case let valType as AnyWrappedJSONCaseIterable.Type:
return valType.allCases
return valType.allCases(using: encoder)
default:
return nil
}
@@ -79,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
}
@@ -96,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
@@ -105,6 +118,126 @@ 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 {
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) }
}
}
extension NoAttributes: Sampleable {
@@ -131,8 +264,32 @@ extension NoLinks: Sampleable {
}
}
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)
}
}
@@ -8,6 +8,7 @@
import XCTest
import JSONAPI
import JSONAPIOpenAPI
import SwiftCheck
import AnyCodable
class JSONAPIAttributeOpenAPITests: XCTestCase {
@@ -504,6 +505,124 @@ extension JSONAPIAttributeOpenAPITests {
}
}
// MARK: - Date
extension JSONAPIAttributeOpenAPITests {
func test_DateStringAttribute() {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
guard case .string(let contextA, let stringContext) = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .generic,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(stringContext, .init())
}
func test_DateNumberAttribute() {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = try! Attribute<Date>.genericOpenAPINode(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .number(.double))
guard case .number(let contextA, let numberContext) = node else {
XCTFail("Expected string Node")
return
}
XCTAssertEqual(contextA, .init(format: .double,
required: true,
nullable: false,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
// func test_NullableEnumAttribute() {
// let node = try! Attribute<EnumAttribute?>.wrappedOpenAPINode()
//
// XCTAssertTrue(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: true,
// nullable: true,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
//
// func test_OptionalEnumAttribute() {
// let node = try! Attribute<EnumAttribute>?.wrappedOpenAPINode()
//
// XCTAssertFalse(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: false,
// nullable: false,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
//
// func test_OptionalNullableEnumAttribute() {
// let node = try! Attribute<EnumAttribute?>?.wrappedOpenAPINode()
//
// XCTAssertFalse(node.required)
// XCTAssertEqual(node.jsonTypeFormat, .string(.generic))
//
// guard case .string(let contextA, let stringContext) = node else {
// XCTFail("Expected string Node")
// return
// }
//
// XCTAssertEqual(contextA, .init(format: .generic,
// required: false,
// nullable: true,
// allowedValues: nil))
//
// XCTAssertEqual(stringContext, .init())
// }
}
// MARK: - Test Types
extension JSONAPIAttributeOpenAPITests {
enum EnumAttribute: String, Codable, CaseIterable {
@@ -511,3 +630,9 @@ extension JSONAPIAttributeOpenAPITests {
case two
}
}
extension Date: Sampleable {
public static var sample: Date {
return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate
}
}
@@ -6,15 +6,23 @@
//
import XCTest
import SwiftCheck
import JSONAPI
import JSONAPIOpenAPI
class JSONAPIDocumentOpenAPITests: XCTestCase {
func test_SingleResourceDocument() {
let node = try! SingleEntityDocument.openAPINode()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
dateFormatter.locale = Locale(identifier: "en_US")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let node = try! SingleEntityDocument.openAPINodeWithExample(using: encoder)
print(String(data: try! encoder.encode(node), encoding: .utf8)!)
}
@@ -27,9 +35,11 @@ extension JSONAPIDocumentOpenAPITests {
struct Attributes: JSONAPI.Attributes, Sampleable {
let name: Attribute<String>
let date: Attribute<Date>
static var sample: Attributes {
return .init(name: "hello world")
return .init(name: "hello world",
date: .init(value: Date()))
}
}
@@ -40,3 +50,29 @@ extension JSONAPIDocumentOpenAPITests {
typealias SingleEntityDocument = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
}
extension Id: Sampleable where RawType == String {
public static var sample: Id<RawType, IdentifiableType> {
return .init(rawValue: String.arbitrary.generate)
}
}
extension JSONAPI.Entity: Sampleable where Description.Attributes: Sampleable, Description.Relationships: Sampleable, MetaType: Sampleable, LinksType: Sampleable, EntityRawIdType == String {
public static var sample: JSONAPI.Entity<Description, MetaType, LinksType, EntityRawIdType> {
return JSONAPI.Entity(id: .sample,
attributes: .sample,
relationships: .sample,
meta: .sample,
links: .sample)
}
}
extension Document: Sampleable where PrimaryResourceBody: Sampleable, MetaType: Sampleable, LinksType: Sampleable, IncludeType: Sampleable, APIDescription: Sampleable, Error: Sampleable {
public static var sample: Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error> {
return Document(apiDescription: .sample,
body: .sample,
includes: .sample,
meta: .sample,
links: .sample)
}
}
@@ -12,7 +12,7 @@ import AnyCodable
class JSONAPIEntityOpenAPITests: XCTestCase {
func test_EmptyEntity() {
let node = try! TestType1.openAPINode()
let node = try! TestType1.openAPINode(using: JSONEncoder())
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -40,7 +40,7 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
}
func test_AttributesEntity() {
let node = try! TestType2.openAPINode()
let node = try! TestType2.openAPINode(using: JSONEncoder())
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
@@ -122,7 +122,7 @@ class JSONAPIEntityOpenAPITests: XCTestCase {
}
func test_RelationshipsEntity() {
let node = try! TestType3.openAPINode()
let node = try! TestType3.openAPINode(using: JSONEncoder())
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))