Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for YAML (un)marshaling #12

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/oapi-codegen/nullable

go 1.20

require gopkg.in/yaml.v3 v3.0.1
2 changes: 1 addition & 1 deletion internal/test/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
79 changes: 78 additions & 1 deletion internal/test/nullable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
50 changes: 50 additions & 0 deletions nullable.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"

"gopkg.in/yaml.v3"

Check failure on line 10 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.20)

missing go.sum entry for module providing package gopkg.in/yaml.v3 (imported by github.com/oapi-codegen/nullable); to add:

Check failure on line 10 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.21)

missing go.sum entry for module providing package gopkg.in/yaml.v3 (imported by github.com/oapi-codegen/nullable); to add:

Check failure on line 10 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.20)

missing go.sum entry for module providing package gopkg.in/yaml.v3 (imported by github.com/oapi-codegen/nullable); to add:

Check failure on line 10 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.21)

missing go.sum entry for module providing package gopkg.in/yaml.v3 (imported by github.com/oapi-codegen/nullable); to add:
)

// Nullable is a generic type, which implements a field that can be one of three states:
Expand Down Expand Up @@ -115,3 +119,49 @@
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 {

Check failure on line 144 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.20)

undefined: yaml (typecheck)

Check failure on line 144 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.21)

undefined: yaml (typecheck)

Check failure on line 144 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.20)

undefined: yaml (typecheck)

Check failure on line 144 in nullable.go

View workflow job for this annotation

GitHub Actions / Build (1.21)

undefined: yaml (typecheck)
// 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
}
Loading