diff --git a/go.mod b/go.mod index 99b48c1..9d8b82f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/oapi-codegen/nullable go 1.20 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/internal/test/go.mod b/internal/test/go.mod index 67ee0a0..fadbbb0 100644 --- a/internal/test/go.mod +++ b/internal/test/go.mod @@ -7,10 +7,10 @@ replace github.com/oapi-codegen/nullable => ../../ require ( github.com/oapi-codegen/nullable v0.0.0-00010101000000-000000000000 github.com/stretchr/testify v1.8.4 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/test/nullable_test.go b/internal/test/nullable_test.go index c12b568..495bcd6 100644 --- a/internal/test/nullable_test.go +++ b/internal/test/nullable_test.go @@ -5,12 +5,13 @@ import ( "testing" "github.com/oapi-codegen/nullable" + "gopkg.in/yaml.v3" "github.com/stretchr/testify/require" ) type Obj struct { - Foo nullable.Nullable[string] `json:"foo,omitempty"` // note "omitempty" is important for fields that are optional + Foo nullable.Nullable[string] `json:"foo,omitempty",yaml:"foo,omitempty"` // note "omitempty" is important for fields that are optional } func TestNullable(t *testing.T) { @@ -88,3 +89,79 @@ func serialize(o Obj, t *testing.T) string { require.NoError(t, err) return string(data) } + +func TestNullableYAML(t *testing.T) { + // --- parsing from json and serializing back to JSON + + // -- case where there is an actual value + data := `foo: bar` + // deserialize from json + myObj := parseYAML(data, t) + require.Equal(t, myObj, Obj{Foo: nullable.Nullable[string]{true: "bar"}}) + require.False(t, myObj.Foo.IsNull()) + require.True(t, myObj.Foo.IsSpecified()) + value, err := myObj.Foo.Get() + require.NoError(t, err) + require.Equal(t, "bar", value) + require.Equal(t, "bar", myObj.Foo.MustGet()) + // serialize back to json: leads to the same data + require.Equal(t, data, serializeYAML(myObj, t)) + + // -- case where no value is specified: parsed from JSON + data = `` + // deserialize from json + myObj = parseYAML(data, t) + require.Equal(t, myObj, Obj{Foo: nil}) + require.False(t, myObj.Foo.IsNull()) + require.False(t, myObj.Foo.IsSpecified()) + _, err = myObj.Foo.Get() + require.ErrorContains(t, err, "value is not specified") + // serialize back to json: leads to the same data + require.Equal(t, data, serializeYAML(myObj, t)) + + // -- case where the specified value is explicitly null + data = `foo:null` + // deserialize from json + myObj = parseYAML(data, t) + require.Equal(t, myObj, Obj{Foo: nullable.Nullable[string]{false: ""}}) + require.True(t, myObj.Foo.IsNull()) + require.True(t, myObj.Foo.IsSpecified()) + _, err = myObj.Foo.Get() + require.ErrorContains(t, err, "value is null") + require.Panics(t, func() { myObj.Foo.MustGet() }) + // serialize back to json: leads to the same data + require.Equal(t, data, serializeYAML(myObj, t)) + + // --- building objects from a Go client + + // - case where there is an actual value + myObj = Obj{} + myObj.Foo.Set("bar") + require.Equal(t, `foo:"bar"`, serialize(myObj, t)) + + // - case where the value should be unspecified + myObj = Obj{} + // do nothing: unspecified by default + require.Equal(t, ``, serializeYAML(myObj, t)) + // explicitly mark unspecified + myObj.Foo.SetUnspecified() + require.Equal(t, ``, serializeYAML(myObj, t)) + + // - case where the value should be null + myObj = Obj{} + myObj.Foo.SetNull() + require.Equal(t, `foo:null`, serialize(myObj, t)) +} + +func parseYAML(data string, t *testing.T) Obj { + var myObj Obj + err := yaml.Unmarshal([]byte(data), &myObj) + require.NoError(t, err) + return myObj +} + +func serializeYAML(o Obj, t *testing.T) string { + data, err := yaml.Marshal(o) + require.NoError(t, err) + return string(data) +} diff --git a/nullable.go b/nullable.go index d2e3238..f2560d7 100644 --- a/nullable.go +++ b/nullable.go @@ -4,6 +4,10 @@ import ( "bytes" "encoding/json" "errors" + "fmt" + "reflect" + + "gopkg.in/yaml.v3" ) // Nullable is a generic type, which implements a field that can be one of three states: @@ -115,3 +119,49 @@ func (t *Nullable[T]) UnmarshalJSON(data []byte) error { t.Set(v) return nil } + +// TODO pointer receiver https://github.com/go-yaml/yaml/issues/134#issuecomment-2044424851 +func (t Nullable[T]) MarshalYAML() (interface{}, error) { + fmt.Println("MarshalYAML") + // if field was specified, and `null`, marshal it + if t.IsNull() { + return []byte("null"), nil + } + + // if field was unspecified, and `omitempty` is set on the field's tags, `json.Marshal` will omit this field + + // otherwise: we have a value, so marshal it + // fmt.Printf("t[true]: %v\n", t[true]) + // b, _ := yaml.Marshal(t[true]) + // fmt.Printf("b: %v\n", b) + // return yaml.Marshal(t[true]) + vv := (t)[true] + fmt.Printf("vv: %v\n", vv) + fmt.Printf("reflect.ValueOf(vv): %v\n", reflect.ValueOf(vv)) + return json.Marshal(t[true]) +} + +func (t *Nullable[T]) UnmarshalYAML(value *yaml.Node) error { + // if field is unspecified, UnmarshalJSON won't be called + // fmt.Printf("value: %v\n", value) + // value.Kind == yaml.Kind + + fmt.Printf("value: %v\n", value) + fmt.Printf("value.Tag: %v\n", value.Tag) + + ////// // if field is specified, and `null` + ////// if bytes.Equal(data, []byte("null")) { + ////// t.SetNull() + ////// return nil + ////// } + // otherwise, we have an actual value, so parse it + var v T + + fmt.Printf("reflect.TypeOf(v): %v\n", reflect.TypeOf(v)) + if err := value.Decode(&v); err != nil { + return err + } + fmt.Printf("v: %v\n", v) + t.Set(v) + return nil +}