Skip to content

Commit 6089cb5

Browse files
committed
feat(HelperCoders): added basic data types decoding helpers
1 parent 2192f11 commit 6089cb5

File tree

7 files changed

+600
-1
lines changed

7 files changed

+600
-1
lines changed

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let package = Package(
1414
],
1515
products: [
1616
.library(name: "MetaCodable", targets: ["MetaCodable"]),
17+
.library(name: "HelperCoders", targets: ["HelperCoders"]),
1718
],
1819
dependencies: [
1920
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
@@ -34,10 +35,11 @@ let package = Package(
3435
]
3536
),
3637
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),
38+
.target(name: "HelperCoders", dependencies: ["MetaCodable"]),
3739
.testTarget(
3840
name: "MetaCodableTests",
3941
dependencies: [
40-
"CodableMacroPlugin", "MetaCodable",
42+
"CodableMacroPlugin", "MetaCodable", "HelperCoders",
4143
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
4244
]
4345
),
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ``HelperCoders``
2+
3+
@Metadata {
4+
@Available(swift, introduced: "5.9")
5+
}
6+
7+
Level up ``/MetaCodable``'s generated implementations with helpers assisting common decoding/encoding requirements.
8+
9+
## Overview
10+
11+
`HelperCoders` aims to provide collection of helpers that can be used for common decoding/encoding tasks, reducing boilerplate. Some of the examples include:
12+
13+
- Decoding basic data type (i.e `Bool`, `Int`, `String`) from any other basic data types (i.e `Bool`, `Int`, `String`).
14+
15+
## Installation
16+
17+
@TabNavigator {
18+
@Tab("Swift Package Manager") {
19+
20+
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler.
21+
22+
Once you have your Swift package set up, adding `MetaCodable` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
23+
24+
```swift
25+
.package(url: "https://github.com/SwiftyLab/MetaCodable.git", from: "1.0.0"),
26+
```
27+
28+
Then you can add the `HelperCoders` module product as dependency to the `target`s of your choosing, by adding it to the `dependencies` value of your `target`s.
29+
30+
```swift
31+
.product(name: "HelperCoders", package: "MetaCodable"),
32+
```
33+
}
34+
}
35+
36+
## Topics
37+
38+
### Basic Data
39+
40+
- ``ValueCoder``
41+
- ``ValueCodingStrategy``
42+
- ``NonConformingCoder``
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Foundation
2+
3+
extension Bool: ValueCodingStrategy {
4+
/// Decodes boolean data from the given `decoder`.
5+
///
6+
/// Decodes basic data type `Bool`, `String`, `Int`, `Float`
7+
/// and converts to boolean representation with following rules.
8+
/// * For `Int` and `Float` types, `1` is mapped to `true`
9+
/// and `0` to `false`, rest throw `DecodingError.typeMismatch` error.
10+
/// * For `String`` type, `1`, `y`, `t`, `yes`, `true` are mapped to
11+
/// `true` and `0`, `n`, `f`, `no`, `false` to `false`,
12+
/// rest throw `DecodingError.typeMismatch` error.
13+
///
14+
/// - Parameter decoder: The decoder to read data from.
15+
/// - Returns: The decoded boolean.
16+
///
17+
/// - Throws: If decoding fails due to corrupted or invalid basic data.
18+
public static func decode(from decoder: Decoder) throws -> Bool {
19+
do {
20+
return try Self(from: decoder)
21+
} catch {
22+
let fallbacks: [(Decoder) throws -> Bool?] = [
23+
String.boolValue,
24+
Int.boolValue,
25+
Float.boolValue,
26+
]
27+
guard
28+
let value = try fallbacks.lazy.compactMap({
29+
return try $0(decoder)
30+
}).first
31+
else { throw error }
32+
return value
33+
}
34+
}
35+
}
36+
37+
private extension String {
38+
/// Decodes optional boolean data from the given `decoder`.
39+
///
40+
/// - Parameter decoder: The decoder to read data from.
41+
/// - Returns: The decoded boolean matching representation,
42+
/// `nil` otherwise.
43+
///
44+
/// - Throws: If decoded data doesn't match boolean representation.
45+
static func boolValue(from decoder: Decoder) throws -> Bool? {
46+
guard let str = try? Self(from: decoder) else { return nil }
47+
let strValue = str.trimmingCharacters(in: .whitespacesAndNewlines)
48+
.lowercased()
49+
switch strValue {
50+
case "1", "y", "t", "yes", "true":
51+
return true
52+
case "0", "n", "f", "no", "false":
53+
return false
54+
default:
55+
switch Double(strValue) {
56+
case 1:
57+
return true
58+
case 0:
59+
return false
60+
case .some:
61+
throw DecodingError.typeMismatch(
62+
Bool.self,
63+
.init(
64+
codingPath: decoder.codingPath,
65+
debugDescription: """
66+
"\(self)" can't be represented as Boolean
67+
"""
68+
)
69+
)
70+
case .none:
71+
return nil
72+
}
73+
}
74+
}
75+
}
76+
77+
private extension ExpressibleByIntegerLiteral
78+
where Self: Decodable, Self: Equatable {
79+
/// Decodes optional boolean data from the given `decoder`.
80+
///
81+
/// - Parameter decoder: The decoder to read data from.
82+
/// - Returns: `true` if decoded `1`, `false` if decoded `0`,
83+
/// `nil` if data of current type couldn't be decoded.
84+
///
85+
/// - Throws: If decoded data doesn't match `0` or `1`.
86+
static func boolValue(from decoder: Decoder) throws -> Bool? {
87+
switch try? Self(from: decoder) {
88+
case 1:
89+
return true
90+
case 0:
91+
return false
92+
case .some:
93+
throw DecodingError.typeMismatch(
94+
Bool.self,
95+
.init(
96+
codingPath: decoder.codingPath,
97+
debugDescription: """
98+
"\(self)" can't be represented as Boolean
99+
"""
100+
)
101+
)
102+
case .none:
103+
return nil
104+
}
105+
}
106+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/// A ``ValueCodingStrategy`` type that specializes decoding/encoding
2+
/// numeric data.
3+
protocol NumberCodingStrategy: ValueCodingStrategy where Value == Self {}
4+
5+
public extension ValueCodingStrategy
6+
where Value: Decodable & ExpressibleByIntegerLiteral & LosslessStringConvertible
7+
{
8+
/// Decodes numeric data from the given `decoder`.
9+
///
10+
/// Decodes basic data type `String,` `Bool`
11+
/// and converts to numeric representation.
12+
///
13+
/// For decoded boolean type `true` is mapped to `1`
14+
/// and `false` to `0`.
15+
///
16+
/// - Parameter decoder: The decoder to read data from.
17+
/// - Returns: The decoded number.
18+
///
19+
/// - Throws: If decoding fails due to corrupted or invalid data
20+
/// or decoded data can't be mapped to numeric type.
21+
static func decode(from decoder: Decoder) throws -> Value {
22+
do {
23+
return try Value(from: decoder)
24+
} catch {
25+
let fallbacks: [(Decoder) -> Value?] = [
26+
String.numberValue,
27+
Bool.numberValue,
28+
Double.numberValue,
29+
]
30+
guard let value = fallbacks.lazy.compactMap({ $0(decoder) }).first
31+
else { throw error }
32+
return value
33+
}
34+
}
35+
}
36+
37+
private extension Bool {
38+
/// Decodes optional numeric data from the given `decoder`.
39+
///
40+
/// - Parameter decoder: The decoder to read data from.
41+
/// - Returns: The decoded number value, `nil` if boolean
42+
/// data can't be decoded.
43+
static func numberValue<Number>(
44+
from decoder: Decoder
45+
) -> Number? where Number: ExpressibleByIntegerLiteral {
46+
guard let boolValue = try? Self(from: decoder) else { return nil }
47+
return boolValue ? 1 : 0
48+
}
49+
}
50+
51+
private extension String {
52+
/// Decodes optional numeric data from the given `decoder`.
53+
///
54+
/// - Parameter decoder: The decoder to read data from.
55+
/// - Returns: The decoded number value, `nil` if text data
56+
/// can't be decoded or converted to numeric representation.
57+
static func numberValue<Number>(
58+
from decoder: Decoder
59+
) -> Number?
60+
where Number: LosslessStringConvertible & ExpressibleByIntegerLiteral {
61+
guard let strValue = try? Self(from: decoder) else { return nil }
62+
return Number(strValue) ?? Number(exact: Double(strValue))
63+
}
64+
}
65+
66+
internal extension Double {
67+
/// Decodes optional numeric data from the given `decoder`.
68+
///
69+
/// - Parameter decoder: The decoder to read data from.
70+
/// - Returns: The decoded number value, `nil` if float
71+
/// data can't be converted to exact number value.
72+
@inlinable
73+
static func numberValue<Number>(
74+
from decoder: Decoder
75+
) -> Number? where Number: ExpressibleByIntegerLiteral {
76+
return Number(exact: try? Self(from: decoder))
77+
}
78+
}
79+
80+
internal extension ExpressibleByIntegerLiteral {
81+
/// Converts optional given float to integer.
82+
///
83+
/// - Parameter float: The float value to convert.
84+
/// - Returns: The integer value, `nil` if float
85+
/// data can't be converted to exact integer value.
86+
@usableFromInline
87+
init?(exact float: Double?) {
88+
guard
89+
let float = float,
90+
let type = Self.self as? any BinaryInteger.Type,
91+
let intVal = type.init(exactly: float) as (any BinaryInteger)?,
92+
let val = intVal as? Self
93+
else { return nil }
94+
self = val
95+
}
96+
}
97+
98+
extension Double: NumberCodingStrategy {}
99+
extension Float: NumberCodingStrategy {}
100+
extension Int: NumberCodingStrategy {}
101+
extension Int64: NumberCodingStrategy {}
102+
extension Int32: NumberCodingStrategy {}
103+
extension Int16: NumberCodingStrategy {}
104+
extension Int8: NumberCodingStrategy {}
105+
extension UInt: NumberCodingStrategy {}
106+
extension UInt64: NumberCodingStrategy {}
107+
extension UInt32: NumberCodingStrategy {}
108+
extension UInt16: NumberCodingStrategy {}
109+
extension UInt8: NumberCodingStrategy {}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
extension String: ValueCodingStrategy {
2+
/// Decodes text data from the given `decoder`.
3+
///
4+
/// Decodes basic data type `String,` `Bool`, `Int`, `UInt`,
5+
/// `Double` and converts to string representation.
6+
///
7+
/// - Parameter decoder: The decoder to read data from.
8+
/// - Returns: The decoded text.
9+
///
10+
/// - Throws: If decoding fails due to corrupted or invalid data
11+
/// or couldn't decode basic data type.
12+
public static func decode(from decoder: Decoder) throws -> String {
13+
do {
14+
return try Self(from: decoder)
15+
} catch {
16+
let fallbackTypes: [(Decodable & CustomStringConvertible).Type] = [
17+
Bool.self,
18+
Int.self,
19+
UInt.self,
20+
Double.self,
21+
]
22+
guard
23+
let value = fallbackTypes.lazy.compactMap({
24+
return (try? $0.init(from: decoder))?.description
25+
}).first
26+
else { throw error }
27+
return value
28+
}
29+
}
30+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import MetaCodable
2+
3+
/// An ``/MetaCodable/HelperCoder`` that helps decoding/encoding
4+
/// basic value types.
5+
///
6+
/// This type can be used to decode/encode dates
7+
/// basic value types, i.e. `Bool`, `Int`, `String` etc.
8+
public struct ValueCoder<Strategy: ValueCodingStrategy>: HelperCoder {
9+
/// Creates a new instance of ``/MetaCodable/HelperCoder`` that decodes/encodes
10+
/// basic value types.
11+
///
12+
/// - Returns: A new basic value type decoder/encoder.
13+
public init() {}
14+
15+
/// Decodes value with the provided `Strategy` from the given `decoder`.
16+
///
17+
/// - Parameter decoder: The decoder to read data from.
18+
/// - Returns: The decoded basic value.
19+
///
20+
/// - Throws: If the provided `Strategy` fails decoding.
21+
@inlinable
22+
public func decode(from decoder: Decoder) throws -> Strategy.Value {
23+
return try Strategy.decode(from: decoder)
24+
}
25+
26+
/// Encodes value with the provided `Strategy` to the given `encoder`.
27+
///
28+
/// - Parameters:
29+
/// - value: The decoded basic value to encode.
30+
/// - encoder: The encoder to write data to.
31+
///
32+
/// - Throws: If the provided `Strategy` fails encoding.
33+
@inlinable
34+
public func encode(_ value: Strategy.Value, to encoder: Encoder) throws {
35+
return try Strategy.encode(value, to: encoder)
36+
}
37+
}
38+
39+
/// A type that helps to decode and encode underlying ``Value`` type
40+
/// from provided `decoder` and to provided `encoder` respectively.
41+
///
42+
/// This type can be used with ``ValueCoder`` to allow
43+
/// decoding/encoding customizations basic value types,
44+
/// i.e. `Bool`, `Int`, `String` etc.
45+
public protocol ValueCodingStrategy {
46+
/// The actual type of value that is going to be decoded/encoded.
47+
///
48+
/// This type can be any basic value type.
49+
associatedtype Value
50+
/// Decodes a value of the ``Value`` type from the given `decoder`.
51+
///
52+
/// - Parameter decoder: The decoder to read data from.
53+
/// - Returns: A value of the ``Value`` type.
54+
///
55+
/// - Throws: If decoding fails due to corrupted or invalid data.
56+
static func decode(from decoder: Decoder) throws -> Value
57+
/// Encodes given value of the ``Value`` type to the provided `encoder`.
58+
///
59+
/// By default, if the ``Value`` value confirms to `Encodable`,
60+
/// then encoding is performed. Otherwise no data written to the encoder.
61+
///
62+
/// - Parameters:
63+
/// - value: The ``Value`` value to encode.
64+
/// - encoder: The encoder to write data to.
65+
///
66+
/// - Throws: If any values are invalid for the given encoder’s format.
67+
static func encode(_ value: Value, to encoder: Encoder) throws
68+
}
69+
70+
public extension ValueCodingStrategy where Value: Encodable {
71+
/// Encodes given value of the ``ValueCodingStrategy/Value`` type
72+
/// to the provided `encoder`.
73+
///
74+
/// The ``ValueCodingStrategy/Value`` value is written to the encoder.
75+
///
76+
/// - Parameters:
77+
/// - value: The ``ValueCodingStrategy/Value`` value to encode.
78+
/// - encoder: The encoder to write data to.
79+
///
80+
/// - Throws: If any values are invalid for the given encoder’s format.
81+
@inlinable
82+
static func encode(_ value: Value, to encoder: Encoder) throws {
83+
try value.encode(to: encoder)
84+
}
85+
}

0 commit comments

Comments
 (0)