Skip to content

Commit 1ce1d01

Browse files
committed
add Encoder and Decoder
add support for streaming encoding and decoding
1 parent b5bdf49 commit 1ce1d01

File tree

2 files changed

+193
-12
lines changed

2 files changed

+193
-12
lines changed

yaml.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, unmarshalFn f
161161
// YAML objects are not completely compatible with JSON objects (e.g. you
162162
// can have non-string keys in YAML). So, convert the YAML-compatible object
163163
// to a JSON-compatible object, failing with an error if irrecoverable
164-
// incompatibilties happen along the way.
164+
// incompatibilities happen along the way.
165165
jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
166166
if err != nil {
167167
return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
@@ -414,3 +414,113 @@ func jsonToYAMLValue(j interface{}) interface{} {
414414
}
415415
return j
416416
}
417+
418+
// An Encoder writes YAML values to an output stream.
419+
type Encoder struct {
420+
encoder *yaml.Encoder
421+
}
422+
423+
// NewEncoder returns a new encoder that writes to w. The Encoder should be closed after use to flush all data to w.
424+
func NewEncoder(w io.Writer) *Encoder {
425+
return &Encoder{
426+
encoder: yaml.NewEncoder(w),
427+
}
428+
}
429+
430+
// Encode writes the YAML encoding of v to the stream.
431+
// If multiple items are encoded to the stream, the second and subsequent document will be preceded with a "---" document separator,
432+
// but the first will not.
433+
//
434+
// See the documentation for Marshal for details about the conversion of Go values to YAML.
435+
func (e *Encoder) Encode(obj interface{}) error {
436+
var buf bytes.Buffer
437+
// Convert an object to the JSON.
438+
if err := json.NewEncoder(&buf).Encode(obj); err != nil {
439+
return fmt.Errorf("error encode into JSON: %w", err)
440+
}
441+
442+
// Convert the JSON to an object.
443+
var j interface{}
444+
// We are using yaml.Decoder.Decode here (instead of json.Decoder.Decode) because the
445+
// Go JSON library doesn't try to pick the right number type (int, float,
446+
// etc.) when unmarshalling to interface{}, it just picks float64
447+
// universally. go-yaml does go through the effort of picking the right
448+
// number type, so we can preserve number type throughout this process.
449+
if err := yaml.NewDecoder(&buf).Decode(&j); err != nil {
450+
return fmt.Errorf("error decode from JSON: %w", err)
451+
}
452+
// Marshal this object into YAML.
453+
if err := e.encoder.Encode(j); err != nil {
454+
return fmt.Errorf("error encode into YAML: %w", err)
455+
}
456+
457+
return nil
458+
}
459+
460+
// Close closes the encoder by writing any remaining data. It does not write a stream terminating string "...".
461+
func (e *Encoder) Close() (err error) {
462+
if err := e.encoder.Close(); err != nil {
463+
return fmt.Errorf("error closing encoder: %w", err)
464+
}
465+
return nil
466+
}
467+
468+
// A Decoder reads and decodes YAML values from an input stream.
469+
type Decoder struct {
470+
opts []JSONOpt
471+
decoder *yaml.Decoder
472+
}
473+
474+
// NewDecoder returns a new decoder that reads from r.
475+
//
476+
// The decoder introduces its own buffering and may read data from r beyond the YAML values requested.
477+
//
478+
// Options for the standard library json.Decoder can be optionally specified, e.g. to decode untyped numbers into json.Number instead of float64,
479+
// or to disallow unknown fields (but for that purpose, see also UnmarshalStrict)
480+
func NewDecoder(r io.Reader, opts ...JSONOpt) *Decoder {
481+
return &Decoder{
482+
opts: opts,
483+
decoder: yaml.NewDecoder(r),
484+
}
485+
}
486+
487+
// Decode reads the next YAML-encoded value from its input and stores it in the value pointed to by obj.
488+
//
489+
// See the documentation for Unmarshal for details about the conversion of YAML into a Go value.
490+
func (dec *Decoder) Decode(obj interface{}) error {
491+
jsonTarget := reflect.ValueOf(obj)
492+
493+
// Convert the YAML to an object.
494+
var yamlObj interface{}
495+
if err := dec.decoder.Decode(&yamlObj); err != nil {
496+
return fmt.Errorf("error converting YAML to JSON: %w", err)
497+
}
498+
499+
// YAML objects are not completely compatible with JSON objects (e.g. you
500+
// can have non-string keys in YAML). So, convert the YAML-compatible object
501+
// to a JSON-compatible object, failing with an error if irrecoverable
502+
// incompatibilities happen along the way.
503+
jsonObj, err := convertToJSONableObject(yamlObj, &jsonTarget)
504+
if err != nil {
505+
return fmt.Errorf("error converting YAML to JSON: %w", err)
506+
}
507+
508+
// Convert this object to JSON
509+
var buf bytes.Buffer
510+
if err := json.NewEncoder(&buf).Encode(jsonObj); err != nil {
511+
return fmt.Errorf("error converting YAML to JSON: %w", err)
512+
}
513+
514+
if err := jsonUnmarshal(&buf, obj, dec.opts...); err != nil {
515+
return fmt.Errorf("error unmarshaling JSON: %w", err)
516+
}
517+
518+
return nil
519+
}
520+
521+
// SetStrict sets whether strict decoding behaviour is enabled when decoding items in the data (see UnmarshalStrict).
522+
// By default, decoding is not strict. It also adds the DisallowUnknownFields option to json decoder.
523+
func (dec *Decoder) SetStrict() {
524+
dec.decoder.SetStrict(true)
525+
dec.opts = append(dec.opts, DisallowUnknownFields)
526+
}

