Skip to content

Commit 787941b

Browse files
authored
Merge pull request #8 from MFB-Technologies-Inc/feature/improve-decodable-configuration
Feature/improve decodable configuration
2 parents faa43b0 + 38bdd47 commit 787941b

File tree

6 files changed

+191
-10
lines changed

6 files changed

+191
-10
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// DecoderUserInfo+OptionHelpers.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import Foundation
7+
8+
/// Helper functions for configuring a decoder's `userInfo` dictionary for decoding `Option`.
9+
/// Each of the overloads that does not require the configuration closure, will configure both
10+
/// `Option<T>` and `Option<T?>`.
11+
///
12+
/// ```swift
13+
/// struct Container: ArgumentGroup, FormatterNode {
14+
/// let flagFormatter: FlagFormatter = .init(prefix: .doubleDash)
15+
/// let optionFormatter: OptionFormatter = .init(prefix: .doubleDash)
16+
/// @Option var option: String = "value"
17+
/// }
18+
/// let encoded = try JSONEncoder().encode(Container())
19+
/// let decoder = JSONDecoder()
20+
/// decoder.userInfo.addOptionConfiguration(for: String.self)
21+
/// let decoded = try decoder.decode(Container.self, from: encoded)
22+
/// // decoded = ["--option", "value"]
23+
/// ```
24+
extension [CodingUserInfoKey: Any] {
25+
public mutating func addOptionConfiguration<T>(
26+
for _: T.Type,
27+
configuration: @escaping Option<T>.DecodingConfiguration
28+
) where T: Decodable {
29+
guard let key = Option<T>.configurationCodingUserInfoKey() else {
30+
return
31+
}
32+
self[key] = configuration
33+
}
34+
35+
public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable,
36+
T: CustomStringConvertible
37+
{
38+
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
39+
addOptionConfiguration(for: T.self, configuration: Option<T?>.unwrap(_:))
40+
}
41+
42+
public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable, T: RawRepresentable,
43+
T.RawValue: CustomStringConvertible
44+
{
45+
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
46+
addOptionConfiguration(for: T.self, configuration: { $0.rawValue.description })
47+
}
48+
49+
public mutating func addOptionConfiguration<T>(for _: T.Type) where T: Decodable, T: CustomStringConvertible,
50+
T: RawRepresentable, T.RawValue: CustomStringConvertible
51+
{
52+
addOptionConfiguration(for: T.self, configuration: Option<T>.unwrap(_:))
53+
addOptionConfiguration(for: T.self, configuration: Option<T?>.unwrap(_:))
54+
}
55+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// DecoderUserInfo+OptionSetHelpers.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import Foundation
7+
8+
/// Helper functions for configuring a decoder's `userInfo` dictionary for decoding `OptionSet`.
9+
/// Each of the overloads that does not require the configuration closure, will configure both
10+
/// `OptionSet<T>` and `OptionSet<T?>`.
11+
///
12+
/// ```swift
13+
/// struct Container: ArgumentGroup, FormatterNode {
14+
/// let flagFormatter: FlagFormatter = .init(prefix: .doubleDash)
15+
/// let optionFormatter: OptionFormatter = .init(prefix: .doubleDash)
16+
/// @OptionSet var option: [String] = ["value1", "value2"]
17+
/// }
18+
/// let encoded = try JSONEncoder().encode(Container())
19+
/// let decoder = JSONDecoder()
20+
/// decoder.userInfo.addOptionConfiguration(for: String.self)
21+
/// let decoded = try decoder.decode(Container.self, from: encoded)
22+
/// // decoded = ["--option", "value1", "--option", "value2"]
23+
/// ```
24+
extension [CodingUserInfoKey: Any] {
25+
public mutating func addOptionSetConfiguration<T>(
26+
for _: OptionSet<T>.Type,
27+
configuration: @escaping OptionSet<T>.DecodingConfiguration
28+
) where T: Decodable {
29+
guard let key = OptionSet<T>.configurationCodingUserInfoKey() else {
30+
return
31+
}
32+
self[key] = configuration
33+
}
34+
35+
public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
36+
T.Element: CustomStringConvertible
37+
{
38+
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
39+
}
40+
41+
public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
42+
T.Element: RawRepresentable, T.Element.RawValue: CustomStringConvertible
43+
{
44+
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
45+
}
46+
47+
public mutating func addOptionSetConfiguration<T>(for _: T.Type) where T: Decodable, T: Sequence,
48+
T.Element: CustomStringConvertible, T.Element: RawRepresentable, T.Element.RawValue: CustomStringConvertible
49+
{
50+
addOptionSetConfiguration(for: OptionSet<T>.self, configuration: OptionSet<T>.unwrap(_:))
51+
}
52+
}

Sources/ArgumentEncoding/Option.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,7 @@ extension Option: DecodableWithConfiguration where Value: Decodable {
284284

285285
extension Option: Decodable where Value: Decodable {
286286
public init(from decoder: Decoder) throws {
287-
let container = try decoder.singleValueContainer()
288-
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
287+
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey() else {
289288
throw DecodingError.dataCorrupted(DecodingError.Context(
290289
codingPath: decoder.codingPath,
291290
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
@@ -307,11 +306,11 @@ extension Option: Decodable where Value: Decodable {
307306
underlyingError: nil
308307
))
309308
}
310-
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
309+
try self.init(from: decoder, configuration: configuration)
311310
}
312311

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

Sources/ArgumentEncoding/OptionSet.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,7 @@ extension OptionSet: DecodableWithConfiguration where Value: Decodable {
210210

211211
extension OptionSet: Decodable where Value: Decodable {
212212
public init(from decoder: Decoder) throws {
213-
let container = try decoder.singleValueContainer()
214-
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
213+
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey() else {
215214
throw DecodingError.dataCorrupted(DecodingError.Context(
216215
codingPath: decoder.codingPath,
217216
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
@@ -233,11 +232,11 @@ extension OptionSet: Decodable where Value: Decodable {
233232
underlyingError: nil
234233
))
235234
}
236-
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
235+
try self.init(from: decoder, configuration: configuration)
237236
}
238237

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// OptionDecodingTests.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import ArgumentEncoding
7+
import Foundation
8+
import XCTest
9+
10+
final class OptionDecodingTests: XCTestCase {
11+
let encoder = JSONEncoder()
12+
13+
func testDecodeOption() throws {
14+
let option = Option(wrappedValue: "value")
15+
let data = try encoder.encode(option)
16+
let decoder = JSONDecoder()
17+
decoder.userInfo.addOptionConfiguration(for: String.self)
18+
let decoded = try decoder.decode(Option<String>.self, from: data)
19+
XCTAssertEqual(decoded, option)
20+
}
21+
22+
func testDecodeOptionContainer() throws {
23+
let container = OptionContainer(option: "value")
24+
let data = try encoder.encode(container)
25+
let decoder = JSONDecoder()
26+
decoder.userInfo.addOptionConfiguration(for: String.self)
27+
let decoded = try decoder.decode(OptionContainer.self, from: data)
28+
XCTAssertEqual(decoded, container)
29+
}
30+
}
31+
32+
private struct OptionContainer: ArgumentGroup, Codable, Equatable {
33+
@Option var option: String
34+
35+
init(option: String) {
36+
_option = Option(wrappedValue: option)
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// OptionSetDecodingTests.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import ArgumentEncoding
7+
import Foundation
8+
import XCTest
9+
10+
final class OptionSetDecodingTests: XCTestCase {
11+
let encoder = JSONEncoder()
12+
13+
func testDecodeOptionSet() throws {
14+
let optionSet = OptionSet(wrappedValue: ["value1", "value2"])
15+
let data = try encoder.encode(optionSet)
16+
let decoder = JSONDecoder()
17+
decoder.userInfo.addOptionSetConfiguration(for: [String].self)
18+
let decoded = try decoder.decode(OptionSet<[String]>.self, from: data)
19+
XCTAssertEqual(decoded, optionSet)
20+
}
21+
22+
func testDecodeOptionSetContainer() throws {
23+
let container = OptionSetContainer(option: ["value1", "value2"])
24+
let data = try encoder.encode(container)
25+
let decoder = JSONDecoder()
26+
decoder.userInfo.addOptionSetConfiguration(for: [String].self)
27+
let decoded = try decoder.decode(OptionSetContainer.self, from: data)
28+
XCTAssertEqual(decoded, container)
29+
}
30+
}
31+
32+
private struct OptionSetContainer: ArgumentGroup, Codable, Equatable {
33+
@OptionSet var option: [String]
34+
35+
init(option: [String]) {
36+
_option = OptionSet(wrappedValue: option)
37+
}
38+
}

0 commit comments

Comments
 (0)