Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ var dblQuotedReplacer = strings.NewReplacer(
"\x7f", `\u007f`,
)

type zeroer interface {
IsZero() bool
}

var (
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
Expand Down Expand Up @@ -116,7 +120,7 @@ func Marshal(v any) ([]byte, error) {
// - bool false
//
// If omitzero is given all int and float types with a value of 0 will be
// skipped.
// skipped, as well as values with an `IsZero() bool` method.
//
// Encoding Go values without a corresponding TOML representation will return an
// error. Examples of this includes maps with non-string keys, slices with nil
Expand All @@ -125,7 +129,9 @@ func Marshal(v any) ([]byte, error) {
// is okay, as is []map[string][]string).
//
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
// keys are silently discarded.
// keys are silently discarded. Go also makes a runtime type distinction between
// fields belonging to addressable and non-addressable values, which impacts whether
// calls to Encode will notice and use Marshaler and TextMarshaler.
type Encoder struct {
Indent string // string for a single indentation level; default is two spaces.
hasWritten bool // written any output to w yet?
Expand Down Expand Up @@ -664,6 +670,10 @@ func getOptions(tag reflect.StructTag) tagOptions {
}

func isZero(rv reflect.Value) bool {
switch v := rv.Interface().(type) {
case zeroer:
return v.IsZero()
}
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
Expand Down
78 changes: 74 additions & 4 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,28 +274,98 @@ slice = ["XXX"]
})
}

type AnEnum int

func (e *AnEnum) MarshalText() ([]byte, error) {
switch *e {
case 0:
return []byte("zero"), nil
case 1:
return []byte("one"), nil
case 2:
return []byte("two"), nil
}
return nil, fmt.Errorf("invalid AnEnum value %d", int(*e))
}

func (e *AnEnum) IsZero() bool {
return *e == 0
}

// This test is surprising. We'd all probably be happier if it failed.
func TestEncodeNonAddressableField(t *testing.T) {
type simple struct {
AnEnumValue AnEnum `toml:"anEnumValue,omitzero"`
}

// NB: value needs to be "addressable" for its type alias fields to
// implement interfaces correctly under reflection
value := simple{AnEnum(1)}
expected := "anEnumValue = 1" // would normally be "one", via MarshalText

encodeExpected(t, "unaddressable struct fields do not implement interfaces", value, expected, nil)
}

func TestEncodeOmitZero(t *testing.T) {
type simple struct {
Number int `toml:"number,omitzero"`
Real float64 `toml:"real,omitzero"`
Unsigned uint `toml:"unsigned,omitzero"`
Number int `toml:"number,omitzero"`
Real float64 `toml:"real,omitzero"`
Unsigned uint `toml:"unsigned,omitzero"`
AnEnumValue AnEnum `toml:"anEnumValue,omitzero"`
}

value := simple{0, 0.0, uint(0)}
enumZero := AnEnum(0)
enumOne := AnEnum(1)
// NB: value needs to be "addressable" for its type alias fields to
// implement interfaces correctly under reflection
value := &simple{0, 0.0, uint(0), enumZero}
expected := ""

encodeExpected(t, "simple with omitzero, all zero", value, expected, nil)

value.Number = 10
value.Real = 20
value.Unsigned = 5
value.AnEnumValue = enumOne
expected = `number = 10
real = 20.0
unsigned = 5
anEnumValue = "one"
`
encodeExpected(t, "simple with omitzero, non-zero", value, expected, nil)
}

func TestEncodeOmitZeroArray(t *testing.T) {
type simple struct {
Number int `toml:"number,omitzero"`
Real float64 `toml:"real,omitzero"`
Unsigned uint `toml:"unsigned,omitzero"`
AnEnumValue AnEnum `toml:"anEnumValue,omitzero"`
}
type list struct {
Simples []simple
}

enumZero := AnEnum(0)
enumOne := AnEnum(1)
value := list{Simples: []simple{{0, 0.0, uint(0), enumZero}}}
expected := "[[Simples]]"

encodeExpected(t, "list of struct with omitzero, all zero", value, expected, nil)

value.Simples[0].Number = 10
value.Simples[0].Real = 20
value.Simples[0].Unsigned = 5
value.Simples[0].AnEnumValue = enumOne
expected = `[[Simples]]
number = 10
real = 20.0
unsigned = 5
anEnumValue = "one"
`
encodeExpected(t, "list of struct with omitzero, non-zero", value, expected, nil)
}

func TestEncodeOmitemptyEmptyName(t *testing.T) {
type simple struct {
S []int `toml:",omitempty"`
Expand Down