Skip to content

Commit c87d0d0

Browse files
authored
Remove ExpressibleByArgument conformance from Optional (#173)
* Remove ExpressibleByArgument conformance for Optional It turns out that the conditional conformance for Optional was a bad idea, and it should be handled more like Array, with specific initializers for the Optional case. Primarily, this is because providing a default value for an optional property doesn't make sense -- the default is already nil, and a non-nil default means that the property will never be nil and therefore shouldn't be optional. * Drop duplicated argument definitions d1 and d6 are duplicates of c2 and c, respectively. * Correctly mark optional args/options as optional * Correct documentation for Option/Argument * Use the correct parameter name in the documentation
1 parent 6c6b77c commit c87d0d0

File tree

6 files changed

+99
-30
lines changed

6 files changed

+99
-30
lines changed

Sources/ArgumentParser/Parsable Properties/Argument.swift

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,9 @@ extension Argument: DecodableParsedWrapper where Value: Decodable {}
7070
extension Argument where Value: ExpressibleByArgument {
7171
/// Creates a property that reads its value from an argument.
7272
///
73-
/// If the property has an `Optional` type, the argument is optional and
74-
/// defaults to `nil`.
75-
///
7673
/// - Parameters:
77-
/// - initial: A default value to use for this property.
74+
/// - initial: A default value to use for this property. If `initial` is
75+
/// `nil`, the user must supply a value for this argument.
7876
/// - help: Information about how to use this argument.
7977
public init(
8078
default initial: Value? = nil,
@@ -130,6 +128,48 @@ public enum ArgumentArrayParsingStrategy {
130128
}
131129

132130
extension Argument {
131+
/// Creates an optional property that reads its value from an argument.
132+
///
133+
/// The argument is optional for the caller of the command and defaults to
134+
/// `nil`.
135+
///
136+
/// - Parameter help: Information about how to use this argument.
137+
public init<T: ExpressibleByArgument>(
138+
help: ArgumentHelp? = nil
139+
) where Value == T? {
140+
self.init(_parsedValue: .init { key in
141+
var arg = ArgumentDefinition(
142+
key: key,
143+
kind: .positional,
144+
parsingStrategy: .nextAsValue,
145+
parser: T.init(argument:),
146+
default: nil)
147+
arg.help.help = help
148+
return ArgumentSet(arg.optional)
149+
})
150+
}
151+
152+
@available(*, deprecated, message: """
153+
Default values don't make sense for optional properties.
154+
Remove the 'default' parameter if its value is nil,
155+
or make your property non-optional if it's non-nil.
156+
""")
157+
public init<T: ExpressibleByArgument>(
158+
default initial: T?,
159+
help: ArgumentHelp? = nil
160+
) where Value == T? {
161+
self.init(_parsedValue: .init { key in
162+
ArgumentSet(
163+
key: key,
164+
kind: .positional,
165+
parsingStrategy: .nextAsValue,
166+
parseType: T.self,
167+
name: .long,
168+
default: initial,
169+
help: help)
170+
})
171+
}
172+
133173
/// Creates a property that reads its value from an argument, parsing with
134174
/// the given closure.
135175
///

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,12 @@ extension Option: DecodableParsedWrapper where Value: Decodable {}
7272
extension Option where Value: ExpressibleByArgument {
7373
/// Creates a property that reads its value from a labeled option.
7474
///
75-
/// If the property has an `Optional` type, or you provide a non-`nil`
76-
/// value for the `initial` parameter, specifying this option is not
77-
/// required.
78-
///
7975
/// - Parameters:
8076
/// - name: A specification for what names are allowed for this flag.
8177
/// - initial: A default value to use for this property. If `initial` is
8278
/// `nil`, this option and value are required from the user.
79+
/// - parsingStrategy: The behavior to use when looking for this option's
80+
/// value.
8381
/// - help: Information about how to use this option.
8482
public init(
8583
name: NameSpecification = .long,
@@ -216,17 +214,66 @@ public enum ArrayParsingStrategy {
216214
}
217215

218216
extension Option {
219-
/// Creates a property that reads its value from a labeled option, parsing
220-
/// with the given closure.
217+
/// Creates a property that reads its value from a labeled option.
221218
///
222219
/// If the property has an `Optional` type, or you provide a non-`nil`
223220
/// value for the `initial` parameter, specifying this option is not
224221
/// required.
225222
///
226223
/// - Parameters:
227224
/// - name: A specification for what names are allowed for this flag.
225+
/// - parsingStrategy: The behavior to use when looking for this option's
226+
/// value.
227+
/// - help: Information about how to use this option.
228+
public init<T: ExpressibleByArgument>(
229+
name: NameSpecification = .long,
230+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
231+
help: ArgumentHelp? = nil
232+
) where Value == T? {
233+
self.init(_parsedValue: .init { key in
234+
var arg = ArgumentDefinition(
235+
key: key,
236+
kind: .name(key: key, specification: name),
237+
parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy),
238+
parser: T.init(argument:),
239+
default: nil)
240+
arg.help.help = help
241+
return ArgumentSet(arg.optional)
242+
})
243+
}
244+
245+
@available(*, deprecated, message: """
246+
Default values don't make sense for optional properties.
247+
Remove the 'default' parameter if its value is nil,
248+
or make your property non-optional if it's non-nil.
249+
""")
250+
public init<T: ExpressibleByArgument>(
251+
name: NameSpecification = .long,
252+
default initial: T?,
253+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
254+
help: ArgumentHelp? = nil
255+
) where Value == T? {
256+
self.init(_parsedValue: .init { key in
257+
var arg = ArgumentDefinition(
258+
key: key,
259+
kind: .name(key: key, specification: name),
260+
parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy),
261+
parser: T.init(argument:),
262+
default: initial)
263+
arg.help.help = help
264+
return ArgumentSet(arg.optional)
265+
})
266+
}
267+
268+
/// Creates a property that reads its value from a labeled option, parsing
269+
/// with the given closure.
270+
///
271+
/// - Parameters:
272+
/// - name: A specification for what names are allowed for this flag.
228273
/// - initial: A default value to use for this property. If `initial` is
229274
/// `nil`, this option and value are required from the user.
275+
/// - parsingStrategy: The behavior to use when looking for this option's
276+
/// value.
230277
/// - help: Information about how to use this option.
231278
/// - transform: A closure that converts a string into this property's
232279
/// type or throws an error.

Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,6 @@ extension String: ExpressibleByArgument {
3333
}
3434
}
3535

36-
extension Optional: ExpressibleByArgument where Wrapped: ExpressibleByArgument {
37-
public init?(argument: String) {
38-
if let value = Wrapped(argument: argument) {
39-
self = value
40-
} else {
41-
return nil
42-
}
43-
}
44-
45-
public var defaultValueDescription: String {
46-
guard let value = self else {
47-
return "none"
48-
}
49-
return "\(value)"
50-
}
51-
}
52-
5336
extension RawRepresentable where Self: ExpressibleByArgument, RawValue: ExpressibleByArgument {
5437
public init?(argument: String) {
5538
if let value = RawValue(argument: argument) {

Sources/ArgumentParser/Parsing/ArgumentDefinition.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ extension ArgumentDefinition: CustomDebugStringConvertible {
145145
extension ArgumentDefinition {
146146
var optional: ArgumentDefinition {
147147
var result = self
148+
148149
result.help.options.insert(.isOptional)
149150
return result
150151
}

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ extension ArgumentSet {
176176

177177
extension ArgumentDefinition {
178178
/// Create a unary / argument that parses using the given closure.
179-
fileprivate init<A>(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?) {
179+
init<A>(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?) {
180180
let initialValueCreator: (InputOrigin, inout ParsedValues) throws -> Void
181181
if let initialValue = initial {
182182
initialValueCreator = { origin, values in

Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,10 @@ fileprivate struct AlmostAllArguments: ParsableArguments {
3939
@Argument(default: 0) var c2: Int?
4040

4141
@Argument(default: 0, help: "", transform: { _ in 0 }) var d: Int?
42-
@Argument(default: 0) var d1: Int?
4342
@Argument(help: "") var d2: Int?
4443
@Argument(transform: { _ in 0 }) var d3: Int?
4544
@Argument(help: "", transform: { _ in 0 }) var d4: Int?
4645
@Argument(default: 0, transform: { _ in 0 }) var d5: Int?
47-
@Argument(default: 0, help: "") var d6: Int?
4846

4947
@Argument(parsing: .remaining, help: "") var e: [Int]
5048
@Argument() var e0: [Int]

0 commit comments

Comments
 (0)