encoding/yaml: expose internal yaml functionality

Only the plain function API is exposed for now.
The Decoder and Encoder are still to be done.

Change-Id: I7e003c59c2dc3bd89896336fc24a7ace39cc5f92
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2564
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/yaml/yaml.go b/encoding/yaml/yaml.go
new file mode 100644
index 0000000..3db8b08
--- /dev/null
+++ b/encoding/yaml/yaml.go
@@ -0,0 +1,100 @@
+// Copyright 2019 The CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package yaml converts YAML encodings to and from CUE. When converting to CUE,
+// comments and position information are retained.
+package yaml
+
+import (
+	"bytes"
+	"io"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/internal/third_party/yaml"
+	goyaml "github.com/ghodss/yaml"
+)
+
+// TODO: replace the ghodss YAML encoder. It has a few major issues:
+//   - it does not expose the underlying error, which means we lose valuable
+//     information.
+//   - comments and other meta data are lost.
+
+// Extract parses the YAML to a CUE expression. Streams are returned as a list
+// of the streamed values.
+func Extract(filename string, src interface{}) (*ast.File, error) {
+	a := []ast.Expr{}
+	d, err := yaml.NewDecoder(filename, src)
+	if err != nil {
+		return nil, err
+	}
+	for {
+		expr, err := d.Decode()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+		a = append(a, expr)
+	}
+	f := &ast.File{Filename: filename}
+	switch len(a) {
+	case 0:
+	case 1:
+		switch x := a[0].(type) {
+		case *ast.StructLit:
+			f.Decls = x.Elts
+		default:
+			f.Decls = []ast.Decl{&ast.EmitDecl{Expr: x}}
+		}
+	default:
+		f.Decls = []ast.Decl{&ast.EmitDecl{Expr: &ast.ListLit{Elts: a}}}
+	}
+	return f, nil
+}
+
+// Decode converts a YAML file to a CUE value. Streams are returned as a list
+// of the streamed values.
+func Decode(r *cue.Runtime, filename string, src interface{}) (*cue.Instance, error) {
+	file, err := Extract(filename, src)
+	if err != nil {
+		return nil, err
+	}
+	return r.CompileFile(file)
+}
+
+// Encode returns the YAML encoding of v.
+func Encode(v cue.Value) ([]byte, error) {
+	b, err := goyaml.Marshal(v)
+	return b, err
+}
+
+// EncodeStream returns the YAML encoding of iter, where consecutive values
+// of iter are separated with a `---`.
+func EncodeStream(iter cue.Iterator) ([]byte, error) {
+	// TODO: return an io.Reader and allow asynchronous processing.
+	buf := &bytes.Buffer{}
+	for i := 0; iter.Next(); i++ {
+		if i > 0 {
+			buf.WriteString("---\n")
+		}
+		b, err := goyaml.Marshal(iter.Value())
+		if err != nil {
+			return nil, err
+		}
+		buf.Write(b)
+	}
+	return buf.Bytes(), nil
+}
diff --git a/encoding/yaml/yaml_test.go b/encoding/yaml/yaml_test.go
new file mode 100644
index 0000000..a9ce43d
--- /dev/null
+++ b/encoding/yaml/yaml_test.go
@@ -0,0 +1,106 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"strings"
+	"testing"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/format"
+)
+
+func TestYAML(t *testing.T) {
+	testCases := []struct {
+		name     string
+		yaml     string
+		want     string
+		isStream bool
+	}{{
+		name: "string literal",
+		yaml: `foo`,
+		want: `"foo"`,
+	}, {
+		name: "struct",
+		yaml: `a: foo
+b: bar`,
+		want: `a: "foo"
+b: "bar"`,
+	}, {
+		name: "stream",
+		yaml: `a: foo
+---
+b: bar
+c: baz
+`,
+		want: `[{
+	a: "foo"
+}, {
+	b: "bar"
+	c: "baz"
+}]`,
+		isStream: true,
+	}, {
+		name:     "emtpy",
+		isStream: true,
+	}}
+	r := &cue.Runtime{}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			f, err := Extract(tc.name, tc.yaml)
+			if err != nil {
+				t.Fatal(err)
+			}
+			b, _ := format.Node(f)
+			if got := strings.TrimSpace(string(b)); got != tc.want {
+				t.Errorf("got %q; want %q", got, tc.want)
+			}
+
+			inst, err := Decode(r, tc.name, tc.yaml)
+			if err != nil {
+				t.Fatal(err)
+			}
+			n := inst.Value().Syntax()
+			if s, ok := n.(*ast.StructLit); ok {
+				n = &ast.File{Decls: s.Elts}
+			}
+			b, _ = format.Node(n)
+			if got := strings.TrimSpace(string(b)); got != tc.want {
+				t.Errorf("got %q; want %q", got, tc.want)
+			}
+
+			inst, _ = r.Compile(tc.name, tc.want)
+			if !tc.isStream {
+				b, err = Encode(inst.Value())
+				if err != nil {
+					t.Error(err)
+				}
+				if got := strings.TrimSpace(string(b)); got != tc.yaml {
+					t.Errorf("got %q; want %q", got, tc.yaml)
+				}
+			} else {
+				iter, _ := inst.Value().List()
+				b, err := EncodeStream(iter)
+				if err != nil {
+					t.Error(err)
+				}
+				if got := string(b); got != tc.yaml {
+					t.Errorf("got %q; want %q", got, tc.yaml)
+				}
+			}
+		})
+	}
+}
diff --git a/internal/third_party/yaml/yaml.go b/internal/third_party/yaml/yaml.go
index 08821c8..20ef3a1 100644
--- a/internal/third_party/yaml/yaml.go
+++ b/internal/third_party/yaml/yaml.go
@@ -94,8 +94,8 @@
 //
 // The decoder introduces its own buffering and may read
 // data from r beyond the YAML values requested.
-func NewDecoder(filename string, r io.Reader) (*Decoder, error) {
-	d, err := newParser(filename, r)
+func NewDecoder(filename string, src interface{}) (*Decoder, error) {
+	d, err := newParser(filename, src)
 	if err != nil {
 		return nil, err
 	}