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