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}