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  }