diff --git a/Sources/OpenAPIKit/AnyCodable/AnyCodable.swift b/Sources/OpenAPIKit/AnyCodable/AnyCodable.swift index b70cb8e42..93923271d 100644 --- a/Sources/OpenAPIKit/AnyCodable/AnyCodable.swift +++ b/Sources/OpenAPIKit/AnyCodable/AnyCodable.swift @@ -32,99 +32,31 @@ import Foundation and other collections that require `Encodable` or `Decodable` conformance by declaring their contained type to be `AnyCodable`. */ -public struct AnyCodable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } +public enum AnyCodable: Equatable { + + case string(String) + case bool(Bool) + case int(Int) + case double(Double) + case object([String: AnyCodable]) + case array([AnyCodable]) + case null } extension AnyCodable: Encodable { public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch value { - #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) - case let number as NSNumber: - try encode(nsnumber: number, into: &container) - #endif - case is NSNull, is Void: + switch self { + case let .string(value): try value.encode(to: encoder) + case let .bool(value): try value.encode(to: encoder) + case let .int(value): try value.encode(to: encoder) + case let .double(value): try value.encode(to: encoder) + case let .object(value): try value.encode(to: encoder) + case let .array(value): try value.encode(to: encoder) + case .null: + var container = encoder.singleValueContainer() try container.encodeNil() - case let bool as Bool: - try container.encode(bool) - case let int as Int: - try container.encode(int) - case let int8 as Int8: - try container.encode(int8) - case let int16 as Int16: - try container.encode(int16) - case let int32 as Int32: - try container.encode(int32) - case let int64 as Int64: - try container.encode(int64) - case let uint as UInt: - try container.encode(uint) - case let uint8 as UInt8: - try container.encode(uint8) - case let uint16 as UInt16: - try container.encode(uint16) - case let uint32 as UInt32: - try container.encode(uint32) - case let uint64 as UInt64: - try container.encode(uint64) - case let float as Float: - try container.encode(float) - case let double as Double: - try container.encode(double) - case let string as String: - try container.encode(string) - case let date as Date: - try container.encode(date) - case let url as URL: - try container.encode(url) - case let array as [Any?]: - try container.encode(array.map { AnyCodable($0) }) - case let dictionary as [String: Any?]: - try container.encode(dictionary.mapValues { AnyCodable($0) }) - default: - let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyCodable value cannot be encoded") - throw EncodingError.invalidValue(value, context) - } - } - - #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) - private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { - switch CFNumberGetType(nsnumber) { - case .charType: - try container.encode(nsnumber.boolValue) - case .sInt8Type: - try container.encode(nsnumber.int8Value) - case .sInt16Type: - try container.encode(nsnumber.int16Value) - case .sInt32Type: - try container.encode(nsnumber.int32Value) - case .sInt64Type: - try container.encode(nsnumber.int64Value) - case .shortType: - try container.encode(nsnumber.uint16Value) - case .longType: - try container.encode(nsnumber.uint32Value) - case .longLongType: - try container.encode(nsnumber.uint64Value) - case .intType, .nsIntegerType, .cfIndexType: - try container.encode(nsnumber.intValue) - case .floatType, .float32Type: - try container.encode(nsnumber.floatValue) - case .doubleType, .float64Type, .cgFloatType: - try container.encode(nsnumber.doubleValue) - #if swift(>=5.0) - @unknown default: - fatalError() - #endif } } - #endif } extension AnyCodable: Decodable { @@ -132,107 +64,49 @@ extension AnyCodable: Decodable { let container = try decoder.singleValueContainer() if container.decodeNil() { - self.init(NSNull()) + self = .null } else if let bool = try? container.decode(Bool.self) { - self.init(bool) + self = .bool(bool) } else if let int = try? container.decode(Int.self) { - self.init(int) - } else if let uint = try? container.decode(UInt.self) { - self.init(uint) + self = .int(int) } else if let double = try? container.decode(Double.self) { - self.init(double) + self = .double(double) } else if let string = try? container.decode(String.self) { - self.init(string) + self = .string(string) } else if let array = try? container.decode([AnyCodable].self) { - self.init(array.map { $0.value }) + self = .array(array) } else if let dictionary = try? container.decode([String: AnyCodable].self) { - self.init(dictionary.mapValues { $0.value }) + self = .object(dictionary) } else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded") } } } -extension AnyCodable: Equatable { - public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case is (Void, Void): - return true - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Int8, rhs as Int8): - return lhs == rhs - case let (lhs as Int16, rhs as Int16): - return lhs == rhs - case let (lhs as Int32, rhs as Int32): - return lhs == rhs - case let (lhs as Int64, rhs as Int64): - return lhs == rhs - case let (lhs as UInt, rhs as UInt): - return lhs == rhs - case let (lhs as UInt8, rhs as UInt8): - return lhs == rhs - case let (lhs as UInt16, rhs as UInt16): - return lhs == rhs - case let (lhs as UInt32, rhs as UInt32): - return lhs == rhs - case let (lhs as UInt64, rhs as UInt64): - return lhs == rhs - case let (lhs as Float, rhs as Float): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs - case let (lhs as [String: String], rhs as [String: String]): - return lhs == rhs - case let (lhs as [String: Int], rhs as [String: Int]): - return lhs == rhs - case let (lhs as [String: Double], rhs as [String: Double]): - return lhs == rhs - case let (lhs as [String: Bool], rhs as [String: Bool]): - return lhs == rhs - case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]): - return lhs == rhs - case let (lhs as [String], rhs as [String]): - return lhs == rhs - case let (lhs as [Int], rhs as [Int]): - return lhs == rhs - case let (lhs as [Double], rhs as [Double]): - return lhs == rhs - case let (lhs as [Bool], rhs as [Bool]): - return lhs == rhs - case let (lhs as [AnyCodable], rhs as [AnyCodable]): - return lhs == rhs - default: - return false - } - } -} - extension AnyCodable: CustomStringConvertible { public var description: String { - switch value { - case is Void: - return String(describing: nil as Any?) - case let value as CustomStringConvertible: - return value.description - default: - return String(describing: value) + switch self { + case .string(let string): + return "\"\(string.description)\"" + case .bool(let bool): + return bool.description + case .int(let int): + return int.description + case .double(let double): + return double.description + case .object(let dictionary): + return "[\(dictionary.sorted(by: { $0.key < $1.key }).map { "\"\($0)\": \($1.description)" }.joined(separator: ", "))]" + case .array(let array): + return "[\(array.map { $0.description }.joined(separator: ", "))]" + case .null: + return "nil" } } } extension AnyCodable: CustomDebugStringConvertible { public var debugDescription: String { - switch value { - case let value as CustomDebugStringConvertible: - return "AnyCodable(\(value.debugDescription))" - default: - return "AnyCodable(\(description))" - } + "AnyCodable(\(description))" } } @@ -246,31 +120,81 @@ extension AnyCodable: ExpressibleByDictionaryLiteral {} extension AnyCodable { public init(nilLiteral _: ()) { - self.init(nil as Any?) + self = .null } public init(booleanLiteral value: Bool) { - self.init(value) + self = .bool(value) } public init(integerLiteral value: Int) { - self.init(value) + self = .int(value) } public init(floatLiteral value: Double) { - self.init(value) + self = .double(value) } public init(stringLiteral value: String) { - self.init(value) + self = .string(value) } - public init(arrayLiteral elements: Any...) { - self.init(elements) + public init(arrayLiteral elements: AnyCodable...) { + self = .array(elements) } - public init(dictionaryLiteral elements: (AnyHashable, Any)...) { - self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first })) + public init(dictionaryLiteral elements: (String, AnyCodable)...) { + self = .object([String: AnyCodable](elements, uniquingKeysWith: { first, _ in first })) + } +} + +// MARK: Backward compatibility +extension AnyCodable { + public var value: Any { + switch self { + case .string(let string): + return string + case .bool(let bool): + return bool + case .int(let int): + return int + case .double(let double): + return double + case .object(let dictionary): + return dictionary.mapValues { $0.value } + case .array(let array): + return array.map { $0.value } + case .null: + return Optional.none as Any + } + } + + public init(_ value: Bool) { + self = .bool(value) + } + + public init(_ value: T) { + self = .int(Int(value)) + } + + public init(_ value: T) { + self = .string(String(value)) + } + + public init(_ value: T) { + self = .double(Double(value)) + } + + public init(_ value: [AnyCodable]) { + self = .array(value) + } + + public init(_ value: [String: AnyCodable]) { + self = .object(value) + } + + public init(_ value: ()) { + self = .null } } diff --git a/Sources/OpenAPIKit/AnyCodable/AnyCodableEncoder.swift b/Sources/OpenAPIKit/AnyCodable/AnyCodableEncoder.swift new file mode 100644 index 000000000..c3894ea87 --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/AnyCodableEncoder.swift @@ -0,0 +1,440 @@ +import Foundation + +public extension AnyCodable { + /// Creates a new instance from the given `Encodable` value. + /// + /// - Parameters: + /// - value: The value to encode. + /// - valueEncodingStrategies: Value encoding strategies to use. + /// - keyEncodingStrategy: The key encoding strategy to use. + /// - Returns: A new instance of `AnyCodable` or `nil` if the given value cannot be encoded. + @_disfavoredOverload + init( + _ value: Encodable, + valueEncodingStrategies: [ValueEncodingStrategy] = [.Decimal.number, .URL.uri, .Data.base64], + keyEncodingStrategy: KeyEncodingStrategy = .default + ) throws { + let newEncoder = AnyCodableEncoder(strategies: valueEncodingStrategies, keyEncodingStrategy: keyEncodingStrategy) + self = try newEncoder.encode(value) + } +} + +private final class AnyCodableEncoder: Encoder { + let codingPath: [CodingKey] + let userInfo: [CodingUserInfoKey: Any] + private var result: AnyCodable + let strategies: [ValueEncodingStrategy] + let keyEncodingStrategy: KeyEncodingStrategy + + init( + codingPath: [CodingKey] = [], + strategies: [ValueEncodingStrategy], + keyEncodingStrategy: KeyEncodingStrategy + ) { + self.codingPath = codingPath + userInfo = [:] + self.strategies = strategies + self.keyEncodingStrategy = keyEncodingStrategy + result = .object([:]) + } + + func container(keyedBy _: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + let container = AnyCodableKeyedEncodingContainer( + codingPath: codingPath, + encoder: self, + result: Ref( + get: { [self] in + guard case let .object(value) = self.result else { return [:] } + return value + }, set: { [self] newValue in + self.result = .object(newValue) + } + ) + ) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + AnyCodableUnkeyedEncodingContainer( + codingPath: codingPath, + encoder: self, + result: Ref( + get: { [self] in + if case let .array(value) = self.result { + return value + } + return [] + }, set: { [self] newValue in + self.result = .array(newValue) + } + ) + ) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + AnyCodableSingleValueEncodingContainer( + codingPath: codingPath, + encoder: self, + result: Ref(self, \.result) + ) + } + + func encode(_ value: Encodable) throws -> AnyCodable { + switch value { + case nil as Any?: + result = .null + + default: + for format in strategies { + if try format.encode(value, self) { + return result + } + } + try value.encode(to: self) + } + return result + } +} + +private struct AnyCodableSingleValueEncodingContainer: SingleValueEncodingContainer { + var codingPath: [CodingKey] + let encoder: AnyCodableEncoder + @Ref var result: AnyCodable + + mutating func encodeNil() throws {} + + mutating func encode(_ value: Bool) throws { + result = .bool(value) + } + + mutating func encode(_ value: String) throws { + result = .string(value) + } + + mutating func encode(_ value: Double) throws { + result = .double(value) + } + + mutating func encode(_ value: Float) throws { + result = .double(Double(value)) + } + + mutating func encode(_ value: Int) throws { + result = .int(value) + } + + mutating func encode(_ value: Int8) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: Int16) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: Int32) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: Int64) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: UInt) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: UInt8) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: UInt16) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: UInt32) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: UInt64) throws { + result = .int(Int(value)) + } + + mutating func encode(_ value: T) throws { + let newEncoder = AnyCodableEncoder(codingPath: codingPath, strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy) + result = try newEncoder.encode(value) + } +} + +private struct AnyCodableKeyedEncodingContainer: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] + let encoder: AnyCodableEncoder + @Ref var result: [String: AnyCodable] + + @inline(__always) + private func str(_ key: Key) -> String { + encoder.keyEncodingStrategy.encode(key.stringValue) + } + + mutating func encodeNil(forKey key: Key) throws { + result[str(key)] = nil + } + + mutating func encode(_ value: Bool, forKey key: Key) throws { + result[str(key)] = .bool(value) + } + + mutating func encode(_ value: String, forKey key: Key) throws { + result[str(key)] = .string(value) + } + + mutating func encode(_ value: Double, forKey key: Key) throws { + result[str(key)] = .double(value) + } + + mutating func encode(_ value: Float, forKey key: Key) throws { + result[str(key)] = .double(Double(value)) + } + + mutating func encode(_ value: Int, forKey key: Key) throws { + result[str(key)] = .int(value) + } + + mutating func encode(_ value: Int8, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: Int16, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: Int32, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: Int64, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: UInt, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: UInt8, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: UInt16, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: UInt32, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: UInt64, forKey key: Key) throws { + result[str(key)] = .int(Int(value)) + } + + mutating func encode(_ value: T, forKey key: Key) throws { + let newEncoder = AnyCodableEncoder(codingPath: nestedPath(for: key), strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy) + result[str(key)] = try newEncoder.encode(value) + } + + mutating func nestedContainer(keyedBy _: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { + let strKey = str(key) + let container = AnyCodableKeyedEncodingContainer( + codingPath: nestedPath(for: key), + encoder: encoder, + result: Ref( + get: { [$result] in + guard + case let .object(value) = $result.wrappedValue[strKey] + else { return [:] } + return value + }, set: { [$result] newValue in + $result.wrappedValue[strKey] = .object(newValue) + } + ) + ) + result[strKey] = .object([:]) + return KeyedEncodingContainer(container) + } + + mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let strKey = str(key) + let container = AnyCodableUnkeyedEncodingContainer( + codingPath: nestedPath(for: key), + encoder: encoder, + result: Ref( + get: { [$result] in + guard + case let .array(value) = $result.wrappedValue[strKey] + else { return [] } + return value + }, set: { [$result] newValue in + $result.wrappedValue[strKey] = .array(newValue) + } + ) + ) + result[strKey] = .array([]) + return container + } + + mutating func superEncoder() -> Encoder { + AnyCodableEncoder(codingPath: codingPath, strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy) + } + + mutating func superEncoder(forKey key: Key) -> Encoder { + result[str(key)] = .object([:]) + return AnyCodableEncoder(codingPath: nestedPath(for: key), strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy) + } + + private func nestedPath(for key: Key) -> [CodingKey] { + codingPath + [key] + } +} + +private struct AnyCodableUnkeyedEncodingContainer: UnkeyedEncodingContainer { + var codingPath: [CodingKey] + var count: Int { result.count } + let encoder: AnyCodableEncoder + @Ref var result: [AnyCodable] + + private var nestedPath: [CodingKey] { + codingPath + [IntKey(intValue: codingPath.count)] + } + + mutating func nestedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { + let index = result.count + let container = AnyCodableKeyedEncodingContainer( + codingPath: nestedPath, + encoder: encoder, + result: Ref( + get: { [$result] in + guard + $result.wrappedValue.indices.contains(index), + case let .object(value) = $result.wrappedValue[index] + else { return [:] } + return value + }, set: { [$result] newValue in + guard $result.wrappedValue.indices.contains(index) else { + return + } + $result.wrappedValue[index] = .object(newValue) + } + ) + ) + result.append(.object([:])) + return KeyedEncodingContainer(container) + } + + mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + let index = result.count + let container = AnyCodableUnkeyedEncodingContainer( + codingPath: nestedPath, + encoder: encoder, + result: Ref( + get: { [$result] in + guard + $result.wrappedValue.indices.contains(index), + case let .array(value) = $result.wrappedValue[index] + else { return [] } + return value + }, set: { [$result] newValue in + guard $result.wrappedValue.indices.contains(index) else { + return + } + $result.wrappedValue[index] = .array(newValue) + } + ) + ) + result.append(.array([])) + return container + } + + mutating func encodeNil() throws {} + + mutating func superEncoder() -> Encoder { + AnyCodableEncoder(codingPath: codingPath, strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy) + } + + mutating func encode(_ value: Bool) throws { + result.append(.bool(value)) + } + + mutating func encode(_ value: String) throws { + result.append(.string(value)) + } + + mutating func encode(_ value: Double) throws { + result.append(.double(value)) + } + + mutating func encode(_ value: Float) throws { + result.append(.double(Double(value))) + } + + mutating func encode(_ value: Int) throws { + result.append(.int(value)) + } + + mutating func encode(_ value: Int8) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: Int16) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: Int32) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: Int64) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: UInt) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: UInt8) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: UInt16) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: UInt32) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: UInt64) throws { + result.append(.int(Int(value))) + } + + mutating func encode(_ value: T) throws { + let newEncoder = AnyCodableEncoder( + codingPath: nestedPath, + strategies: encoder.strategies, + keyEncodingStrategy: encoder.keyEncodingStrategy + ) + try result.append(newEncoder.encode(value)) + } +} + +private struct IntKey: CodingKey { + let intValue: Int? + let stringValue: String + + init(intValue: Int) { + self.intValue = intValue + stringValue = intValue.description + } + + init?(stringValue: String) { + intValue = Int(stringValue) + self.stringValue = stringValue + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/DataEncodingStrategies.swift b/Sources/OpenAPIKit/AnyCodable/DataEncodingStrategies.swift new file mode 100644 index 000000000..38db3fb57 --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/DataEncodingStrategies.swift @@ -0,0 +1,25 @@ +import Foundation + +public extension ValueEncodingStrategy { + + /// Data encoding strategy to use when encoding `AnyCodable` values. + enum Data { + } +} + +public extension ValueEncodingStrategy.Data { + static var `default`: ValueEncodingStrategy = .Data.base64 + + /// Base64 string, schema: .string(format: .byte) + static var base64: ValueEncodingStrategy { + .Data.base64(options: []) + } + + /// Base64 string, schema: .string(format: .byte) + static func base64(options: Data.Base64EncodingOptions) -> ValueEncodingStrategy { + ValueEncodingStrategy(Data.self) { data, encoder in + var container = encoder.singleValueContainer() + try container.encode(data.base64EncodedString(options: options)) + } + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/DateEncodingStrategies.swift b/Sources/OpenAPIKit/AnyCodable/DateEncodingStrategies.swift new file mode 100644 index 000000000..24e64875c --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/DateEncodingStrategies.swift @@ -0,0 +1,73 @@ +import Foundation + +public extension ValueEncodingStrategy { + + /// Date encoding strategy to use when encoding `AnyCodable` values. + enum Date { + } +} + +public extension ValueEncodingStrategy.Date { + static var `default`: ValueEncodingStrategy = .Date.dateTime + + /// full-date notation as defined by RFC 3339, section 5.6, for example, 2017-07-21, schema: .string(format: .date) + static var date: ValueEncodingStrategy { + .Date.custom { date, encoder in + try encoder.encode(ValueEncodingStrategy.Date.date(date)) + } + } + + /// the date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z, schema: .string(format: .dateTime) + static var dateTime: ValueEncodingStrategy { + .Date.custom { date, encoder in + try encoder.encode(ValueEncodingStrategy.Date.dateTime(date)) + } + } + + /// the interval between the date value and 00:00:00 UTC on 1 January 1970, schema: .number(format: .other("timestamp")) + static var timestamp: ValueEncodingStrategy { + .Date.custom { date, encoder in + try encoder.encode(date.timeIntervalSince1970) + } + } + + /// Custom date encoding strategy + static func custom( + encode: @escaping (Date, inout SingleValueEncodingContainer) throws -> Void + ) -> ValueEncodingStrategy { + ValueEncodingStrategy(Date.self) { + var container = $1.singleValueContainer() + try encode($0, &container) + } + } + + /// Custom date encoding strategy + static func custom( + _ dataFormat: JSONTypeFormat.StringFormat, + formatter: DateFormatter + ) -> ValueEncodingStrategy { + .Date.custom { date, encoder in + try encoder.encode(formatter.string(from: date)) + } + } +} + +extension ValueEncodingStrategy.Date { + static func dateTime(_ date: Date) -> String { + isoFormatter.string(from: date) + } + + static func date(_ date: Date) -> String { + dateFormatter.dateFormat = "yyyy-MM-dd" + return dateFormatter.string(from: date) + } +} + +private let isoFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter +}() +private let dateFormatter = DateFormatter() diff --git a/Sources/OpenAPIKit/AnyCodable/DecimalEncodingStrategies.swift b/Sources/OpenAPIKit/AnyCodable/DecimalEncodingStrategies.swift new file mode 100644 index 000000000..3d57d58b5 --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/DecimalEncodingStrategies.swift @@ -0,0 +1,28 @@ +import Foundation + +public extension ValueEncodingStrategy { + + /// Decimal encoding strategy to use when encoding `AnyCodable` values. + enum Decimal { + } +} + +public extension ValueEncodingStrategy.Decimal { + static var `default`: ValueEncodingStrategy = .Decimal.number + + /// Quoted string + static var string: ValueEncodingStrategy { + ValueEncodingStrategy(Decimal.self) { decimal, encoder in + var container = encoder.singleValueContainer() + try container.encode(decimal.description) + } + } + + /// Number + static var number: ValueEncodingStrategy { + ValueEncodingStrategy(Decimal.self) { decimal, encoder in + var container = encoder.singleValueContainer() + try container.encode((decimal as NSDecimalNumber).doubleValue) + } + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/KeyEncodingStrategy.swift b/Sources/OpenAPIKit/AnyCodable/KeyEncodingStrategy.swift new file mode 100644 index 000000000..c483f382b --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/KeyEncodingStrategy.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Key encoding strategy to use when encoding `AnyCodable` values. +public struct KeyEncodingStrategy { + public let encode: (String) -> String +} + +public extension KeyEncodingStrategy { + static var `default`: KeyEncodingStrategy = .useDefaultKeys + + /// Does not change the key + static var useDefaultKeys: KeyEncodingStrategy = .custom { $0 } + + /// Custom key encoding strategy + static func custom(_ encode: @escaping (String) -> String) -> KeyEncodingStrategy { + KeyEncodingStrategy(encode: encode) + } + + /// Encodes from camelCase to snake_case + static var convertToSnakeCase: KeyEncodingStrategy { + .convertToSnakeCase(separator: "_") + } + + /// Encodes from camelCase to snake_case with a custom separator + static func convertToSnakeCase(separator: String) -> KeyEncodingStrategy { + .custom { + $0.toSnakeCase(separator: separator) + } + } +} + +private extension String { + func toSnakeCase(separator: String = "_") -> String { + var result = "" + + for character in self { + if character.isUppercase { + result += separator + character.lowercased() + } else { + result += String(character) + } + } + + return result + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/Ref.swift b/Sources/OpenAPIKit/AnyCodable/Ref.swift new file mode 100644 index 000000000..7fb47396f --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/Ref.swift @@ -0,0 +1,40 @@ +import Foundation + +@propertyWrapper +struct Ref { + + let get: () -> Value + let set: (Value) -> Void + + var wrappedValue: Value { + get { get() } + nonmutating set { set(newValue) } + } + + var projectedValue: Ref { + get { self } + set { self = newValue } + } +} + +extension Ref { + + static func constant(_ value: Value) -> Ref { + self.init( + get: { + value + }, set: { _ in + } + ) + } + + init(_ value: T, _ keyPath: ReferenceWritableKeyPath) { + self.init( + get: { + value[keyPath: keyPath] + }, set: { newValue in + value[keyPath: keyPath] = newValue + } + ) + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/URLEncodingStrategies.swift b/Sources/OpenAPIKit/AnyCodable/URLEncodingStrategies.swift new file mode 100644 index 000000000..53d1e5866 --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/URLEncodingStrategies.swift @@ -0,0 +1,20 @@ +import Foundation + +public extension ValueEncodingStrategy { + + /// URL encoding strategy to use when encoding `AnyCodable` values. + enum URL { + } +} + +public extension ValueEncodingStrategy.URL { + static var `default`: ValueEncodingStrategy = .URL.uri + + /// URI string, schema: .string(format: .other("uri")) + static var uri: ValueEncodingStrategy { + ValueEncodingStrategy(URL.self) { url, encoder in + var container = encoder.singleValueContainer() + try container.encode(url.absoluteString) + } + } +} diff --git a/Sources/OpenAPIKit/AnyCodable/ValueEncodingStrategy.swift b/Sources/OpenAPIKit/AnyCodable/ValueEncodingStrategy.swift new file mode 100644 index 000000000..8868ed60c --- /dev/null +++ b/Sources/OpenAPIKit/AnyCodable/ValueEncodingStrategy.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct ValueEncodingStrategy { + + public let encode: (Encodable, Encoder) throws -> Bool + + public init( + _ type: T.Type, + encode: @escaping (T, Encoder) throws -> Void + ) { + self.encode = { + guard let value = $0 as? T else { + return false + } + try encode(value, $1) + return true + } + } +} diff --git a/Sources/OpenAPIKit/CodableVendorExtendable.swift b/Sources/OpenAPIKit/CodableVendorExtendable.swift index 4203bfa3c..9e2120179 100644 --- a/Sources/OpenAPIKit/CodableVendorExtendable.swift +++ b/Sources/OpenAPIKit/CodableVendorExtendable.swift @@ -70,16 +70,12 @@ extension CodableVendorExtendable { internal static func extensions(from decoder: Decoder) throws -> VendorExtensions { - let decoded = try AnyCodable(from: decoder).value + let decoded = try AnyCodable(from: decoder) - guard (decoded as? [Any]) == nil else { + guard case .object(let decodedAny) = decoded else { throw VendorExtensionDecodingError.selfIsArrayNotDict } - guard let decodedAny = decoded as? [String: Any] else { - throw VendorExtensionDecodingError.foundNonStringKeys - } - let extensions = decodedAny.filter { let key = CodingKeys.key(for: $0.key) @@ -96,7 +92,7 @@ extension CodableVendorExtendable { ) } - return extensions.mapValues(AnyCodable.init) + return extensions } internal func encodeExtensions(to container: inout T) throws where T.Key == Self.CodingKeys { diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index dc47a5e18..f8db911f6 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -702,15 +702,7 @@ extension JSONSchema.CoreContext: Decodable { description = try container.decodeIfPresent(String.self, forKey: .description) discriminator = try container.decodeIfPresent(OpenAPI.Discriminator.self, forKey: .discriminator) externalDocs = try container.decodeIfPresent(OpenAPI.ExternalDocumentation.self, forKey: .externalDocs) - if Format.self == JSONTypeFormat.StringFormat.self { - if (nullable ?? false) { - allowedValues = try container.decodeIfPresent([String?].self, forKey: .allowedValues)?.map(AnyCodable.init) - } else { - allowedValues = try container.decodeIfPresent([String].self, forKey: .allowedValues)?.map(AnyCodable.init) - } - } else { - allowedValues = try container.decodeIfPresent([AnyCodable].self, forKey: .allowedValues) - } + allowedValues = try container.decodeIfPresent([AnyCodable].self, forKey: .allowedValues) defaultValue = try container.decodeIfPresent(AnyCodable.self, forKey: .defaultValue) let readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) diff --git a/Tests/AnyCodableTests/AnyCodableTests.swift b/Tests/AnyCodableTests/AnyCodableTests.swift index 51bf55c27..22db2889e 100644 --- a/Tests/AnyCodableTests/AnyCodableTests.swift +++ b/Tests/AnyCodableTests/AnyCodableTests.swift @@ -2,50 +2,83 @@ import XCTest class AnyCodableTests: XCTestCase { - func testInit() throws { - let _ = AnyCodable("hi") - let _: AnyCodable = nil - let _: AnyCodable = true - let _: AnyCodable = 10 - let _: AnyCodable = 3.4 - let _: AnyCodable = "hello" - let _: AnyCodable = ["hi", "there"] - let _: AnyCodable = ["hi": "there"] - } - - func testEquality() throws { - XCTAssertEqual(AnyCodable(()), AnyCodable(())) - XCTAssertEqual(AnyCodable(true), AnyCodable(true)) - XCTAssertEqual(AnyCodable(2), AnyCodable(2)) - XCTAssertEqual(AnyCodable(Int8(2)), AnyCodable(Int8(2))) - XCTAssertEqual(AnyCodable(Int16(2)), AnyCodable(Int16(2))) - XCTAssertEqual(AnyCodable(Int32(2)), AnyCodable(Int32(2))) - XCTAssertEqual(AnyCodable(Int64(2)), AnyCodable(Int64(2))) - XCTAssertEqual(AnyCodable(UInt(2)), AnyCodable(UInt(2))) - XCTAssertEqual(AnyCodable(UInt8(2)), AnyCodable(UInt8(2))) - XCTAssertEqual(AnyCodable(UInt16(2)), AnyCodable(UInt16(2))) - XCTAssertEqual(AnyCodable(UInt32(2)), AnyCodable(UInt32(2))) - XCTAssertEqual(AnyCodable(UInt64(2)), AnyCodable(UInt64(2))) - XCTAssertEqual(AnyCodable(Float(2)), AnyCodable(Float(2))) - XCTAssertEqual(AnyCodable(Double(2)), AnyCodable(Double(2))) - XCTAssertEqual(AnyCodable("hi"), AnyCodable("hi")) - XCTAssertEqual(AnyCodable(["hi": AnyCodable(2)]), AnyCodable(["hi": AnyCodable(2)])) - XCTAssertEqual(AnyCodable([AnyCodable("hi"), AnyCodable("there")]), AnyCodable([AnyCodable("hi"), AnyCodable("there")])) - XCTAssertEqual(AnyCodable(["hi":1]), AnyCodable(["hi":1])) - XCTAssertEqual(AnyCodable(["hi":1.2]), AnyCodable(["hi":1.2])) - XCTAssertEqual(AnyCodable(["hi"]), AnyCodable(["hi"])) - XCTAssertEqual(AnyCodable([1]), AnyCodable([1])) - XCTAssertEqual(AnyCodable([1.2]), AnyCodable([1.2])) - XCTAssertEqual(AnyCodable([true]), AnyCodable([true])) - - XCTAssertNotEqual(AnyCodable(()), AnyCodable(true)) - } - - func testVoidDescription() { - XCTAssertEqual(String(describing: AnyCodable(Void())), "nil") - } - - func testJSONDecoding() throws { + func test_equality() throws { + XCTAssertEqual(AnyCodable.null, AnyCodable.null) + XCTAssertEqual(AnyCodable.bool(true), AnyCodable.bool(true)) + XCTAssertEqual(AnyCodable.int(2), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable.double(2), AnyCodable.double(2)) + XCTAssertEqual(AnyCodable.string("hi"), AnyCodable.string("hi")) + XCTAssertEqual(AnyCodable.object(["hi": .int(2)]), AnyCodable.object(["hi": .int(2)])) + XCTAssertEqual(AnyCodable.array([.string("hi")]), AnyCodable.array([.string("hi")])) + XCTAssertEqual(AnyCodable.array([.int(1)]), AnyCodable.array([.int(1)])) + + XCTAssertNotEqual(AnyCodable.null, AnyCodable.bool(true)) + XCTAssertNotEqual(AnyCodable.null, AnyCodable.int(2)) + XCTAssertNotEqual(AnyCodable.int(4), AnyCodable.string("hi")) + XCTAssertNotEqual(AnyCodable.string("hi"), AnyCodable.array([.string("hi")])) + XCTAssertNotEqual(AnyCodable.object(["hi": .int(2)]), AnyCodable.object(["hi": .double(3)])) + } + + func test_inits() throws { + let falseBool = false + XCTAssertEqual(AnyCodable(()), AnyCodable.null) + XCTAssertEqual(AnyCodable(true), AnyCodable.bool(true)) + XCTAssertEqual(AnyCodable(falseBool), AnyCodable.bool(false)) + XCTAssertEqual(AnyCodable(2), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(Int8(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(Int16(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(Int32(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(Int64(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(UInt(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(UInt8(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(UInt16(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(UInt32(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(UInt64(2)), AnyCodable.int(2)) + XCTAssertEqual(AnyCodable(Float(2)), AnyCodable.double(2)) + XCTAssertEqual(AnyCodable(Double(2)), AnyCodable.double(2)) + XCTAssertEqual(AnyCodable("hi"), AnyCodable.string("hi")) + XCTAssertEqual(AnyCodable(["hi": 2]), AnyCodable.object(["hi": .int(2)])) + XCTAssertEqual(AnyCodable(["hi", "there"]), AnyCodable.array([.string("hi"), .string("there")])) + XCTAssertEqual(AnyCodable(["hi": 1]), AnyCodable.object(["hi": .int(1)])) + XCTAssertEqual(AnyCodable([1]), AnyCodable.array([.int(1)])) + XCTAssertEqual(AnyCodable([1.2]), AnyCodable.array([.double(1.2)])) + XCTAssertEqual(AnyCodable([true]), AnyCodable.array([.bool(true)])) + } + + func test_expressible() throws { + XCTAssertEqual(AnyCodable.string("hi"), "hi") + XCTAssertEqual(AnyCodable.bool(true), true) + XCTAssertEqual(AnyCodable.null, nil) + XCTAssertEqual(AnyCodable.int(2), 2) + XCTAssertEqual(AnyCodable.double(3.4), 3.4) + XCTAssertEqual(AnyCodable.object(["hi": .string("there")]), ["hi": "there"]) + } + + func test_equalityFromJSON() throws { + let json = """ + { + "boolean": true, + "integer": 1, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + } + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let anyCodable0 = try decoder.decode(AnyCodable.self, from: json) + let anyCodable1 = try decoder.decode(AnyCodable.self, from: json) + XCTAssertEqual(anyCodable0, anyCodable1) + } + + func test_VoidDescription() { + XCTAssertEqual(String(describing: AnyCodable(())), "nil") + } + + func test_JSONDecodingByKeys() throws { let json = """ { "boolean": true, @@ -72,7 +105,45 @@ class AnyCodableTests: XCTestCase { XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) } - func testJSONEncoding() throws { + func test_JSONDecodingFull() throws { + let json = """ + { + "boolean": true, + "integer": 1, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + } + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let anyCodable = try decoder.decode(AnyCodable.self, from: json) + + XCTAssertEqual( + anyCodable, + .object( + [ + "boolean": .bool(true), + "integer": .int(1), + "string": .string("string"), + "array": .array([.int(1), .int(2), .int(3)]), + "nested": .object( + [ + "a": .string("alpha"), + "b": .string("bravo"), + "c": .string("charlie") + ] + ), + ] + ) + ) + } + + func test_JSONEncoding() throws { let dictionary: [String: AnyCodable] = [ "boolean": true, "integer": 1, @@ -109,27 +180,6 @@ class AnyCodableTests: XCTestCase { ) } - func testEncodeNSNumber() throws { - #if os(macOS) - let dictionary: [String: NSNumber] = [ - "boolean": true, - "integer": 1, - ] - - let result = try testStringFromEncoding(of: AnyCodable(dictionary)) - - assertJSONEquivalent( - result, - """ - { - "boolean" : true, - "integer" : 1 - } - """ - ) - #endif - } - let testEncoder: JSONEncoder = { let encoder = JSONEncoder() if #available(macOS 10.13, *) { @@ -177,8 +227,201 @@ class AnyCodableTests: XCTestCase { XCTAssertEqual(string, #"{"value":"https:\/\/hello.com"}"#) } + + func test_encodeEncodable() throws { + let anyCodable = try AnyCodable(EncodableStruct()) + let expectedAnyCodable: AnyCodable = [ + "int": 1, + "string": "hello", + "bool": true, + "array": [1, 2, 3], + "dictionary": ["a": 1, "b": 2, "c": 3], + "data": "aGVsbG8=", + "decimal": 10.0, + "url": "https://google.com" + ] + XCTAssertEqual(anyCodable, expectedAnyCodable) + + let data = try JSONEncoder().encode(anyCodable) + let decodedValue = try JSONDecoder().decode(EncodableStruct.self, from: data) + XCTAssertEqual(decodedValue, EncodableStruct()) + } + + func test_dateEncodableInit() throws { + let anyCodable = try AnyCodable( + EncodableStruct(), + keyEncodingStrategy: .default + ) + let expectedAnyCodable: AnyCodable = [ + "int": 1, + "string": "hello", + "bool": true, + "array": [1, 2, 3], + "dictionary": ["a": 1, "b": 2, "c": 3], + "data": "aGVsbG8=", + "decimal": 10.0, + "url": "https://google.com" + ] + XCTAssertEqual(anyCodable, expectedAnyCodable) + + let data = try JSONEncoder().encode(anyCodable) + let decodedValue = try JSONDecoder().decode(EncodableStruct.self, from: data) + XCTAssertEqual(decodedValue, EncodableStruct()) + } + + func test_keyEncodingStrategy() throws { + let camelCaseString = "thisIsCamelCase" + XCTAssertEqual(KeyEncodingStrategy.convertToSnakeCase.encode(camelCaseString), "this_is_camel_case") + + let anyCodable = try AnyCodable(StructWithLargeNameProperty(), keyEncodingStrategy: .convertToSnakeCase) + let expectedAnyCodable: AnyCodable = [ + "this_is_a_very_long_property_name_that_will_be_encoded": "hello", + ] + XCTAssertEqual(anyCodable, expectedAnyCodable) + } + + func test_dateEncodingStrategies() throws { + let date = Date(timeIntervalSince1970: 0) + let anyCodable = try AnyCodable(date, valueEncodingStrategies: [.Date.timestamp]) + let expectedAnyCodable: AnyCodable = 0.0 + XCTAssertEqual(anyCodable, expectedAnyCodable) + + let data = try JSONEncoder().encode([anyCodable]) // Use array for swift 5.1 support + let string = String(data: data, encoding: .utf8) + XCTAssertEqual(string, "[0]") + + let anyCodable3 = try AnyCodable(date, valueEncodingStrategies: [.Date.dateTime]) + let expectedAnyCodable3: AnyCodable = "1970-01-01T00:00:00.000Z" + XCTAssertEqual(anyCodable3, expectedAnyCodable3) + + let data3 = try JSONEncoder().encode([anyCodable3]) // Use array for swift 5.1 support + let string3 = String(data: data3, encoding: .utf8) + XCTAssertEqual(string3, #"["1970-01-01T00:00:00.000Z"]"#) + + let anyCodable4 = try AnyCodable(date, valueEncodingStrategies: [.Date.date]) + let expectedAnyCodable4: AnyCodable = "1970-01-01" + XCTAssertEqual(anyCodable4, expectedAnyCodable4) + + let data4 = try JSONEncoder().encode([anyCodable4]) // Use array for swift 5.1 support + let string4 = String(data: data4, encoding: .utf8) + XCTAssertEqual(string4, #"["1970-01-01"]"#) + } + + func test_dataEncodingStrategies() throws { + let data = Data([0x01, 0x02, 0x03]) + let anyCodable1 = try AnyCodable(data, valueEncodingStrategies: [.Data.base64]) + let expectedAnyCodable1: AnyCodable = "AQID" + XCTAssertEqual(anyCodable1, expectedAnyCodable1) + + let data1 = try JSONEncoder().encode([anyCodable1]) // Use array for swift 5.1 support + let string1 = String(data: data1, encoding: .utf8) + XCTAssertEqual(string1, #"["AQID"]"#) + + let anyCodable2 = try AnyCodable(data, valueEncodingStrategies: [.Data.base64(options: .endLineWithCarriageReturn)]) + let expectedAnyCodable2: AnyCodable = "AQID" + XCTAssertEqual(anyCodable2, expectedAnyCodable2) + + let data2 = try JSONEncoder().encode([anyCodable2]) // Use array for swift 5.1 support + let string2 = String(data: data2, encoding: .utf8) + XCTAssertEqual(string2, #"["AQID"]"#) + } + + func test_urlEncodingStrategies() throws { + let url = URL(string: "https://google.com")! + let anyCodable = try AnyCodable(url, valueEncodingStrategies: [.URL.uri]) + let expectedAnyCodable: AnyCodable = "https://google.com" + XCTAssertEqual(anyCodable, expectedAnyCodable) + + let data = try JSONEncoder().encode([anyCodable]) // Use array for swift 5.1 support + let string = String(data: data, encoding: .utf8) + XCTAssertEqual(string, #"["https:\/\/google.com"]"#) + } + + func test_decimalEncodingStrategies() throws { + let decimal = Decimal(10) + + let anyCodable1 = try AnyCodable(decimal, valueEncodingStrategies: [.Decimal.number]) + let expectedAnyCodable1: AnyCodable = 10.0 + XCTAssertEqual(anyCodable1, expectedAnyCodable1) + + let data1 = try JSONEncoder().encode([anyCodable1]) // Use array for swift 5.1 support + let string1 = String(data: data1, encoding: .utf8) + XCTAssertEqual(string1, "[10]") + + let anyCodable2 = try AnyCodable(decimal, valueEncodingStrategies: [.Decimal.string]) + let expectedAnyCodable2: AnyCodable = "10" + XCTAssertEqual(anyCodable2, expectedAnyCodable2) + + let data2 = try JSONEncoder().encode([anyCodable2]) // Use array for swift 5.1 support + let string2 = String(data: data2, encoding: .utf8) + XCTAssertEqual(string2, #"["10"]"#) + } + + func test_RefInit() { + var value = 0 + let ref = Ref(get: { + value + }, set: { + value = $0 + }) + XCTAssertEqual(ref.wrappedValue, 0) + ref.wrappedValue = 1 + XCTAssertEqual(ref.wrappedValue, 1) + } + + func test_RefConstant() { + let ref = Ref.constant(1) + XCTAssertEqual(ref.wrappedValue, 1) + ref.wrappedValue = 2 + XCTAssertEqual(ref.wrappedValue, 1) + } + + func test_RefByKeyPath() { + let ref = Ref(DateFormatter(), \.dateFormat) + ref.wrappedValue = "yyyy-MM-dd" + XCTAssertEqual(ref.wrappedValue, "yyyy-MM-dd") + ref.wrappedValue = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + XCTAssertEqual(ref.wrappedValue, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") + } + + func test_description() { + XCTAssertEqual(AnyCodable(0).description, "0") + XCTAssertEqual(AnyCodable(0.0).description, "0.0") + XCTAssertEqual(AnyCodable(()).description, "nil") + XCTAssertEqual(AnyCodable("hello").description, "\"hello\"") + XCTAssertEqual(AnyCodable(true).description, "true") + XCTAssertEqual(AnyCodable(false).description, "false") + XCTAssertEqual(AnyCodable([1, 2, 3]).description, "[1, 2, 3]") + XCTAssertEqual(AnyCodable(["a": 1, "b": 2]).description, "[\"a\": 1, \"b\": 2]") + } + + func test_debugDescription() { + XCTAssertEqual(AnyCodable(0).debugDescription, "AnyCodable(0)") + XCTAssertEqual(AnyCodable(0.0).debugDescription, "AnyCodable(0.0)") + XCTAssertEqual(AnyCodable(()).debugDescription, "AnyCodable(nil)") + XCTAssertEqual(AnyCodable("hello").debugDescription, "AnyCodable(\"hello\")") + XCTAssertEqual(AnyCodable(true).debugDescription, "AnyCodable(true)") + XCTAssertEqual(AnyCodable(false).debugDescription, "AnyCodable(false)") + XCTAssertEqual(AnyCodable([1, 2, 3]).debugDescription, "AnyCodable([1, 2, 3])") + XCTAssertEqual(AnyCodable(["a": 1, "b": 2]).debugDescription, "AnyCodable([\"a\": 1, \"b\": 2])") + } } -fileprivate struct Wrapper: Codable { +private struct Wrapper: Codable { let value: AnyCodable } + +private struct EncodableStruct: Codable, Equatable { + var int = 1 + var string = "hello" + var bool = true + var array = [1, 2, 3] + var dictionary = ["a": 1, "b": 2, "c": 3] + var data = "hello".data(using: .utf8) + var decimal = Decimal(10) + var url = URL(string: "https://google.com")! +} + +private struct StructWithLargeNameProperty: Codable { + var thisIsAVeryLongPropertyNameThatWillBeEncoded: String = "hello" +} diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift index 07e088dab..35d00f2f3 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift @@ -1158,7 +1158,7 @@ final class SchemaObjectTests: XCTestCase { func test_withAddedExample() throws { let object = try JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - .with(example: AnyCodable([String: String]())) + .with(example: [:]) let array = try JSONSchema.array(.init(), .init()) .with(example: ["hello"]) @@ -1166,8 +1166,6 @@ final class SchemaObjectTests: XCTestCase { .with(example: true) let double = try JSONSchema.number .with(example: 10.5) - let float = try JSONSchema.number - .with(example: AnyCodable(Float(2.5))) let integer = try JSONSchema.integer .with(example: 3) let string = try JSONSchema.string @@ -1191,7 +1189,6 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(boolean.example?.value as? Bool, true) XCTAssertEqual(double.example?.value as? Double, 10.5) - XCTAssertEqual(float.example?.value as? Float, 2.5 as Float) XCTAssertEqual(integer.example?.value as? Int, 3) XCTAssertEqual(string.example?.value as? String, "hello world") diff --git a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift index b73966d41..27adfe9d5 100644 --- a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift @@ -1337,7 +1337,6 @@ final class ValidatorTests: XCTestCase { "x-double": 10.5, "x-dict": [ "string": "world"], "x-array": AnyCodable(["hello", nil, "world"]), - "x-float": AnyCodable(22.5 as Float), "x-bool": true ] ) @@ -1359,7 +1358,6 @@ final class ValidatorTests: XCTestCase { .validating("string", check: \String.self == "hiya", when: \.codingPath.last?.stringValue == "x-string") .validating("int", check: \Int.self == 3) .validating("double", check: \Double.self == 10.5) - .validating("float", check: \Float.self == 22.5) .validating("bool", check: \Bool.self == true) try document.validate(using: validator)