github.com/solo-io/cue@v0.4.7/internal/encoding/encoding.go (about) 1 // Copyright 2020 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 // TODO: make this package public in cuelang.org/go/encoding 16 // once stabalized. 17 18 package encoding 19 20 import ( 21 "bytes" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/url" 26 "os" 27 "strings" 28 29 "github.com/solo-io/cue/cue" 30 "github.com/solo-io/cue/cue/ast" 31 "github.com/solo-io/cue/cue/build" 32 "github.com/solo-io/cue/cue/errors" 33 "github.com/solo-io/cue/cue/format" 34 "github.com/solo-io/cue/cue/literal" 35 "github.com/solo-io/cue/cue/parser" 36 "github.com/solo-io/cue/cue/token" 37 "github.com/solo-io/cue/encoding/json" 38 "github.com/solo-io/cue/encoding/jsonschema" 39 "github.com/solo-io/cue/encoding/openapi" 40 "github.com/solo-io/cue/encoding/protobuf" 41 "github.com/solo-io/cue/encoding/protobuf/jsonpb" 42 "github.com/solo-io/cue/encoding/protobuf/textproto" 43 "github.com/solo-io/cue/internal" 44 "github.com/solo-io/cue/internal/filetypes" 45 "github.com/solo-io/cue/internal/third_party/yaml" 46 "golang.org/x/text/encoding/unicode" 47 "golang.org/x/text/transform" 48 ) 49 50 type Decoder struct { 51 cfg *Config 52 closer io.Closer 53 next func() (ast.Expr, error) 54 rewriteFunc rewriteFunc 55 interpretFunc interpretFunc 56 interpretation build.Interpretation 57 expr ast.Expr 58 file *ast.File 59 filename string // may change on iteration for some formats 60 id string 61 index int 62 err error 63 } 64 65 type interpretFunc func(*cue.Instance) (file *ast.File, id string, err error) 66 type rewriteFunc func(*ast.File) (file *ast.File, err error) 67 68 // ID returns a canonical identifier for the decoded object or "" if no such 69 // identifier could be found. 70 func (i *Decoder) ID() string { 71 return i.id 72 } 73 func (i *Decoder) Filename() string { return i.filename } 74 75 // Interpretation returns the current interpretation detected by Detect. 76 func (i *Decoder) Interpretation() build.Interpretation { 77 return i.interpretation 78 } 79 func (i *Decoder) Index() int { return i.index } 80 func (i *Decoder) Done() bool { return i.err != nil } 81 82 func (i *Decoder) Next() { 83 if i.err != nil { 84 return 85 } 86 // Decoder level 87 i.file = nil 88 i.expr, i.err = i.next() 89 i.index++ 90 if i.err != nil { 91 return 92 } 93 i.doInterpret() 94 } 95 96 func (i *Decoder) doInterpret() { 97 if i.rewriteFunc != nil { 98 i.file = i.File() 99 var err error 100 i.file, err = i.rewriteFunc(i.file) 101 if err != nil { 102 i.err = err 103 return 104 } 105 } 106 if i.interpretFunc != nil { 107 var r cue.Runtime 108 i.file = i.File() 109 inst, err := r.CompileFile(i.file) 110 if err != nil { 111 i.err = err 112 return 113 } 114 i.file, i.id, i.err = i.interpretFunc(inst) 115 } 116 } 117 118 func toFile(x ast.Expr) *ast.File { 119 return internal.ToFile(x) 120 } 121 122 func valueToFile(v cue.Value) *ast.File { 123 return internal.ToFile(v.Syntax()) 124 } 125 126 func (i *Decoder) File() *ast.File { 127 if i.file != nil { 128 return i.file 129 } 130 return toFile(i.expr) 131 } 132 133 func (i *Decoder) Err() error { 134 if i.err == io.EOF { 135 return nil 136 } 137 return i.err 138 } 139 140 func (i *Decoder) Close() { 141 i.closer.Close() 142 } 143 144 type Config struct { 145 Mode filetypes.Mode 146 147 // Out specifies an overwrite destination. 148 Out io.Writer 149 Stdin io.Reader 150 Stdout io.Writer 151 152 PkgName string // package name for files to generate 153 154 Force bool // overwrite existing files. 155 Strict bool 156 Stream bool // will potentially write more than one document per file 157 AllErrors bool 158 159 Schema cue.Value // used for schema-based decoding 160 161 EscapeHTML bool 162 ProtoPath []string 163 Format []format.Option 164 ParseFile func(name string, src interface{}) (*ast.File, error) 165 } 166 167 // NewDecoder returns a stream of non-rooted data expressions. The encoding 168 // type of f must be a data type, but does not have to be an encoding that 169 // can stream. stdin is used in case the file is "-". 170 func NewDecoder(f *build.File, cfg *Config) *Decoder { 171 if cfg == nil { 172 cfg = &Config{} 173 } 174 i := &Decoder{filename: f.Filename, cfg: cfg} 175 i.next = func() (ast.Expr, error) { 176 if i.err != nil { 177 return nil, i.err 178 } 179 return nil, io.EOF 180 } 181 182 if file, ok := f.Source.(*ast.File); ok { 183 i.file = file 184 i.closer = ioutil.NopCloser(strings.NewReader("")) 185 i.validate(file, f) 186 return i 187 } 188 189 rc, err := reader(f, cfg.Stdin) 190 i.closer = rc 191 i.err = err 192 if err != nil { 193 return i 194 } 195 196 // For now we assume that all encodings require UTF-8. This will not be the 197 // case for some binary protocols. We need to exempt those explicitly here 198 // once we introduce them. 199 // TODO: this code also allows UTF16, which is too permissive for some 200 // encodings. Switch to unicode.UTF8Sig once available. 201 t := unicode.BOMOverride(unicode.UTF8.NewDecoder()) 202 r := transform.NewReader(rc, t) 203 204 switch f.Interpretation { 205 case "": 206 case build.Auto: 207 openAPI := openAPIFunc(cfg, f) 208 jsonSchema := jsonSchemaFunc(cfg, f) 209 i.interpretFunc = func(inst *cue.Instance) (file *ast.File, id string, err error) { 210 switch i.interpretation = Detect(inst.Value()); i.interpretation { 211 case build.JSONSchema: 212 return jsonSchema(inst) 213 case build.OpenAPI: 214 return openAPI(inst) 215 } 216 return i.file, "", i.err 217 } 218 case build.OpenAPI: 219 i.interpretation = build.OpenAPI 220 i.interpretFunc = openAPIFunc(cfg, f) 221 case build.JSONSchema: 222 i.interpretation = build.JSONSchema 223 i.interpretFunc = jsonSchemaFunc(cfg, f) 224 case build.ProtobufJSON: 225 i.interpretation = build.ProtobufJSON 226 i.rewriteFunc = protobufJSONFunc(cfg, f) 227 default: 228 i.err = fmt.Errorf("unsupported interpretation %q", f.Interpretation) 229 } 230 231 path := f.Filename 232 switch f.Encoding { 233 case build.CUE: 234 if cfg.ParseFile == nil { 235 i.file, i.err = parser.ParseFile(path, r, parser.ParseComments) 236 } else { 237 i.file, i.err = cfg.ParseFile(path, r) 238 } 239 i.validate(i.file, f) 240 if i.err == nil { 241 i.doInterpret() 242 } 243 case build.JSON, build.JSONL: 244 i.next = json.NewDecoder(nil, path, r).Extract 245 i.Next() 246 case build.YAML: 247 d, err := yaml.NewDecoder(path, r) 248 i.err = err 249 i.next = d.Decode 250 i.Next() 251 case build.Text: 252 b, err := ioutil.ReadAll(r) 253 i.err = err 254 i.expr = ast.NewString(string(b)) 255 case build.Binary: 256 b, err := ioutil.ReadAll(r) 257 i.err = err 258 s := literal.Bytes.WithTabIndent(1).Quote(string(b)) 259 i.expr = ast.NewLit(token.STRING, s) 260 case build.Protobuf: 261 paths := &protobuf.Config{ 262 Paths: cfg.ProtoPath, 263 PkgName: cfg.PkgName, 264 } 265 i.file, i.err = protobuf.Extract(path, r, paths) 266 case build.TextProto: 267 b, err := ioutil.ReadAll(r) 268 i.err = err 269 if err == nil { 270 d := textproto.NewDecoder() 271 i.expr, i.err = d.Parse(cfg.Schema, path, b) 272 } 273 default: 274 i.err = fmt.Errorf("unsupported encoding %q", f.Encoding) 275 } 276 277 return i 278 } 279 280 func jsonSchemaFunc(cfg *Config, f *build.File) interpretFunc { 281 return func(i *cue.Instance) (file *ast.File, id string, err error) { 282 id = f.Tags["id"] 283 if id == "" { 284 id, _ = i.Lookup("$id").String() 285 } 286 if id != "" { 287 u, err := url.Parse(id) 288 if err != nil { 289 return nil, "", errors.Wrapf(err, token.NoPos, "invalid id") 290 } 291 u.Scheme = "" 292 id = strings.TrimPrefix(u.String(), "//") 293 } 294 cfg := &jsonschema.Config{ 295 ID: id, 296 PkgName: cfg.PkgName, 297 298 Strict: cfg.Strict, 299 } 300 file, err = jsonschema.Extract(i, cfg) 301 // TODO: simplify currently erases file line info. Reintroduce after fix. 302 // file, err = simplify(file, err) 303 return file, id, err 304 } 305 } 306 307 func openAPIFunc(c *Config, f *build.File) interpretFunc { 308 cfg := &openapi.Config{PkgName: c.PkgName} 309 return func(i *cue.Instance) (file *ast.File, id string, err error) { 310 file, err = openapi.Extract(i, cfg) 311 // TODO: simplify currently erases file line info. Reintroduce after fix. 312 // file, err = simplify(file, err) 313 return file, "", err 314 } 315 } 316 317 func protobufJSONFunc(cfg *Config, file *build.File) rewriteFunc { 318 return func(f *ast.File) (*ast.File, error) { 319 if !cfg.Schema.Exists() { 320 return f, errors.Newf(token.NoPos, 321 "no schema specified for protobuf interpretation.") 322 } 323 return f, jsonpb.NewDecoder(cfg.Schema).RewriteFile(f) 324 } 325 } 326 327 func reader(f *build.File, stdin io.Reader) (io.ReadCloser, error) { 328 switch s := f.Source.(type) { 329 case nil: 330 // Use the file name. 331 case string: 332 return ioutil.NopCloser(strings.NewReader(s)), nil 333 case []byte: 334 return ioutil.NopCloser(bytes.NewReader(s)), nil 335 case *bytes.Buffer: 336 // is io.Reader, but it needs to be readable repeatedly 337 if s != nil { 338 return ioutil.NopCloser(bytes.NewReader(s.Bytes())), nil 339 } 340 default: 341 return nil, fmt.Errorf("invalid source type %T", f.Source) 342 } 343 // TODO: should we allow this? 344 if f.Filename == "-" { 345 return ioutil.NopCloser(stdin), nil 346 } 347 return os.Open(f.Filename) 348 } 349 350 func shouldValidate(i *filetypes.FileInfo) bool { 351 // TODO: We ignore attributes for now. They should be enabled by default. 352 return false || 353 !i.Definitions || 354 !i.Data || 355 !i.Optional || 356 !i.Constraints || 357 !i.References || 358 !i.Cycles || 359 !i.KeepDefaults || 360 !i.Incomplete || 361 !i.Imports || 362 !i.Docs 363 } 364 365 type validator struct { 366 allErrors bool 367 count int 368 errs errors.Error 369 fileinfo *filetypes.FileInfo 370 } 371 372 func (d *Decoder) validate(f *ast.File, b *build.File) { 373 if d.err != nil { 374 return 375 } 376 fi, err := filetypes.FromFile(b, filetypes.Input) 377 if err != nil { 378 d.err = err 379 return 380 } 381 if !shouldValidate(fi) { 382 return 383 } 384 385 v := validator{fileinfo: fi, allErrors: d.cfg.AllErrors} 386 ast.Walk(f, v.validate, nil) 387 d.err = v.errs 388 } 389 390 func (v *validator) validate(n ast.Node) bool { 391 if v.count > 10 { 392 return false 393 } 394 395 i := v.fileinfo 396 397 // TODO: Cycles 398 399 ok := true 400 check := func(n ast.Node, option bool, s string, cond bool) { 401 if !option && cond { 402 v.errs = errors.Append(v.errs, errors.Newf(n.Pos(), 403 "%s not allowed in %s mode", s, v.fileinfo.Form)) 404 v.count++ 405 ok = false 406 } 407 } 408 409 // For now we don't make any distinction between these modes. 410 411 constraints := i.Constraints && i.Incomplete && i.Optional && i.References 412 413 check(n, i.Docs, "comments", len(ast.Comments(n)) > 0) 414 415 switch x := n.(type) { 416 case *ast.CommentGroup: 417 check(n, i.Docs, "comments", len(ast.Comments(n)) > 0) 418 return false 419 420 case *ast.ImportDecl, *ast.ImportSpec: 421 check(n, i.Imports, "imports", true) 422 423 case *ast.Field: 424 check(n, i.Definitions, "definitions", 425 x.Token == token.ISA || internal.IsDefinition(x.Label)) 426 check(n, i.Data, "regular fields", internal.IsRegularField(x)) 427 check(n, constraints, "optional fields", x.Optional != token.NoPos) 428 429 _, _, err := ast.LabelName(x.Label) 430 check(n, constraints, "optional fields", err != nil) 431 432 check(n, i.Attributes, "attributes", len(x.Attrs) > 0) 433 ast.Walk(x.Value, v.validate, nil) 434 return false 435 436 case *ast.UnaryExpr: 437 switch x.Op { 438 case token.MUL: 439 check(n, i.KeepDefaults, "default values", true) 440 case token.SUB, token.ADD: 441 // The parser represents negative numbers as an unary expression. 442 // Allow one `-` or `+`. 443 _, ok := x.X.(*ast.BasicLit) 444 check(n, constraints, "expressions", !ok) 445 case token.LSS, token.LEQ, token.EQL, token.GEQ, token.GTR, 446 token.NEQ, token.NMAT, token.MAT: 447 check(n, constraints, "constraints", true) 448 default: 449 check(n, constraints, "expressions", true) 450 } 451 452 case *ast.BinaryExpr, *ast.ParenExpr, *ast.IndexExpr, *ast.SliceExpr, 453 *ast.CallExpr, *ast.Comprehension, *ast.Interpolation: 454 check(n, constraints, "expressions", true) 455 456 case *ast.Ellipsis: 457 check(n, constraints, "ellipsis", true) 458 459 case *ast.Ident, *ast.SelectorExpr, *ast.Alias, *ast.LetClause: 460 check(n, i.References, "references", true) 461 462 default: 463 // Other types are either always okay or handled elsewhere. 464 } 465 return ok 466 } 467 468 // simplify reformats a File. To be used as a wrapper for Extract functions. 469 // 470 // It currently does so by formatting the file using fmt.Format and then 471 // reparsing it. This is not ideal, but the package format does not provide a 472 // way to do so differently. 473 func simplify(f *ast.File, err error) (*ast.File, error) { 474 if err != nil { 475 return nil, err 476 } 477 // This needs to be a function that modifies f in order to maintain line 478 // number information. 479 b, err := format.Node(f, format.Simplify()) 480 if err != nil { 481 return nil, err 482 } 483 return parser.ParseFile(f.Filename, b, parser.ParseComments) 484 }