Skip to content

Commit 09106ba

Browse files
authored
Add a validation message for an invalid decoder (#487)
If a ParsableArguments type doesn't implement init(from:) correctly, it isn't decodable by the parser. This improves the validation failure message for such types.
1 parent d7aa440 commit 09106ba

File tree

2 files changed

+79
-17
lines changed

2 files changed

+79
-17
lines changed

Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,29 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator {
146146

147147
/// This error indicates that an option, a flag, or an argument of
148148
/// a `ParsableArguments` is defined without a corresponding `CodingKey`.
149-
struct Error: ParsableArgumentsValidatorError, CustomStringConvertible {
149+
struct MissingKeysError: ParsableArgumentsValidatorError, CustomStringConvertible {
150150
let missingCodingKeys: [String]
151151

152152
var description: String {
153+
let resolution = """
154+
To resolve this error, make sure that all properties have corresponding
155+
cases in your custom `CodingKey` enumeration.
156+
"""
157+
153158
if missingCodingKeys.count > 1 {
154-
return "Arguments \(missingCodingKeys.map({ "`\($0)`" }).joined(separator: ",")) are defined without corresponding `CodingKey`s."
159+
return """
160+
Arguments \(missingCodingKeys.map({ "`\($0)`" }).joined(separator: ",")) \
161+
are defined without corresponding `CodingKey`s.
162+
163+
\(resolution)
164+
"""
155165
} else {
156-
return "Argument `\(missingCodingKeys[0])` is defined without a corresponding `CodingKey`."
166+
return """
167+
Argument `\(missingCodingKeys[0])` is defined without a corresponding \
168+
`CodingKey`.
169+
170+
\(resolution)
171+
"""
157172
}
158173
}
159174

@@ -162,6 +177,23 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator {
162177
}
163178
}
164179

180+
struct InvalidDecoderError: ParsableArgumentsValidatorError, CustomStringConvertible {
181+
let type: ParsableArguments.Type
182+
183+
var description: String {
184+
"""
185+
The implementation of `init(from:)` for `\(type)`
186+
is not compatible with ArgumentParser. To resolve this issue, make sure
187+
that `init(from:)` calls the `container(keyedBy:)` method on the given
188+
decoder and decodes each of its properties using the returned decoder.
189+
"""
190+
}
191+
192+
var kind: ValidatorErrorKind {
193+
.failure
194+
}
195+
}
196+
165197
static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? {
166198
let argumentKeys: [String] = Mirror(reflecting: type.init())
167199
.children
@@ -179,11 +211,11 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator {
179211
}
180212
do {
181213
let _ = try type.init(from: Validator(argumentKeys: argumentKeys))
182-
fatalError("The validator should always throw.")
214+
return InvalidDecoderError(type: type)
183215
} catch let result as Validator.ValidationResult {
184216
switch result {
185217
case .missingCodingKeys(let keys):
186-
return Error(missingCodingKeys: keys)
218+
return MissingKeysError(missingCodingKeys: keys)
187219
case .success:
188220
return nil
189221
}
@@ -244,9 +276,11 @@ struct NonsenseFlagsValidator: ParsableArgumentsValidator {
244276
"""
245277
One or more Boolean flags is declared with an initial value of `true`.
246278
This results in the flag always being `true`, no matter whether the user
247-
specifies the flag or not. To resolve this error, change the default to
248-
`false`, provide a value for the `inversion:` parameter, or remove the
249-
`@Flag` property wrapper altogether.
279+
specifies the flag or not.
280+
281+
To resolve this error, change the default to `false`, provide a value
282+
for the `inversion:` parameter, or remove the `@Flag` property wrapper
283+
altogether.
250284
251285
Affected flag(s):
252286
\(names.joined(separator: "\n"))

Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,29 +85,53 @@ final class ParsableArgumentsValidationTests: XCTestCase {
8585
XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self))
8686

8787
if let error = ParsableArgumentsCodingKeyValidator.validate(C.self)
88-
as? ParsableArgumentsCodingKeyValidator.Error
88+
as? ParsableArgumentsCodingKeyValidator.MissingKeysError
8989
{
9090
XCTAssert(error.missingCodingKeys == ["count"])
9191
} else {
9292
XCTFail()
9393
}
9494

9595
if let error = ParsableArgumentsCodingKeyValidator.validate(D.self)
96-
as? ParsableArgumentsCodingKeyValidator.Error
96+
as? ParsableArgumentsCodingKeyValidator.MissingKeysError
9797
{
9898
XCTAssert(error.missingCodingKeys == ["phrase"])
9999
} else {
100100
XCTFail()
101101
}
102102

103103
if let error = ParsableArgumentsCodingKeyValidator.validate(E.self)
104-
as? ParsableArgumentsCodingKeyValidator.Error
104+
as? ParsableArgumentsCodingKeyValidator.MissingKeysError
105105
{
106106
XCTAssert(error.missingCodingKeys == ["phrase", "includeCounter"])
107107
} else {
108108
XCTFail()
109109
}
110110
}
111+
112+
private struct TypeWithInvalidDecoder: ParsableArguments {
113+
@Argument(help: "The phrase to repeat.")
114+
var phrase: String = ""
115+
116+
@Option(help: "The number of times to repeat 'phrase'.")
117+
var count: Int = 0
118+
119+
init() {}
120+
121+
init(from decoder: Decoder) throws {
122+
self.init()
123+
}
124+
}
125+
126+
func testCustomDecoderValidation() throws {
127+
if let error = ParsableArgumentsCodingKeyValidator.validate(TypeWithInvalidDecoder.self)
128+
as? ParsableArgumentsCodingKeyValidator.InvalidDecoderError
129+
{
130+
XCTAssert(error.type == TypeWithInvalidDecoder.self)
131+
} else {
132+
XCTFail()
133+
}
134+
}
111135

112136
private struct F: ParsableArguments {
113137
@Argument()
@@ -413,9 +437,11 @@ final class ParsableArgumentsValidationTests: XCTestCase {
413437
"""
414438
One or more Boolean flags is declared with an initial value of `true`.
415439
This results in the flag always being `true`, no matter whether the user
416-
specifies the flag or not. To resolve this error, change the default to
417-
`false`, provide a value for the `inversion:` parameter, or remove the
418-
`@Flag` property wrapper altogether.
440+
specifies the flag or not.
441+
442+
To resolve this error, change the default to `false`, provide a value
443+
for the `inversion:` parameter, or remove the `@Flag` property wrapper
444+
altogether.
419445
420446
Affected flag(s):
421447
--nonsense
@@ -448,9 +474,11 @@ final class ParsableArgumentsValidationTests: XCTestCase {
448474
"""
449475
One or more Boolean flags is declared with an initial value of `true`.
450476
This results in the flag always being `true`, no matter whether the user
451-
specifies the flag or not. To resolve this error, change the default to
452-
`false`, provide a value for the `inversion:` parameter, or remove the
453-
`@Flag` property wrapper altogether.
477+
specifies the flag or not.
478+
479+
To resolve this error, change the default to `false`, provide a value
480+
for the `inversion:` parameter, or remove the `@Flag` property wrapper
481+
altogether.
454482
455483
Affected flag(s):
456484
--stuff

0 commit comments

Comments
 (0)