go-arg icon indicating copy to clipboard operation
go-arg copied to clipboard

Args with type parameters don't work when not passed

Open slnt opened this issue 2 years ago • 5 comments

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

slnt avatar Jun 07 '22 17:06 slnt

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?

alexflint avatar Jun 07 '22 20:06 alexflint

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"`
}

slnt avatar Jun 08 '22 19:06 slnt

Is this perhaps related to #160? I was expecting that the arg not being passed would have just left the zero value in place.

slnt avatar Jun 08 '22 19:06 slnt

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.

alexflint avatar Jun 09 '22 14:06 alexflint

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)
}

alexflint avatar Jun 09 '22 15:06 alexflint

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.

alexflint avatar Oct 29 '22 16:10 alexflint