github.com/solo-io/cue@v0.4.7/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/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/solo-io/cue/cue"
    27  	"github.com/solo-io/cue/cue/ast"
    28  	"github.com/solo-io/cue/cue/build"
    29  	"github.com/solo-io/cue/cue/errors"
    30  	"github.com/solo-io/cue/cue/format"
    31  	"github.com/solo-io/cue/cue/token"
    32  	"github.com/solo-io/cue/encoding/openapi"
    33  	"github.com/solo-io/cue/encoding/protobuf/jsonpb"
    34  	"github.com/solo-io/cue/encoding/protobuf/textproto"
    35  	"github.com/solo-io/cue/internal"
    36  	"github.com/solo-io/cue/internal/filetypes"
    37  	"github.com/solo-io/cue/pkg/encoding/yaml"
    38  )
    39  
    40  // An Encoder converts CUE to various file formats, including CUE itself.
    41  // An Encoder allows
    42  type Encoder struct {
    43  	cfg          *Config
    44  	close        func() error
    45  	interpret    func(cue.Value) (*ast.File, error)
    46  	encFile      func(*ast.File) error
    47  	encValue     func(cue.Value) error
    48  	autoSimplify bool
    49  	concrete     bool
    50  	instance     *cue.Instance
    51  }
    52  
    53  // IsConcrete reports whether the output is required to be concrete.
    54  //
    55  // INTERNAL ONLY: this is just to work around a problem related to issue #553
    56  // of catching errors ony after syntax generation, dropping line number
    57  // information.
    58  func (e *Encoder) IsConcrete() bool {
    59  	return e.concrete
    60  }
    61  
    62  func (e Encoder) Close() error {
    63  	if e.close == nil {
    64  		return nil
    65  	}
    66  	return e.close()
    67  }
    68  
    69  // NewEncoder writes content to the file with the given specification.
    70  func NewEncoder(f *build.File, cfg *Config) (*Encoder, error) {
    71  	w, close, err := writer(f, cfg)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	e := &Encoder{
    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  			i := e.instance
    87  			if i == nil {
    88  				i = internal.MakeInstance(v).(*cue.Instance)
    89  			}
    90  			return openapi.Generate(i, cfg)
    91  		}
    92  	case build.ProtobufJSON:
    93  		e.interpret = func(v cue.Value) (*ast.File, error) {
    94  			f := valueToFile(v)
    95  			return f, jsonpb.NewEncoder(v).RewriteFile(f)
    96  		}
    97  
    98  	// case build.JSONSchema:
    99  	// 	// TODO: get encoding options
   100  	// 	cfg := openapi.Config{}
   101  	// 	i.interpret = func(inst *cue.Instance) (*ast.File, error) {
   102  	// 		return jsonschmea.Generate(inst, cfg)
   103  	// 	}
   104  	default:
   105  		return nil, fmt.Errorf("unsupported interpretation %q", f.Interpretation)
   106  	}
   107  
   108  	switch f.Encoding {
   109  	case build.CUE:
   110  		fi, err := filetypes.FromFile(f, cfg.Mode)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		e.concrete = !fi.Incomplete
   115  
   116  		synOpts := []cue.Option{}
   117  		if !fi.KeepDefaults || !fi.Incomplete {
   118  			synOpts = append(synOpts, cue.Final())
   119  		}
   120  
   121  		synOpts = append(synOpts,
   122  			cue.Docs(fi.Docs),
   123  			cue.Attributes(fi.Attributes),
   124  			cue.Optional(fi.Optional),
   125  			cue.Concrete(!fi.Incomplete),
   126  			cue.Definitions(fi.Definitions),
   127  			cue.ResolveReferences(!fi.References),
   128  			cue.DisallowCycles(!fi.Cycles),
   129  		)
   130  
   131  		opts := []format.Option{}
   132  		opts = append(opts, cfg.Format...)
   133  
   134  		useSep := false
   135  		format := func(name string, n ast.Node) error {
   136  			if name != "" && cfg.Stream {
   137  				// TODO: make this relative to DIR
   138  				fmt.Fprintf(w, "// %s\n", filepath.Base(name))
   139  			} else if useSep {
   140  				fmt.Println("// ---")
   141  			}
   142  			useSep = true
   143  
   144  			opts := opts
   145  			if e.autoSimplify {
   146  				opts = append(opts, format.Simplify())
   147  			}
   148  
   149  			// Casting an ast.Expr to an ast.File ensures that it always ends
   150  			// with a newline.
   151  			b, err := format.Node(internal.ToFile(n), opts...)
   152  			if err != nil {
   153  				return err
   154  			}
   155  			_, err = w.Write(b)
   156  			return err
   157  		}
   158  		e.encValue = func(v cue.Value) error {
   159  			return format("", v.Syntax(synOpts...))
   160  		}
   161  		e.encFile = func(f *ast.File) error { return format(f.Filename, f) }
   162  
   163  	case build.JSON, build.JSONL:
   164  		e.concrete = true
   165  		d := json.NewEncoder(w)
   166  		d.SetIndent("", "    ")
   167  		d.SetEscapeHTML(cfg.EscapeHTML)
   168  		e.encValue = func(v cue.Value) error {
   169  			err := d.Encode(v)
   170  			if x, ok := err.(*json.MarshalerError); ok {
   171  				err = x.Err
   172  			}
   173  			return err
   174  		}
   175  
   176  	case build.YAML:
   177  		e.concrete = true
   178  		streamed := false
   179  		e.encValue = func(v cue.Value) error {
   180  			if streamed {
   181  				fmt.Fprintln(w, "---")
   182  			}
   183  			streamed = true
   184  
   185  			str, err := yaml.Marshal(v)
   186  			if err != nil {
   187  				return err
   188  			}
   189  			_, err = fmt.Fprint(w, str)
   190  			return err
   191  		}
   192  
   193  	case build.TextProto:
   194  		// TODO: verify that the schema is given. Otherwise err out.
   195  		e.concrete = true
   196  		e.encValue = func(v cue.Value) error {
   197  			v = v.Unify(cfg.Schema)
   198  			b, err := textproto.NewEncoder().Encode(v)
   199  			if err != nil {
   200  				return err
   201  			}
   202  
   203  			_, err = w.Write(b)
   204  			return err
   205  		}
   206  
   207  	case build.Text:
   208  		e.concrete = true
   209  		e.encValue = func(v cue.Value) error {
   210  			s, err := v.String()
   211  			if err != nil {
   212  				return err
   213  			}
   214  			_, err = fmt.Fprint(w, s)
   215  			if err != nil {
   216  				return err
   217  			}
   218  			_, err = fmt.Fprintln(w)
   219  			return err
   220  		}
   221  
   222  	case build.Binary:
   223  		e.concrete = true
   224  		e.encValue = func(v cue.Value) error {
   225  			b, err := v.Bytes()
   226  			if err != nil {
   227  				return err
   228  			}
   229  			_, err = w.Write(b)
   230  			return err
   231  		}
   232  
   233  	default:
   234  		return nil, fmt.Errorf("unsupported encoding %q", f.Encoding)
   235  	}
   236  
   237  	return e, nil
   238  }
   239  
   240  func (e *Encoder) EncodeFile(f *ast.File) error {
   241  	e.autoSimplify = false
   242  	return e.encodeFile(f, e.interpret)
   243  }
   244  
   245  // EncodeInstance is as Encode, but stores instance information. This should
   246  // all be retrievable from the value itself.
   247  func (e *Encoder) EncodeInstance(v *cue.Instance) error {
   248  	e.instance = v
   249  	err := e.Encode(v.Value())
   250  	e.instance = nil
   251  	return err
   252  }
   253  
   254  func (e *Encoder) Encode(v cue.Value) error {
   255  	e.autoSimplify = true
   256  	if e.interpret != nil {
   257  		f, err := e.interpret(v)
   258  		if err != nil {
   259  			return err
   260  		}
   261  		return e.encodeFile(f, nil)
   262  	}
   263  	if err := v.Validate(cue.Concrete(e.concrete)); err != nil {
   264  		return err
   265  	}
   266  	if e.encValue != nil {
   267  		return e.encValue(v)
   268  	}
   269  	return e.encFile(valueToFile(v))
   270  }
   271  
   272  func (e *Encoder) encodeFile(f *ast.File, interpret func(cue.Value) (*ast.File, error)) error {
   273  	if interpret == nil && e.encFile != nil {
   274  		return e.encFile(f)
   275  	}
   276  	e.autoSimplify = true
   277  	var r cue.Runtime
   278  	inst, err := r.CompileFile(f)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	if interpret != nil {
   283  		return e.Encode(inst.Value())
   284  	}
   285  	v := inst.Value()
   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  	if !cfg.Force {
   304  		if _, err := os.Stat(path); err == nil {
   305  			return nil, nil, errors.Wrapf(os.ErrExist, token.NoPos,
   306  				"error writing %q", path)
   307  		}
   308  	}
   309  	// Delay opening the file until we can write it to completion. This will
   310  	// prevent clobbering the file in case of a crash.
   311  	b := &bytes.Buffer{}
   312  	fn := func() error {
   313  		return ioutil.WriteFile(path, b.Bytes(), 0644)
   314  	}
   315  	return b, fn, nil
   316  }