Add ability to retrieve list of possible options for Option property#647
Conversation
An EnumerableOption protocol has been created to allow users to specify help/discussion information on a custom Option type itself. This information can be accessed further down the pipeline, which permits the retrieval of all the option types and their descriptions for dump/help generation. Note that this has yet to be formatted and is currently WIP.
cmcgee1024
left a comment
There was a problem hiding this comment.
Overall this looks good to me. I've added thoughts and questions along the way. I don't feel comfortable enough with this repo to approve at this time.
* Pulled out redundant Discussion structs; to continue * Make Discussion an optional property where applicable * EnumerableOptionValue moved to its own file * Added tests to appropriate files * Refactored to fit repo code formatting; to continue This commit largely addresses comments made to the previous commit. Some aspsects are still in progress.
Created an extension to Option that handles optional EnumerableOptionValue types within its initializers.
3f11961 to
9a0a2b4
Compare
* ArgumentDiscussion struct represents a standardized way to define an argument's discussion block, handling both the static block of text as well as a list of EnumerableOptionValues * Update Option initializers to better handle cases where the type implements `EnumerableOptionValue`. * Allow users to specify a discussion block alongside their EnumerableOptionValue types, which will print out a preamble block of text before the list of option values and descriptions.
* Support the Discussion struct in the Single/Multi page manual generation * Refactor the encoding method for Discussion; generates a list of objects containing 'value' and 'description' properties in the resulting JSON, keeping the order of the properties consistent across runs.
natecook1000
left a comment
There was a problem hiding this comment.
Thanks so much for looking at this, @bripeticca! The updated help screens look really clear, and it's great that adoption is all opt in. I think we can cut back on the new API a little bit – if you agree, we just have a protocol name to discuss. Thanks!
| /// Can include specific abstracts about the argument's possible values (e.g. | ||
| /// for a custom `EnumerableOptionValue` type), or can describe | ||
| /// a static block of text that extends the description of the argument. | ||
| public var discussion: ArgumentDiscussion? |
There was a problem hiding this comment.
This doesn't seem like the right place for a property this type, since a command generally contains a group of flags, options, and arguments. Can you say more about why CommandConfiguration should include an ArgumentDiscussion?
Separately, changing the type of discussion is source breaking – we'll need a different strategy if we do add this.
There was a problem hiding this comment.
Ah, good point! I had changed this as I was making my way through the DumpHelpGenerator and updating the inits for CommandInfoV0, ArgumentInfoV0, etc. to consider ArgumentDiscussion. Along the way, I updated each struct that contained a discussion string to instead use ArgumentDiscussion - it seems like for CommandConfiguration this is unnecessary though! Since it's a breaking change, I can revert.
| usage: String? = nil, | ||
| discussion: String = "", | ||
| discussion: String? = nil, | ||
| options: (any EnumerableOptionValue.Type)? = nil, |
| /// | ||
| /// In any case where the argument type is not `EnumerableOptionValue`, the default implementation | ||
| /// will use the `.staticText` case and will print a block of discussion text. | ||
| public enum ArgumentDiscussion { |
There was a problem hiding this comment.
If we don't use this in CommandConfiguration, it also looks like it doesn't need to be public.
| /// } | ||
| /// } | ||
| /// ``` | ||
| public protocol EnumerableOptionValue: CaseIterable, ExpressibleByArgument, RawRepresentable where RawValue: ExpressibleByArgument, AllCases == [Self] { |
There was a problem hiding this comment.
This is a tricky protocol to name – I'd like to make sure we leave room for adopting this named-value feature for arguments, as well. This command type should be able to have the nice value-based help text for the color argument:
struct Example: ParsableCommand {
@Argument var color: Color
}We don't need to land @Argument support for this, but we should be able to make room for it.
There was a problem hiding this comment.
That's a great point! The naming is definitely tricky :) Perhaps something a little more generic like EnumerableValue could be used instead?
| parsing parsingStrategy: SingleValueParsingStrategy = .next, | ||
| help: ArgumentHelp? = nil, | ||
| completion: CompletionKind? = nil | ||
| ) where Value: EnumerableOptionValue { |
There was a problem hiding this comment.
Since Value.self ends up getting erased into (any EnumerableOptionValue)? anyway, I wonder if we can skip all these overloads and just try casting the type in the regular ExpressibleByArgument initializers.
There was a problem hiding this comment.
For sure - I just tried this out locally by adding the following to the beginning of each ExpressibleByArgument init:
var help = help
if let type = Value.self as? (any EnumerableOptionValue.Type) {
help = ArgumentHelp.init(
help?.abstract ?? "",
discussion: help?.discussion,
options: type, // retaining necessary type information for enumerable values here
valueName: help?.valueName,
visibility: help?.visibility ?? .default
)
}This way we can still retain the modified ArgumentHelp for types implementing EnumerableOptionValue (or whatever we decide to rename it to!).
Alternatively, I can modify the ArgumentHelp init to take the options parameter and have it be optional, and can remove some code so it comes down to this:
// ArgumentHelp init:
public init(
_ abstract: String = "",
discussion: String? = nil,
valueName: String? = nil,
options: (any EnumerableOptionValue.Type)? = nil, // made this optional
visibility: ArgumentVisibility = .default)
{
self.abstract = abstract
self.discussion = discussion
self.valueName = valueName
self.options = options
self.visibility = visibility
} and the resulting ExpressibleByArgument inits would have some additional code that looks something like this:
self.init(_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Bare<Value>.self,
key: key,
kind: .name(key: key, specification: name),
help: .init(
help?.abstract ?? "",
discussion: help?.discussion,
valueName: help?.valueName,
options: Value.self as? (any EnumerableOptionValue.Type),
visibility: help?.visibility ?? .default
),
parsingStrategy: parsingStrategy.base,
initial: wrappedValue,
completion: completion)
return ArgumentSet(arg)
})If this seems good to you, I can go ahead and change it!
There was a problem hiding this comment.
Sorry for the delay here! I've been wrestling with whether we can avoid the EnumerableOptionValue protocol altogether, since it seems to be a refinement of the ExpressibleByArgument protocol that we're already handling for a couple slightly different uses. In particular, adding features like this one to the library is challenging in that we need to design a source-compatible addition that makes some kind of intuitive sense with the features that are already present.
The most related existing feature supported by the ExpressibleByArgument protocol is the ability to automatically include value names in the help description when a type is CaseIterable. Since the change in this PR kind of augments that to include descriptions as well as value names, I think we can piggy back on that existing design.
In particular, what would you think about adding another customization point to ExpressibleByArgument:
public protocol ExpressibleByArgument {
// ...other members...
// Existing static var for value strings:
/// An array of all possible strings that can convert to a value of this
/// type, for display in the help screen.
///
/// The default implementation of this property returns an empty array. If the
/// conforming type is also `CaseIterable`, the default implementation returns
/// an array with a value for each case.
static var allValueStrings: [String] { get }
// New static var for value descriptions:
static var allValueDescriptions: [String: String] { get }
}We could provide a default implementation that returns an empty dictionary, and then any type that wants to provide a more expanded help screen could implement allValueDescriptions to provide corresponding descriptions for each element of the allValueStrings array. Internally, we can use similar machinery to capture those descriptions for use when displaying the help screen or generating the JSON help description.
There was a problem hiding this comment.
This is a great idea! Thanks for the feedback - I tried to implement this locally and it seems to work well.
- Removes extra Option initializers by attempting a type cast to `EnumerableOptionValue` in ExpressibleByArgument inits - Revert `CommandConfiguration` discussion property to String - ArgumentDiscussion no longer public
Since `ExpressibleByArgument` already maintains a list of enumerable values for an argument, we can extend this to serve as an ordered list for a new dictionary property that maps the value name to its description, if applicable. The new property is a static variable on `ExpressibleByArgument` labelled `allValueDescriptions`. If the description string for a value is the same as the value string, it's assumed that the description is not implemented. This will replace the use of the EnumerableOption protocol.
|
@swift-ci please test |
Remove previous expectations for dump help test results.
|
@swift-ci please test |
|
@swift-ci please test |
natecook1000
left a comment
There was a problem hiding this comment.
This is looking like the right API! Just a couple of nits and then a question about testing - should be good to go after that!
| /// Can include specific abstracts about the argument's possible values (e.g. | ||
| /// for a custom `CaseIterable` type), or can describe | ||
| /// a static block of text that extends the description of the argument. | ||
| public var discussion: Discussion? |
There was a problem hiding this comment.
@rauhul Do we need to update the tool info version for a change like this?
There was a problem hiding this comment.
Yeah this will need a version bump and will be source break. Thats not a deal breaker, but can be annoying.
Alternatively we can call this discussion2 and deprecate the old one.
There was a problem hiding this comment.
I'll go ahead and rename the property in that case! Should this deprecation strategy also be applied to ArgumentInfoV0?
If an option case's label was too long for the description to fit on the same line, it would omit the label entirely. Append the label to the rendered help text with a newline if the label is too long for the description to fit on the same line. Added test cases to address scenarios where the option cases either have: * a label that is too long for the description to fit on the same line * a description that needs to be wrapped * a label that is too long and a description that needs to be wrapped * all of the above cases when an Option also has a discussion, which further indents the list of possible option values in the rendered help text.
|
@swift-ci please test |
| --argument <argument> A collection of cases with varying lengths of | ||
| labels/descriptions. | ||
| This discussion should make the list of options wrap differently. | ||
| Values: |
There was a problem hiding this comment.
Is there a real need for "Values:"? I dont think I like the difference in indentation with/without a "discussion"
There was a problem hiding this comment.
No real need, I can change it!
| Section(title: "description") { | ||
| if let discussion = command.discussion { | ||
| discussion | ||
| if case let .staticText(text) = discussion { |
There was a problem hiding this comment.
Can we factor this body out into a Discussion type and use it the 4 places changed here?
There was a problem hiding this comment.
I've added a DiscussionText struct to the project that encapsulates this logic!
* Removed extra 'Values:' text in enumerated argument discussion * DiscussionText struct MDocComponent created to wrap discussion component creation * Apply deprecation strategy to CommandInfoV0.discussion and ArgumentInfoV0.discussion properties - rename new property to discussion2 * Updated tests
|
@swift-ci please test |
| var b: OptionValues? | ||
| } | ||
|
|
||
| func testEnumerableValuesWithPreamble() { |
There was a problem hiding this comment.
Im a little confused by how this list view interacts with the change introduced in #594. Do you have a test that covers both features together?
There was a problem hiding this comment.
I can add a test that demonstrates the differences between both of these features!
The idea is that if the type for the @Option property is an enum that implements ExpressibleByArgument and if the enum also implements the defaultValueDescription property for its cases, then the generated help text will enumerate each of the enum's values with the description defined in defaultValueDescription.
For example:
enum OptionEnumerated: String, CaseIterable, ExpressibleByArgument {
case one
case two
case three
public var defaultValueDescription: String {
switch self {
case .one:
return "The number one."
case .two:
return "The number two."
case .three:
return "The number three."
}
}
}
struct MyCommand: ParsableCommand {
@Option(help: "The values for this option should be enumerated with a description.")
var list: OptionEnumerated
}
// Example generated help text:
...
--list <list> The values for this option should be enumerated with a description.
one - The number one.
two - The number two.
three - The number three.
...
Otherwise if this property is not implemented in the enum, then it will generate the help text like so:
enum OptionWithoutEnumeratedHelpText: String, CaseIterable, ExpressibleByArgument {
case one = "1"
case two = "2"
case three = "3"
}
struct MyOtherCommand: ParsableCommand {
@Option(help: "This is an option without explicit enumeration in the help text.")
var values: OptionWithoutEnumeratedHelpText
}
// Example generated help text:
...
--values <values> This is an option without explicit enumeration in the
help text. (values: 1, 2, 3)
...
A new test has been implemented to showcase the difference between the enumerated help descriptions feature and the default generated help text for options with multiple possible values.
|
@swift-ci please test |
|
@swift-ci please test |
rauhul
left a comment
There was a problem hiding this comment.
I'm pretty happy with this change! If @natecook1000 is happy with it, we can merge :)
|
@swift-ci Please test |
7b0fd4f to
6f73d3b
Compare
|
@swift-ci Please test |
|
Thanks so much for your patience on this, @bripeticca! 🎉🎉🎉 |
Cleans up some lingering odds and ends from #647. Specifically switches CommandInfoV0 to use the old style description and updates ArgumentInfov0 to emit value descriptions as a separate dictionary.
Cleans up some lingering odds and ends from #647. Specifically switches CommandInfoV0 to use the old style description and updates ArgumentInfov0 to emit value descriptions as a separate dictionary.
Cleans up some lingering odds and ends from #647. Specifically switches CommandInfoV0 to use the old style description and updates ArgumentInfov0 to emit value descriptions as a separate dictionary.
Cleans up some lingering odds and ends from #647. Specifically switches CommandInfoV0 to use the old style description and updates ArgumentInfoV0 to emit value descriptions as a separate dictionary.
Previously, information regarding a custom Option type's possible values was lost when generating subsequent structures in the pipeline to describe the argument. This would largely have to be defined by the user in the
discussionstring property inArgumentHelpand would provide no mechanism to directly access the allowable values for a customOption.This adds an extra static property on
ExpressibleByArgumentlabelleddefaultValueDescriptions, which maps the value name to its description. It is up to the user to implement thedefaultValueDescriptionfor their customCaseIterabletype to fully support this behaviour. Further refactoring has been made downstream to the ArgumentDefinition and subsequent structures describing the argument to allow the ability to retrieve the value descriptions from theExpressibleByArgumenttype, making information about the property more visible and accessible.A new enumeration
ArgumentDiscussionhas been introduced to better encapsulate the discussion information for an argument, and includes cases for discussion sections that contain only static text, as well as cases that include a list of argument values that derive from a type that implementsExpressibleByArgument,CaseIterableandRawRepresentable.Addresses #637
Checklist