diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index abf3668..27ef739 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -1,5 +1,6 @@ import Foundation import JSONAPI +import Poly // MARK: - Preamble (setup) diff --git a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift index b9a4f1e..412a409 100644 --- a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift @@ -16,14 +16,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("====") diff --git a/Package.resolved b/Package.resolved index 69dfbfa..d2b976b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,9 +23,9 @@ "package": "Poly", "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { - "branch": "master", + "branch": null, "revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07", - "version": null + "version": "1.0.0" } }, { diff --git a/Package.swift b/Package.swift index 3622d82..9359a14 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( targets: ["JSONAPIOpenAPI"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), + .package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"), .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0"), .package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.11.0") ], diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift index 9b9e456..6f7f966 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift @@ -6,6 +6,7 @@ // import JSONAPI +import Foundation extension Includes: OpenAPINodeType where I: OpenAPINodeType { public static func openAPINode() throws -> JSONNode { diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index cc5efde..4ac1da3 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -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.openAPINode() } catch let err as OpenAPITypeError { - guard err == .invalidNode else { + guard case .invalidNode = err else { throw err } includeNode = nil diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift new file mode 100644 index 0000000..c153fe4 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift @@ -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()) + } + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index 6778e3b..c966a30 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -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(from input: [T]) throws -> [AnyCodable] { + static func allCases(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(example codableExample: T) throws -> JSONNode { + public func with(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 diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift index ae03f6a..19ee4da 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift @@ -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)) ?? [] } } diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift deleted file mode 100644 index c730791..0000000 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift new file mode 100644 index 0000000..895e561 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift @@ -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 { + 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 { + return .init(A.sample) + } + + public static var samples: [Include1] { + return A.samples.map(Include1.init) + } +} + +extension Include2: Sampleable where A: Sampleable, B: Sampleable { + public static var sample: Include2 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include2.init) + + B.samples.map(Include2.init) + } +} + +extension Include3: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable { + public static var sample: Include3 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include3.init) + + B.samples.map(Include3.init) + + C.samples.map(Include3.init) + } +} + +extension Include4: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable { + public static var sample: Include4 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include4.init) + + B.samples.map(Include4.init) + + C.samples.map(Include4.init) + + D.samples.map(Include4.init) + } +} + +extension Include5: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable { + public static var sample: Include5 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include5] = A.samples.map(Include5.init) + + B.samples.map(Include5.init) + + C.samples.map(Include5.init) + + let set2: [Include5] = D.samples.map(Include5.init) + + E.samples.map(Include5.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 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include6] = A.samples.map(Include6.init) + + B.samples.map(Include6.init) + + C.samples.map(Include6.init) + + let set2: [Include6] = D.samples.map(Include6.init) + + E.samples.map(Include6.init) + + F.samples.map(Include6.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 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include7] = A.samples.map(Include7.init) + + B.samples.map(Include7.init) + + C.samples.map(Include7.init) + + let set2: [Include7] = D.samples.map(Include7.init) + + E.samples.map(Include7.init) + + F.samples.map(Include7.init) + + let set3: [Include7] = G.samples.map(Include7.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 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include8] = A.samples.map(Include8.init) + + B.samples.map(Include8.init) + + C.samples.map(Include8.init) + + let set2: [Include8] = D.samples.map(Include8.init) + + E.samples.map(Include8.init) + + F.samples.map(Include8.init) + + let set3: [Include8] = G.samples.map(Include8.init) + + H.samples.map(Include8.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 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include9] = A.samples.map(Include9.init) + + B.samples.map(Include9.init) + + C.samples.map(Include9.init) + + let set2: [Include9] = D.samples.map(Include9.init) + + E.samples.map(Include9.init) + + F.samples.map(Include9.init) + + let set3: [Include9] = G.samples.map(Include9.init) + + H.samples.map(Include9.init) + + I.samples.map(Include9.init) + + return set1 + set2 + set3 + } +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift new file mode 100644 index 0000000..bcb832b --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift @@ -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 { + return .init(value: RawValue.sample) + } +} + +extension SingleResourceBody: Sampleable where Entity: Sampleable { + public static var sample: SingleResourceBody { + return .init(entity: Entity.sample) + } +} + +extension ManyResourceBody: Sampleable where Entity: Sampleable { + public static var sample: ManyResourceBody { + return .init(entities: Entity.samples) + } +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift new file mode 100644 index 0000000..070a6db --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift @@ -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: Encodable { + let primitive: Wrapped +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift new file mode 100644 index 0000000..0d3cfc7 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -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(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] { + return S1.samples.map(constructor) + } + + public static func samples(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(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(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(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(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(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(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: 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: 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: 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) } + } +} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift index 642b95e..0acab15 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -8,6 +8,7 @@ import XCTest import JSONAPI import JSONAPIOpenAPI +import SwiftCheck import AnyCodable class JSONAPIAttributeOpenAPITests: XCTestCase { @@ -504,6 +505,361 @@ extension JSONAPIAttributeOpenAPITests { } } +// MARK: - Date +extension JSONAPIAttributeOpenAPITests { + func test_DateStringAttribute() { + // TEST: + // Encoder is set to use + // formatter with date + // with no time. + + 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 = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.date)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .date, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_DateStringAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // formatter with date + // with no time. + + 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.genericOpenAPINode(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.date)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .date, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_DateTimeStringAttribute() { + // TEST: + // Encoder is set to use + // formatter with date + // with time. + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_DateTimeStringAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // formatter with date + // with time. + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_8601DateStringAttribute() { + if #available(OSX 10.12, *) { + // TEST: + // Encoder is set to use + // iso8601 date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .iso8601 + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + } + + func test_8601DateStringAttribute_Sampleable() { + if #available(OSX 10.12, *) { + // TEST: + // Encoder is set to use + // iso8601 date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .iso8601 + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + } + + func test_DateNumberAttribute() { + // TEST: + // Encoder is set to use + // seconds since 1970 as + // date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .secondsSince1970 + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + 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_DateNumberAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // seconds since 1970 as + // date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .secondsSince1970 + + let node = try! Attribute.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_DateDeferredAttribute() { + // TEST: + // Encoder is set to use + // Date default encoding + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .deferredToDate + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNil(node) + } + + func test_DateDeferredAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // Date default encoding + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .deferredToDate + + let node = try! Attribute.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.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?.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?.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 +867,9 @@ extension JSONAPIAttributeOpenAPITests { case two } } + +extension Date: SampleableOpenAPIType { + public static var sample: Date { + return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate + } +} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift index f709f17..e82ca34 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift @@ -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 + let date: Attribute 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, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> } + +extension Id: Sampleable where RawType == String { + public static var sample: Id { + 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 { + 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 { + return Document(apiDescription: .sample, + body: .sample, + includes: .sample, + meta: .sample, + links: .sample) + } +} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index abaf9d1..a9e6b76 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -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,16 @@ class JSONAPIEntityOpenAPITests: XCTestCase { } func test_AttributesEntity() { - let node = try! TestType2.openAPINode() + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! TestType2.openAPINode(using: encoder) XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -83,9 +92,9 @@ class JSONAPIEntityOpenAPITests: XCTestCase { nullable: false, allowedValues: nil)) - XCTAssertEqual(attributesContext.minProperties, 3) - XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "nullableProperty"])) - XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"])) + XCTAssertEqual(attributesContext.minProperties, 4) + XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "dateProperty", "nullableProperty"])) + XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "dateProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"])) XCTAssertEqual(attributesContext.properties["stringProperty"], .string(.init(format: .generic, @@ -99,6 +108,13 @@ class JSONAPIEntityOpenAPITests: XCTestCase { allowedValues: ["one", "two"].map(AnyCodable.init)), .init())) + XCTAssertEqual(attributesContext.properties["dateProperty"], + .string(.init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil), + .init())) + XCTAssertEqual(attributesContext.properties["optionalProperty"], .string(.init(format: .generic, required: false, @@ -122,7 +138,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)) @@ -268,6 +284,7 @@ extension JSONAPIEntityOpenAPITests { public struct Attributes: JSONAPI.Attributes, Sampleable { let stringProperty: Attribute let enumProperty: Attribute + let dateProperty: Attribute let optionalProperty: Attribute? let nullableProperty: Attribute let nullableOptionalProperty: Attribute? @@ -278,6 +295,7 @@ extension JSONAPIEntityOpenAPITests { public static var sample: Attributes { return Attributes(stringProperty: .init(value: "hello"), enumProperty: .init(value: .one), + dateProperty: .init(value: Date()), optionalProperty: nil, nullableProperty: .init(value: nil), nullableOptionalProperty: nil)