Skip to content

Commit

Permalink
feat(HelperCoders): added basic data types decoding helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Nov 2, 2023
1 parent 2192f11 commit 6089cb5
Show file tree
Hide file tree
Showing 7 changed files with 600 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let package = Package(
],
products: [
.library(name: "MetaCodable", targets: ["MetaCodable"]),
.library(name: "HelperCoders", targets: ["HelperCoders"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
Expand All @@ -34,10 +35,11 @@ let package = Package(
]
),
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),
.target(name: "HelperCoders", dependencies: ["MetaCodable"]),
.testTarget(
name: "MetaCodableTests",
dependencies: [
"CodableMacroPlugin", "MetaCodable",
"CodableMacroPlugin", "MetaCodable", "HelperCoders",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
Expand Down
42 changes: 42 additions & 0 deletions Sources/HelperCoders/HelperCoders.docc/HelperCoders.md
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``
106 changes: 106 additions & 0 deletions Sources/HelperCoders/ValueCoders/Bool.swift
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
}
}
}
109 changes: 109 additions & 0 deletions Sources/HelperCoders/ValueCoders/Number.swift
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 {}
30 changes: 30 additions & 0 deletions Sources/HelperCoders/ValueCoders/String.swift
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
}
}
}
85 changes: 85 additions & 0 deletions Sources/HelperCoders/ValueCoders/ValueCoder.swift
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)
}
}

0 comments on commit 6089cb5

Please sign in to comment.