github.com/solo-io/cue@v0.4.7/encoding/json/json.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package json converts JSON to and from CUE.
    16  package json
    17  
    18  import (
    19  	gojson "encoding/json"
    20  	"io"
    21  	"strings"
    22  
    23  	"github.com/solo-io/cue/cue"
    24  	"github.com/solo-io/cue/cue/ast"
    25  	"github.com/solo-io/cue/cue/ast/astutil"
    26  	"github.com/solo-io/cue/cue/errors"
    27  	"github.com/solo-io/cue/cue/literal"
    28  	"github.com/solo-io/cue/cue/parser"
    29  	"github.com/solo-io/cue/cue/token"
    30  	"github.com/solo-io/cue/pkg/encoding/json"
    31  )
    32  
    33  // Valid reports whether data is a valid JSON encoding.
    34  func Valid(b []byte) bool {
    35  	return gojson.Valid(b)
    36  }
    37  
    38  // Validate validates JSON and confirms it matches the constraints
    39  // specified by v.
    40  func Validate(b []byte, v cue.Value) error {
    41  	_, err := json.Validate(b, v)
    42  	return err
    43  }
    44  
    45  // Extract parses JSON-encoded data to a CUE expression, using path for
    46  // position information.
    47  func Extract(path string, data []byte) (ast.Expr, error) {
    48  	expr, err := extract(path, data)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	patchExpr(expr)
    53  	return expr, nil
    54  }
    55  
    56  // Decode parses JSON-encoded data to a CUE value, using path for position
    57  // information.
    58  //
    59  // Deprecated: use Extract and build using cue.Context.BuildExpr.
    60  func Decode(r *cue.Runtime, path string, data []byte) (*cue.Instance, error) {
    61  	expr, err := extract(path, data)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	return r.CompileExpr(expr)
    66  }
    67  
    68  func extract(path string, b []byte) (ast.Expr, error) {
    69  	expr, err := parser.ParseExpr(path, b)
    70  	if err != nil || !gojson.Valid(b) {
    71  		p := token.NoPos
    72  		if pos := errors.Positions(err); len(pos) > 0 {
    73  			p = pos[0]
    74  		}
    75  		var x interface{}
    76  		err := gojson.Unmarshal(b, &x)
    77  		return nil, errors.Wrapf(err, p, "invalid JSON for file %q", path)
    78  	}
    79  	return expr, nil
    80  }
    81  
    82  // NewDecoder configures a JSON decoder. The path is used to associate position
    83  // information with each node. The runtime may be nil if the decoder
    84  // is only used to extract to CUE ast objects.
    85  //
    86  // The runtime may be nil if Decode isn't used.
    87  func NewDecoder(r *cue.Runtime, path string, src io.Reader) *Decoder {
    88  	return &Decoder{
    89  		r:      r,
    90  		path:   path,
    91  		dec:    gojson.NewDecoder(src),
    92  		offset: 1,
    93  	}
    94  }
    95  
    96  // A Decoder converts JSON values to CUE.
    97  type Decoder struct {
    98  	r      *cue.Runtime
    99  	path   string
   100  	dec    *gojson.Decoder
   101  	offset int
   102  }
   103  
   104  // Extract converts the current JSON value to a CUE ast. It returns io.EOF
   105  // if the input has been exhausted.
   106  func (d *Decoder) Extract() (ast.Expr, error) {
   107  	expr, err := d.extract()
   108  	if err != nil {
   109  		return expr, err
   110  	}
   111  	patchExpr(expr)
   112  	return expr, nil
   113  }
   114  
   115  func (d *Decoder) extract() (ast.Expr, error) {
   116  	var raw gojson.RawMessage
   117  	err := d.dec.Decode(&raw)
   118  	if err == io.EOF {
   119  		return nil, err
   120  	}
   121  	offset := d.offset
   122  	d.offset += len(raw)
   123  	if err != nil {
   124  		pos := token.NewFile(d.path, offset, len(raw)).Pos(0, 0)
   125  		return nil, errors.Wrapf(err, pos, "invalid JSON for file %q", d.path)
   126  	}
   127  	expr, err := parser.ParseExpr(d.path, []byte(raw), parser.FileOffset(offset))
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return expr, nil
   132  }
   133  
   134  // Decode converts the current JSON value to a CUE instance. It returns io.EOF
   135  // if the input has been exhausted.
   136  //
   137  // Deprecated: use Extract and build with cue.Context.BuildExpr.
   138  func (d *Decoder) Decode() (*cue.Instance, error) {
   139  	expr, err := d.Extract()
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	return d.r.CompileExpr(expr)
   144  }
   145  
   146  // patchExpr simplifies the AST parsed from JSON.
   147  // TODO: some of the modifications are already done in format, but are
   148  // a package deal of a more aggressive simplify. Other pieces of modification
   149  // should probably be moved to format.
   150  func patchExpr(n ast.Node) {
   151  	type info struct {
   152  		reflow bool
   153  	}
   154  	stack := []info{{true}}
   155  
   156  	afterFn := func(n ast.Node) {
   157  		switch n.(type) {
   158  		case *ast.ListLit, *ast.StructLit:
   159  			stack = stack[:len(stack)-1]
   160  		}
   161  	}
   162  
   163  	var beforeFn func(n ast.Node) bool
   164  
   165  	beforeFn = func(n ast.Node) bool {
   166  		isLarge := n.End().Offset()-n.Pos().Offset() > 50
   167  		descent := true
   168  
   169  		switch x := n.(type) {
   170  		case *ast.ListLit:
   171  			reflow := true
   172  			if !isLarge {
   173  				for _, e := range x.Elts {
   174  					if hasSpaces(e) {
   175  						reflow = false
   176  						break
   177  					}
   178  				}
   179  			}
   180  			stack = append(stack, info{reflow})
   181  			if reflow {
   182  				x.Lbrack = x.Lbrack.WithRel(token.NoRelPos)
   183  				x.Rbrack = x.Rbrack.WithRel(token.NoRelPos)
   184  			}
   185  			return true
   186  
   187  		case *ast.StructLit:
   188  			reflow := true
   189  			if !isLarge {
   190  				for _, e := range x.Elts {
   191  					if f, ok := e.(*ast.Field); !ok || hasSpaces(f) || hasSpaces(f.Value) {
   192  						reflow = false
   193  						break
   194  					}
   195  				}
   196  			}
   197  			stack = append(stack, info{reflow})
   198  			if reflow {
   199  				x.Lbrace = x.Lbrace.WithRel(token.NoRelPos)
   200  				x.Rbrace = x.Rbrace.WithRel(token.NoRelPos)
   201  			}
   202  			return true
   203  
   204  		case *ast.Field:
   205  			// label is always a string for JSON.
   206  			switch {
   207  			case true:
   208  				s, ok := x.Label.(*ast.BasicLit)
   209  				if !ok || s.Kind != token.STRING {
   210  					break // should not happen: implies invalid JSON
   211  				}
   212  
   213  				u, err := literal.Unquote(s.Value)
   214  				if err != nil {
   215  					break // should not happen: implies invalid JSON
   216  				}
   217  
   218  				// TODO(legacy): remove checking for '_' prefix once hidden
   219  				// fields are removed.
   220  				if !ast.IsValidIdent(u) || strings.HasPrefix(u, "_") {
   221  					break // keep string
   222  				}
   223  
   224  				x.Label = ast.NewIdent(u)
   225  				astutil.CopyMeta(x.Label, s)
   226  			}
   227  			ast.Walk(x.Value, beforeFn, afterFn)
   228  			descent = false
   229  
   230  		case *ast.BasicLit:
   231  			if x.Kind == token.STRING && len(x.Value) > 10 {
   232  				s, err := literal.Unquote(x.Value)
   233  				if err != nil {
   234  					break // should not happen: implies invalid JSON
   235  				}
   236  
   237  				x.Value = literal.String.WithOptionalTabIndent(len(stack)).Quote(s)
   238  			}
   239  		}
   240  
   241  		if stack[len(stack)-1].reflow {
   242  			ast.SetRelPos(n, token.NoRelPos)
   243  		}
   244  		return descent
   245  	}
   246  
   247  	ast.Walk(n, beforeFn, afterFn)
   248  }
   249  
   250  func hasSpaces(n ast.Node) bool {
   251  	return n.Pos().RelPos() > token.NoSpace
   252  }