Skip to content

Commit

Permalink
Merge pull request #167 from goccy/feature/support-json-marshaler-unm…
Browse files Browse the repository at this point in the history
…arshaler

Support YAMLToJSON / JSONToYAML and UseJSONMarshaler / UseJSONUnmarshaler option
  • Loading branch information
goccy authored Oct 26, 2020
2 parents 81f720d + 52a9ce6 commit c48b2e8
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ coverage:
status:
project:
default:
target: 80%
target: 75%
threshold: 2%
patch: off
changes: no
Expand Down
17 changes: 17 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Decoder struct {
disallowUnknownField bool
disallowDuplicateKey bool
useOrderedMap bool
useJSONUnmarshaler bool
parsedFile *ast.File
streamIndex int
}
Expand Down Expand Up @@ -488,6 +489,10 @@ func (d *Decoder) unmarshalableText(node ast.Node) ([]byte, bool) {
return nil, false
}

type jsonUnmarshaler interface {
UnmarshalJSON([]byte) error
}

func (d *Decoder) decodeValue(dst reflect.Value, src ast.Node) error {
if src.Type() == ast.AnchorType {
anchorName := src.(*ast.AnchorNode).Name.GetToken().Value
Expand Down Expand Up @@ -525,6 +530,18 @@ func (d *Decoder) decodeValue(dst reflect.Value, src ast.Node) error {
}
return nil
}
} else if d.useJSONUnmarshaler {
if unmarshaler, ok := dst.Addr().Interface().(jsonUnmarshaler); ok {
jsonBytes, err := YAMLToJSON(d.unmarshalableDocument(src))
if err != nil {
return errors.Wrapf(err, "failed to convert yaml to json")
}
jsonBytes = bytes.TrimRight(jsonBytes, "\n")
if err := unmarshaler.UnmarshalJSON(jsonBytes); err != nil {
return errors.Wrapf(err, "failed to UnmarshalJSON")
}
return nil
}
}
switch valueType.Kind() {
case reflect.Ptr:
Expand Down
23 changes: 23 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,29 @@ B: d
// c
}

type useJSONUnmarshalerTest struct {
s string
}

func (t *useJSONUnmarshalerTest) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
t.s = s
return nil
}

func TestDecoder_UseJSONUnmarshaler(t *testing.T) {
var v useJSONUnmarshalerTest
if err := yaml.UnmarshalWithOptions([]byte(`"a"`), &v, yaml.UseJSONUnmarshaler()); err != nil {
t.Fatal(err)
}
if v.s != "a" {
t.Fatalf("unexpected decoded value: %s", v.s)
}
}

