-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(
HelperCoders
): added basic data types decoding helpers
- Loading branch information
1 parent
2192f11
commit 6089cb5
Showing
7 changed files
with
600 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# ``HelperCoders`` | ||
|
||
@Metadata { | ||
@Available(swift, introduced: "5.9") | ||
} | ||
|
||
Level up ``/MetaCodable``'s generated implementations with helpers assisting common decoding/encoding requirements. | ||
|
||
## Overview | ||
|
||
`HelperCoders` aims to provide collection of helpers that can be used for common decoding/encoding tasks, reducing boilerplate. Some of the examples include: | ||
|
||
- Decoding basic data type (i.e `Bool`, `Int`, `String`) from any other basic data types (i.e `Bool`, `Int`, `String`). | ||
|
||
## Installation | ||
|
||
@TabNavigator { | ||
@Tab("Swift Package Manager") { | ||
|
||
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. | ||
|
||
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`. | ||
|
||
```swift | ||
.package(url: "https://github.com/SwiftyLab/MetaCodable.git", from: "1.0.0"), | ||
``` | ||
|
||
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. | ||
|
||
```swift | ||
.product(name: "HelperCoders", package: "MetaCodable"), | ||
``` | ||
} | ||
} | ||
|
||
## Topics | ||
|
||
### Basic Data | ||
|
||
- ``ValueCoder`` | ||
- ``ValueCodingStrategy`` | ||
- ``NonConformingCoder`` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import Foundation | ||
|
||
extension Bool: ValueCodingStrategy { | ||
/// Decodes boolean data from the given `decoder`. | ||
/// | ||
/// Decodes basic data type `Bool`, `String`, `Int`, `Float` | ||
/// and converts to boolean representation with following rules. | ||
/// * For `Int` and `Float` types, `1` is mapped to `true` | ||
/// and `0` to `false`, rest throw `DecodingError.typeMismatch` error. | ||
/// * For `String`` type, `1`, `y`, `t`, `yes`, `true` are mapped to | ||
/// `true` and `0`, `n`, `f`, `no`, `false` to `false`, | ||
/// rest throw `DecodingError.typeMismatch` error. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded boolean. | ||
/// | ||
/// - Throws: If decoding fails due to corrupted or invalid basic data. | ||
public static func decode(from decoder: Decoder) throws -> Bool { | ||
do { | ||
return try Self(from: decoder) | ||
} catch { | ||
let fallbacks: [(Decoder) throws -> Bool?] = [ | ||
String.boolValue, | ||
Int.boolValue, | ||
Float.boolValue, | ||
] | ||
guard | ||
let value = try fallbacks.lazy.compactMap({ | ||
return try $0(decoder) | ||
}).first | ||
else { throw error } | ||
return value | ||
} | ||
} | ||
} | ||
|
||
private extension String { | ||
/// Decodes optional boolean data from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded boolean matching representation, | ||
/// `nil` otherwise. | ||
/// | ||
/// - Throws: If decoded data doesn't match boolean representation. | ||
static func boolValue(from decoder: Decoder) throws -> Bool? { | ||
guard let str = try? Self(from: decoder) else { return nil } | ||
let strValue = str.trimmingCharacters(in: .whitespacesAndNewlines) | ||
.lowercased() | ||
switch strValue { | ||
case "1", "y", "t", "yes", "true": | ||
return true | ||
case "0", "n", "f", "no", "false": | ||
return false | ||
default: | ||
switch Double(strValue) { | ||
case 1: | ||
return true | ||
case 0: | ||
return false | ||
case .some: | ||
throw DecodingError.typeMismatch( | ||
Bool.self, | ||
.init( | ||
codingPath: decoder.codingPath, | ||
debugDescription: """ | ||
"\(self)" can't be represented as Boolean | ||
""" | ||
) | ||
) | ||
case .none: | ||
return nil | ||
} | ||
} | ||
} | ||
} | ||
|
||
private extension ExpressibleByIntegerLiteral | ||
where Self: Decodable, Self: Equatable { | ||
/// Decodes optional boolean data from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: `true` if decoded `1`, `false` if decoded `0`, | ||
/// `nil` if data of current type couldn't be decoded. | ||
/// | ||
/// - Throws: If decoded data doesn't match `0` or `1`. | ||
static func boolValue(from decoder: Decoder) throws -> Bool? { | ||
switch try? Self(from: decoder) { | ||
case 1: | ||
return true | ||
case 0: | ||
return false | ||
case .some: | ||
throw DecodingError.typeMismatch( | ||
Bool.self, | ||
.init( | ||
codingPath: decoder.codingPath, | ||
debugDescription: """ | ||
"\(self)" can't be represented as Boolean | ||
""" | ||
) | ||
) | ||
case .none: | ||
return nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/// A ``ValueCodingStrategy`` type that specializes decoding/encoding | ||
/// numeric data. | ||
protocol NumberCodingStrategy: ValueCodingStrategy where Value == Self {} | ||
|
||
public extension ValueCodingStrategy | ||
where Value: Decodable & ExpressibleByIntegerLiteral & LosslessStringConvertible | ||
{ | ||
/// Decodes numeric data from the given `decoder`. | ||
/// | ||
/// Decodes basic data type `String,` `Bool` | ||
/// and converts to numeric representation. | ||
/// | ||
/// For decoded boolean type `true` is mapped to `1` | ||
/// and `false` to `0`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded number. | ||
/// | ||
/// - Throws: If decoding fails due to corrupted or invalid data | ||
/// or decoded data can't be mapped to numeric type. | ||
static func decode(from decoder: Decoder) throws -> Value { | ||
do { | ||
return try Value(from: decoder) | ||
} catch { | ||
let fallbacks: [(Decoder) -> Value?] = [ | ||
String.numberValue, | ||
Bool.numberValue, | ||
Double.numberValue, | ||
] | ||
guard let value = fallbacks.lazy.compactMap({ $0(decoder) }).first | ||
else { throw error } | ||
return value | ||
} | ||
} | ||
} | ||
|
||
private extension Bool { | ||
/// Decodes optional numeric data from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded number value, `nil` if boolean | ||
/// data can't be decoded. | ||
static func numberValue<Number>( | ||
from decoder: Decoder | ||
) -> Number? where Number: ExpressibleByIntegerLiteral { | ||
guard let boolValue = try? Self(from: decoder) else { return nil } | ||
return boolValue ? 1 : 0 | ||
} | ||
} | ||
|
||
private extension String { | ||
/// Decodes optional numeric data from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded number value, `nil` if text data | ||
/// can't be decoded or converted to numeric representation. | ||
static func numberValue<Number>( | ||
from decoder: Decoder | ||
) -> Number? | ||
where Number: LosslessStringConvertible & ExpressibleByIntegerLiteral { | ||
guard let strValue = try? Self(from: decoder) else { return nil } | ||
return Number(strValue) ?? Number(exact: Double(strValue)) | ||
} | ||
} | ||
|
||
internal extension Double { | ||
/// Decodes optional numeric data from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded number value, `nil` if float | ||
/// data can't be converted to exact number value. | ||
@inlinable | ||
static func numberValue<Number>( | ||
from decoder: Decoder | ||
) -> Number? where Number: ExpressibleByIntegerLiteral { | ||
return Number(exact: try? Self(from: decoder)) | ||
} | ||
} | ||
|
||
internal extension ExpressibleByIntegerLiteral { | ||
/// Converts optional given float to integer. | ||
/// | ||
/// - Parameter float: The float value to convert. | ||
/// - Returns: The integer value, `nil` if float | ||
/// data can't be converted to exact integer value. | ||
@usableFromInline | ||
init?(exact float: Double?) { | ||
guard | ||
let float = float, | ||
let type = Self.self as? any BinaryInteger.Type, | ||
let intVal = type.init(exactly: float) as (any BinaryInteger)?, | ||
let val = intVal as? Self | ||
else { return nil } | ||
self = val | ||
} | ||
} | ||
|
||
extension Double: NumberCodingStrategy {} | ||
extension Float: NumberCodingStrategy {} | ||
extension Int: NumberCodingStrategy {} | ||
extension Int64: NumberCodingStrategy {} | ||
extension Int32: NumberCodingStrategy {} | ||
extension Int16: NumberCodingStrategy {} | ||
extension Int8: NumberCodingStrategy {} | ||
extension UInt: NumberCodingStrategy {} | ||
extension UInt64: NumberCodingStrategy {} | ||
extension UInt32: NumberCodingStrategy {} | ||
extension UInt16: NumberCodingStrategy {} | ||
extension UInt8: NumberCodingStrategy {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
extension String: ValueCodingStrategy { | ||
/// Decodes text data from the given `decoder`. | ||
/// | ||
/// Decodes basic data type `String,` `Bool`, `Int`, `UInt`, | ||
/// `Double` and converts to string representation. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded text. | ||
/// | ||
/// - Throws: If decoding fails due to corrupted or invalid data | ||
/// or couldn't decode basic data type. | ||
public static func decode(from decoder: Decoder) throws -> String { | ||
do { | ||
return try Self(from: decoder) | ||
} catch { | ||
let fallbackTypes: [(Decodable & CustomStringConvertible).Type] = [ | ||
Bool.self, | ||
Int.self, | ||
UInt.self, | ||
Double.self, | ||
] | ||
guard | ||
let value = fallbackTypes.lazy.compactMap({ | ||
return (try? $0.init(from: decoder))?.description | ||
}).first | ||
else { throw error } | ||
return value | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import MetaCodable | ||
|
||
/// An ``/MetaCodable/HelperCoder`` that helps decoding/encoding | ||
/// basic value types. | ||
/// | ||
/// This type can be used to decode/encode dates | ||
/// basic value types, i.e. `Bool`, `Int`, `String` etc. | ||
public struct ValueCoder<Strategy: ValueCodingStrategy>: HelperCoder { | ||
/// Creates a new instance of ``/MetaCodable/HelperCoder`` that decodes/encodes | ||
/// basic value types. | ||
/// | ||
/// - Returns: A new basic value type decoder/encoder. | ||
public init() {} | ||
|
||
/// Decodes value with the provided `Strategy` from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The decoded basic value. | ||
/// | ||
/// - Throws: If the provided `Strategy` fails decoding. | ||
@inlinable | ||
public func decode(from decoder: Decoder) throws -> Strategy.Value { | ||
return try Strategy.decode(from: decoder) | ||
} | ||
|
||
/// Encodes value with the provided `Strategy` to the given `encoder`. | ||
/// | ||
/// - Parameters: | ||
/// - value: The decoded basic value to encode. | ||
/// - encoder: The encoder to write data to. | ||
/// | ||
/// - Throws: If the provided `Strategy` fails encoding. | ||
@inlinable | ||
public func encode(_ value: Strategy.Value, to encoder: Encoder) throws { | ||
return try Strategy.encode(value, to: encoder) | ||
} | ||
} | ||
|
||
/// A type that helps to decode and encode underlying ``Value`` type | ||
/// from provided `decoder` and to provided `encoder` respectively. | ||
/// | ||
/// This type can be used with ``ValueCoder`` to allow | ||
/// decoding/encoding customizations basic value types, | ||
/// i.e. `Bool`, `Int`, `String` etc. | ||
public protocol ValueCodingStrategy { | ||
/// The actual type of value that is going to be decoded/encoded. | ||
/// | ||
/// This type can be any basic value type. | ||
associatedtype Value | ||
/// Decodes a value of the ``Value`` type from the given `decoder`. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: A value of the ``Value`` type. | ||
/// | ||
/// - Throws: If decoding fails due to corrupted or invalid data. | ||
static func decode(from decoder: Decoder) throws -> Value | ||
/// Encodes given value of the ``Value`` type to the provided `encoder`. | ||
/// | ||
/// By default, if the ``Value`` value confirms to `Encodable`, | ||
/// then encoding is performed. Otherwise no data written to the encoder. | ||
/// | ||
/// - Parameters: | ||
/// - value: The ``Value`` value to encode. | ||
/// - encoder: The encoder to write data to. | ||
/// | ||
/// - Throws: If any values are invalid for the given encoder’s format. | ||
static func encode(_ value: Value, to encoder: Encoder) throws | ||
} | ||
|
||
public extension ValueCodingStrategy where Value: Encodable { | ||
/// Encodes given value of the ``ValueCodingStrategy/Value`` type | ||
/// to the provided `encoder`. | ||
/// | ||
/// The ``ValueCodingStrategy/Value`` value is written to the encoder. | ||
/// | ||
/// - Parameters: | ||
/// - value: The ``ValueCodingStrategy/Value`` value to encode. | ||
/// - encoder: The encoder to write data to. | ||
/// | ||
/// - Throws: If any values are invalid for the given encoder’s format. | ||
@inlinable | ||
static func encode(_ value: Value, to encoder: Encoder) throws { | ||
try value.encode(to: encoder) | ||
} | ||
} |
Oops, something went wrong.