this repo has no description
at master 336 lines 8.3 kB view raw
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 15package encoding 16 17import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "io" 22 "io/fs" 23 "os" 24 "path/filepath" 25 26 "cuelang.org/go/cue" 27 "cuelang.org/go/cue/ast" 28 "cuelang.org/go/cue/build" 29 "cuelang.org/go/cue/errors" 30 "cuelang.org/go/cue/format" 31 "cuelang.org/go/cue/token" 32 "cuelang.org/go/encoding/jsonschema" 33 "cuelang.org/go/encoding/openapi" 34 "cuelang.org/go/encoding/protobuf/jsonpb" 35 "cuelang.org/go/encoding/protobuf/textproto" 36 "cuelang.org/go/encoding/toml" 37 "cuelang.org/go/encoding/yaml" 38 "cuelang.org/go/internal" 39 "cuelang.org/go/internal/filetypes" 40) 41 42// An Encoder converts CUE to various file formats, including CUE itself. 43// An Encoder allows 44type Encoder struct { 45 ctx *cue.Context 46 cfg *Config 47 close func() error 48 interpret func(cue.Value) (*ast.File, error) 49 encFile func(*ast.File) error 50 encValue func(cue.Value) error 51 autoSimplify bool 52 concrete bool 53} 54 55// IsConcrete reports whether the output is required to be concrete. 56// 57// INTERNAL ONLY: this is just to work around a problem related to issue #553 58// of catching errors only after syntax generation, dropping line number 59// information. 60func (e *Encoder) IsConcrete() bool { 61 return e.concrete 62} 63 64func (e Encoder) Close() error { 65 if e.close == nil { 66 return nil 67 } 68 return e.close() 69} 70 71// NewEncoder writes content to the file with the given specification. 72func NewEncoder(ctx *cue.Context, f *build.File, cfg *Config) (*Encoder, error) { 73 w, close := writer(f, cfg) 74 e := &Encoder{ 75 ctx: ctx, 76 cfg: cfg, 77 close: close, 78 } 79 80 switch f.Interpretation { 81 case "": 82 case build.OpenAPI: 83 // TODO: get encoding options 84 cfg := &openapi.Config{} 85 e.interpret = func(v cue.Value) (*ast.File, error) { 86 return openapi.Generate(v, cfg) 87 } 88 case build.JSONSchema: 89 // TODO: get encoding options 90 cfg := &jsonschema.GenerateConfig{} 91 e.interpret = func(v cue.Value) (*ast.File, error) { 92 expr, err := jsonschema.Generate(v, cfg) 93 if err != nil { 94 return nil, err 95 } 96 return internal.ToFile(expr), nil 97 } 98 case build.ProtobufJSON: 99 e.interpret = func(v cue.Value) (*ast.File, error) { 100 f := internal.ToFile(v.Syntax()) 101 return f, jsonpb.NewEncoder(v).RewriteFile(f) 102 } 103 default: 104 return nil, fmt.Errorf("unsupported interpretation %q", f.Interpretation) 105 } 106 107 switch f.Encoding { 108 case build.CUE: 109 fi, err := filetypes.FromFile(f, cfg.Mode) 110 if err != nil { 111 return nil, err 112 } 113 e.concrete = !fi.Incomplete 114 115 synOpts := []cue.Option{} 116 if !fi.KeepDefaults || !fi.Incomplete { 117 synOpts = append(synOpts, cue.Final()) 118 } 119 120 synOpts = append(synOpts, 121 cue.Docs(fi.Docs), 122 cue.Attributes(fi.Attributes), 123 cue.Optional(fi.Optional), 124 cue.Concrete(!fi.Incomplete), 125 cue.Definitions(fi.Definitions), 126 cue.DisallowCycles(!fi.Cycles), 127 cue.InlineImports(cfg.InlineImports), 128 ) 129 130 opts := []format.Option{} 131 opts = append(opts, cfg.Format...) 132 133 useSep := false 134 format := func(name string, n ast.Node) error { 135 if name != "" && cfg.Stream { 136 // TODO: make this relative to DIR 137 fmt.Fprintf(w, "// %s\n", filepath.Base(name)) 138 } else if useSep { 139 fmt.Println("// ---") 140 } 141 useSep = true 142 143 opts := opts 144 if e.autoSimplify { 145 opts = append(opts, format.Simplify()) 146 } 147 148 // Casting an ast.Expr to an ast.File ensures that it always ends 149 // with a newline. 150 f := internal.ToFile(n) 151 if e.cfg.PkgName != "" && f.PackageName() == "" { 152 pkg := &ast.Package{ 153 PackagePos: token.NoPos.WithRel(token.NewSection), 154 Name: ast.NewIdent(e.cfg.PkgName), 155 } 156 doc, rest := internal.FileComments(f) 157 ast.SetComments(pkg, doc) 158 ast.SetComments(f, rest) 159 f.Decls = append([]ast.Decl{pkg}, f.Decls...) 160 } 161 b, err := format.Node(f, opts...) 162 if err != nil { 163 return err 164 } 165 _, err = w.Write(b) 166 return err 167 } 168 e.encValue = func(v cue.Value) error { 169 return format("", v.Syntax(synOpts...)) 170 } 171 e.encFile = func(f *ast.File) error { return format(f.Filename, f) } 172 173 case build.JSON, build.JSONL: 174 e.concrete = true 175 d := json.NewEncoder(w) 176 d.SetIndent("", " ") 177 d.SetEscapeHTML(cfg.EscapeHTML) 178 e.encValue = func(v cue.Value) error { 179 err := d.Encode(v) 180 if x, ok := err.(*json.MarshalerError); ok { 181 err = x.Err 182 } 183 return err 184 } 185 186 case build.YAML: 187 e.concrete = true 188 streamed := false 189 // TODO(mvdan): use a NewEncoder API like in TOML below. 190 e.encValue = func(v cue.Value) error { 191 if streamed { 192 fmt.Fprintln(w, "---") 193 } 194 streamed = true 195 196 b, err := yaml.Encode(v) 197 if err != nil { 198 return err 199 } 200 _, err = w.Write(b) 201 return err 202 } 203 204 case build.TOML: 205 e.concrete = true 206 enc := toml.NewEncoder(w) 207 e.encValue = enc.Encode 208 209 case build.TextProto: 210 // TODO: verify that the schema is given. Otherwise err out. 211 e.concrete = true 212 e.encValue = func(v cue.Value) error { 213 v = v.Unify(cfg.Schema) 214 b, err := textproto.NewEncoder().Encode(v) 215 if err != nil { 216 return err 217 } 218 219 _, err = w.Write(b) 220 return err 221 } 222 223 case build.Text: 224 e.concrete = true 225 e.encValue = func(v cue.Value) error { 226 s, err := v.String() 227 if err != nil { 228 return err 229 } 230 _, err = fmt.Fprint(w, s) 231 if err != nil { 232 return err 233 } 234 _, err = fmt.Fprintln(w) 235 return err 236 } 237 238 case build.Binary: 239 e.concrete = true 240 e.encValue = func(v cue.Value) error { 241 b, err := v.Bytes() 242 if err != nil { 243 return err 244 } 245 _, err = w.Write(b) 246 return err 247 } 248 249 default: 250 return nil, fmt.Errorf("unsupported encoding %q", f.Encoding) 251 } 252 253 return e, nil 254} 255 256func (e *Encoder) EncodeFile(f *ast.File) error { 257 if e.interpret == nil && e.encFile != nil { 258 // TODO it's not clear that it's actually desirable to turn 259 // off simplification in this case. This case generally arises 260 // when we're producing CUE code with `cue eval` and 261 // simplified results seem generally preferable. 262 e.autoSimplify = false 263 return e.encFile(f) 264 } 265 e.autoSimplify = true 266 return e.Encode(e.ctx.BuildFile(f)) 267} 268 269func (e *Encoder) Encode(v cue.Value) error { 270 e.autoSimplify = true 271 if e.interpret == nil { 272 if err := v.Validate(cue.Concrete(e.concrete)); err != nil { 273 return err 274 } 275 return e.encValue(v) 276 } 277 if err := v.Validate(); err != nil { 278 return err 279 } 280 f, err := e.interpret(v) 281 if err != nil { 282 return err 283 } 284 if e.encFile != nil { 285 return e.encFile(f) 286 } 287 v = e.ctx.BuildFile(f) 288 if err := v.Validate(cue.Concrete(e.concrete)); err != nil { 289 return err 290 } 291 return e.encValue(v) 292} 293 294func writer(f *build.File, cfg *Config) (_ io.Writer, close func() error) { 295 if cfg.Out != nil { 296 return cfg.Out, nil 297 } 298 path := f.Filename 299 if path == "-" { 300 if cfg.Stdout == nil { 301 return os.Stdout, nil 302 } 303 return cfg.Stdout, nil 304 } 305 // Delay opening the file until we can write it to completion. 306 // This prevents clobbering the file in case of a crash. 307 b := &bytes.Buffer{} 308 fn := func() error { 309 mode := os.O_WRONLY | os.O_CREATE | os.O_EXCL 310 if cfg.Force { 311 // Swap O_EXCL for O_TRUNC to allow replacing an entire existing file. 312 mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC 313 } 314 f, err := os.OpenFile(path, mode, 0o666) 315 if errors.Is(err, fs.ErrExist) { 316 // If we failed because the file already existed, 317 // but the file in question is not regular, allow writing to it. 318 // This is done as a retry to avoid a Stat call before every OpenFile. 319 stat, err2 := os.Stat(path) 320 if err2 == nil && !stat.Mode().IsRegular() { 321 f, err = os.OpenFile(path, os.O_WRONLY, 0o666) 322 } else { 323 return errors.Wrapf(fs.ErrExist, token.NoPos, "error writing %q", path) 324 } 325 } 326 if err != nil { 327 return err 328 } 329 _, err = f.Write(b.Bytes()) 330 if err1 := f.Close(); err1 != nil && err == nil { 331 err = err1 332 } 333 return err 334 } 335 return b, fn 336}