Skip to content

Commit

Permalink
Merge pull request #8 from MFB-Technologies-Inc/feature/improve-decod…
Browse files Browse the repository at this point in the history
…able-configuration

Feature/improve decodable configuration
  • Loading branch information
roanutil authored Jun 2, 2023
2 parents faa43b0 + 38bdd47 commit 787941b
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 10 deletions.
55 changes: 55 additions & 0 deletions Sources/ArgumentEncoding/DecoderUserInfo+OptionHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// DecoderUserInfo+OptionHelpers.swift
// ArgumentEncoding
//
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.

import Foundation

/// Helper functions for configuring a decoder's `userInfo` dictionary for decoding `Option`.
/// Each of the overloads that does not require the configuration closure, will configure both
/// `Option<T>` and `Option<T?>`.
///
/// ```swift
/// struct Container: ArgumentGroup, FormatterNode {
/// let flagFormatter: FlagFormatter = .init(prefix: .doubleDash)
/// let optionFormatter: OptionFormatter = .init(prefix: .doubleDash)
/// @Option var option: String = "value"
/// }
/// let encoded = try JSONEncoder().encode(Container())
/// let decoder = JSONDecoder()
/// decoder.userInfo.addOptionConfiguration(for: String.self)
/// let decoded = try decoder.decode(Container.self, from: encoded)
/// // decoded = ["--option", "value"]
/// ```
extension [CodingUserInfoKey: Any] {
public mutating func addOptionConfiguration<T>(
for _: T.Type,
configuration: @escaping Option<T>.DecodingConfiguration
) where T: Decodable {
guard let key = Option<T>.configurationCodingUserInfoKey() else {
return
}
self[key] = configuration
}

public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable,
T: CustomStringConvertible
{
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
addOptionConfiguration(for: T.self, configuration: Option<T?>.unwrap(_:))
}

public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable, T: RawRepresentable,
T.RawValue: CustomStringConvertible
{
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
addOptionConfiguration(for: T.self, configuration: { $0.rawValue.description })
}

public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable, T: CustomStringConvertible,
T: RawRepresentable, T.RawValue: CustomStringConvertible
{
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
addOptionConfiguration(for: T.self, configuration: Option<T?>.unwrap(_:))
}
}
52 changes: 52 additions & 0 deletions Sources/ArgumentEncoding/DecoderUserInfo+OptionSetHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// DecoderUserInfo+OptionSetHelpers.swift
// ArgumentEncoding
//
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.

import Foundation

/// Helper functions for configuring a decoder's `userInfo` dictionary for decoding `OptionSet`.
/// Each of the overloads that does not require the configuration closure, will configure both
/// `OptionSet<T>` and `OptionSet<T?>`.
///
/// ```swift
/// struct Container: ArgumentGroup, FormatterNode {
/// let flagFormatter: FlagFormatter = .init(prefix: .doubleDash)
/// let optionFormatter: OptionFormatter = .init(prefix: .doubleDash)
/// @OptionSet var option: [String] = ["value1", "value2"]
/// }
/// let encoded = try JSONEncoder().encode(Container())
/// let decoder = JSONDecoder()
/// decoder.userInfo.addOptionConfiguration(for: String.self)
/// let decoded = try decoder.decode(Container.self, from: encoded)
/// // decoded = ["--option", "value1", "--option", "value2"]
/// ```
extension [CodingUserInfoKey: Any] {
public mutating func addOptionSetConfiguration<T>(
for _: OptionSet<T>.Type,
configuration: @escaping OptionSet<T>.DecodingConfiguration
) where T: Decodable {
guard let key = OptionSet<T>.configurationCodingUserInfoKey() else {
return
}
self[key] = configuration
}

public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
T.Element: CustomStringConvertible
{
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
}

public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
T.Element: RawRepresentable, T.Element.RawValue: CustomStringConvertible
{
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
}

public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
T.Element: CustomStringConvertible, T.Element: RawRepresentable, T.Element.RawValue: CustomStringConvertible
{
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
}
}
9 changes: 4 additions & 5 deletions Sources/ArgumentEncoding/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,7 @@ extension Option: DecodableWithConfiguration where Value: Decodable {

extension Option: Decodable where Value: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey() else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
Expand All @@ -307,11 +306,11 @@ extension Option: Decodable where Value: Decodable {
underlyingError: nil
))
}
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
try self.init(from: decoder, configuration: configuration)
}

