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