| // 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 json converts JSON to and from CUE. |
| package json |
| |
| import ( |
| gojson "encoding/json" |
| "io" |
| "strconv" |
| "strings" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/ast/astutil" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/literal" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/pkg/encoding/json" |
| ) |
| |
| // Valid reports whether data is a valid JSON encoding. |
| func Valid(b []byte) bool { |
| return gojson.Valid(b) |
| } |
| |
| // Validate validates JSON and confirms it matches the constraints |
| // specified by v. |
| func Validate(b []byte, v cue.Value) error { |
| _, err := json.Validate(b, v) |
| return err |
| } |
| |
| // Extract parses JSON-encoded data to a CUE expression, using path for |
| // position information. |
| func Extract(path string, data []byte) (ast.Expr, error) { |
| expr, err := extract(path, data) |
| if err != nil { |
| return nil, err |
| } |
| patchExpr(expr) |
| return expr, nil |
| } |
| |
| // Decode parses JSON-encoded data to a CUE value, using path for position |
| // information. |
| func Decode(r *cue.Runtime, path string, data []byte) (*cue.Instance, error) { |
| expr, err := extract(path, data) |
| if err != nil { |
| return nil, err |
| } |
| return r.CompileExpr(expr) |
| } |
| |
| func extract(path string, b []byte) (ast.Expr, error) { |
| expr, err := parser.ParseExpr(path, b) |
| if err != nil || !gojson.Valid(b) { |
| p := token.NoPos |
| if pos := errors.Positions(err); len(pos) > 0 { |
| p = pos[0] |
| } |
| var x interface{} |
| err := gojson.Unmarshal(b, &x) |
| return nil, errors.Wrapf(err, p, "invalid JSON for file %q", path) |
| } |
| return expr, nil |
| } |
| |
| // NewDecoder configures a JSON decoder. The path is used to associate position |
| // information with each node. The runtime may be nil if the decoder |
| // is only used to extract to CUE ast objects. |
| func NewDecoder(r *cue.Runtime, path string, src io.Reader) *Decoder { |
| return &Decoder{ |
| r: r, |
| path: path, |
| dec: gojson.NewDecoder(src), |
| offset: 1, |
| } |
| } |
| |
| // A Decoder converts JSON values to CUE. |
| type Decoder struct { |
| r *cue.Runtime |
| path string |
| dec *gojson.Decoder |
| offset int |
| } |
| |
| // Extract converts the current JSON value to a CUE ast. It returns io.EOF |
| // if the input has been exhausted. |
| func (d *Decoder) Extract() (ast.Expr, error) { |
| expr, err := d.extract() |
| if err != nil { |
| return expr, err |
| } |
| patchExpr(expr) |
| return expr, nil |
| } |
| |
| func (d *Decoder) extract() (ast.Expr, error) { |
| var raw gojson.RawMessage |
| err := d.dec.Decode(&raw) |
| if err == io.EOF { |
| return nil, err |
| } |
| offset := d.offset |
| d.offset += len(raw) |
| if err != nil { |
| pos := token.NewFile(d.path, offset, len(raw)).Pos(0, 0) |
| return nil, errors.Wrapf(err, pos, "invalid JSON for file %q", d.path) |
| } |
| expr, err := parser.ParseExpr(d.path, []byte(raw), parser.FileOffset(offset)) |
| if err != nil { |
| return nil, err |
| } |
| return expr, nil |
| } |
| |
| // Decode converts the current JSON value to a CUE instance. It returns io.EOF |
| // if the input has been exhausted. |
| func (d *Decoder) Decode() (*cue.Instance, error) { |
| expr, err := d.Extract() |
| if err != nil { |
| return nil, err |
| } |
| return d.r.CompileExpr(expr) |
| } |
| |
| // patchExpr simplifies the AST parsed from JSON. |
| // TODO: some of the modifications are already done in format, but are |
| // a package deal of a more aggressive simplify. Other pieces of modification |
| // should probably be moved to format. |
| func patchExpr(n ast.Node) { |
| type info struct { |
| reflow bool |
| } |
| stack := []info{{true}} |
| |
| afterFn := func(n ast.Node) { |
| switch n.(type) { |
| case *ast.ListLit, *ast.StructLit: |
| stack = stack[:len(stack)-1] |
| } |
| } |
| |
| var beforeFn func(n ast.Node) bool |
| |
| beforeFn = func(n ast.Node) bool { |
| isLarge := n.End().Offset()-n.Pos().Offset() > 50 |
| descent := true |
| |
| switch x := n.(type) { |
| case *ast.ListLit: |
| reflow := true |
| if !isLarge { |
| for _, e := range x.Elts { |
| if hasSpaces(e) { |
| reflow = false |
| break |
| } |
| } |
| } |
| stack = append(stack, info{reflow}) |
| if reflow { |
| x.Lbrack = x.Lbrack.WithRel(token.NoRelPos) |
| x.Rbrack = x.Rbrack.WithRel(token.NoRelPos) |
| } |
| return true |
| |
| case *ast.StructLit: |
| reflow := true |
| if !isLarge { |
| for _, e := range x.Elts { |
| if f, ok := e.(*ast.Field); !ok || hasSpaces(f) || hasSpaces(f.Value) { |
| reflow = false |
| break |
| } |
| } |
| } |
| stack = append(stack, info{reflow}) |
| if reflow { |
| x.Lbrace = x.Lbrace.WithRel(token.NoRelPos) |
| x.Rbrace = x.Rbrace.WithRel(token.NoRelPos) |
| } |
| return true |
| |
| case *ast.Field: |
| // label is always a string for JSON. |
| switch { |
| case true: |
| s, ok := x.Label.(*ast.BasicLit) |
| if !ok || s.Kind != token.STRING { |
| break // should not happen: implies invalid JSON |
| } |
| |
| u, err := literal.Unquote(s.Value) |
| if err != nil { |
| break // should not happen: implies invalid JSON |
| } |
| |
| // TODO(legacy): remove checking for '_' prefix once hidden |
| // fields are removed. |
| if !ast.IsValidIdent(u) || strings.HasPrefix(u, "_") { |
| break // keep string |
| } |
| |
| x.Label = ast.NewIdent(u) |
| astutil.CopyMeta(x.Label, s) |
| } |
| ast.Walk(x.Value, beforeFn, afterFn) |
| descent = false |
| |
| case *ast.BasicLit: |
| if x.Kind == token.STRING && len(x.Value) > 10 { |
| s, err := literal.Unquote(x.Value) |
| if err != nil { |
| break // should not happen: implies invalid JSON |
| } |
| |
| lines := strings.Split(s, "\n") |
| if len(lines) == 1 { |
| break |
| } |
| x.Value = quoteMulti(lines, len(stack)) |
| } |
| } |
| |
| if stack[len(stack)-1].reflow { |
| ast.SetRelPos(n, token.NoRelPos) |
| } |
| return descent |
| } |
| |
| ast.Walk(n, beforeFn, afterFn) |
| } |
| |
| func hasSpaces(n ast.Node) bool { |
| return n.Pos().RelPos() > token.NoSpace |
| } |
| |
| func quoteMulti(a []string, indent int) string { |
| b := strings.Builder{} |
| prefix := "\n" + strings.Repeat("\t", indent) |
| b.WriteString(`"""`) |
| for _, s := range a { |
| b.WriteString(prefix) |
| q := strconv.Quote(s) |
| b.WriteString(q[1 : len(q)-1]) |
| } |
| b.WriteString(prefix) |
| b.WriteString(`"""`) |
| return b.String() |
| } |