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