blob: e273686c7d6f11aac9327ba66af1b66dca5029c1 [file] [log] [blame]
// 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 the JSON to a CUE expression.
//
// If src != nil, Extract parses the source from src and the path is for
// position information. The type of the argument for the src parameter must be
// string, []byte, or io.Reader. If src == nil, ParseFile parses the file
// specified by filename.
func Extract(path string, b []byte) (ast.Expr, error) {
expr, err := extract(path, b)
if err != nil {
return nil, err
}
patchExpr(expr)
return expr, nil
}
// Decode converts a JSON file to a CUE value.
//
// If src != nil, Extract parses the source from src and the path is for
// position information. The type of the argument for the src parameter must be
// string, []byte, or io.Reader. If src == nil, ParseFile parses the file
// specified by filename.
func Decode(r *cue.Runtime, path string, b []byte) (*cue.Instance, error) {
expr, err := extract(path, b)
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
}
if q, err := ast.QuoteIdent(u); err != nil || q != u {
break
}
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()
}