internal/encoding/json: implement Encode

internal/encoding/json: implement Encode

Encode allows an ast.File to be encoded directly
to JSON, provided the AST only has nodes that
have a direct equivalent in JSON.

This serves two purposes:
- allow JSON to be represented as CUE (allowing
  trim on JSON, etc.)
- allow Encodings like OpenAPI to encode directly
  into CUE, instead of needing things like OrderedMap.

Change-Id: I1072f5c7472bc4f5d71403a783dfcd2355d9d9c6
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5187
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go
new file mode 100644
index 0000000..7a6b857
--- /dev/null
+++ b/internal/encoding/json/encode.go
@@ -0,0 +1,306 @@
+// Copyright 2020 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 json
+
+import (
+	"bytes"
+	"encoding/json"
+	"math/big"
+	"strings"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/literal"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
+)
+
+// Encode converts a CUE AST to JSON.
+//
+// The given file must only contain values that can be directly supported by
+// JSON:
+//    Type          Restrictions
+//    BasicLit
+//    File          no imports, aliases, or definitions
+//    StructLit     no embeddings, aliases, or definitions
+//    List
+//    Field         must be regular; label must be a BasicLit or Ident
+//
+// Comments and attributes are ignored.
+func Encode(n ast.Node) (b []byte, err error) {
+	e := encoder{}
+	err = e.encode(n)
+	if err != nil {
+		return nil, err
+	}
+	return e.w.Bytes(), nil
+}
+
+type encoder struct {
+	w              bytes.Buffer
+	tab            []byte
+	indentsAtLevel []int
+	indenting      bool
+	unIndenting    int
+}
+
+func (e *encoder) writeIndent(b byte) {
+	if e.indenting {
+		e.indentsAtLevel[len(e.indentsAtLevel)-1]++
+	} else {
+		e.indentsAtLevel = append(e.indentsAtLevel, 0)
+	}
+	e.indenting = true
+	_ = e.w.WriteByte(b)
+}
+
+func (e *encoder) writeUnindent(b byte, pos, def token.Pos) {
+	if e.unIndenting > 0 {
+		e.unIndenting--
+	} else {
+		e.unIndenting = e.indentsAtLevel[len(e.indentsAtLevel)-1]
+		e.indentsAtLevel = e.indentsAtLevel[:len(e.indentsAtLevel)-1]
+	}
+	e.indenting = false
+	e.ws(pos, def.RelPos())
+	_ = e.w.WriteByte(b)
+}
+
+func (e *encoder) writeString(s string) {
+	_, _ = e.w.WriteString(s)
+	e.indenting = false
+}
+
+func (e *encoder) writeByte(b byte) {
+	_ = e.w.WriteByte(b)
+}
+
+func (e *encoder) write(b []byte) {
+	_, _ = e.w.Write(b)
+	e.indenting = false
+}
+
+func (e *encoder) indent() {
+	for range e.indentsAtLevel {
+		e.write(e.tab)
+	}
+}
+
+func (e *encoder) ws(pos token.Pos, default_ token.RelPos) {
+	rel := pos.RelPos()
+	if pos == token.NoPos {
+		rel = default_
+	}
+	switch rel {
+	case token.NoSpace:
+	case token.Blank:
+		e.writeByte(' ')
+	case token.Newline:
+		e.writeByte('\n')
+		e.indent()
+	case token.NewSection:
+		e.writeString("\n\n")
+		e.indent()
+	}
+}
+func (e *encoder) encode(n ast.Node) error {
+	if e.tab == nil {
+		e.tab = []byte("    ")
+	}
+	const defPos = token.NoSpace
+	switch x := n.(type) {
+	case *ast.BasicLit:
+		e.ws(x.Pos(), defPos)
+		return e.encodeScalar(x, true)
+
+	case *ast.ListLit:
+		e.ws(foldNewline(x.Pos()), token.NoRelPos)
+		if len(x.Elts) == 0 {
+			e.writeString("[]")
+			return nil
+		}
+		e.writeIndent('[')
+		for i, x := range x.Elts {
+			if i > 0 {
+				e.writeString(",")
+			}
+			if err := e.encode(x); err != nil {
+				return err
+			}
+		}
+		e.writeUnindent(']', x.Rbrack, compactNewline(x.Elts[0].Pos()))
+		return nil
+
+	case *ast.StructLit:
+		e.ws(foldNewline(n.Pos()), token.NoRelPos)
+		return e.encodeDecls(x.Elts, x.Rbrace)
+
+	case *ast.File:
+		return e.encodeDecls(x.Decls, token.NoPos)
+
+	case *ast.UnaryExpr:
+		e.ws(foldNewline(x.Pos()), defPos)
+		l, ok := x.X.(*ast.BasicLit)
+		if ok && x.Op == token.SUB && (l.Kind == token.INT || l.Kind == token.FLOAT) {
+			e.writeByte('-')
+			return e.encodeScalar(l, false)
+		}
+	}
+	return errors.Newf(n.Pos(), "json: unsupported node %s (%T)", internal.DebugStr(n), n)
+}
+
+func (e *encoder) encodeScalar(l *ast.BasicLit, allowMinus bool) error {
+	switch l.Kind {
+	case token.INT:
+		var x big.Int
+		return e.setNum(l, allowMinus, &x)
+
+	case token.FLOAT:
+		var x big.Float
+		return e.setNum(l, allowMinus, &x)
+
+	case token.TRUE:
+		e.writeString("true")
+
+	case token.FALSE:
+		e.writeString("false")
+
+	case token.NULL:
+		e.writeString("null")
+
+	case token.STRING:
+		str, err := literal.Unquote(l.Value)
+		if err != nil {
+			return err
+		}
+		b, err := json.Marshal(str)
+		if err != nil {
+			return err
+		}
+		e.write(b)
+
+	default:
+		return errors.Newf(l.Pos(), "unknown literal type %v", l.Kind)
+	}
+	return nil
+}
+
+func (e *encoder) setNum(l *ast.BasicLit, allowMinus bool, x interface{}) error {
+	if !allowMinus && strings.HasPrefix(l.Value, "-") {
+		return errors.Newf(l.Pos(), "double minus not allowed")
+	}
+	var ni literal.NumInfo
+	if err := literal.ParseNum(l.Value, &ni); err != nil {
+		return err
+	}
+	e.writeString(ni.String())
+	return nil
+}
+
+// encodeDecls converts a sequence of declarations to a value. If it encounters
+// an embedded value, it will return this expression. This is more relaxed for
+// structs than is currently allowed for CUE, but the expectation is that this
+// will be allowed at some point. The input would still be illegal CUE.
+func (e *encoder) encodeDecls(decls []ast.Decl, endPos token.Pos) error {
+	var embed ast.Expr
+	var fields []*ast.Field
+
+	for _, d := range decls {
+		switch x := d.(type) {
+		default:
+			return errors.Newf(x.Pos(), "json: unsupported node %s (%T)", internal.DebugStr(x), x)
+
+		case *ast.Package:
+			if embed != nil || fields != nil {
+				return errors.Newf(x.Pos(), "invalid package clause")
+			}
+			continue
+
+		case *ast.Field:
+			if x.Token == token.ISA {
+				return errors.Newf(x.TokenPos, "json: definition not allowed")
+			}
+			if x.Optional != token.NoPos {
+				return errors.Newf(x.Optional, "json: optional fields not allowed")
+			}
+			fields = append(fields, x)
+
+		case *ast.EmbedDecl:
+			if embed != nil {
+				return errors.Newf(x.Pos(), "json: multiple embedded values")
+			}
+			embed = x.Expr
+
+		case *ast.CommentGroup:
+		}
+	}
+
+	if embed != nil {
+		if fields != nil {
+			return errors.Newf(embed.Pos(), "json: embedding mixed with fields")
+		}
+		return e.encode(embed)
+	}
+
+	if len(fields) == 0 {
+		e.writeString("{}")
+		return nil
+	}
+
+	e.writeIndent('{')
+	pos := compactNewline(fields[0].Pos())
+	if endPos == token.NoPos && pos.RelPos() == token.Blank {
+		pos = token.NoPos
+	}
+	firstPos := pos
+	const defPos = token.NoRelPos
+	for i, x := range fields {
+		if i > 0 {
+			e.writeByte(',')
+			pos = x.Pos()
+		}
+		name, _, err := ast.LabelName(x.Label)
+		if err != nil {
+			return errors.Newf(x.Label.Pos(), "json: only literal labels allowed")
+		}
+		b, err := json.Marshal(name)
+		if err != nil {
+			return err
+		}
+		e.ws(pos, defPos)
+		e.write(b)
+		e.writeByte(':')
+
+		if err := e.encode(x.Value); err != nil {
+			return err
+		}
+	}
+	e.writeUnindent('}', endPos, firstPos)
+	return nil
+}
+
+func compactNewline(pos token.Pos) token.Pos {
+	if pos.RelPos() == token.NewSection {
+		pos = token.Newline.Pos()
+	}
+	return pos
+}
+
+func foldNewline(pos token.Pos) token.Pos {
+	if pos.RelPos() >= token.Newline {
+		pos = token.Blank.Pos()
+	}
+	return pos
+}