-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
feat: add TextFlag #2055
base: main
Are you sure you want to change the base?
feat: add TextFlag #2055
Conversation
* Added `TextFlag` which supports setting values for types that satisfies the `encoding.TextMarshaller` and `encoding.TextUnmarshaller` which is very handy when you want to set log levels or string-like types that satifies the interfaces. Fixes: urfave#2051 Signed-off-by: Tobias Dahlberg <[email protected]>
Signed-off-by: Eng Zer Jun <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution! 😊
Just a few comments regarding the tests. If possible, please add a few more test cases to cover edge cases and improve coverage.
flag_text_test.go
Outdated
if err := tt.flag.Apply(set); err != nil { | ||
t.Fatalf("Apply(%v) failed: %v", tt.args, err) | ||
} | ||
|
||
err := set.Parse(tt.args) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) | ||
|
||
return | ||
} else if (err != nil) == tt.wantErr { | ||
// Expected error. | ||
return | ||
} | ||
|
||
if got := tt.flag.GetValue(); got != tt.want { | ||
t.Errorf("Value = %v, want %v", got, tt.want) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the testify package for assertions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't use testify and I don't see it as idiomatic Go.
But I've added it and I hope that I'm using it the correct way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests should now have full coverage.
flag_text.go
Outdated
return v.Value | ||
} | ||
|
||
func (v TextValue) Create(t TextMarshalUnMarshaller, _ *TextMarshalUnMarshaller, c StringConfig) Value { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func (v TextValue) Create(t TextMarshalUnMarshaller, _ *TextMarshalUnMarshaller, c StringConfig) Value { | |
func (v TextValue) Create(t TextMarshalUnMarshaller, p *TextMarshalUnMarshaller, c StringConfig) Value { | |
b, _ := p.MarshalText() | |
t.UnmarshalText(b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made the necessary changes.
flag_text.go
Outdated
type TextFlag = FlagBase[TextMarshalUnMarshaller, StringConfig, TextValue] | ||
|
||
type TextValue struct { | ||
Value TextMarshalUnMarshaller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Value TextMarshalUnMarshaller | |
Value *TextMarshalUnMarshaller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made the necessary changes.
flag_text.go
Outdated
|
||
func (v TextValue) Create(t TextMarshalUnMarshaller, _ *TextMarshalUnMarshaller, c StringConfig) Value { | ||
return &TextValue{ | ||
Value: t, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Value: t, | |
Value: p, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made the necessary changes.
flag_text.go
Outdated
|
||
func (v TextValue) Set(s string) error { | ||
if v.Config.TrimSpace { | ||
return v.Value.UnmarshalText([]byte(strings.TrimSpace(s))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return v.Value.UnmarshalText([]byte(strings.TrimSpace(s))) | |
s = strings.TrimSpace(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made the necessary changes.
flag_text_test.go
Outdated
name: "empty", | ||
flag: TextFlag{ | ||
Name: "log-level", | ||
Value: &slog.LevelVar{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that Value is used to initialize the flag. The actual value of field should be in Destination
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the flag_generic.go as an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've addressed this in the latest version of the PR.
0876e6b
to
33d1db5
Compare
d9552c8
to
35656a1
Compare
Signed-off-by: Eng Zer Jun <[email protected]>
I've discovered a limitation of the way the So the interface that I created does not equate to those two to the extent that it allows the user of the package to just put their implementation in like you can with text := &MockMarshaller{}
cmd := &Command{
Name: "foo",
Flags: []Flag{
&TextFlag{
Name: "mytext",
Value: text,
Destination: &text,
},
},
} This is not valid. Because So I would either have to not use the |
@somebadcode can you send me the test code you are using for this ? I think it should be like this.
|
Test code: type MockMarshaller struct {
text []byte
}
var _ encoding.TextMarshaler = (*MockMarshaller)(nil)
var _ encoding.TextUnmarshaler = (*MockMarshaller)(nil)
func (m *MockMarshaller) MarshalText() ([]byte, error) {
return m.text, nil
}
func (m *MockMarshaller) UnmarshalText(text []byte) error {
m.text = text
return nil
}
func TestTextFlagValueFromCommand(t *testing.T) {
text := &MockMarshaller{}
cmd := &Command{
Name: "foo",
Flags: []Flag{
&TextFlag{
Name: "mytext",
Value: text,
Destination: &text,
},
},
}
} Code that I'm trying to test: func (cmd *Command) Text(name string) TextMarshalUnmarshaler {
if v, ok := cmd.Value(name).(TextMarshalUnmarshaler); ok {
tracef("text available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("text NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
} This is what I've added but not committed yet. |
@somebadcode I think the problem is you are trying to use the same instance for both Value and Destination. Generally this distinction is present when you have "simple" types
You should test your code like this
where text1 is a pointer to marshaller/unmarshaller and text2 is a simple reference |
Why would pointing back to the same one matter? In your example Both The type Because of the constraints of So Separating the two would make this implementation less complicated and those who want to add their own custom implementations for their special flags would have an easier time if it wouldn't be this constrained. So if type FlagBase[T any, T2 any, C any, VC ValueCreator[T, T2, C]] struct {
// ...
Value T `json:"defaultValue"`
Destination T2 `json:"-"`
// ...
} This would enable us to do:
This would make the implementation cleaner, more explicit and easier to use. Simple types such as type StringFlag = FlagBase[string, *string, StringConfig, StringValue] More explicit typing reduces the constraints and increases the versatility at minimal cost (no increased runtime cost). Look at the standard library's |
@somebadcode I like that idea. Do you want include the FlagBase changes in this PR ? or should I make a separate PR for that ? |
That would be a big change not directly related to this PR but an improvement on So this PR can be rebased on top of that once the changes are merged. Also, what would the generic type be? |
@somebadcode I started working on adding a new Destination Type and ran into some issues. I dont think its that trivial to add. I dont have cycles to spare this week so I'm not sure when I will get around to it. For now I would suggest if you use GenericValue as your base or do this particular flag as a custom flag satisfying all the necessary interfaces. When we get around to making the Destination Type change I can look into compacting TextFlag into this. |
I tried to do the same but ran into issues with I'll look into changing the |
TextFlag
which supports setting values for types that satisfies bothencoding.TextMarshaller
andencoding.TextUnmarshaller
which is very handy when you want to set log levels or string-like types that satisfies the interfaces.Fixes: #2051
What type of PR is this?
What this PR does / why we need it:
Making it easier to set types that satisfies
encoding.TextMarshaller
andencoding.TextUnmarshaller
such aslog/slog.LogLevelVar
, using flags as see in the standard library'sflag
.Which issue(s) this PR fixes:
Fixes #2051
Release Notes