From dd0f5932b3eae076c6f6ad9cbb026bfcad93afc2 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 4 Nov 2025 14:43:30 +0100 Subject: [PATCH] Switch from BoolTrue to optional.Option[bool] (#5693) --- go.mod | 2 +- .../frontend/yaml/constraint/constraint.go | 12 +- pipeline/frontend/yaml/constraint/path.go | 23 ++- pipeline/frontend/yaml/types/base/bool.go | 57 ------- .../frontend/yaml/types/base/bool_test.go | 78 --------- shared/optional/option.go | 82 ++++++++++ shared/optional/option_test.go | 99 ++++++++++++ shared/optional/serialization.go | 48 ++++++ shared/optional/serialization_json_test.go | 91 +++++++++++ shared/optional/serialization_test.go | 19 +++ shared/optional/serialization_yaml_test.go | 148 ++++++++++++++++++ 11 files changed, 513 insertions(+), 146 deletions(-) delete mode 100644 pipeline/frontend/yaml/types/base/bool.go delete mode 100644 pipeline/frontend/yaml/types/base/bool_test.go create mode 100644 shared/optional/option.go create mode 100644 shared/optional/option_test.go create mode 100644 shared/optional/serialization.go create mode 100644 shared/optional/serialization_json_test.go create mode 100644 shared/optional/serialization_test.go create mode 100644 shared/optional/serialization_yaml_test.go diff --git a/go.mod b/go.mod index c961ba37b..75d84b977 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/hashicorp/go-plugin v1.7.0 github.com/jellydator/ttlcache/v3 v3.4.0 github.com/joho/godotenv v1.5.1 + github.com/json-iterator/go v1.1.12 github.com/kinbiko/jsonassert v1.2.0 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.32 @@ -140,7 +141,6 @@ require ( github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/pipeline/frontend/yaml/constraint/constraint.go b/pipeline/frontend/yaml/constraint/constraint.go index d5b2736bd..c6c3afedc 100644 --- a/pipeline/frontend/yaml/constraint/constraint.go +++ b/pipeline/frontend/yaml/constraint/constraint.go @@ -25,6 +25,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base" + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" ) type ( @@ -43,7 +44,7 @@ type ( Cron List `yaml:"cron,omitempty"` Status List `yaml:"status,omitempty"` Matrix Map `yaml:"matrix,omitempty"` - Local yamlBaseTypes.BoolTrue `yaml:"local,omitempty"` + Local optional.Option[bool] `yaml:"local,omitempty"` Path Path `yaml:"path,omitempty"` Evaluate string `yaml:"evaluate,omitempty"` Event yamlBaseTypes.StringOrSlice `yaml:"event,omitempty"` @@ -102,7 +103,7 @@ func (when *When) IncludesStatusSuccess() bool { // False if (any) non local. func (when *When) IsLocal() bool { for _, c := range when.Constraints { - if !c.Local.Bool() { + if !c.Local.ValueOrDefault(true) { return false } } @@ -132,6 +133,13 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error { // MarshalYAML implements custom Yaml marshaling. func (when When) MarshalYAML() (any, error) { + // clean up local if true make it none as we will default to true + for i := range when.Constraints { + if when.Constraints[i].Local.ValueOrDefault(true) { + when.Constraints[i].Local = optional.None[bool]() + } + } + switch len(when.Constraints) { case 0: return nil, nil diff --git a/pipeline/frontend/yaml/constraint/path.go b/pipeline/frontend/yaml/constraint/path.go index 5c03ff42a..87a8d1e2e 100644 --- a/pipeline/frontend/yaml/constraint/path.go +++ b/pipeline/frontend/yaml/constraint/path.go @@ -22,14 +22,15 @@ import ( "gopkg.in/yaml.v3" yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base" + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" ) // Path defines a runtime constrain for exclude & include paths. type Path struct { - Include []string `yaml:"include,omitempty"` - Exclude []string `yaml:"exclude,omitempty"` - IgnoreMessage string `yaml:"ignore_message,omitempty"` - OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"` + Include []string `yaml:"include,omitempty"` + Exclude []string `yaml:"exclude,omitempty"` + IgnoreMessage string `yaml:"ignore_message,omitempty"` + OnEmpty optional.Option[bool] `yaml:"on_empty,omitempty"` } // UnmarshalYAML unmarshal the constraint. @@ -38,7 +39,7 @@ func (c *Path) UnmarshalYAML(value *yaml.Node) error { Include yamlBaseTypes.StringOrSlice `yaml:"include"` Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude"` IgnoreMessage string `yaml:"ignore_message"` - OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty"` + OnEmpty optional.Option[bool] `yaml:"on_empty"` }{} var out2 yamlBaseTypes.StringOrSlice @@ -67,18 +68,24 @@ func (c Path) MarshalYAML() (any, error) { // if only Include is set return simple syntax if len(c.Exclude) == 0 && len(c.IgnoreMessage) == 0 && - c.OnEmpty.Bool() { + c.OnEmpty.ValueOrDefault(true) { if len(c.Include) == 0 { return nil, nil } return yamlBaseTypes.StringOrSlice(c.Include), nil } + + // clean up on_empty if true make it none as we will default to true + if c.OnEmpty.ValueOrDefault(true) { + c.OnEmpty = optional.None[bool]() + } + // we can not return type Path as it would lead to infinite recursion :/ return struct { Include yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"` Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"` IgnoreMessage string `yaml:"ignore_message,omitempty"` - OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"` + OnEmpty optional.Option[bool] `yaml:"on_empty,omitempty"` }{ Include: c.Include, Exclude: c.Exclude, @@ -97,7 +104,7 @@ func (c *Path) Match(v []string, message string) bool { // return value based on 'on_empty', if there are no commit files (empty commit) if len(v) == 0 { - return c.OnEmpty.Bool() + return c.OnEmpty.ValueOrDefault(true) } if len(c.Exclude) > 0 && c.Excludes(v) { diff --git a/pipeline/frontend/yaml/types/base/bool.go b/pipeline/frontend/yaml/types/base/bool.go deleted file mode 100644 index 44d544838..000000000 --- a/pipeline/frontend/yaml/types/base/bool.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2023 Woodpecker Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package base - -import ( - "strconv" - - "gopkg.in/yaml.v3" -) - -// BoolTrue is a custom Yaml boolean type that defaults to true. -type BoolTrue struct { - value bool -} - -// UnmarshalYAML implements custom Yaml unmarshal. -func (b *BoolTrue) UnmarshalYAML(value *yaml.Node) error { - var s string - if err := value.Decode(&s); err != nil { - return err - } - - v, err := strconv.ParseBool(s) - if err == nil { - b.value = !v - } - if s != "" && err != nil { - return err - } - return nil -} - -// MarshalYAML implements custom Yaml marshaling. -func (b BoolTrue) MarshalYAML() (any, error) { - return b.Bool(), nil -} - -// Bool returns the bool value. -func (b BoolTrue) Bool() bool { - return !b.value -} - -func ToBoolTrue(v bool) BoolTrue { - return BoolTrue{value: !v} -} diff --git a/pipeline/frontend/yaml/types/base/bool_test.go b/pipeline/frontend/yaml/types/base/bool_test.go deleted file mode 100644 index 68711794c..000000000 --- a/pipeline/frontend/yaml/types/base/bool_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2023 Woodpecker Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package base - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -func TestBoolTrue(t *testing.T) { - t.Run("unmarshal true", func(t *testing.T) { - in := []byte("true") - out := BoolTrue{} - err := yaml.Unmarshal(in, &out) - assert.NoError(t, err) - assert.True(t, out.Bool()) - }) - - t.Run("unmarshal false", func(t *testing.T) { - in := []byte("false") - out := BoolTrue{} - err := yaml.Unmarshal(in, &out) - assert.NoError(t, err) - assert.False(t, out.Bool()) - }) - - t.Run("unmarshal true when empty", func(t *testing.T) { - in := []byte("") - out := BoolTrue{} - err := yaml.Unmarshal(in, &out) - assert.NoError(t, err) - assert.True(t, out.Bool()) - }) - - t.Run("throw error when invalid", func(t *testing.T) { - in := []byte("abc") // string value should fail parse - out := BoolTrue{} - err := yaml.Unmarshal(in, &out) - assert.Error(t, err) - }) - - t.Run("marshal", func(t *testing.T) { - t.Run("marshal empty", func(t *testing.T) { - in := &BoolTrue{} - out, err := yaml.Marshal(&in) - assert.NoError(t, err) - assert.EqualValues(t, "true\n", string(out)) - }) - - t.Run("marshal true", func(t *testing.T) { - in := ToBoolTrue(true) - out, err := yaml.Marshal(&in) - assert.NoError(t, err) - assert.EqualValues(t, "true\n", string(out)) - }) - - t.Run("marshal false", func(t *testing.T) { - in := ToBoolTrue(false) - out, err := yaml.Marshal(&in) - assert.NoError(t, err) - assert.EqualValues(t, "false\n", string(out)) - }) - }) -} diff --git a/shared/optional/option.go b/shared/optional/option.go new file mode 100644 index 000000000..4f2eaa717 --- /dev/null +++ b/shared/optional/option.go @@ -0,0 +1,82 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 The Gitea Authors. +// +// Licensed under the MIT License. + +package optional + +import "reflect" + +type Option[T any] []T + +func None[T any]() Option[T] { + return nil +} + +func Some[T any](v T) Option[T] { + return Option[T]{v} +} + +func FromPtr[T any](v *T) Option[T] { + if v == nil { + return None[T]() + } + return Some(*v) +} + +func FromNonDefault[T comparable](v T) Option[T] { + var zero T + if v == zero { + return None[T]() + } + return Some(v) +} + +func (o Option[T]) Has() bool { + return o != nil +} + +func (o Option[T]) Value() T { + var zero T + return o.ValueOrDefault(zero) +} + +func (o Option[T]) ValueOrDefault(v T) T { + if o.Has() { + return o[0] + } + return v +} + +func (o Option[T]) ToPtr() *T { + if o.Has() { + return &o[0] + } + return nil +} + +// ExtractValue return value or nil and bool if object was an Optional +// it should only be used if you already have to deal with interface{} values +// and expect an Option type within it. +func ExtractValue(obj any) (any, bool) { + rt := reflect.TypeOf(obj) + if rt.Kind() != reflect.Slice { + return nil, false + } + + type hasHasFunc interface { + Has() bool + } + if hasObj, ok := obj.(hasHasFunc); !ok { + return nil, false + } else if !hasObj.Has() { + return nil, true + } + + rv := reflect.ValueOf(obj) + if rv.Len() != 1 { + // it's still false as optional.Option[T] types would have reported with hasObj.Has() that it is empty + return nil, false + } + return rv.Index(0).Interface(), true +} diff --git a/shared/optional/option_test.go b/shared/optional/option_test.go new file mode 100644 index 000000000..d46e1f532 --- /dev/null +++ b/shared/optional/option_test.go @@ -0,0 +1,99 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 The Gitea Authors. +// +// Licensed under the MIT License. + +package optional_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" +) + +func TestOption(t *testing.T) { + var uninitialized optional.Option[int] + assert.False(t, uninitialized.Has()) + assert.Equal(t, int(0), uninitialized.Value()) + assert.Equal(t, int(1), uninitialized.ValueOrDefault(1)) + + none := optional.None[int]() + assert.False(t, none.Has()) + assert.Equal(t, int(0), none.Value()) + assert.Equal(t, int(1), none.ValueOrDefault(1)) + + some := optional.Some[int](1) + assert.True(t, some.Has()) + assert.Equal(t, int(1), some.Value()) + assert.Equal(t, int(1), some.ValueOrDefault(2)) + + var ptr *int + assert.False(t, optional.FromPtr(ptr).Has()) + + var boolPtr *bool + assert.Equal(t, boolPtr, optional.None[bool]().ToPtr()) + + boolPtr = optional.Some[bool](false).ToPtr() + assert.Equal(t, toPtr(false), boolPtr) + + opt1 := optional.FromPtr(toPtr(1)) + assert.True(t, opt1.Has()) + assert.Equal(t, int(1), opt1.Value()) + + assert.False(t, optional.FromNonDefault("").Has()) + + opt2 := optional.FromNonDefault("test") + assert.True(t, opt2.Has()) + assert.Equal(t, "test", opt2.Value()) + + assert.False(t, optional.FromNonDefault(0).Has()) + + opt3 := optional.FromNonDefault(1) + assert.True(t, opt3.Has()) + assert.Equal(t, int(1), opt3.Value()) +} + +func TestExtractValue(t *testing.T) { + val, ok := optional.ExtractValue("aaaa") + assert.False(t, ok) + assert.Nil(t, val) + + val, ok = optional.ExtractValue(optional.Some("aaaa")) + assert.True(t, ok) + if assert.NotNil(t, val) { + val, ok := val.(string) + assert.True(t, ok) + assert.EqualValues(t, "aaaa", val) + } + + val, ok = optional.ExtractValue(optional.None[float64]()) + assert.True(t, ok) + assert.Nil(t, val) + + val, ok = optional.ExtractValue(&fakeHas{}) + assert.False(t, ok) + assert.Nil(t, val) + + wrongType := make(fakeHas2, 0, 1) + val, ok = optional.ExtractValue(wrongType) + assert.False(t, ok) + assert.Nil(t, val) +} + +func toPtr[T any](val T) *T { + return &val +} + +type fakeHas struct{} + +func (fakeHas) Has() bool { + return true +} + +type fakeHas2 []string + +func (fakeHas2) Has() bool { + return true +} diff --git a/shared/optional/serialization.go b/shared/optional/serialization.go new file mode 100644 index 000000000..c59a6e4d7 --- /dev/null +++ b/shared/optional/serialization.go @@ -0,0 +1,48 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 "6543". +// +// Licensed under the MIT License. + +package optional + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +func (o *Option[T]) UnmarshalJSON(data []byte) error { + var v *T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *o = FromPtr(v) + return nil +} + +func (o Option[T]) MarshalJSON() ([]byte, error) { + if !o.Has() { + return []byte("null"), nil + } + + return json.Marshal(o.Value()) +} + +func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { + var v *T + if err := value.Decode(&v); err != nil { + return err + } + *o = FromPtr(v) + return nil +} + +func (o Option[T]) MarshalYAML() (any, error) { + if !o.Has() { + return nil, nil + } + + value := new(yaml.Node) + err := value.Encode(o.Value()) + return value, err +} diff --git a/shared/optional/serialization_json_test.go b/shared/optional/serialization_json_test.go new file mode 100644 index 000000000..452b6bdc4 --- /dev/null +++ b/shared/optional/serialization_json_test.go @@ -0,0 +1,91 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 "6543". +// +// Licensed under the MIT License. + +package optional_test + +import ( + "encoding/json" + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" +) + +func TestOptionalToJson(t *testing.T) { + tests := []struct { + name string + obj *testSerializationStruct + want string + }{ + { + name: "empty", + obj: new(testSerializationStruct), + want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`, + }, + { + name: "some", + obj: &testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + b, err := json.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "gitea json module returned unexpected") + + b, err = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "std json module returned unexpected") + }) + } +} + +func TestOptionalFromJson(t *testing.T) { + tests := []struct { + name string + data string + want testSerializationStruct + }{ + { + name: "empty", + data: `{}`, + want: testSerializationStruct{ + NormalString: "", + OptBool: optional.None[bool](), + }, + }, + { + name: "some", + data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + want: testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var obj1 testSerializationStruct + err := json.Unmarshal([]byte(tc.data), &obj1) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj1, "gitea json module returned unexpected") + + var obj2 testSerializationStruct + err = jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal([]byte(tc.data), &obj2) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj2, "std json module returned unexpected") + }) + } +} diff --git a/shared/optional/serialization_test.go b/shared/optional/serialization_test.go new file mode 100644 index 000000000..474159d93 --- /dev/null +++ b/shared/optional/serialization_test.go @@ -0,0 +1,19 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 "6543". +// +// Licensed under the MIT License. + +package optional_test + +import ( + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" +) + +type testSerializationStruct struct { + NormalString string `json:"normal_string" yaml:"normal_string"` + NormalBool bool `json:"normal_bool" yaml:"normal_bool"` + OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` + OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"` + OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"` +} diff --git a/shared/optional/serialization_yaml_test.go b/shared/optional/serialization_yaml_test.go new file mode 100644 index 000000000..c5f79730b --- /dev/null +++ b/shared/optional/serialization_yaml_test.go @@ -0,0 +1,148 @@ +// Copyright 2025 Woodpecker Authors. +// Copyright 2024 "6543". +// +// Licensed under the MIT License. + +package optional_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "go.woodpecker-ci.org/woodpecker/v3/shared/optional" +) + +type testBoolStruct struct { + OptBoolOmitEmpty1 optional.Option[bool] `json:"opt_bool_omit_empty_1,omitempty" yaml:"opt_bool_omit_empty_1,omitempty"` + OptBoolOmitEmpty2 optional.Option[bool] `json:"opt_bool_omit_empty_2,omitempty" yaml:"opt_bool_omit_empty_2,omitempty"` + OptBoolOmitEmpty3 optional.Option[bool] `json:"opt_bool_omit_empty_3,omitempty" yaml:"opt_bool_omit_empty_3,omitempty"` + OptBool4 optional.Option[bool] `json:"opt_bool_4" yaml:"opt_bool_4"` + OptBool5 optional.Option[bool] `json:"opt_bool_5" yaml:"opt_bool_5"` + OptBool6 optional.Option[bool] `json:"opt_bool_6" yaml:"opt_bool_6"` +} + +func TestOptionalBoolYaml(t *testing.T) { + tYaml := ` +opt_bool_omit_empty_1: false +opt_bool_omit_empty_2: true +opt_bool_4: false +opt_bool_5: true +` + + tObj := new(testBoolStruct) + t.Run("Unmarshal", func(t *testing.T) { + err := yaml.Unmarshal([]byte(tYaml), tObj) + require.NoError(t, err) + assert.EqualValues(t, &testBoolStruct{ + OptBoolOmitEmpty1: optional.Some(false), + OptBoolOmitEmpty2: optional.Some(true), + OptBoolOmitEmpty3: optional.None[bool](), + OptBool4: optional.Some(false), + OptBool5: optional.Some(true), + OptBool6: optional.None[bool](), + }, tObj) + }) + t.Run("Marshal", func(t *testing.T) { + tBytes, err := yaml.Marshal(tObj) + require.NoError(t, err) + assert.EqualValues(t, `opt_bool_omit_empty_1: false +opt_bool_omit_empty_2: true +opt_bool_4: false +opt_bool_5: true +opt_bool_6: null +`, string(tBytes)) + }) +} + +func TestOptionalToYaml(t *testing.T) { + tests := []struct { + name string + obj *testSerializationStruct + want string + }{ + { + name: "empty", + obj: new(testSerializationStruct), + want: `normal_string: "" +normal_bool: false +optional_two_bool: null +optional_two_string: null +`, + }, + { + name: "some", + obj: &testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + want: `normal_string: a string +normal_bool: true +optional_bool: false +optional_string: "" +optional_two_bool: null +optional_two_string: null +`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + b, err := yaml.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "yaml module returned unexpected") + }) + } +} + +func TestOptionalFromYaml(t *testing.T) { + tests := []struct { + name string + data string + want testSerializationStruct + }{ + { + name: "empty", + data: ``, + want: testSerializationStruct{}, + }, + { + name: "empty but init", + data: `normal_string: "" +normal_bool: false +optional_bool: +optional_two_bool: +optional_two_string: +`, + want: testSerializationStruct{}, + }, + { + name: "some", + data: ` +normal_string: a string +normal_bool: true +optional_bool: false +optional_string: "" +optional_two_bool: null +optional_twostring: null +`, + want: testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var obj testSerializationStruct + err := yaml.Unmarshal([]byte(tc.data), &obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj, "yaml module returned unexpected") + }) + } +}