mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
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.
This commit is contained in:
@@ -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<Include>.openAPINode()
|
||||
} catch let err as OpenAPITypeError {
|
||||
guard err == .invalidNode else {
|
||||
guard case .invalidNode = err else {
|
||||
throw err
|
||||
}
|
||||
includeNode = nil
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Date+OpenAPI.swift
|
||||
// JSONAPIOpenAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 1/24/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Date: DateOpenAPINodeType {
|
||||
public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? {
|
||||
|
||||
switch encoder.dateEncodingStrategy {
|
||||
case .deferredToDate, .custom:
|
||||
// I don't know if we can say anything about this case without
|
||||
// encoding the Date and looking at it, which is what `primitiveGuess()`
|
||||
// does.
|
||||
return nil
|
||||
|
||||
case .secondsSince1970,
|
||||
.millisecondsSince1970:
|
||||
return .number(.init(format: .double,
|
||||
required: true),
|
||||
.init())
|
||||
|
||||
case .iso8601:
|
||||
return .string(.init(format: .dateTime,
|
||||
required: true),
|
||||
.init())
|
||||
|
||||
case .formatted(let formatter):
|
||||
let hasTime = formatter.timeStyle != .none
|
||||
let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date
|
||||
|
||||
return .string(.init(format: format,
|
||||
required: true),
|
||||
.init())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
}
|
||||
@@ -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<Wrapped: Encodable>: Encodable {
|
||||
let primitive: Wrapped
|
||||
}
|
||||
|
||||
extension Sampleable {
|
||||
public static func samples<S1: Sampleable>(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] {
|
||||
return S1.samples.map(constructor)
|
||||
@@ -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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
@@ -521,14 +560,14 @@ extension JSONAPIAttributeOpenAPITests {
|
||||
let node = try! Attribute<Date>.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<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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user