Announcing GoReleaser v2.8
Happy March! Another release is here with several improvements across the board.
string
and []string
can be the same thing…
As a maintainer of GoReleaser, I try to avoid breaking changes.
One thing that came up a couple of times is a configuration field that was a
string
, but now needs to be a []string
.
I could of course just change it to []string
, but would break anyone using it
as a string still.
A better way to make this work is implementing yaml.Unmarshaler
.
Let’s say we have this structure:
type Config struct {
MaybeStringArray string `yaml:"foo,omitempty"`
}
Which would allow YAML like this:
foo: "some value"
But now we want to allow the users to use it like this as well, while keeping compatibility with the previous version:
foo:
- "some value"
- "other value"
The yaml.v3 package has a very similar API to the standard library’s json
package.
Both have Marshal
and Unmarshal
, as well as a couple of similar interfaces -
and we are about to use them!
First step would be to create a type alias for []string
and change our
Config
type to use it:
type MaybeStringArray []string
type Config struct {
Foo MaybeStringArray `yaml:"foo,omitempty"`
}
Then, we need to implement yaml.Unmarshaler
:
// compile errors if interface is not implemented.
var _ yaml.Unmarshaler = &MaybeStringArray{}
// UnmarshalYAML implements yaml.Unmarshaler.
func (a *MaybeStringArray) UnmarshalYAML(value *yaml.Node) error {
var slice []string
// Try to decode into a []string, if it succeeds, we're done, return it!
if err := value.Decode(&slice); err == nil {
*a = slice
return nil
}
// If not, try to decode into a string.
var single string
if err := value.Decode(&single); err != nil {
return err
}
*a = []string{single}
return nil
}
With that, both our YAML configuration in the beginning of the post can be parsed properly! Pretty neat, right?
Another case is that you might want to marshal it back to YAML, keeping the single string API if it’s a single item, or an array if it’s more than one.
To do that, we can implement yaml.Marshaler
:
// compile errors if interface is not implemented.
var _ yaml.Marshaler = MaybeStringArray{}
// MarshalYAML implements yaml.Marshaler.
func (a MaybeStringArray) MarshalYAML() (interface{}, error) {
switch len(a) {
case 0:
return nil, nil
case 1:
return a[0], nil
default:
return []string(a), nil
}
}
You can do the same thing with JSON, and the way to do it is basically the same as well:
var _ json.Unmarshaler = &MaybeStringArray{}
var _ json.Marshaler = MaybeStringArray{}
// UnmarshalJSON implements json.Unmarshaler.
func (a *MaybeStringArray) UnmarshalJSON(data []byte) error {
var slice []string
if err := json.Unmarshal(data, &slice); err == nil {
*a = slice
return nil
}
var single string
if err := json.Unmarshal(data, &single); err != nil {
return err
}
*a = []string{single}
return nil
}
// MarshalJSON implements json.Marshaler.
func (a MaybeStringArray) MarshalJSON() ([]byte, error) {
switch len(a) {
case 0:
return json.Marshal(nil)
case 1:
return json.Marshal(a[0])
default:
return json.Marshal([]string(a))
}
}
There you have it!
I wrote this blog post because I can never find examples of this when I need to. Hopefully it helps you, and I’m sure it will help future me as well. :)
I made the entire source code available in a repository so you can play with it if you want!