Skip to content

Commit

Permalink
add value encoding strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Sep 22, 2023
1 parent 584a84b commit 35ed5c4
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 134 deletions.
68 changes: 20 additions & 48 deletions Sources/OpenAPIKit/AnyCodable/AnyCodableEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,35 @@ public extension AnyCodable {
///
/// - Parameters:
/// - value: The value to encode.
/// - dateFormat: The date encoding format to use.
/// - 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,
dateFormat: DateEncodingFormat = .default,
keyEncodingStrategy: KeyEncodingStrategy = .default
) {
do {
self = try .encodable(value, dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy)
} catch {
return nil
}
}

/// Creates a new instance from the given `Encodable` value.
///
/// - Parameters:
/// - value: The value to encode.
/// - dateFormat: The date encoding format to use.
/// - keyEncodingStrategy: The key encoding strategy to use.
/// - Returns: A new instance of `AnyCodable` or `nil` if the given value cannot be encoded.
/// - Throws: An error if any value throws an error during encoding.
static func encodable(
init(
_ value: Encodable,
dateFormat: DateEncodingFormat = .default,
valueEncodingStrategies: [ValueEncodingStrategy] = [.Decimal.number, .URL.uri, .Data.base64],
keyEncodingStrategy: KeyEncodingStrategy = .default
) throws -> AnyCodable {
let newEncoder = AnyCodableEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy)
return try newEncoder.encode(value)
) 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 dateFormat: DateEncodingFormat
let strategies: [ValueEncodingStrategy]
let keyEncodingStrategy: KeyEncodingStrategy

init(
codingPath: [CodingKey] = [],
dateFormat: DateEncodingFormat,
strategies: [ValueEncodingStrategy],
keyEncodingStrategy: KeyEncodingStrategy
) {
self.codingPath = codingPath
userInfo = [:]
self.dateFormat = dateFormat
self.strategies = strategies
self.keyEncodingStrategy = keyEncodingStrategy
result = .object([:])
}
Expand Down Expand Up @@ -101,23 +81,15 @@ private final class AnyCodableEncoder: Encoder {

func encode(_ value: Encodable) throws -> AnyCodable {
switch value {
case let date as Date:
var container = singleValueContainer()
try dateFormat.encode(date, &container)

case let data as Data:
try data.base64EncodedString().encode(to: self)

case let url as URL:
try url.absoluteString.encode(to: self)

case let decimal as Decimal:
try decimal.description.encode(to: self)

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
Expand Down Expand Up @@ -188,7 +160,7 @@ private struct AnyCodableSingleValueEncodingContainer: SingleValueEncodingContai
}

mutating func encode<T: Encodable>(_ value: T) throws {
let newEncoder = AnyCodableEncoder(codingPath: codingPath, dateFormat: encoder.dateFormat, keyEncodingStrategy: encoder.keyEncodingStrategy)
let newEncoder = AnyCodableEncoder(codingPath: codingPath, strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy)
result = try newEncoder.encode(value)
}
}
Expand Down Expand Up @@ -264,7 +236,7 @@ private struct AnyCodableKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingCo
}

mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
let newEncoder = AnyCodableEncoder(codingPath: nestedPath(for: key), dateFormat: encoder.dateFormat, keyEncodingStrategy: encoder.keyEncodingStrategy)
let newEncoder = AnyCodableEncoder(codingPath: nestedPath(for: key), strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy)
result[str(key)] = try newEncoder.encode(value)
}

Expand Down Expand Up @@ -309,12 +281,12 @@ private struct AnyCodableKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingCo
}

mutating func superEncoder() -> Encoder {
AnyCodableEncoder(codingPath: codingPath, dateFormat: encoder.dateFormat, keyEncodingStrategy: encoder.keyEncodingStrategy)
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), dateFormat: encoder.dateFormat, keyEncodingStrategy: encoder.keyEncodingStrategy)
return AnyCodableEncoder(codingPath: nestedPath(for: key), strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy)
}

private func nestedPath(for key: Key) -> [CodingKey] {
Expand Down Expand Up @@ -383,7 +355,7 @@ private struct AnyCodableUnkeyedEncodingContainer: UnkeyedEncodingContainer {
mutating func encodeNil() throws {}

mutating func superEncoder() -> Encoder {
AnyCodableEncoder(codingPath: codingPath, dateFormat: encoder.dateFormat, keyEncodingStrategy: encoder.keyEncodingStrategy)
AnyCodableEncoder(codingPath: codingPath, strategies: encoder.strategies, keyEncodingStrategy: encoder.keyEncodingStrategy)
}

mutating func encode(_ value: Bool) throws {
Expand Down Expand Up @@ -445,7 +417,7 @@ private struct AnyCodableUnkeyedEncodingContainer: UnkeyedEncodingContainer {
mutating func encode<T: Encodable>(_ value: T) throws {
let newEncoder = AnyCodableEncoder(
codingPath: nestedPath,
dateFormat: encoder.dateFormat,
strategies: encoder.strategies,
keyEncodingStrategy: encoder.keyEncodingStrategy
)
try result.append(newEncoder.encode(value))
Expand Down
25 changes: 25 additions & 0 deletions Sources/OpenAPIKit/AnyCodable/DataEncodingStrategies.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
77 changes: 0 additions & 77 deletions Sources/OpenAPIKit/AnyCodable/DateEncodingFormat.swift

This file was deleted.

73 changes: 73 additions & 0 deletions Sources/OpenAPIKit/AnyCodable/DateEncodingStrategies.swift
Original file line number Diff line number Diff line change
@@ -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()
28 changes: 28 additions & 0 deletions Sources/OpenAPIKit/AnyCodable/DecimalEncodingStrategies.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
20 changes: 20 additions & 0 deletions Sources/OpenAPIKit/AnyCodable/URLEncodingStrategies.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
19 changes: 19 additions & 0 deletions Sources/OpenAPIKit/AnyCodable/ValueEncodingStrategy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

public struct ValueEncodingStrategy {

public let encode: (Encodable, Encoder) throws -> Bool

public init<T: Encodable>(
_ 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
}
}
}
Loading

0 comments on commit 35ed5c4

Please sign in to comment.