Compare commits

..

31 Commits

Author SHA1 Message Date
Mathew Polzin 11a7727ac9 Fix Include OpenAPI support. Fix bug with single include being considered 'OneOf' even though that is an overcomplication of the schema. Add tests for single include type and two include types on document 2019-01-27 22:47:01 -08:00
Mathew Polzin e8bfbc881b Added a couple of test cases for JSONDocuments 2019-01-27 22:24:13 -08:00
Mathew Polzin af757a2fac Add emphasis to README line. 2019-01-27 21:46:22 -08:00
Mathew Polzin 84955872f8 Finish writing Date Attribute test cases and add support for Optional Date OpenAPI Node guesses. 2019-01-27 21:39:18 -08:00
Mathew Polzin cb2800abd4 Add support for RequestBody on an OpenAPI Operation. 2019-01-27 13:57:04 -08:00
Mathew Polzin 85d5fef3c8 Fix bug where some references were correctly encoded as objects and others were just encoded as strings 2019-01-27 13:22:18 -08:00
Mathew Polzin e4ef61fd56 bugfix: OpenAPI path components should begin with a slash. 2019-01-27 12:46:13 -08:00
Mathew Polzin 75ec4f156e Fix a third dictionary-as-array bug 2019-01-26 20:04:11 -08:00
Mathew Polzin b45fc73188 Merge pull request #15 from mattpolzin/feature/OpenAPISchema
Feature/open api schema
2019-01-26 19:52:15 -08:00
Mathew Polzin 3b73dc9989 Fix super shitty bug caused by Apples implementation of Dictionary's conformance to Encodable sometimes encoding the dictionary as an array. 2019-01-26 19:50:20 -08:00
Mathew Polzin b2c81026f4 remove 'id' from Unidentified Entity OpenAPI node. 2019-01-26 18:55:47 -08:00
Mathew Polzin 8d057b4398 Merge pull request #14 from mattpolzin/feature/OpenAPISchema
Just enough OpenAPI Schema stuff to be dangerous
2019-01-25 18:25:19 -08:00
Mathew Polzin c8421cdd58 just throw that json extension on the filename to make everything super real 2019-01-25 18:20:04 -08:00
Mathew Polzin cde10a8491 Finish implementing remaining first wave of encodable conformances for OpenAPI 2019-01-25 18:17:58 -08:00
Mathew Polzin ad05d3908a Add a simple test OpenAPISchema and start to tweak things to get it workable. 2019-01-25 12:49:59 -08:00
Mathew Polzin 23b2b2e04f merge w/ master 2019-01-25 12:01:01 -08:00
Mathew Polzin 2988503d7d Add Sampleable conformance to Unidentified. Rename file slightly. 2019-01-25 11:59:05 -08:00
Mathew Polzin 5ea83b07c1 Hopefully remove some ambiguity. 2019-01-24 19:09:46 -08:00
Mathew Polzin 2b59f54067 Fix bug causing a supplied encoder to be used for generating an example but not for helping determine the correct Date formatting. 2019-01-24 17:47:44 -08:00
Mathew Polzin 58a7c82436 Restructure files a bit. Make Date handling relatively robust compared to my first pass at it. Make the failure to construct a generic open API node type throw an error rather than silently omit the node. 2019-01-24 17:25:34 -08:00
Mathew Polzin dc30cb3b9e Make Attribute Sampleable where its RawValue is Sampleable. Allow Sampleable things to provide a best guess for their node type based on the result of encoding and then deserializing them. 2019-01-24 00:47:24 -08:00
Mathew Polzin 7045373708 update and make sure all Playground pages run 2019-01-23 23:24:53 -08:00
Mathew Polzin 952fe8ba7e Remove encoder requirement to almost all Open API Node constructors. Made a new protocol for the few places where an encoder did need to be passed in. 2019-01-23 23:21:16 -08:00
Mathew Polzin 951c04ad44 Add Sampleable conformances. Make blanket JSONEncoder change I am not happy with; will try to walk back requirement that encoder gets passed to all functions creating OpenAPI Nodes 2019-01-23 22:21:27 -08:00
Mathew Polzin 57df6b147e Add Sampleable conformance to UnknownJSONAPIError. Add Sampleable conformance to all Include types and the Includes type. 2019-01-23 11:52:16 -08:00
Mathew Polzin a88844fe5e Switch to Poly 1.0.0 (same commit as previously used but by version rather than branch) 2019-01-23 00:34:14 -08:00
Mathew Polzin d1cf19f9fe Neck deep in stubbing out OpenAPI types 2019-01-22 22:14:36 -08:00
Mathew Polzin 59835fbe11 Add JSON Reference and OpenAPIComponents (initially just with schemas) 2019-01-22 16:08:47 -08:00
Mathew Polzin 850a713dcb Merge pull request #13 from mattpolzin/feature/OpenAPISchema
Add Include support to OpenAPI schema of JSONAPI Document.
2019-01-22 12:01:40 -08:00
Mathew Polzin e3c637a41e update README and add associated type labels to patterns for clarity 2019-01-22 11:57:18 -08:00
Mathew Polzin 9cbc626410 Add Include support to OpenAPI schema of JSONAPI Document. 2019-01-22 11:53:32 -08:00
22 changed files with 2495 additions and 370 deletions
@@ -1,5 +1,6 @@
import Foundation
import JSONAPI
import Poly
// MARK: - Preamble (setup)
@@ -3,6 +3,7 @@
import Foundation
import JSONAPI
import JSONAPIOpenAPI
import Poly
// print Entity Schema
let encoder = JSONEncoder()
@@ -15,9 +16,31 @@ 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(using: encoder))
print("Batch Person Document Schema")
print("====")
print(batchPersonSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
print("====")
let tmp: [String: OpenAPIComponents.SchemasDict.RefType] = [
"BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample()
]
let components = OpenAPIComponents(schemas: tmp)
let batchPeopleRef = JSONReference(type: \OpenAPIComponents.schemas, selector: "BatchPerson")
let tmp2 = JSONNode.reference(batchPeopleRef)
print("====")
print("====")
//print(String(data: try! encoder.encode(components), encoding: .utf8)!)
print(String(data: try! encoder.encode(tmp2), encoding: .utf8)!)
@@ -5,13 +5,28 @@ import JSONAPIOpenAPI
import SwiftCheck
import JSONAPIArbitrary
extension PersonDescription.Attributes: Sampleable {
extension PersonDescription.Attributes: Arbitrary, Sampleable {
public static var arbitrary: Gen<PersonDescription.Attributes> {
return Gen.compose { c in
return PersonDescription.Attributes(name: c.generate(),
favoriteColor: c.generate())
}
}
public static var sample: PersonDescription.Attributes {
return .init(name: ["Abbie", "Eibba"], favoriteColor: "Blue")
}
}
extension PersonDescription.Relationships: Sampleable {
extension PersonDescription.Relationships: Arbitrary, Sampleable {
public static var arbitrary: Gen<PersonDescription.Relationships> {
return Gen.compose { c in
return PersonDescription.Relationships(friends: c.generate(),
dogs: c.generate(),
home: c.generate())
}
}
public static var sample: PersonDescription.Relationships {
return .init(friends: ["1", "2"], dogs: ["2"], home: "1")
}
+2 -2
View File
@@ -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"
}
},
{
+1 -1
View File
@@ -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")
],
+2 -2
View File
@@ -104,7 +104,7 @@ Note that Playground support for importing non-system Frameworks is still a bit
- `included`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
- [x] OpenAPI
- `errors`
- [x] Encoding/Decoding
- [x] Arbitrary
@@ -843,4 +843,4 @@ This Framework is currently undocumented, but familiarity with `SwiftCheck` will
# JSONAPI+OpenAPI
The `JSONAPIOpenAPI` framework adds the ability to generate OpenAPI compliant JSON documentation of a JSONAPI Document.
This library is in its infancy. The documentation will grow as the framework becomes more complete.
*This library is in its infancy. The documentation will grow as the framework becomes more complete.*
@@ -0,0 +1,132 @@
//
// JSONAPIInclude+OpenAPI.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/22/19.
//
import JSONAPI
import Foundation
extension Includes: OpenAPIEncodedNodeType where I: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
let includeNode = try I.openAPINode(using: encoder)
return .array(.init(format: .generic,
required: true),
.init(items: includeNode,
uniqueItems: true))
}
}
extension Include0: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
throw OpenAPITypeError.invalidNode
}
}
extension Include1: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try A.openAPINode(using: encoder)
}
}
extension Include2: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder)
])
}
}
extension Include3: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder)
])
}
}
extension Include4: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder)
])
}
}
extension Include5: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder),
E.openAPINode(using: encoder)
])
}
}
extension Include6: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder),
E.openAPINode(using: encoder),
F.openAPINode(using: encoder)
])
}
}
extension Include7: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder),
E.openAPINode(using: encoder),
F.openAPINode(using: encoder),
G.openAPINode(using: encoder)
])
}
}
extension Include8: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder),
E.openAPINode(using: encoder),
F.openAPINode(using: encoder),
G.openAPINode(using: encoder),
H.openAPINode(using: encoder)
])
}
}
extension Include9: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType, I: OpenAPIEncodedNodeType {
public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode {
return try .one(of: [
A.openAPINode(using: encoder),
B.openAPINode(using: encoder),
C.openAPINode(using: encoder),
D.openAPINode(using: encoder),
E.openAPINode(using: encoder),
F.openAPINode(using: encoder),
G.openAPINode(using: encoder),
H.openAPINode(using: encoder),
I.openAPINode(using: encoder)
])
}
}
@@ -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
@@ -47,15 +53,40 @@ extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType {
}
}
extension Attribute: GenericOpenAPINodeType where RawValue: GenericOpenAPINodeType {
public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if try !RawValue.genericOpenAPINode(using: encoder).required {
return try RawValue.genericOpenAPINode(using: encoder).requiredNode().nullableNode()
}
return try RawValue.genericOpenAPINode(using: encoder)
}
}
extension Attribute: DateOpenAPINodeType where RawValue: DateOpenAPINodeType {
public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
// If the RawValue is not required, we actually consider it
// nullable. To be not required is for the Attribute itself
// to be optional.
if
!(RawValue.dateOpenAPINodeGuess(using: encoder)?.required ?? true) {
return RawValue.dateOpenAPINodeGuess(using: encoder)?.requiredNode().nullableNode()
}
return RawValue.dateOpenAPINodeGuess(using: encoder)
}
}
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 +102,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,17 +154,19 @@ 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.
// TODO: metadata, links
let idNode = JSONNode.string(.init(format: .generic,
required: true),
.init())
let idProperty = ("id", idNode)
let idNode: JSONNode? = Id.RawType.self != Unidentified.self
? JSONNode.string(.init(format: .generic,
required: true),
.init())
: nil
let idProperty = idNode.map { ("id", $0) }
let typeNode = JSONNode.string(.init(format: .generic,
required: true,
@@ -141,13 +176,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,31 +199,44 @@ 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 {
public static func openAPINode() throws -> JSONNode {
// TODO: metadata, links, api description, includes, errors
extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPIEncodedNodeType {
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) }
let includeNode: JSONNode?
do {
includeNode = try Includes<Include>.openAPINode(using: encoder)
} catch let err as OpenAPITypeError {
guard case .invalidNode = err else {
throw err
}
includeNode = nil
}
let includeProperty = includeNode.map { ("included", $0) }
let propertiesDict = Dictionary([
primaryDataProperty
primaryDataProperty,
includeProperty
].compactMap { $0 }) { _, value in value }
return .object(.init(format: .generic,
@@ -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())
}
}
}
@@ -0,0 +1,454 @@
//
// OpenAPITypes+Codable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/14/19.
//
extension JSONNode.Context: Encodable {
private enum CodingKeys: String, CodingKey {
case type
case format
case allowedValues = "enum"
case nullable
case example
// case constantValue = "const"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(format.jsonType, forKey: .type)
if format != Format.unspecified {
try container.encode(format, forKey: .format)
}
if allowedValues != nil {
try container.encode(allowedValues, forKey: .allowedValues)
}
// if constantValue != nil {
// try container.encode(constantValue, forKey: .constantValue)
// }
try container.encode(nullable, forKey: .nullable)
if example != nil {
try container.encode(example, forKey: .example)
}
}
}
extension JSONNode.NumericContext: Encodable {
private enum CodingKeys: String, CodingKey {
case multipleOf
case maximum
case exclusiveMaximum
case minimum
case exclusiveMinimum
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if multipleOf != nil {
try container.encode(multipleOf, forKey: .multipleOf)
}
if maximum != nil {
try container.encode(maximum, forKey: .maximum)
}
if exclusiveMaximum != nil {
try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum)
}
if minimum != nil {
try container.encode(minimum, forKey: .minimum)
}
if exclusiveMinimum != nil {
try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum)
}
}
}
extension JSONNode.StringContext: Encodable {
private enum CodingKeys: String, CodingKey {
case maxLength
case minLength
case pattern
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if maxLength != nil {
try container.encode(maxLength, forKey: .maxLength)
}
try container.encode(minLength, forKey: .minLength)
if pattern != nil {
try container.encode(pattern, forKey: .pattern)
}
}
}
extension JSONNode.ArrayContext: Encodable {
private enum CodingKeys: String, CodingKey {
case items
case maxItems
case minItems
case uniqueItems
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
if maxItems != nil {
try container.encode(maxItems, forKey: .maxItems)
}
try container.encode(minItems, forKey: .minItems)
try container.encode(uniqueItems, forKey: .uniqueItems)
}
}
extension JSONNode.ObjectContext : Encodable {
private enum CodingKeys: String, CodingKey {
case maxProperties
case minProperties
case properties
case additionalProperties
case required
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if maxProperties != nil {
try container.encode(maxProperties, forKey: .maxProperties)
}
try container.encode(properties, forKey: .properties)
if additionalProperties != nil {
try container.encode(additionalProperties, forKey: .additionalProperties)
}
try container.encode(requiredProperties, forKey: .required)
try container.encode(minProperties, forKey: .minProperties)
}
}
extension JSONNode: Encodable {
private enum SubschemaCodingKeys: String, CodingKey {
case allOf
case oneOf
case anyOf
case not
}
public func encode(to encoder: Encoder) throws {
switch self {
case .boolean(let context):
try context.encode(to: encoder)
case .object(let contextA as Encodable, let contextB as Encodable),
.array(let contextA as Encodable, let contextB as Encodable),
.number(let contextA as Encodable, let contextB as Encodable),
.integer(let contextA as Encodable, let contextB as Encodable),
.string(let contextA as Encodable, let contextB as Encodable):
try contextA.encode(to: encoder)
try contextB.encode(to: encoder)
case .all(of: let nodes):
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
try container.encode(nodes, forKey: .allOf)
case .one(of: let nodes):
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
try container.encode(nodes, forKey: .oneOf)
case .any(of: let nodes):
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
try container.encode(nodes, forKey: .anyOf)
case .not(let node):
var container = encoder.container(keyedBy: SubschemaCodingKeys.self)
try container.encode(node, forKey: .not)
case .reference(let reference):
var container = encoder.singleValueContainer()
try container.encode(reference)
}
}
}
extension JSONReference: Encodable {
private enum CodingKeys: String, CodingKey {
case ref = "$ref"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let referenceString: String = {
switch self {
case .file(let reference):
return reference
case .node(let reference):
return "#/\(Root.refName)/\(reference.refName)/\(reference.selector)"
}
}()
try container.encode(referenceString, forKey: .ref)
}
}
extension RefDict: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(dict)
}
}
extension OpenAPIResponse.Code: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let string: String
switch self {
case .`default`:
string = "default"
case .status(code: let code):
string = String(code)
}
try container.encode(string)
}
}
extension OpenAPIRequestBody: Encodable {
private enum CodingKeys: String, CodingKey {
case description
case content
case required
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if description != nil {
try container.encode(description, forKey: .description)
}
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
content.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .content)
try container.encode(required, forKey: .required)
}
}
extension OpenAPIPathItem.PathProperties.Operation: Encodable {
private enum CodingKeys: String, CodingKey {
case tags
case summary
case description
case externalDocs
case operationId
case parameters
case requestBody
case responses
case callbacks
case deprecated
case security
case servers
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if tags != nil {
try container.encode(tags, forKey: .tags)
}
if summary != nil {
try container.encode(summary, forKey: .summary)
}
if description != nil {
try container.encode(description, forKey: .description)
}
try container.encode(operationId, forKey: .operationId)
try container.encode(parameters, forKey: .parameters)
if requestBody != nil {
try container.encode(requestBody, forKey: .requestBody)
}
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
responses.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .responses)
try container.encode(deprecated, forKey: .deprecated)
}
}
extension OpenAPIPathItem.PathProperties: Encodable {
private enum CodingKeys: String, CodingKey {
case summary
case description
case servers
case parameters
case get
case put
case post
case delete
case options
case head
case patch
case trace
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if summary != nil {
try container.encode(summary, forKey: .summary)
}
if description != nil {
try container.encode(description, forKey: .description)
}
try container.encode(parameters, forKey: .parameters)
if get != nil {
try container.encode(get, forKey: .get)
}
if put != nil {
try container.encode(put, forKey: .put)
}
if post != nil {
try container.encode(post, forKey: .post)
}
if delete != nil {
try container.encode(delete, forKey: .delete)
}
if options != nil {
try container.encode(options, forKey: .options)
}
if head != nil {
try container.encode(head, forKey: .head)
}
if patch != nil {
try container.encode(patch, forKey: .patch)
}
if trace != nil {
try container.encode(trace, forKey: .trace)
}
}
}
extension OpenAPIResponse: Encodable {
private enum CodingKeys: String, CodingKey {
case description
case headers
case content
case links
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(description, forKey: .description)
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
content.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .content)
}
}
extension OpenAPIPathItem: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .reference(let reference):
try container.encode(reference)
case .operations(let operations):
try container.encode(operations)
}
}
}
extension OpenAPISchema: Encodable {
private enum CodingKeys: String, CodingKey {
case openAPIVersion = "openapi"
case info
case servers
case paths
case components
case security
case tags
case externalDocs
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(openAPIVersion, forKey: .openAPIVersion)
try container.encode(info, forKey: .info)
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
paths.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .paths)
try container.encode(components, forKey: .components)
}
}
@@ -6,6 +6,7 @@
//
import AnyCodable
import Foundation
/**
@@ -56,8 +57,14 @@ 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)) ?? []
}
}
extension Optional: DateOpenAPINodeType where Wrapped: DateOpenAPINodeType {
static public func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
return Wrapped.dateOpenAPINodeGuess(using: encoder)?.optionalNode()
}
}
@@ -1,182 +0,0 @@
//
// OpenAPITypes+Codable.swift
// JSONAPIOpenAPI
//
// Created by Mathew Polzin on 1/14/19.
//
extension JSONNode.Context: Encodable {
private enum CodingKeys: String, CodingKey {
case type
case format
case allowedValues = "enum"
case nullable
case example
// case constantValue = "const"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(format.jsonType, forKey: .type)
if format != Format.unspecified {
try container.encode(format, forKey: .format)
}
if allowedValues != nil {
try container.encode(allowedValues, forKey: .allowedValues)
}
// if constantValue != nil {
// try container.encode(constantValue, forKey: .constantValue)
// }
try container.encode(nullable, forKey: .nullable)
if example != nil {
try container.encode(example, forKey: .example)
}
}
}
extension JSONNode.NumericContext: Encodable {
private enum CodingKeys: String, CodingKey {
case multipleOf
case maximum
case exclusiveMaximum
case minimum
case exclusiveMinimum
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if multipleOf != nil {
try container.encode(multipleOf, forKey: .multipleOf)
}
if maximum != nil {
try container.encode(maximum, forKey: .maximum)
}
if exclusiveMaximum != nil {
try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum)
}
if minimum != nil {
try container.encode(minimum, forKey: .minimum)
}
if exclusiveMinimum != nil {
try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum)
}
}
}
extension JSONNode.StringContext: Encodable {
private enum CodingKeys: String, CodingKey {
case maxLength
case minLength
case pattern
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if maxLength != nil {
try container.encode(maxLength, forKey: .maxLength)
}
try container.encode(minLength, forKey: .minLength)
if pattern != nil {
try container.encode(pattern, forKey: .pattern)
}
}
}
extension JSONNode.ArrayContext: Encodable {
private enum CodingKeys: String, CodingKey {
case items
case maxItems
case minItems
case uniqueItems
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
if maxItems != nil {
try container.encode(maxItems, forKey: .maxItems)
}
try container.encode(minItems, forKey: .minItems)
try container.encode(uniqueItems, forKey: .uniqueItems)
}
}
extension JSONNode.ObjectContext : Encodable {
private enum CodingKeys: String, CodingKey {
case maxProperties
case minProperties
case properties
case additionalProperties
case required
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if maxProperties != nil {
try container.encode(maxProperties, forKey: .maxProperties)
}
try container.encode(properties, forKey: .properties)
if additionalProperties != nil {
try container.encode(additionalProperties, forKey: .additionalProperties)
}
try container.encode(requiredProperties, forKey: .required)
try container.encode(minProperties, forKey: .minProperties)
}
}
extension JSONNode: Encodable {
public func encode(to encoder: Encoder) throws {
switch self {
case .boolean(let context):
try context.encode(to: encoder)
case .object(let contextA as Encodable, let contextB as Encodable),
.array(let contextA as Encodable, let contextB as Encodable),
.number(let contextA as Encodable, let contextB as Encodable),
.integer(let contextA as Encodable, let contextB as Encodable),
.string(let contextA as Encodable, let contextB as Encodable):
try contextA.encode(to: encoder)
try contextB.encode(to: encoder)
case .allOf(let nodes):
// TODO
print("TODO")
case .oneOf(let nodes):
// TODO
print("TODO")
case .anyOf(let nodes):
// TODO
print("TODO")
case .not(let node):
// TODO
print("TODO")
}
}
}
-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) }
}
}
@@ -8,6 +8,7 @@
import XCTest
import JSONAPI
import JSONAPIOpenAPI
import SwiftCheck
import AnyCodable
class JSONAPIAttributeOpenAPITests: XCTestCase {
@@ -504,6 +505,379 @@ 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<Date>.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<Date>.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<Date>.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<Date>.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<Date>.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<Date>.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<Date>.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<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_DateDeferredAttribute() {
// TEST:
// Encoder is set to use
// Date default encoding
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .deferredToDate
let node = Attribute<Date>.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<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_NullableDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date?>.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: true,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_OptionalDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date>?.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertFalse(node?.required ?? true)
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: false,
nullable: false,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
func test_OptionalNullableDateAttribute() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .secondsSince1970
let node = Attribute<Date?>?.dateOpenAPINodeGuess(using: encoder)
XCTAssertNotNil(node)
XCTAssertFalse(node?.required ?? true)
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: false,
nullable: true,
allowedValues: nil))
XCTAssertEqual(numberContext, .init())
}
}
// MARK: - Test Types
extension JSONAPIAttributeOpenAPITests {
enum EnumAttribute: String, Codable, CaseIterable {
@@ -511,3 +885,9 @@ extension JSONAPIAttributeOpenAPITests {
case two
}
}
extension Date: SampleableOpenAPIType {
public static var sample: Date {
return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate
}
}
@@ -6,17 +6,307 @@
//
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)
print(String(data: try! encoder.encode(node), encoding: .utf8)!)
let node = try! SingleEntityDocument.openAPINodeWithExample(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected JSON Document to be an Object Node")
return
}
XCTAssertNotNil(contextA.example)
XCTAssertFalse(contextA.nullable)
XCTAssertEqual(contextA.format, .generic)
XCTAssertTrue(contextA.required)
XCTAssertEqual(objectContext1.minProperties, 1)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data"]))
guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else {
XCTFail("Expected Data field of JSON Document to be an Object Node")
return
}
XCTAssertFalse(contextB.nullable)
XCTAssertEqual(contextB.format, .generic)
XCTAssertTrue(contextB.required)
XCTAssertEqual(objectContext2.minProperties, 3)
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext2.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
}
func test_ManyResourceDocument() {
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! ManyEntityDocument.openAPINodeWithExample(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected JSON Document to be an Object Node")
return
}
XCTAssertNotNil(contextA.example)
XCTAssertFalse(contextA.nullable)
XCTAssertEqual(contextA.format, .generic)
XCTAssertTrue(contextA.required)
XCTAssertEqual(objectContext1.minProperties, 1)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data"]))
guard case let .array(contextB, arrayContext)? = objectContext1.properties["data"] else {
XCTFail("Expected Data field of JSON Document to be an Array Node")
return
}
XCTAssertFalse(contextB.nullable)
XCTAssertEqual(contextB.format, .generic)
XCTAssertTrue(contextB.required)
XCTAssertFalse(arrayContext.uniqueItems)
XCTAssertEqual(arrayContext.minItems, 0)
guard case let .object(contextC, objectContext2) = arrayContext.items else {
XCTFail("Expected Items of Array under Data to be an Object Node")
return
}
XCTAssertFalse(contextC.nullable)
XCTAssertEqual(contextC.format, .generic)
XCTAssertTrue(contextC.required)
XCTAssertEqual(objectContext2.minProperties, 3)
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext2.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
}
func test_DocumentWithOneIncludeType() {
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! DocumentWithIncludes.openAPINodeWithExample(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected JSON Document to be an Object Node")
return
}
XCTAssertNotNil(contextA.example)
XCTAssertFalse(contextA.nullable)
XCTAssertEqual(contextA.format, .generic)
XCTAssertTrue(contextA.required)
XCTAssertEqual(objectContext1.minProperties, 2)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"]))
guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else {
XCTFail("Expected Data field of JSON Document to be an Object Node")
return
}
XCTAssertFalse(contextB.nullable)
XCTAssertEqual(contextB.format, .generic)
XCTAssertTrue(contextB.required)
XCTAssertEqual(objectContext2.minProperties, 3)
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext2.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else {
XCTFail("Expected Includes field of JSON Document to be an Array Node")
return
}
XCTAssertFalse(contextC.nullable)
XCTAssertEqual(contextC.format, .generic)
XCTAssertTrue(contextC.required)
XCTAssertTrue(arrayContext.uniqueItems)
XCTAssertEqual(arrayContext.minItems, 0)
guard case let .object(contextD, objectContext3) = arrayContext.items else {
XCTFail("Expected Items of Array under Data to be an Object Node")
return
}
XCTAssertFalse(contextD.nullable)
XCTAssertEqual(contextD.format, .generic)
XCTAssertTrue(contextD.required)
XCTAssertEqual(objectContext3.minProperties, 3)
XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext3.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
}
func test_DocumentWithTwoIncludeTypes() {
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! DocumentWithMultipleTypesOfIncludes.openAPINodeWithExample(using: encoder)
XCTAssertTrue(node.required)
XCTAssertEqual(node.jsonTypeFormat, .object(.generic))
guard case let .object(contextA, objectContext1) = node else {
XCTFail("Expected JSON Document to be an Object Node")
return
}
XCTAssertNotNil(contextA.example)
XCTAssertFalse(contextA.nullable)
XCTAssertEqual(contextA.format, .generic)
XCTAssertTrue(contextA.required)
XCTAssertEqual(objectContext1.minProperties, 2)
XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"]))
XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"]))
guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else {
XCTFail("Expected Data field of JSON Document to be an Object Node")
return
}
XCTAssertFalse(contextB.nullable)
XCTAssertEqual(contextB.format, .generic)
XCTAssertTrue(contextB.required)
XCTAssertEqual(objectContext2.minProperties, 3)
XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext2.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else {
XCTFail("Expected Includes field of JSON Document to be an Array Node")
return
}
XCTAssertFalse(contextC.nullable)
XCTAssertEqual(contextC.format, .generic)
XCTAssertTrue(contextC.required)
XCTAssertTrue(arrayContext.uniqueItems)
XCTAssertEqual(arrayContext.minItems, 0)
guard case let .one(of: includeNodes) = arrayContext.items else {
XCTFail("Expected Included to contain multiple types of items.")
return
}
XCTAssertEqual(includeNodes.count, 2)
guard case let .object(contextD, objectContext3) = includeNodes[0] else {
XCTFail("Expected Items of OneOf under Array under Data to be an Object Node")
return
}
XCTAssertFalse(contextD.nullable)
XCTAssertEqual(contextD.format, .generic)
XCTAssertTrue(contextD.required)
XCTAssertEqual(objectContext3.minProperties, 3)
XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"]))
XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"]))
XCTAssertEqual(objectContext3.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test")]),
.init()))
guard case let .object(contextE, objectContext4) = includeNodes[1] else {
XCTFail("Expected Items of OneOf under Array under Data to be an Object Node")
return
}
XCTAssertFalse(contextE.nullable)
XCTAssertEqual(contextE.format, .generic)
XCTAssertTrue(contextE.required)
XCTAssertEqual(objectContext4.minProperties, 2)
XCTAssertEqual(Set(objectContext4.requiredProperties), Set(["id", "type"]))
XCTAssertEqual(Set(objectContext4.properties.keys), Set(["id", "type"]))
XCTAssertEqual(objectContext4.properties["type"],
JSONNode.string(.init(format: .generic,
required: true,
allowedValues: [.init("test2")]),
.init()))
}
}
@@ -27,9 +317,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()))
}
}
@@ -39,4 +331,46 @@ extension JSONAPIDocumentOpenAPITests {
typealias TestEntity = BasicEntity<TestEntityDescription>
typealias SingleEntityDocument = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
typealias ManyEntityDocument = Document<ManyResourceBody<TestEntity>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
typealias DocumentWithIncludes = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, Include1<TestEntity>, NoAPIDescription, UnknownJSONAPIError>
enum TestEntityDescription2: EntityDescription {
static var jsonType: String { return "test2" }
typealias Attributes = NoAttributes
typealias Relationships = NoRelationships
}
typealias TestEntity2 = BasicEntity<TestEntityDescription2>
typealias DocumentWithMultipleTypesOfIncludes = Document<SingleResourceBody<TestEntity>, NoMetadata, NoLinks, Include2<TestEntity, TestEntity2>, 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)
}
}

Some files were not shown because too many files have changed in this diff Show More