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. 16package json 17 18import ( 19 gojson "encoding/json" 20 "io" 21 "strings" 22 23 "cuelang.org/go/cue" 24 "cuelang.org/go/cue/ast" 25 "cuelang.org/go/cue/ast/astutil" 26 "cuelang.org/go/cue/errors" 27 "cuelang.org/go/cue/literal" 28 "cuelang.org/go/cue/parser" 29 "cuelang.org/go/cue/token" 30 "cuelang.org/go/pkg/encoding/json" 31) 32 33// Valid reports whether data is a valid JSON encoding. 34func 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. 40func 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. 47func 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. 60func 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 68func 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. 87func 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. 97type 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. 106func (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 115func (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. 138func (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. 150func 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 250func hasSpaces(n ast.Node) bool { 251 return n.Pos().RelPos() > token.NoSpace 252} 253