public static func configurationCodingUserInfoKey(for _: (some Any).Type) -> CodingUserInfoKey? {
CodingUserInfoKey(rawValue: ObjectIdentifier(Self.self).debugDescription)
public static func configurationCodingUserInfoKey() -> CodingUserInfoKey? {
CodingUserInfoKey(rawValue: "\(Self.self) - " + ObjectIdentifier(Self.self).debugDescription)
}
}

Expand Down
9 changes: 4 additions & 5 deletions Sources/ArgumentEncoding/OptionSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ extension OptionSet: DecodableWithConfiguration where Value: Decodable {

extension OptionSet: Decodable where Value: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey() else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
Expand All @@ -233,11 +232,11 @@ extension OptionSet: Decodable where Value: Decodable {
underlyingError: nil
))
}
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
try self.init(from: decoder, configuration: configuration)
}

public static func configurationCodingUserInfoKey(for _: (some Any).Type) -> CodingUserInfoKey? {
CodingUserInfoKey(rawValue: ObjectIdentifier(Self.self).debugDescription)
public static func configurationCodingUserInfoKey() -> CodingUserInfoKey? {
CodingUserInfoKey(rawValue: "\(Self.self) - " + ObjectIdentifier(Self.self).debugDescription)
}
}

Expand Down
38 changes: 38 additions & 0 deletions Tests/ArgumentEncodingTests/OptionDecodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// OptionDecodingTests.swift
// ArgumentEncoding
//
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.

import ArgumentEncoding
import Foundation
import XCTest

final class OptionDecodingTests: XCTestCase {
let encoder = JSONEncoder()

func testDecodeOption() throws {
let option = Option(wrappedValue: "value")
let data = try encoder.encode(option)
let decoder = JSONDecoder()
decoder.userInfo.addOptionConfiguration(for: String.self)
let decoded = try decoder.decode(Option<String>.self, from: data)
XCTAssertEqual(decoded, option)
}

func testDecodeOptionContainer() throws {
let container = OptionContainer(option: "value")
let data = try encoder.encode(container)
let decoder = JSONDecoder()
decoder.userInfo.addOptionConfiguration(for: String.self)
let decoded = try decoder.decode(OptionContainer.self, from: data)
XCTAssertEqual(decoded, container)
}
}

private struct OptionContainer: ArgumentGroup, Codable, Equatable {
@Option var option: String

init(option: String) {
_option = Option(wrappedValue: option)
}
}
38 changes: 38 additions & 0 deletions Tests/ArgumentEncodingTests/OptionSetDecodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// OptionSetDecodingTests.swift
// ArgumentEncoding
//
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.

import ArgumentEncoding
import Foundation
import XCTest

final class OptionSetDecodingTests: XCTestCase {
let encoder = JSONEncoder()

func testDecodeOptionSet() throws {
let optionSet = OptionSet(wrappedValue: ["value1", "value2"])
let data = try encoder.encode(optionSet)
let decoder = JSONDecoder()
decoder.userInfo.addOptionSetConfiguration(for: [String].self)
let decoded = try decoder.decode(OptionSet<[String]>.self, from: data)
XCTAssertEqual(decoded, optionSet)
}

func testDecodeOptionSetContainer() throws {
let container = OptionSetContainer(option: ["value1", "value2"])
let data = try encoder.encode(container)
let decoder = JSONDecoder()
decoder.userInfo.addOptionSetConfiguration(for: [String].self)
let decoded = try decoder.decode(OptionSetContainer.self, from: data)
XCTAssertEqual(decoded, container)
}
}

private struct OptionSetContainer: ArgumentGroup, Codable, Equatable {
@OptionSet var option: [String]

init(option: [String]) {
_option = OptionSet(wrappedValue: option)
}
}

0 comments on commit 787941b

Please sign in to comment.