blob: cc72f622ccab0560964eefc9142395fa4859576d [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.
//go:generate go run gen.go
package filetypes
import (
"path/filepath"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
)
// Mode indicate the base mode of operation and indicates a different set of
// defaults.
type Mode int
const (
Input Mode = iota // The default
Export
Def
Eval
)
func (m Mode) String() string {
switch m {
default:
return "input"
case Eval:
return "eval"
case Export:
return "export"
case Def:
return "def"
}
}
// FileInfo defines the parsing plan for a file.
type FileInfo struct {
*build.File
Definitions bool `json:"definitions"` // include/allow definition fields
Data bool `json:"data"` // include/allow regular fields
Optional bool `json:"optional"` // include/allow definition fields
Constraints bool `json:"constraints"` // include/allow constraints
References bool `json:"references"` // don't resolve/allow references
Cycles bool `json:"cycles"` // cycles are permitted
KeepDefaults bool `json:"keepDefaults"` // select/allow default values
Incomplete bool `json:"incomplete"` // permit incomplete values
Imports bool `json:"imports"` // don't expand/allow imports
Stream bool `json:"stream"` // permit streaming
Docs bool `json:"docs"` // show/allow docs
Attributes bool `json:"attributes"` // include/allow attributes
}
// FromFile return detailed file info for a given build file.
// Encoding must be specified.
// TODO: mode should probably not be necessary here.
func FromFile(b *build.File, mode Mode) (*FileInfo, error) {
// Handle common case. This allows certain test cases to be analyzed in
// isolation without interference from evaluating these files.
if mode == Input &&
b.Encoding == build.CUE &&
b.Form == build.Schema &&
b.Interpretation == "" {
return &FileInfo{
File: b,
Definitions: true,
Data: true,
Optional: true,
Constraints: true,
References: true,
Cycles: true,
KeepDefaults: true,
Incomplete: true,
Imports: true,
Stream: true,
Docs: true,
Attributes: true,
}, nil
}
i := cuegenInstance.Value()
i, errs := update(nil, i, i, "modes", mode.String())
v := i.LookupDef("FileInfo")
v = v.Fill(b)
if b.Encoding == "" {
ext := i.Lookup("extensions", filepath.Ext(b.Filename))
if ext.Exists() {
v = v.Unify(ext)
}
}
interpretation, _ := v.Lookup("interpretation").String()
if b.Form != "" {
v, errs = update(errs, v, i, "forms", string(b.Form))
// may leave some encoding-dependent options open in data mode.
} else if interpretation != "" {
// always sets schema form.
v, errs = update(errs, v, i, "interpretations", interpretation)
}
if interpretation == "" {
s, err := v.Lookup("encoding").String()
if err != nil {
return nil, err
}
v, errs = update(errs, v, i, "encodings", s)
}
fi := &FileInfo{}
if err := v.Decode(fi); err != nil {
return nil, errors.Wrapf(err, token.NoPos, "could not parse arguments")
}
return fi, errs
}
func update(errs errors.Error, v, i cue.Value, field, value string) (cue.Value, errors.Error) {
v = v.Unify(i.Lookup(field, value))
if err := v.Err(); err != nil {
errs = errors.Append(errs,
errors.Newf(token.NoPos, "unknown %s %s", field, value))
}
return v, errs
}
// ParseArgs converts a sequence of command line arguments representing
// files into a sequence of build file specifications.
//
// The arguments are of the form
//
// file* (spec: file+)*
//
// where file is a filename and spec is itself of the form
//
// tag[=value]('+'tag[=value])*
//
// A file type spec applies to all its following files and until a next spec
// is found.
//
// Examples:
// json: foo.data bar.data json+schema: bar.schema
//
func ParseArgs(args []string) (files []*build.File, err error) {
var inst, v cue.Value
qualifier := ""
hasFiles := false
for i, s := range args {
a := strings.Split(s, ":")
switch {
case len(a) == 1 || len(a[0]) == 1: // filename
if !v.Exists() {
if len(a) == 1 && strings.HasSuffix(a[0], ".cue") {
// Handle majority case.
files = append(files, &build.File{
Filename: a[0],
Encoding: build.CUE,
Form: build.Schema,
})
hasFiles = true
continue
}
inst, v, err = parseType("", Input)
if err != nil {
return nil, err
}
}
f, err := toFile(inst, v, s)
if err != nil {
return nil, err
}
files = append(files, f)
hasFiles = true
case len(a) > 2 || a[0] == "":
return nil, errors.Newf(token.NoPos,
"unsupported file name %q: may not have ':'", s)
case a[1] != "":
return nil, errors.Newf(token.NoPos, "cannot combine scope with file")
default: // scope
switch {
case i == len(args)-1:
qualifier = a[0]
fallthrough
case qualifier != "" && !hasFiles:
return nil, errors.Newf(token.NoPos, "scoped qualifier %q without file", qualifier+":")
}
inst, v, err = parseType(a[0], Input)
if err != nil {
return nil, err
}
qualifier = a[0]
hasFiles = false
}
}
return files, nil
}
// ParseFile parses a single-argument file specifier, such as when a file is
// passed to a command line argument.
//
// Example:
// cue eval -o yaml:foo.data
//
func ParseFile(s string, mode Mode) (*build.File, error) {
scope := ""
file := s
if p := strings.LastIndexByte(s, ':'); p >= 0 {
scope = s[:p]
file = s[p+1:]
if scope == "" {
return nil, errors.Newf(token.NoPos, "unsupported file name %q: may not have ':", s)
}
}
if file == "" {
return nil, errors.Newf(token.NoPos, "empty file name in %q", s)
}
inst, val, err := parseType(scope, mode)
if err != nil {
return nil, err
}
return toFile(inst, val, file)
}
func hasEncoding(v cue.Value) (concrete, hasDefault bool) {
enc := v.Lookup("encoding")
d, _ := enc.Default()
return enc.IsConcrete(), d.IsConcrete()
}
func toFile(i, v cue.Value, filename string) (*build.File, error) {
v = v.Fill(filename, "filename")
if concrete, hasDefault := hasEncoding(v); !concrete {
if filename == "-" {
if !hasDefault {
v = v.Unify(i.LookupDef("Default"))
}
} else if ext := filepath.Ext(filename); ext != "" {
if x := i.Lookup("extensions", ext); x.Exists() || !hasDefault {
v = v.Unify(x)
if err := v.Err(); err != nil {
return nil, errors.Newf(token.NoPos,
"unknown file extension %s", ext)
}
}
} else if !hasDefault {
return nil, errors.Newf(token.NoPos,
"no encoding specified for file %q", filename)
}
}
f := &build.File{}
if err := v.Decode(&f); err != nil {
return nil, errors.Wrapf(err, token.NoPos,
"could not determine file type")
}
return f, nil
}
func parseType(s string, mode Mode) (inst, val cue.Value, err error) {
i := cuegenInstance.Value()
i = i.Unify(i.Lookup("modes", mode.String()))
v := i.LookupDef("File")
if s != "" {
for _, t := range strings.Split(s, "+") {
if p := strings.IndexByte(t, '='); p >= 0 {
v = v.Fill(t[p+1:], "tags", t[:p])
} else {
info := i.Lookup("tags", t)
if !info.Exists() {
return inst, val, errors.Newf(token.NoPos,
"unknown filetype %s", t)
}
v = v.Unify(info)
}
}
}
return i, v, nil
}