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 }