func Example_JSONTags() {
yml := `---
foo: 1
Expand Down
30 changes: 26 additions & 4 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Encoder struct {
indent int
isFlowStyle bool
isJSONStyle bool
useJSONMarshaler bool
anchorCallback func(*ast.AnchorNode, interface{}) error
anchorPtrToNameMap map[uintptr]string

Expand Down Expand Up @@ -118,12 +119,17 @@ func (e *Encoder) isInvalidValue(v reflect.Value) bool {
return false
}

type jsonMarshaler interface {
MarshalJSON() ([]byte, error)
}

func (e *Encoder) encodeValue(v reflect.Value, column int) (ast.Node, error) {
if e.isInvalidValue(v) {
return e.encodeNil(), nil
}
if v.CanInterface() {
if marshaler, ok := v.Interface().(BytesMarshaler); ok {
iface := v.Interface()
if marshaler, ok := iface.(BytesMarshaler); ok {
doc, err := marshaler.MarshalYAML()
if err != nil {
return nil, errors.Wrapf(err, "failed to MarshalYAML")
Expand All @@ -133,15 +139,15 @@ func (e *Encoder) encodeValue(v reflect.Value, column int) (ast.Node, error) {
return nil, errors.Wrapf(err, "failed to encode document")
}
return node, nil
} else if marshaler, ok := v.Interface().(InterfaceMarshaler); ok {
} else if marshaler, ok := iface.(InterfaceMarshaler); ok {
marshalV, err := marshaler.MarshalYAML()
if err != nil {
return nil, errors.Wrapf(err, "failed to MarshalYAML")
}
return e.encodeValue(reflect.ValueOf(marshalV), column)
} else if t, ok := v.Interface().(time.Time); ok {
} else if t, ok := iface.(time.Time); ok {
return e.encodeTime(t, column), nil
} else if marshaler, ok := v.Interface().(encoding.TextMarshaler); ok {
} else if marshaler, ok := iface.(encoding.TextMarshaler); ok {
doc, err := marshaler.MarshalText()
if err != nil {
return nil, errors.Wrapf(err, "failed to MarshalText")
Expand All @@ -151,6 +157,22 @@ func (e *Encoder) encodeValue(v reflect.Value, column int) (ast.Node, error) {
return nil, errors.Wrapf(err, "failed to encode document")
}
return node, nil
} else if e.useJSONMarshaler {
if marshaler, ok := iface.(jsonMarshaler); ok {
jsonBytes, err := marshaler.MarshalJSON()
if err != nil {
return nil, errors.Wrapf(err, "failed to MarshalJSON")
}
doc, err := JSONToYAML(jsonBytes)
if err != nil {
return nil, errors.Wrapf(err, "failed to convert json to yaml")
}
node, err := e.encodeDocument(doc)
if err != nil {
return nil, errors.Wrapf(err, "failed to encode document")
}
return node, nil
}
}
}
switch v.Type().Kind() {
Expand Down
22 changes: 22 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,28 @@ queues:
}
}

type useJSONMarshalerTest struct{}

func (t useJSONMarshalerTest) MarshalJSON() ([]byte, error) {
return []byte(`{"a":[1, 2, 3]}`), nil
}

func TestEncoder_UseJSONMarshaler(t *testing.T) {
got, err := yaml.MarshalWithOptions(useJSONMarshalerTest{}, yaml.UseJSONMarshaler())
if err != nil {
t.Fatal(err)
}
expected := `
a:
- 1
- 2
- 3
`
if expected != "\n"+string(got) {
t.Fatalf("failed to use json marshaler. expected [%q] but got [%q]", expected, string(got))
}
}

func Example_Marshal_ExplicitAnchorAlias() {
type T struct {
A int
Expand Down
19 changes: 19 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ func UseOrderedMap() DecodeOption {
}
}

// UseJSONUnmarshaler if neither `BytesUnmarshaler` nor `InterfaceUnmarshaler` is implemented
// and `UnmashalJSON([]byte)error` is implemented, convert the argument from `YAML` to `JSON` and then call it.
func UseJSONUnmarshaler() DecodeOption {
return func(d *Decoder) error {
d.useJSONUnmarshaler = true
return nil
}
}

// EncodeOption functional option type for Encoder
type EncodeOption func(e *Encoder) error

Expand Down Expand Up @@ -120,3 +129,13 @@ func MarshalAnchor(callback func(*ast.AnchorNode, interface{}) error) EncodeOpti
return nil
}
}

// UseJSONMarshaler if neither `BytesMarshaler` nor `InterfaceMarshaler`
// nor `encoding.TextMarshaler` is implemented and `MarshalJSON()([]byte, error)` is implemented,
// call `MarshalJSON` to convert the returned `JSON` to `YAML` for processing.
func UseJSONMarshaler() EncodeOption {
return func(e *Encoder) error {
e.useJSONMarshaler = true
return nil
}
}
26 changes: 26 additions & 0 deletions yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,29 @@ func FormatError(e error, colored, inclSource bool) string {

return e.Error()
}

// YAMLToJSON convert YAML bytes to JSON.
func YAMLToJSON(bytes []byte) ([]byte, error) {
var v interface{}
if err := UnmarshalWithOptions(bytes, &v, UseOrderedMap()); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal")
}
out, err := MarshalWithOptions(v, JSON())
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal with json option")
}
return out, nil
}

// JSONToYAML convert JSON bytes to YAML.
func JSONToYAML(bytes []byte) ([]byte, error) {
var v interface{}
if err := UnmarshalWithOptions(bytes, &v, UseOrderedMap()); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal from json bytes")
}
out, err := Marshal(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal")
}
return out, nil
}
38 changes: 38 additions & 0 deletions yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,41 @@ b: *b`
t.Fatalf("failed to marshal: expected:[%q] but got [%q]", expected, actual)
}
}

func Test_YAMLToJSON(t *testing.T) {
yml := `
foo:
bar:
- a
- b
- c
a: 1
`
actual, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
t.Fatal(err)
}
expected := `{"foo": {"bar": ["a", "b", "c"]}, "a": 1}`
if expected+"\n" != string(actual) {
t.Fatalf("failed to convert yaml to json: expected [%q] but got [%q]", expected, actual)
}
}

func Test_JSONToYAML(t *testing.T) {
json := `{"foo": {"bar": ["a", "b", "c"]}, "a": 1}`
expected := `
foo:
bar:
- a
- b
- c
a: 1
`
actual, err := yaml.JSONToYAML([]byte(json))
if err != nil {
t.Fatal(err)
}
if expected != "\n"+string(actual) {
t.Fatalf("failed to convert json to yaml: expected [%q] but got [%q]", expected, actual)
}
}

0 comments on commit c48b2e8

Please sign in to comment.