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 }