go-arg
go-arg copied to clipboard
Args with type parameters don't work when not passed
So I have this struct I've defined:
type JSONValue[T any] struct {
val T
}
func (v *JSONValue[T]) Get() T {
return v.val
}
func (v *JSONValue[T]) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
I am trying to use it my args like this:
type Args struct {
// ...
Environment JSONValue[map[string]string] `arg:"--env"`
// ....
}
(It has to be this way for Reasons™)
When I pass --env
it works just fine, however if I omit it then I get the following error:
error: error processing default value for --environment: invalid character 'm' looking for beginning of object key string
Do you have a default value configured, perhaps with a struct tag like default:"..."
?
type Args struct {
// ...
Environment JSONValue[map[string]string] `arg:"--env" default:"something"`
// ....
}
If so then go-arg will use that default value and pass it to your UnmarshalText. If you have a default
tag and its value causes an error when passed to JSONValue[map[string]string].UnmarshalText, then you might see this error. Perhaps this is the source of your problem?
I do not have any default value configured. :(
I'm currently on version v1.4.2 if that'd affect things.
The full entry is:
type Args struct {
Environment JSONValue[map[string]string] `arg:"ENVIRONMENT" help:"a JSON blob containing a dict of NAME=VALUE environment variables"`
}
Is this perhaps related to #160? I was expecting that the arg not being passed would have just left the zero value in place.
Very interesting, I was able to reproduce this in a unit test! I don't know exactly what's happening but a short-term fix seems to be to make your argument a pointer, like this:
type Args struct {
Environment *JSONValue[map[string]string] `arg:"ENVIRONMENT" help:"a JSON blob containing a dict of NAME=VALUE environment variables"`
}
Nevertheless, this is still definitely a bug and I will continue to investigate.
OK I have a fix but it will require a bit more work to merge it. A second short-term fix for you is to implement encoding.TextMarshaler
on your type, as in:
func (v *JSONValue[T]) MarshalText() ([]byte, error) {
return json.Marshal(v.val)
}
The basic problem here is that when a struct has a non-zero value, we take its value and turn it into a string to use as a default value in help text. Then, later, when a struct supports encoding.TextUnmarshaler
we decode its default value from string. However, the stringified version of JSONValue
looks like map[string]string{...}
whereas the UnmarshalText
function on the same struct expects json, not go-style structs. In the end it was a mistake to assume that we could turn structs into strings and then back into structs, and we shouldn't assume that we can do so.
The fix should be to store default values both as strings and structs, and do the decoding from strings to structs in cmdFromStruct
rather than in Parse
.