diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6a9b57f..1e2a31a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -9,7 +9,7 @@ jobs: build: strategy: matrix: - go-versions: [1.13.x, 1.14.x, 1.15.x] + go-versions: [1.13.x, 1.14.x, 1.15.x, 1.16.x] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..b0f5205 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,48 @@ +package yaml + +import ( + "testing" +) + +type benchStruct struct { + a int + b string + c map[string]float32 +} + +func BenchmarkMarshal(b *testing.B) { + s := benchStruct{ + a: 5, + b: "foobar", + c: map[string]float32{ + "bish": 1.2, + "bash": 3.4, + "bosh": 5.6, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := Marshal(s); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUnmarshal(b *testing.B) { + yaml := []byte(`a: 5 +b: "foobar" +c: + bish: 1.2 + bash: 3.4 + bosh: 5.6 +`) + var s benchStruct + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := Unmarshal(yaml, &s); err != nil { + b.Fatal(err) + } + } +} diff --git a/go.mod b/go.mod index 7224f34..818bbb5 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 - gopkg.in/yaml.v2 v2.2.8 + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 6898f93..b7b8cbb 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/yaml.go b/yaml.go index efbc535..87cfedc 100644 --- a/yaml.go +++ b/yaml.go @@ -11,20 +11,98 @@ import ( "gopkg.in/yaml.v2" ) -// Marshal marshals the object into JSON then converts JSON to YAML and returns the -// YAML. -func Marshal(o interface{}) ([]byte, error) { - j, err := json.Marshal(o) - if err != nil { - return nil, fmt.Errorf("error marshaling into JSON: %v", err) +// An Encoder serializes values into YAML and writes them using its io.Writer. +type Encoder struct { + w io.Writer +} + +// NewEncoder creates a new Encoder. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} + +// Encode serializes a value into YAML and writes it using the Encoder's +// io.Writer. +func (e *Encoder) Encode(o interface{}) error { + pipeReader, pipeWriter := io.Pipe() + defer pipeReader.Close() + + var intermediate interface{} + jsonEnc := json.NewEncoder(pipeWriter) + // We are using yaml.Decoder here (instead of json.Decoder) because the Go + // JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshalling to interface{}; it always picks float64. + // go-yaml preserves the number type. + yamlDec := yaml.NewDecoder(pipeReader) + yamlEnc := yaml.NewEncoder(e.w) + + var jsonErr error + go func() { + jsonErr = jsonEnc.Encode(o) + pipeWriter.Close() + }() + yamlErr := yamlDec.Decode(&intermediate) + if jsonErr != nil { + return fmt.Errorf("error marshalling into JSON: %v", jsonErr) + } + if yamlErr != nil { + return fmt.Errorf("error converting JSON to YAML: %v", yamlErr) + } + if err := yamlEnc.Encode(intermediate); err != nil { + return err } + return yamlEnc.Close() +} - y, err := JSONToYAML(j) - if err != nil { - return nil, fmt.Errorf("error converting JSON to YAML: %v", err) +// A Decoder reads YAML from its io.Reader and de-serializes it into values. +type Decoder struct { + dec *yaml.Decoder + opts []JSONOpt +} + +// NewDecoder creates a new Decoder. +func NewDecoder(r io.Reader, opts ...JSONOpt) *Decoder { + return &Decoder{ + dec: yaml.NewDecoder(r), + opts: opts, + } +} + +// SetStrict sets whether strict decoding behaviour is enabled when decoding +// items in the data (see UnmarshalStrict). By default, decoding is not strict. +func (d *Decoder) SetStrict(strict bool) { + d.dec.SetStrict(strict) +} + +// Decode reads YAML from its io.Reader and de-serializes it into the given +// value. +func (d *Decoder) Decode(o interface{}) error { + vo := reflect.ValueOf(o) + r, w := io.Pipe() + defer r.Close() + // go test -race complains if we set the error via closure (e.g. see Encode). + errChan := make(chan error, 1) + go func() { + errChan <- yamlToJSON(&vo, d.dec, w) + w.Close() + }() + jsonErr := jsonUnmarshal(r, o, d.opts...) + yamlErr := <-errChan + if yamlErr != nil { + return fmt.Errorf("error converting YAML to JSON: %v", yamlErr) + } + if jsonErr != nil { + return fmt.Errorf("error unmarshalling JSON: %v", jsonErr) } + return nil +} - return y, nil +// Marshal serializes the value provided into a YAML document. +func Marshal(o interface{}) ([]byte, error) { + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + err := enc.Encode(o) + return buf.Bytes(), err } // JSONOpt is a decoding option for decoding from JSON format. @@ -33,34 +111,18 @@ type JSONOpt func(*json.Decoder) *json.Decoder // Unmarshal converts YAML to JSON then uses JSON to unmarshal into an object, // optionally configuring the behavior of the JSON unmarshal. func Unmarshal(y []byte, o interface{}, opts ...JSONOpt) error { - return yamlUnmarshal(y, o, false, opts...) + buf := bytes.NewBuffer(y) + dec := NewDecoder(buf, opts...) + return dec.Decode(o) } // UnmarshalStrict strictly converts YAML to JSON then uses JSON to unmarshal // into an object, optionally configuring the behavior of the JSON unmarshal. func UnmarshalStrict(y []byte, o interface{}, opts ...JSONOpt) error { - return yamlUnmarshal(y, o, true, append(opts, DisallowUnknownFields)...) -} - -// yamlUnmarshal unmarshals the given YAML byte stream into the given interface, -// optionally performing the unmarshalling strictly -func yamlUnmarshal(y []byte, o interface{}, strict bool, opts ...JSONOpt) error { - vo := reflect.ValueOf(o) - unmarshalFn := yaml.Unmarshal - if strict { - unmarshalFn = yaml.UnmarshalStrict - } - j, err := yamlToJSON(y, &vo, unmarshalFn) - if err != nil { - return fmt.Errorf("error converting YAML to JSON: %v", err) - } - - err = jsonUnmarshal(bytes.NewReader(j), o, opts...) - if err != nil { - return fmt.Errorf("error unmarshaling JSON: %v", err) - } - - return nil + buf := bytes.NewBuffer(y) + dec := NewDecoder(buf, append(opts, DisallowUnknownFields)...) + dec.SetStrict(true) + return dec.Decode(o) } // jsonUnmarshal unmarshals the JSON byte stream from the given reader into the @@ -109,21 +171,29 @@ func JSONToYAML(j []byte) ([]byte, error) { // // For strict decoding of YAML, use YAMLToJSONStrict. func YAMLToJSON(y []byte) ([]byte, error) { - return yamlToJSON(y, nil, yaml.Unmarshal) + bufIn := bytes.NewBuffer(y) + bufOut := &bytes.Buffer{} + dec := yaml.NewDecoder(bufIn) + err := yamlToJSON(nil, dec, bufOut) + return bytes.TrimSpace(bufOut.Bytes()), err } // YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding, // returning an error on any duplicate field names. func YAMLToJSONStrict(y []byte) ([]byte, error) { - return yamlToJSON(y, nil, yaml.UnmarshalStrict) + bufIn := bytes.NewBuffer(y) + bufOut := &bytes.Buffer{} + dec := yaml.NewDecoder(bufIn) + dec.SetStrict(true) + err := yamlToJSON(nil, dec, bufOut) + return bytes.TrimSpace(bufOut.Bytes()), err } -func yamlToJSON(y []byte, jsonTarget *reflect.Value, yamlUnmarshal func([]byte, interface{}) error) ([]byte, error) { +func yamlToJSON(jsonTarget *reflect.Value, dec *yaml.Decoder, tgt io.Writer) error { // Convert the YAML to an object. var yamlObj interface{} - err := yamlUnmarshal(y, &yamlObj) - if err != nil { - return nil, err + if err := dec.Decode(&yamlObj); err != nil { + return err } // YAML objects are not completely compatible with JSON objects (e.g. you @@ -132,11 +202,11 @@ func yamlToJSON(y []byte, jsonTarget *reflect.Value, yamlUnmarshal func([]byte, // incompatibilties happen along the way. jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget) if err != nil { - return nil, err + return err } // Convert this object to JSON and return the data. - return json.Marshal(jsonObj) + return json.NewEncoder(tgt).Encode(jsonObj) } func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) { diff --git a/yaml_test.go b/yaml_test.go index 8572c73..779f04d 100644 --- a/yaml_test.go +++ b/yaml_test.go @@ -242,7 +242,7 @@ func unmarshalStrict(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) func unmarshalStrictFail(t *testing.T, y []byte, s interface{}, opts ...JSONOpt) { err := UnmarshalStrict(y, s, opts...) if err == nil { - t.Errorf("error unmarshaling YAML: %v", err) + t.Errorf("expected strict unmarshalling to fail:\n%s", y) } }