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