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