@@ -2,84 +2,166 @@ import Foundation
22
33public typealias LosslessStringCodable = LosslessStringConvertible & Codable
44
5+ /// Provides an ordered list of types for decoding the lossless value, prioritizing the first type that successfully decodes as the inferred type.
6+ ///
7+ /// `LosslessDecodingStrategy` provides a generic strategy that the `LosslessValueCodable` property wrapper can use to provide
8+ /// the ordered list of decodable types in order to maximize preservation for the inferred type.
9+ public protocol LosslessDecodingStrategy {
10+ associatedtype Value : LosslessStringCodable
11+
12+ /// An ordered list of decodable scenarios used to infer the encoded type
13+ static var losslessDecodableTypes : [ ( Decoder ) -> LosslessStringCodable ? ] { get }
14+ }
15+
516/// Decodes Codable values into their respective preferred types.
617///
7- /// `@LosslessValue ` attempts to decode Codable types into their respective preferred types while preserving the data.
18+ /// `@LosslessValueCodable ` attempts to decode Codable types into their preferred order while preserving the data in the most lossless format .
819///
9- /// This is useful when data may return unpredictable values when a consumer is expecting a certain type. For instance,
10- /// if an API sends SKUs as either an `Int` or `String`, then a `@LosslessValue` can ensure the types are always decoded
11- /// as `String`s.
20+ /// The preferred type order is provided by a generic `LosslessDecodingStrategy` that provides an ordered list of `losslessDecodableTypes`.
1221@propertyWrapper
13- public struct LosslessValue < T : LosslessStringCodable > : Codable {
22+ public struct LosslessValueCodable < Strategy : LosslessDecodingStrategy > : Codable {
1423 private let type : LosslessStringCodable . Type
15-
16- public var wrappedValue : T
1724
18- public init ( wrappedValue: T ) {
25+ public var wrappedValue : Strategy . Value
26+
27+ public init ( wrappedValue: Strategy . Value ) {
1928 self . wrappedValue = wrappedValue
20- self . type = T . self
29+ self . type = Strategy . Value . self
2130 }
22-
31+
2332 public init ( from decoder: Decoder ) throws {
2433 do {
25- self . wrappedValue = try T . init ( from: decoder)
26- self . type = T . self
27-
34+ self . wrappedValue = try Strategy . Value. init ( from: decoder)
35+ self . type = Strategy . Value. self
2836 } catch let error {
29- func decode< T: LosslessStringCodable > ( _: T . Type ) -> ( Decoder ) -> LosslessStringCodable ? {
30- return { try ? T . init ( from: $0) }
31- }
32-
33- func decodeBoolFromNSNumber( ) -> ( Decoder ) -> LosslessStringCodable ? {
34- return { ( try ? Int . init ( from: $0) ) . flatMap { Bool ( exactly: NSNumber ( value: $0) ) } }
35- }
36-
37- let types : [ ( Decoder ) -> LosslessStringCodable ? ] = [
38- decode ( String . self) ,
39- decodeBoolFromNSNumber ( ) ,
40- decode ( Bool . self) ,
41- decode ( Int . self) ,
42- decode ( Int8 . self) ,
43- decode ( Int16 . self) ,
44- decode ( Int64 . self) ,
45- decode ( UInt . self) ,
46- decode ( UInt8 . self) ,
47- decode ( UInt16 . self) ,
48- decode ( UInt64 . self) ,
49- decode ( Double . self) ,
50- decode ( Float . self) ,
51- ]
52-
5337 guard
54- let rawValue = types . lazy. compactMap ( { $0 ( decoder) } ) . first,
55- let value = T . init ( " \( rawValue) " )
56- else { throw error }
57-
38+ let rawValue = Strategy . losslessDecodableTypes . lazy. compactMap ( { $0 ( decoder) } ) . first,
39+ let value = Strategy . Value . init ( " \( rawValue) " )
40+ else { throw error }
41+
5842 self . wrappedValue = value
5943 self . type = Swift . type ( of: rawValue)
6044 }
6145 }
62-
46+
6347 public func encode( to encoder: Encoder ) throws {
6448 let string = String ( describing: wrappedValue)
65-
49+
6650 guard let original = type. init ( string) else {
6751 let description = " Unable to encode ' \( wrappedValue) ' back to source type ' \( type) ' "
6852 throw EncodingError . invalidValue ( string, . init( codingPath: [ ] , debugDescription: description) )
6953 }
70-
54+
7155 try original. encode ( to: encoder)
7256 }
7357}
7458
75- extension LosslessValue : Equatable where T : Equatable {
76- public static func == ( lhs: LosslessValue < T > , rhs: LosslessValue < T > ) -> Bool {
59+ extension LosslessValueCodable : Equatable where Strategy . Value : Equatable {
60+ public static func == ( lhs: LosslessValueCodable < Strategy > , rhs: LosslessValueCodable < Strategy > ) -> Bool {
7761 return lhs. wrappedValue == rhs. wrappedValue
7862 }
7963}
8064
81- extension LosslessValue : Hashable where T : Hashable {
65+ extension LosslessValueCodable : Hashable where Strategy . Value : Hashable {
8266 public func hash( into hasher: inout Hasher ) {
8367 hasher. combine ( wrappedValue)
8468 }
8569}
70+
71+ public struct LosslessDefaultStrategy < Value: LosslessStringCodable > : LosslessDecodingStrategy {
72+ public static var losslessDecodableTypes : [ ( Decoder ) -> LosslessStringCodable ? ] {
73+ @inline ( __always)
74+ func decode< T: LosslessStringCodable > ( _: T . Type ) -> ( Decoder ) -> LosslessStringCodable ? {
75+ return { try ? T . init ( from: $0) }
76+ }
77+
78+ return [
79+ decode ( String . self) ,
80+ decode ( Bool . self) ,
81+ decode ( Int . self) ,
82+ decode ( Int8 . self) ,
83+ decode ( Int16 . self) ,
84+ decode ( Int64 . self) ,
85+ decode ( UInt . self) ,
86+ decode ( UInt8 . self) ,
87+ decode ( UInt16 . self) ,
88+ decode ( UInt64 . self) ,
89+ decode ( Double . self) ,
90+ decode ( Float . self) ,
91+ ]
92+ }
93+ }
94+
95+ public struct LosslessBooleanStrategy < Value: LosslessStringCodable > : LosslessDecodingStrategy {
96+ public static var losslessDecodableTypes : [ ( Decoder ) -> LosslessStringCodable ? ] {
97+ @inline ( __always)
98+ func decode< T: LosslessStringCodable > ( _: T . Type ) -> ( Decoder ) -> LosslessStringCodable ? {
99+ return { try ? T . init ( from: $0) }
100+ }
101+
102+ @inline ( __always)
103+ func decodeBoolFromNSNumber( ) -> ( Decoder ) -> LosslessStringCodable ? {
104+ return { ( try ? Int . init ( from: $0) ) . flatMap { Bool ( exactly: NSNumber ( value: $0) ) } }
105+ }
106+
107+ return [
108+ decode ( String . self) ,
109+ decodeBoolFromNSNumber ( ) ,
110+ decode ( Bool . self) ,
111+ decode ( Int . self) ,
112+ decode ( Int8 . self) ,
113+ decode ( Int16 . self) ,
114+ decode ( Int64 . self) ,
115+ decode ( UInt . self) ,
116+ decode ( UInt8 . self) ,
117+ decode ( UInt16 . self) ,
118+ decode ( UInt64 . self) ,
119+ decode ( Double . self) ,
120+ decode ( Float . self) ,
121+ ]
122+ }
123+ }
124+
125+ /// Decodes Codable values into their respective preferred types.
126+ ///
127+ /// `@LosslessValue` attempts to decode Codable types into their respective preferred types while preserving the data.
128+ ///
129+ /// This is useful when data may return unpredictable values when a consumer is expecting a certain type. For instance,
130+ /// if an API sends SKUs as either an `Int` or `String`, then a `@LosslessValue` can ensure the types are always decoded
131+ /// as `String`s.
132+ ///
133+ /// ```
134+ /// struct Product: Codable {
135+ /// @LosslessValue var sku: String
136+ /// @LosslessValue var id: String
137+ /// }
138+ ///
139+ /// // json: { "sku": 87, "id": 123 }
140+ /// let value = try JSONDecoder().decode(Product.self, from: json)
141+ /// // value.sku == "87"
142+ /// // value.id == "123"
143+ /// ```
144+ public typealias LosslessValue < T> = LosslessValueCodable < LosslessDefaultStrategy < T > > where T: LosslessStringCodable
145+
146+ /// Decodes Codable values into their respective preferred types.
147+ ///
148+ /// `@LosslessBoolValue` attempts to decode Codable types into their respective preferred types while preserving the data.
149+ ///
150+ /// - Note:
151+ /// This uses a `LosslessBooleanStrategy` in order to prioritize boolean values, and as such, some integer values will be lossy.
152+ ///
153+ /// For instance, if you decode `{ "some_type": 1 }` then `some_type` will be `true` and not `1`. If you do not want this
154+ /// behavior then use `@LosslessValue` or create a custom `LosslessDecodingStrategy`.
155+ ///
156+ /// ```
157+ /// struct Example: Codable {
158+ /// @LosslessBoolValue var foo: Bool
159+ /// @LosslessValue var bar: Int
160+ /// }
161+ ///
162+ /// // json: { "foo": 1, "bar": 2 }
163+ /// let value = try JSONDecoder().decode(Fixture.self, from: json)
164+ /// // value.foo == true
165+ /// // value.bar == 2
166+ /// ```
167+ public typealias LosslessBoolValue < T> = LosslessValueCodable < LosslessBooleanStrategy < T > > where T: LosslessStringCodable
0 commit comments