@@ -2,84 +2,166 @@ import Foundation
2
2
3
3
public typealias LosslessStringCodable = LosslessStringConvertible & Codable
4
4
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
+
5
16
/// Decodes Codable values into their respective preferred types.
6
17
///
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 .
8
19
///
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`.
12
21
@propertyWrapper
13
- public struct LosslessValue < T : LosslessStringCodable > : Codable {
22
+ public struct LosslessValueCodable < Strategy : LosslessDecodingStrategy > : Codable {
14
23
private let type : LosslessStringCodable . Type
15
-
16
- public var wrappedValue : T
17
24
18
- public init ( wrappedValue: T ) {
25
+ public var wrappedValue : Strategy . Value
26
+
27
+ public init ( wrappedValue: Strategy . Value ) {
19
28
self . wrappedValue = wrappedValue
20
- self . type = T . self
29
+ self . type = Strategy . Value . self
21
30
}
22
-
31
+
23
32
public init ( from decoder: Decoder ) throws {
24
33
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
28
36
} 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
-
53
37
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
+
58
42
self . wrappedValue = value
59
43
self . type = Swift . type ( of: rawValue)
60
44
}
61
45
}
62
-
46
+
63
47
public func encode( to encoder: Encoder ) throws {
64
48
let string = String ( describing: wrappedValue)
65
-
49
+
66
50
guard let original = type. init ( string) else {
67
51
let description = " Unable to encode ' \( wrappedValue) ' back to source type ' \( type) ' "
68
52
throw EncodingError . invalidValue ( string, . init( codingPath: [ ] , debugDescription: description) )
69
53
}
70
-
54
+
71
55
try original. encode ( to: encoder)
72
56
}
73
57
}
74
58
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 {
77
61
return lhs. wrappedValue == rhs. wrappedValue
78
62
}
79
63
}
80
64
81
- extension LosslessValue : Hashable where T : Hashable {
65
+ extension LosslessValueCodable : Hashable where Strategy . Value : Hashable {
82
66
public func hash( into hasher: inout Hasher ) {
83
67
hasher. combine ( wrappedValue)
84
68
}
85
69
}
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