diff --git a/.codecov.yml b/.codecov.yml index df2aba5c..887054a2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,7 +9,7 @@ coverage: status: project: default: - target: 80% + target: 75% threshold: 2% patch: off changes: no diff --git a/decode.go b/decode.go index 3889b864..e310d8e6 100644 --- a/decode.go +++ b/decode.go @@ -36,6 +36,7 @@ type Decoder struct { disallowUnknownField bool disallowDuplicateKey bool useOrderedMap bool + useJSONUnmarshaler bool parsedFile *ast.File streamIndex int } @@ -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 @@ -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: diff --git a/decode_test.go b/decode_test.go index f4b8d6f1..01dc3350 100644 --- a/decode_test.go +++ b/decode_test.go @@ -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 diff --git a/encode.go b/encode.go index e3fef008..9bcd7eca 100644 --- a/encode.go +++ b/encode.go @@ -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 @@ -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") @@ -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") @@ -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() { diff --git a/encode_test.go b/encode_test.go index 093ceab0..4d5cef47 100644 --- a/encode_test.go +++ b/encode_test.go @@ -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 diff --git a/option.go b/option.go index 2184b7cf..343fc55d 100644 --- a/option.go +++ b/option.go @@ -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 @@ -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 + } +} diff --git a/yaml.go b/yaml.go index b4e76b48..8f56c713 100644 --- a/yaml.go +++ b/yaml.go @@ -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 +} diff --git a/yaml_test.go b/yaml_test.go index b2c34d67..8141dd22 100644 --- a/yaml_test.go +++ b/yaml_test.go @@ -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) + } +}