Skip to content

Allow normal Swift default property initialization syntax #170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f918b83
Allow normal Swift default property initialization syntax
MPLew-is May 30, 2020
7517781
Merge branch 'master' into default-property-initialization
MPLew-is Jun 14, 2020
caa77a5
Add simple tests for default property initialization
MPLew-is Jun 14, 2020
07fd64f
Centralize some constructor logic into a private `init`
MPLew-is Jun 14, 2020
60ed36f
Deprecate previous `Option.init` with `default` parameter
MPLew-is Jun 14, 2020
b70dbf1
Document added test cases
MPLew-is Jun 14, 2020
3e32345
Correct punctuation
MPLew-is Jun 14, 2020
79f4fb6
Extend standard default initialization syntax to `Option`s with `tran…
MPLew-is Jun 14, 2020
83cd53f
Actually replace previous `init` with private version
MPLew-is Jun 16, 2020
28d7179
Clean up usage of default parameter values
MPLew-is Jun 16, 2020
a8e2a7d
Clean up documentation
MPLew-is Jun 16, 2020
5c7dba0
Extend standard default initialization to `Argument`s
MPLew-is Jun 16, 2020
f3a6b00
Extend standard default initialization to `Flag`s
MPLew-is Jun 16, 2020
83a70e4
Default flags with inversions to nil/required
MPLew-is Jun 19, 2020
e7397e9
Extend standard default initialization to no-inversion boolean `Flags`
MPLew-is Jun 21, 2020
a371be6
Eliminate deprecation spam from default value initialization
MPLew-is Jun 21, 2020
d41e4f2
Add source compatibility tests for new default syntax and associated …
MPLew-is Jun 21, 2020
e05678f
Update top-level documentation
MPLew-is Jun 21, 2020
8c573a5
Merge branch 'master' into default-property-initialization
MPLew-is Jun 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 159 additions & 10 deletions Sources/ArgumentParser/Parsable Properties/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ extension Option: DecodableParsedWrapper where Value: Decodable {}
// MARK: Property Wrapper Initializers

extension Option where Value: ExpressibleByArgument {
/// Creates a property with an optional default value, intended to be called by other constructors to centralize logic.
///
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
private init(
name: NameSpecification = .long,
initial: Value? = nil,
parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil
) {
self.init(_parsedValue: .init { key in
ArgumentSet(
key: key,
kind: .name(key: key, specification: name),
parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy),
parseType: Value.self,
name: name,
default: initial, help: help)
})
}

/// Creates a property that reads its value from a labeled option.
///
/// - Parameters:
Expand All @@ -79,21 +99,67 @@ extension Option where Value: ExpressibleByArgument {
/// - parsingStrategy: The behavior to use when looking for this option's
/// value.
/// - help: Information about how to use this option.
@available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)")
public init(
name: NameSpecification = .long,
default initial: Value? = nil,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil
) {
self.init(_parsedValue: .init { key in
ArgumentSet(
key: key,
kind: .name(key: key, specification: name),
parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy),
parseType: Value.self,
name: name,
default: initial, help: help)
})
self.init(
name: name,
initial: initial,
parsingStrategy: parsingStrategy,
help: help
)
}

/// Creates a property with a default value provided by standard Swift default value syntax.
///
/// For instance, the following are now equivalent:
/// - `@Option(default: "bar") var foo: String`
/// - `@Option() var foo: String = "bar"`
///
/// This syntax allows defaults to be set for options much more naturally for the developer.
///
/// - Parameters:
/// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during `propertyWrapper` initialization.
/// - name: A specification for what names are allowed for this flag.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
public init(
wrappedValue: Value,
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil
) {
self.init(
name: name,
initial: wrappedValue,
parsingStrategy: parsingStrategy,
help: help
)
}

/// Creates a property with no default value.
///
/// With the addition of standard default property initialization syntax and the deprecation of the previous `init` with a `default` parameter, we must also provide a separate `init` with no default for when the older method is eventually removed.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
public init(
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil
) {
self.init(
name: name,
initial: nil,
parsingStrategy: parsingStrategy,
help: help
)
}
}

Expand Down Expand Up @@ -305,7 +371,90 @@ extension Option {
return ArgumentSet(alternatives: [arg])
})
}


/// Creates a property that reads its value from a labeled option, parsing
/// with the given closure.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - initial: A default value to use for this property. If `initial` is
/// `nil`, this option and value are required from the user.
/// - parsingStrategy: The behavior to use when looking for this option's
/// value.
/// - help: Information about how to use this option.
/// - transform: A closure that converts a string into this property's
/// type or throws an error.
@available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)")
public init(
name: NameSpecification = .long,
default initial: Value? = nil,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
transform: @escaping (String) throws -> Value
) {
self.init(
name: name,
initial: initial,
parsingStrategy: parsingStrategy,
help: help,
transform: transform
)
}

/// Creates a property with a default value provided by standard Swift default value syntax, parsing with the given closure.
///
/// For instance, the following are now equivalent:
/// - `@Option(default: "bar", transform: someFunction) var foo: String`
/// - `@Option(transform: someFunction) var foo: String = "bar"`
///
/// This syntax allows defaults to be set for options much more naturally for the developer.
///
/// - Parameters:
/// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during `propertyWrapper` initialization.
/// - name: A specification for what names are allowed for this flag.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - transform: A closure that converts a string into this property's type or throws an error.
public init(
wrappedValue: Value,
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
transform: @escaping (String) throws -> Value
) {
self.init(
name: name,
initial: wrappedValue,
parsingStrategy: parsingStrategy,
help: help,
transform: transform
)
}

/// Creates a property with no default value, parsing with the given closure.
///
/// With the addition of standard default property initialization syntax and the deprecation of the previous `init` with a `default` parameter, we must also provide a separate `init` with no default for when the older method is eventually removed.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - transform: A closure that converts a string into this property's type or throws an error.
public init(
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
transform: @escaping (String) throws -> Value
) {
self.init(
name: name,
initial: nil,
parsingStrategy: parsingStrategy,
help: help,
transform: transform
)
}

/// Creates an array property that reads its values from zero or more
/// labeled options.
///
Expand Down
68 changes: 68 additions & 0 deletions Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,71 @@ extension DefaultsEndToEndTests {
}
}


fileprivate func exclaim(_ input: String) throws -> String {
return input + "!"
}

fileprivate struct OptionPropertyInitArguments_Default: ParsableArguments {
@Option
var data: String = "test"

@Option(transform: exclaim)
var transformedData: String = "test"
}

fileprivate struct OptionPropertyInitArguments_NoDefault_NoTransform: ParsableArguments {
@Option()
var data: String
}

fileprivate struct OptionPropertyInitArguments_NoDefault_Transform: ParsableArguments {
@Option(transform: exclaim)
var transformedData: String
}

extension DefaultsEndToEndTests {
/// Tests that using default property initialization syntax parses the default value for the argument when nothing is provided from the command-line.
func testParsing_OptionPropertyInit_Default_NoTransform_UseDefault() throws {
AssertParse(OptionPropertyInitArguments_Default.self, []) { arguments in
XCTAssertEqual(arguments.data, "test")
}
}

/// Tests that using default property initialization syntax parses the command-line-provided value for the argument when provided.
func testParsing_OptionPropertyInit_Default_NoTransform_OverrideDefault() throws {
AssertParse(OptionPropertyInitArguments_Default.self, ["--data", "test2"]) { arguments in
XCTAssertEqual(arguments.data, "test2")
}
}

/// Tests that *not* providing a default value still parses the argument correctly from the command-line.
/// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality.
func testParsing_OptionPropertyInit_NoDefault_NoTransform() throws {
AssertParse(OptionPropertyInitArguments_NoDefault_NoTransform.self, ["--data", "test"]) { arguments in
XCTAssertEqual(arguments.data, "test")
}
}

/// Tests that using default property initialization syntax on a property with a `transform` function provided parses the default value for the argument when nothing is provided from the command-line.
func testParsing_OptionPropertyInit_Default_Transform_UseDefault() throws {
AssertParse(OptionPropertyInitArguments_Default.self, []) { arguments in
XCTAssertEqual(arguments.transformedData, "test")
}
}

/// Tests that using default property initialization syntax on a property with a `transform` function provided parses and transforms the command-line-provided value for the argument when provided.
func testParsing_OptionPropertyInit_Default_Transform_OverrideDefault() throws {
AssertParse(OptionPropertyInitArguments_Default.self, ["--transformed-data", "test2"]) { arguments in
XCTAssertEqual(arguments.transformedData, "test2!")
}
}

/// Tests that *not* providing a default value for a property with a `transform` function still parses the argument correctly from the command-line.
/// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality.
func testParsing_OptionPropertyInit_NoDefault_Transform() throws {
AssertParse(OptionPropertyInitArguments_NoDefault_Transform.self, ["--transformed-data", "test"]) { arguments in
XCTAssertEqual(arguments.transformedData, "test!")
}
}
}