| // 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/astinternal" |
| ) |
| |
| // 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)", astinternal.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)", astinternal.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 |
| } |