cuelang.org/go@v0.10.1/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/internal" 37 "cuelang.org/go/internal/filetypes" 38 "cuelang.org/go/pkg/encoding/yaml" 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, err := writer(f, cfg) 73 if err != nil { 74 return nil, err 75 } 76 e := &Encoder{ 77 ctx: ctx, 78 cfg: cfg, 79 close: close, 80 } 81 82 switch f.Interpretation { 83 case "": 84 case build.OpenAPI: 85 // TODO: get encoding options 86 cfg := &openapi.Config{} 87 e.interpret = func(v cue.Value) (*ast.File, error) { 88 return openapi.Generate(v, cfg) 89 } 90 case build.ProtobufJSON: 91 e.interpret = func(v cue.Value) (*ast.File, error) { 92 f := internal.ToFile(v.Syntax()) 93 return f, jsonpb.NewEncoder(v).RewriteFile(f) 94 } 95 96 // case build.JSONSchema: 97 // // TODO: get encoding options 98 // cfg := openapi.Config{} 99 // i.interpret = func(inst *cue.Instance) (*ast.File, error) { 100 // return jsonschmea.Generate(inst, cfg) 101 // } 102 default: 103 return nil, fmt.Errorf("unsupported interpretation %q", f.Interpretation) 104 } 105 106 switch f.Encoding { 107 case build.CUE: 108 fi, err := filetypes.FromFile(f, cfg.Mode) 109 if err != nil { 110 return nil, err 111 } 112 e.concrete = !fi.Incomplete 113 114 synOpts := []cue.Option{} 115 if !fi.KeepDefaults || !fi.Incomplete { 116 synOpts = append(synOpts, cue.Final()) 117 } 118 119 synOpts = append(synOpts, 120 cue.Docs(fi.Docs), 121 cue.Attributes(fi.Attributes), 122 cue.Optional(fi.Optional), 123 cue.Concrete(!fi.Incomplete), 124 cue.Definitions(fi.Definitions), 125 cue.DisallowCycles(!fi.Cycles), 126 cue.InlineImports(cfg.InlineImports), 127 ) 128 129 opts := []format.Option{} 130 opts = append(opts, cfg.Format...) 131 132 useSep := false 133 format := func(name string, n ast.Node) error { 134 if name != "" && cfg.Stream { 135 // TODO: make this relative to DIR 136 fmt.Fprintf(w, "// %s\n", filepath.Base(name)) 137 } else if useSep { 138 fmt.Println("// ---") 139 } 140 useSep = true 141 142 opts := opts 143 if e.autoSimplify { 144 opts = append(opts, format.Simplify()) 145 } 146 147 // Casting an ast.Expr to an ast.File ensures that it always ends 148 // with a newline. 149 f := internal.ToFile(n) 150 if e.cfg.PkgName != "" && f.PackageName() == "" { 151 f.Decls = append([]ast.Decl{ 152 &ast.Package{ 153 Name: ast.NewIdent(e.cfg.PkgName), 154 }, 155 }, 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 e.encValue = func(v cue.Value) error { 186 if streamed { 187 fmt.Fprintln(w, "---") 188 } 189 streamed = true 190 191 str, err := yaml.Marshal(v) 192 if err != nil { 193 return err 194 } 195 _, err = fmt.Fprint(w, str) 196 return err 197 } 198 199 case build.TOML: 200 e.concrete = true 201 enc := toml.NewEncoder(w) 202 e.encValue = enc.Encode 203 204 case build.TextProto: 205 // TODO: verify that the schema is given. Otherwise err out. 206 e.concrete = true 207 e.encValue = func(v cue.Value) error { 208 v = v.Unify(cfg.Schema) 209 b, err := textproto.NewEncoder().Encode(v) 210 if err != nil { 211 return err 212 } 213 214 _, err = w.Write(b) 215 return err 216 } 217 218 case build.Text: 219 e.concrete = true 220 e.encValue = func(v cue.Value) error { 221 s, err := v.String() 222 if err != nil { 223 return err 224 } 225 _, err = fmt.Fprint(w, s) 226 if err != nil { 227 return err 228 } 229 _, err = fmt.Fprintln(w) 230 return err 231 } 232 233 case build.Binary: 234 e.concrete = true 235 e.encValue = func(v cue.Value) error { 236 b, err := v.Bytes() 237 if err != nil { 238 return err 239 } 240 _, err = w.Write(b) 241 return err 242 } 243 244 default: 245 return nil, fmt.Errorf("unsupported encoding %q", f.Encoding) 246 } 247 248 return e, nil 249 } 250 251 func (e *Encoder) EncodeFile(f *ast.File) error { 252 e.autoSimplify = false 253 return e.encodeFile(f, e.interpret) 254 } 255 256 func (e *Encoder) Encode(v cue.Value) error { 257 e.autoSimplify = true 258 if err := v.Validate(cue.Concrete(e.concrete)); err != nil { 259 return err 260 } 261 if e.interpret != nil { 262 f, err := e.interpret(v) 263 if err != nil { 264 return err 265 } 266 return e.encodeFile(f, nil) 267 } 268 if e.encValue != nil { 269 return e.encValue(v) 270 } 271 return e.encFile(internal.ToFile(v.Syntax())) 272 } 273 274 func (e *Encoder) encodeFile(f *ast.File, interpret func(cue.Value) (*ast.File, error)) error { 275 if interpret == nil && e.encFile != nil { 276 return e.encFile(f) 277 } 278 e.autoSimplify = true 279 v := e.ctx.BuildFile(f) 280 if err := v.Err(); err != nil { 281 return err 282 } 283 if interpret != nil { 284 return e.Encode(v) 285 } 286 if err := v.Validate(cue.Concrete(e.concrete)); err != nil { 287 return err 288 } 289 return e.encValue(v) 290 } 291 292 func writer(f *build.File, cfg *Config) (_ io.Writer, close func() error, err error) { 293 if cfg.Out != nil { 294 return cfg.Out, nil, nil 295 } 296 path := f.Filename 297 if path == "-" { 298 if cfg.Stdout == nil { 299 return os.Stdout, nil, nil 300 } 301 return cfg.Stdout, nil, nil 302 } 303 // Delay opening the file until we can write it to completion. 304 // This prevents clobbering the file in case of a crash. 305 b := &bytes.Buffer{} 306 fn := func() error { 307 mode := os.O_WRONLY | os.O_CREATE | os.O_EXCL 308 if cfg.Force { 309 // Swap O_EXCL for O_TRUNC to allow replacing an entire existing file. 310 mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC 311 } 312 f, err := os.OpenFile(path, mode, 0o644) 313 if err != nil { 314 if errors.Is(err, fs.ErrExist) { 315 return errors.Wrapf(fs.ErrExist, token.NoPos, "error writing %q", path) 316 } 317 return err 318 } 319 _, err = f.Write(b.Bytes()) 320 if err1 := f.Close(); err1 != nil && err == nil { 321 err = err1 322 } 323 return err 324 } 325 return b, fn, nil 326 }