yaml_test.go

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package yaml
1818

1919
import (
20+
"bytes"
2021
"encoding/json"
2122
"fmt"
2223
"math"
@@ -26,7 +27,7 @@ import (
2627
"testing"
2728

2829
"github.com/davecgh/go-spew/spew"
29-
yaml "gopkg.in/yaml.v2"
30+
"gopkg.in/yaml.v2"
3031
)
3132

3233
/* Test helper functions */
@@ -59,6 +60,16 @@ var (
5960
funcUnmarshalStrict testUnmarshalFunc = func(yamlBytes []byte, obj interface{}) error {
6061
return UnmarshalStrict(yamlBytes, obj)
6162
}
63+
64+
funcDecode testUnmarshalFunc = func(yamlBytes []byte, obj interface{}) error {
65+
return NewDecoder(bytes.NewReader(yamlBytes)).Decode(obj)
66+
}
67+
68+
funcDecodeStrict testUnmarshalFunc = func(yamlBytes []byte, obj interface{}) error {
69+
decoder := NewDecoder(bytes.NewReader(yamlBytes))
70+
decoder.SetStrict()
71+
return decoder.Decode(obj)
72+
}
6273
)
6374

6475
func testUnmarshal(t *testing.T, f testUnmarshalFunc, tests map[string]unmarshalTestCase) {
@@ -168,6 +179,43 @@ func testYAMLToJSON(t *testing.T, f testYAMLToJSONFunc, tests map[string]yamlToJ
168179
}
169180
}
170181

182+
type testMarshalFunc = func(obj interface{}) ([]byte, error)
183+
184+
var (
185+
funcMarshal testMarshalFunc = func(obj interface{}) ([]byte, error) {
186+
return Marshal(obj)
187+
}
188+
funcEncode testMarshalFunc = func(obj interface{}) ([]byte, error) {
189+
var buf bytes.Buffer
190+
encoder := NewEncoder(&buf)
191+
if err := encoder.Encode(obj); err != nil {
192+
return nil, err
193+
}
194+
return buf.Bytes(), nil
195+
}
196+
)
197+
198+
type marshalTestCase struct {
199+
obj interface{}
200+
encoded []byte
201+
}
202+
203+
func testMarshal(t *testing.T, f testMarshalFunc, tests map[string]marshalTestCase) {
204+
for testName, test := range tests {
205+
t.Run(testName, func(t *testing.T) {
206+
y, err := f(test.obj)
207+
if err != nil {
208+
t.Errorf("error marshaling YAML: %v", err)
209+
}
210+
211+
if !reflect.DeepEqual(y, test.encoded) {
212+
t.Errorf("marshal YAML was unsuccessful, expected: %#v, got: %#v",
213+
string(test.encoded), string(y))
214+
}
215+
})
216+
}
217+
}
218+
171219
/* Start tests */
172220

173221
type MarshalTest struct {
@@ -180,18 +228,20 @@ type MarshalTest struct {
180228

181229
func TestMarshal(t *testing.T) {
182230
f32String := strconv.FormatFloat(math.MaxFloat32, 'g', -1, 32)
183-
s := MarshalTest{"a", math.MaxInt64, math.MaxFloat32}
184-
e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String))
185-
186-
y, err := Marshal(s)
187-
if err != nil {
188-
t.Errorf("error marshaling YAML: %v", err)
231+
tests := map[string]marshalTestCase{
232+
"max": {
233+
obj: MarshalTest{"a", math.MaxInt64, math.MaxFloat32},
234+
encoded: []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String)),
235+
},
189236
}
190237

191-
if !reflect.DeepEqual(y, e) {
192-
t.Errorf("marshal YAML was unsuccessful, expected: %#v, got: %#v",
193-
string(e), string(y))
194-
}
238+
t.Run("Marshal", func(t *testing.T) {
239+
testMarshal(t, funcMarshal, tests)
240+
})
241+
242+
t.Run("Encode", func(t *testing.T) {
243+
testMarshal(t, funcEncode, tests)
244+
})
195245
}
196246

197247
type UnmarshalUntaggedStruct struct {
@@ -550,6 +600,14 @@ func TestUnmarshal(t *testing.T) {
550600
t.Run("UnmarshalStrict", func(t *testing.T) {
551601
testUnmarshal(t, funcUnmarshalStrict, tests)
552602
})
603+
604+
t.Run("Decode", func(t *testing.T) {
605+
testUnmarshal(t, funcDecode, tests)
606+
})
607+
608+
t.Run("DecodeStrict", func(t *testing.T) {
609+
testUnmarshal(t, funcDecodeStrict, tests)
610+
})
553611
}
554612

555613
func TestUnmarshalStrictFails(t *testing.T) {
@@ -618,6 +676,19 @@ func TestUnmarshalStrictFails(t *testing.T) {
618676
}
619677
testUnmarshal(t, funcUnmarshalStrict, failTests)
620678
})
679+
680+
t.Run("Decode", func(t *testing.T) {
681+
testUnmarshal(t, funcDecode, tests)
682+
})
683+
684+
t.Run("DecodeStrict", func(t *testing.T) {
685+
failTests := map[string]unmarshalTestCase{}
686+
for name, test := range tests {
687+
test.err = fatalErrorsType
688+
failTests[name] = test
689+
}
690+
testUnmarshal(t, funcDecodeStrict, failTests)
691+
})
621692
}
622693

623694
func TestYAMLToJSON(t *testing.T) {

0 commit comments

Comments
 (0)