diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index 6c07525..e15f3a2 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -65,6 +65,18 @@ extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseItera } } +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) + } +} + extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { static public func openAPINode() throws -> JSONNode { // If the RawValue is not required, we actually consider it @@ -199,7 +211,7 @@ extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourc 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 14a3a24..9f283f6 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -74,6 +74,12 @@ 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 { @@ -603,6 +609,7 @@ public enum OpenAPICodableError: Swift.Error, Equatable { case primitiveGuessFailed } -public enum OpenAPITypeError: Swift.Error, Equatable { +public enum OpenAPITypeError: Swift.Error { case invalidNode + case unknownNodeType(Any.Type) } diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift new file mode 100644 index 0000000..6dd53f2 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift @@ -0,0 +1,62 @@ +// +// Sampleable+JSONAPI.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 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 index ae7062b..0d3cfc7 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -5,9 +5,7 @@ // Created by Mathew Polzin on 1/15/19. // -import JSONAPI import Foundation -import AnyCodable /// A Sampleable type can provide a sample value. /// This is useful for reflection. @@ -37,8 +35,6 @@ public protocol Sampleable { static var samples: [Self] { get } } -public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType - public extension Sampleable { // default implementation: public static var successSample: Self? { return nil } @@ -50,141 +46,6 @@ public extension Sampleable { public static var samples: [Self] { return [Self.sample] } } -extension Sampleable where Self: Encodable { - public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { - let mirror = Mirror(reflecting: Self.sample) - let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in - - // 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) - - 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) } - } - - // 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) - - 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 -} - extension Sampleable { public static func samples(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] { return S1.samples.map(constructor) @@ -239,57 +100,3 @@ extension Sampleable { return zip(a, zip(b, zip(c, zip(d, e)))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1.0, arg.1.1.1.1) } } } - -extension NoAttributes: Sampleable { - 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 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/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift index ebb1085..0acab15 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -508,6 +508,45 @@ 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 @@ -521,14 +560,14 @@ extension JSONAPIAttributeOpenAPITests { let node = try! Attribute.genericOpenAPINode(using: encoder) XCTAssertTrue(node.required) - XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + XCTAssertEqual(node.jsonTypeFormat, .string(.date)) guard case .string(let contextA, let stringContext) = node else { XCTFail("Expected string Node") return } - XCTAssertEqual(contextA, .init(format: .generic, + XCTAssertEqual(contextA, .init(format: .date, required: true, nullable: false, allowedValues: nil)) @@ -536,17 +575,215 @@ extension JSONAPIAttributeOpenAPITests { XCTAssertEqual(stringContext, .init()) } - func test_DateNumberAttribute() { + func test_DateTimeStringAttribute() { + // TEST: + // Encoder is set to use + // formatter with date + // with time. let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none + 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) @@ -631,7 +868,7 @@ extension JSONAPIAttributeOpenAPITests { } } -extension Date: Sampleable { +extension Date: SampleableOpenAPIType { public static var sample: Date { return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate }