blob: 22871eaa3b4f33bd66e279374f9928dd8f127912 [file] [log] [blame]
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/parser"
"cuelang.org/go/encoding/json"
"cuelang.org/go/encoding/protobuf"
"cuelang.org/go/internal/filetypes"
"cuelang.org/go/internal/third_party/yaml"
)
type Decoder struct {
closer io.Closer
next func() (ast.Expr, error)
expr ast.Expr
file *ast.File
filename string // may change on iteration for some formats
index int
err error
}
func (i *Decoder) Expr() ast.Expr { return i.expr }
func (i *Decoder) Filename() string { return i.filename }
func (i *Decoder) Index() int { return i.index }
func (i *Decoder) Done() bool { return i.err != nil }
func (i *Decoder) Next() {
if i.err == nil {
i.expr, i.err = i.next()
i.index++
}
}
func toFile(x ast.Expr) *ast.File {
switch x := x.(type) {
case nil:
return nil
case *ast.StructLit:
return &ast.File{Decls: x.Elts}
default:
return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}}
}
}
func valueToFile(v cue.Value) *ast.File {
switch x := v.Syntax().(type) {
case *ast.File:
return x
case ast.Expr:
return toFile(x)
default:
panic("unrreachable")
}
}
func (i *Decoder) File() *ast.File {
if i.file != nil {
return i.file
}
return toFile(i.expr)
}
func (i *Decoder) Err() error {
if i.err == io.EOF {
return nil
}
return i.err
}
func (i *Decoder) Close() {
i.closer.Close()
}
type Config struct {
Mode filetypes.Mode
// Out specifies an overwrite destination.
Out io.Writer
Stdin io.Reader
Stdout io.Writer
Force bool // overwrite existing files.
Stream bool // will potentially write more than one document per file
EscapeHTML bool
ProtoPath []string
Format []format.Option
}
// NewDecoder returns a stream of non-rooted data expressions. The encoding
// type of f must be a data type, but does not have to be an encoding that
// can stream. stdin is used in case the file is "-".
func NewDecoder(f *build.File, cfg *Config) *Decoder {
r, err := reader(f, cfg.Stdin)
i := &Decoder{
closer: r,
err: err,
filename: f.Filename,
next: func() (ast.Expr, error) {
if err == nil {
err = io.EOF
}
return nil, io.EOF
},
}
if err != nil {
return i
}
path := f.Filename
switch f.Encoding {
case build.CUE:
i.file, i.err = parser.ParseFile(path, r, parser.ParseComments)
// TODO: verify input format
case build.JSON, build.JSONL:
i.next = json.NewDecoder(nil, path, r).Extract
i.Next()
case build.YAML:
d, err := yaml.NewDecoder(path, r)
i.err = err
i.next = d.Decode
i.Next()
case build.Text:
b, err := ioutil.ReadAll(r)
i.err = err
i.expr = ast.NewString(string(b))
case build.Protobuf:
paths := &protobuf.Config{Paths: cfg.ProtoPath}
i.file, i.err = protobuf.Extract(path, r, paths)
default:
i.err = fmt.Errorf("unsupported stream type %q", f.Encoding)
}
return i
}
func reader(f *build.File, stdin io.Reader) (io.ReadCloser, error) {
switch s := f.Source.(type) {
case nil:
// Use the file name.
case string:
return ioutil.NopCloser(strings.NewReader(s)), nil
case []byte:
return ioutil.NopCloser(bytes.NewReader(s)), nil
case *bytes.Buffer:
// is io.Reader, but it needs to be readable repeatedly
if s != nil {
return ioutil.NopCloser(bytes.NewReader(s.Bytes())), nil
}
default:
return nil, fmt.Errorf("invalid source type %T", f.Source)
}
// TODO: should we allow this?
if f.Filename == "-" {
return ioutil.NopCloser(stdin), nil
}
return os.Open(f.Filename)
}