Skip to content

Commit 7cff84a

Browse files
authored
Merge pull request #6 from MFB-Technologies-Inc/feature/improve-option-decodable-conformance
Feature/improve option decodable conformance
2 parents 88d8003 + f734f04 commit 7cff84a

File tree

5 files changed

+165
-14
lines changed

5 files changed

+165
-14
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
run: xcrun llvm-cov export -format="lcov" .build/debug/swift-argument-encodingPackageTests.xctest/Contents/MacOS/swift-argument-encodingPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov
3737
- uses: codecov/codecov-action@v3
3838
with:
39+
token: ${{ secrets.CODECOV_TOKEN }}
3940
fail_ci_if_error: true # optional (default = false)
4041
linux-test:
4142
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Pods/
1414
.DS_Store
1515
.AppleDouble
1616
.LSOverride
17+
/.default.profraw
1718

1819
# Icon must end with two \r
1920
Icon

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/ArgumentEncoding/Option.swift

Lines changed: 121 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
55

66
import Dependencies
7+
import Foundation
78

89
/// A key/value pair argument that provides a given value for a option or variable.
910
///
@@ -112,7 +113,7 @@ extension Option where Value: CustomStringConvertible {
112113
public init(key: some CustomStringConvertible, value: Value) {
113114
keyOverride = key.description
114115
wrappedValue = value
115-
unwrap = { [$0.description] }
116+
unwrap = Self.unwrap(_:)
116117
}
117118

118119
/// Initializes a new option when used as a `@propertyWrapper`
@@ -123,7 +124,72 @@ extension Option where Value: CustomStringConvertible {
123124
public init(wrappedValue: Value, _ key: String? = nil) {
124125
keyOverride = key
125126
self.wrappedValue = wrappedValue
126-
unwrap = { [$0.description] }
127+
unwrap = Self.unwrap(_:)
128+
}
129+
130+
@Sendable
131+
public static func unwrap(_ value: Value) -> [String] {
132+
[value.description]
133+
}
134+
}
135+
136+
extension Option where Value: RawRepresentable, Value.RawValue: CustomStringConvertible {
137+
/// Initializes a new option when not used as a `@propertyWrapper`
138+
///
139+
/// - Parameters
140+
/// - key: Explicit key value
141+
/// - wrappedValue: The underlying value
142+
public init(key: some CustomStringConvertible, value: Value) {
143+
keyOverride = key.description
144+
wrappedValue = value
145+
unwrap = Self.unwrap(_:)
146+
}
147+
148+
/// Initializes a new option when used as a `@propertyWrapper`
149+
///
150+
/// - Parameters
151+
/// - wrappedValue: The underlying value
152+
/// - _ key: Optional explicit key value
153+
public init(wrappedValue: Value, _ key: String? = nil) {
154+
keyOverride = key
155+
self.wrappedValue = wrappedValue
156+
unwrap = Self.unwrap(_:)
157+
}
158+
159+
@Sendable
160+
public static func unwrap(_ value: Value) -> [String] {
161+
[value.rawValue.description]
162+
}
163+
}
164+
165+
extension Option where Value: CustomStringConvertible, Value: RawRepresentable,
166+
Value.RawValue: CustomStringConvertible
167+
{
168+
/// Initializes a new option when not used as a `@propertyWrapper`
169+
///
170+
/// - Parameters
171+
/// - key: Explicit key value
172+
/// - wrappedValue: The underlying value
173+
public init(key: some CustomStringConvertible, value: Value) {
174+
keyOverride = key.description
175+
wrappedValue = value
176+
unwrap = Self.unwrap(_:)
177+
}
178+
179+
/// Initializes a new option when used as a `@propertyWrapper`
180+
///
181+
/// - Parameters
182+
/// - wrappedValue: The underlying value
183+
/// - _ key: Optional explicit key value
184+
public init(wrappedValue: Value, _ key: String? = nil) {
185+
keyOverride = key
186+
self.wrappedValue = wrappedValue
187+
unwrap = Self.unwrap(_:)
188+
}
189+
190+
@Sendable
191+
public static func unwrap(_ value: Value) -> [String] {
192+
[value.rawValue.description]
127193
}
128194
}
129195

@@ -140,7 +206,7 @@ extension Option {
140206
{
141207
keyOverride = key.description
142208
wrappedValue = value
143-
unwrap = { [$0?.description].compactMap { $0 } }
209+
unwrap = Self.unwrap(_:)
144210
}
145211

146212
/// Initializes a new option when used as a `@propertyWrapper`
@@ -153,7 +219,14 @@ extension Option {
153219
{
154220
keyOverride = key
155221
self.wrappedValue = wrappedValue
156-
unwrap = { [$0?.description].compactMap { $0 } }
222+
unwrap = Self.unwrap(_:)
223+
}
224+
225+
@Sendable
226+
public static func unwrap<Wrapped>(_ value: Wrapped?) -> [String] where Wrapped: CustomStringConvertible,
227+
Value == Wrapped?
228+
{
229+
[value?.description].compactMap { $0 }
157230
}
158231
}
159232

@@ -170,7 +243,7 @@ extension Option {
170243
{
171244
keyOverride = key.description
172245
wrappedValue = values
173-
unwrap = { $0.map(\E.description) }
246+
unwrap = Self.unwrap(_:)
174247
}
175248

176249
/// Initializes a new option when used as a `@propertyWrapper`
@@ -183,7 +256,14 @@ extension Option {
183256
{
184257
keyOverride = key
185258
self.wrappedValue = wrappedValue
186-
unwrap = { $0.map(\E.description) }
259+
unwrap = Self.unwrap(_:)
260+
}
261+
262+
@Sendable
263+
public static func unwrap<E>(_ value: Value) -> [String] where Value: Sequence, Value.Element == E,
264+
E: CustomStringConvertible
265+
{
266+
value.map(\E.description)
187267
}
188268
}
189269

@@ -227,10 +307,43 @@ extension Option: ExpressibleByStringInterpolation where Value: StringProtocol {
227307
}
228308
}
229309

230-
extension Option: Decodable where Value: Decodable & CustomStringConvertible {
310+
extension Option: DecodableWithConfiguration where Value: Decodable {
311+
public init(from decoder: Decoder, configuration: @escaping @Sendable (Value) -> [String]) throws {
312+
let container = try decoder.singleValueContainer()
313+
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
314+
}
315+
}
316+
317+
extension Option: Decodable where Value: Decodable {
231318
public init(from decoder: Decoder) throws {
232319
let container = try decoder.singleValueContainer()
233-
try self.init(wrappedValue: container.decode(Value.self))
320+
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
321+
throw DecodingError.dataCorrupted(DecodingError.Context(
322+
codingPath: decoder.codingPath,
323+
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
324+
underlyingError: nil
325+
))
326+
}
327+
guard let _configuration = decoder.userInfo[configurationCodingUserInfoKey] else {
328+
throw DecodingError.dataCorrupted(DecodingError.Context(
329+
codingPath: decoder.codingPath,
330+
debugDescription: "No DecodingConfiguration found for key: \(configurationCodingUserInfoKey.rawValue)",
331+
underlyingError: nil
332+
))
333+
}
334+
guard let configuration = _configuration as? Self.DecodingConfiguration else {
335+
let desc = "Invalid DecodingConfiguration found for key: \(configurationCodingUserInfoKey.rawValue)"
336+
throw DecodingError.dataCorrupted(DecodingError.Context(
337+
codingPath: decoder.codingPath,
338+
debugDescription: desc,
339+
underlyingError: nil
340+
))
341+
}
342+
try self.init(wrappedValue: container.decode(Value.self), nil, configuration)
343+
}
344+
345+
public static func configurationCodingUserInfoKey(for _: (some Any).Type) -> CodingUserInfoKey? {
346+
CodingUserInfoKey(rawValue: ObjectIdentifier(Self.self).debugDescription)
234347
}
235348
}
236349

Tests/ArgumentEncodingTests/OptionTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,40 @@ final class OptionTests: XCTestCase {
1717
}
1818
XCTAssertEqual(args, ["--configuration", "release"])
1919
}
20+
21+
func testBothRawValueAndStringConvertible() throws {
22+
let option = Option(key: "configuration", value: RawValueCustomStringConvertible(rawValue: "release"))
23+
let args = withDependencies { values in
24+
values.optionFormatter = .doubleDashPrefix
25+
} operation: {
26+
option.arguments()
27+
}
28+
XCTAssertEqual(args, ["--configuration", "release"])
29+
}
30+
31+
func testBothRawValueAndStringConvertibleContainer() throws {
32+
let container = Container(configuration: RawValueCustomStringConvertible(rawValue: "release"))
33+
let args = withDependencies { values in
34+
values.optionFormatter = .doubleDashPrefix
35+
} operation: {
36+
container.arguments()
37+
}
38+
XCTAssertEqual(args, ["--configuration", "release"])
39+
}
40+
}
41+
42+
private struct RawValueCustomStringConvertible: RawRepresentable, CustomStringConvertible {
43+
var rawValue: String
44+
45+
var description: String {
46+
"description=" + rawValue
47+
}
48+
}
49+
50+
private struct Container: ArgumentGroup {
51+
@Option var configuration: RawValueCustomStringConvertible
52+
53+
init(configuration: RawValueCustomStringConvertible) {
54+
_configuration = Option(wrappedValue: configuration)
55+
}
2056
}

0 commit comments

Comments
 (0)