...

Source file src/cuelang.org/go/encoding/json/json.go

Documentation: cuelang.org/go/encoding/json

     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  	"cuelang.org/go/cue"
    25  	"cuelang.org/go/cue/ast"
    26  	"cuelang.org/go/cue/ast/astutil"
    27  	"cuelang.org/go/cue/errors"
    28  	"cuelang.org/go/cue/literal"
    29  	"cuelang.org/go/cue/parser"
    30  	"cuelang.org/go/cue/token"
    31  	"cuelang.org/go/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 v.Validate(cue.Final())
    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  	}
   106  }
   107  
   108  // A Decoder converts JSON values to CUE.
   109  type Decoder struct {
   110  	r    *cue.Runtime
   111  	path string
   112  	dec  *json.Decoder
   113  }
   114  
   115  // Extract converts the current JSON value to a CUE ast. It returns io.EOF
   116  // if the input has been exhausted.
   117  func (d *Decoder) Extract() (ast.Expr, error) {
   118  	expr, err := d.extract()
   119  	if err != nil {
   120  		return expr, err
   121  	}
   122  	patchExpr(expr)
   123  	return expr, nil
   124  }
   125  
   126  func (d *Decoder) extract() (ast.Expr, error) {
   127  	var raw json.RawMessage
   128  	err := d.dec.Decode(&raw)
   129  	if err == io.EOF {
   130  		return nil, err
   131  	}
   132  	if err != nil {
   133  		pos := token.NewFile(d.path, -1, len(raw)).Pos(0, 0)
   134  		return nil, errors.Wrapf(err, pos, "invalid JSON for file %q", d.path)
   135  	}
   136  	expr, err := parser.ParseExpr(d.path, []byte(raw))
   137  
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	return expr, nil
   142  }
   143  
   144  // Decode converts the current JSON value to a CUE instance. It returns io.EOF
   145  // if the input has been exhausted.
   146  //
   147  // Deprecated: use Extract and build with cue.Context.BuildExpr.
   148  func (d *Decoder) Decode() (*cue.Instance, error) {
   149  	expr, err := d.Extract()
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	return d.r.CompileExpr(expr)
   154  }
   155  
   156  // patchExpr simplifies the AST parsed from JSON.
   157  // TODO: some of the modifications are already done in format, but are
   158  // a package deal of a more aggressive simplify. Other pieces of modification
   159  // should probably be moved to format.
   160  func patchExpr(n ast.Node) {
   161  	type info struct {
   162  		reflow bool
   163  	}
   164  	stack := []info{{true}}
   165  
   166  	afterFn := func(n ast.Node) {
   167  		switch n.(type) {
   168  		case *ast.ListLit, *ast.StructLit:
   169  			stack = stack[:len(stack)-1]
   170  		}
   171  	}
   172  
   173  	var beforeFn func(n ast.Node) bool
   174  
   175  	beforeFn = func(n ast.Node) bool {
   176  		isLarge := n.End().Offset()-n.Pos().Offset() > 50
   177  		descent := true
   178  
   179  		switch x := n.(type) {
   180  		case *ast.ListLit:
   181  			reflow := true
   182  			if !isLarge {
   183  				for _, e := range x.Elts {
   184  					if hasSpaces(e) {
   185  						reflow = false
   186  						break
   187  					}
   188  				}
   189  			}
   190  			stack = append(stack, info{reflow})
   191  			if reflow {
   192  				x.Lbrack = x.Lbrack.WithRel(token.NoRelPos)
   193  				x.Rbrack = x.Rbrack.WithRel(token.NoRelPos)
   194  			}
   195  			return true
   196  
   197  		case *ast.StructLit:
   198  			reflow := true
   199  			if !isLarge {
   200  				for _, e := range x.Elts {
   201  					if f, ok := e.(*ast.Field); !ok || hasSpaces(f) || hasSpaces(f.Value) {
   202  						reflow = false
   203  						break
   204  					}
   205  				}
   206  			}
   207  			stack = append(stack, info{reflow})
   208  			if reflow {
   209  				x.Lbrace = x.Lbrace.WithRel(token.NoRelPos)
   210  				x.Rbrace = x.Rbrace.WithRel(token.NoRelPos)
   211  			}
   212  			return true
   213  
   214  		case *ast.Field:
   215  			// label is always a string for JSON.
   216  			switch {
   217  			case true:
   218  				s, ok := x.Label.(*ast.BasicLit)
   219  				if !ok || s.Kind != token.STRING {
   220  					break // should not happen: implies invalid JSON
   221  				}
   222  
   223  				u, err := literal.Unquote(s.Value)
   224  				if err != nil {
   225  					break // should not happen: implies invalid JSON
   226  				}
   227  
   228  				// TODO(legacy): remove checking for '_' prefix once hidden
   229  				// fields are removed.
   230  				if !ast.IsValidIdent(u) || strings.HasPrefix(u, "_") {
   231  					break // keep string
   232  				}
   233  
   234  				x.Label = ast.NewIdent(u)
   235  				astutil.CopyMeta(x.Label, s)
   236  			}
   237  			ast.Walk(x.Value, beforeFn, afterFn)
   238  			descent = false
   239  
   240  		case *ast.BasicLit:
   241  			if x.Kind == token.STRING && len(x.Value) > 10 {
   242  				s, err := literal.Unquote(x.Value)
   243  				if err != nil {
   244  					break // should not happen: implies invalid JSON
   245  				}
   246  
   247  				x.Value = literal.String.WithOptionalTabIndent(len(stack)).Quote(s)
   248  			}
   249  		}
   250  
   251  		if stack[len(stack)-1].reflow {
   252  			ast.SetRelPos(n, token.NoRelPos)
   253  		}
   254  		return descent
   255  	}
   256  
   257  	ast.Walk(n, beforeFn, afterFn)
   258  }
   259  
   260  func hasSpaces(n ast.Node) bool {
   261  	return n.Pos().RelPos() > token.NoSpace
   262  }
   263  

View as plain text