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
+}