cuelang.org/go@v0.13.0/internal/filetypes/filetypes.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 filetypes
    16  
    17  import (
    18  	"iter"
    19  	"path/filepath"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"cuelang.org/go/cue/build"
    24  	"cuelang.org/go/cue/errors"
    25  	"cuelang.org/go/cue/token"
    26  	"cuelang.org/go/internal/filetypes/internal"
    27  )
    28  
    29  // Mode indicate the base mode of operation and indicates a different set of
    30  // defaults.
    31  type Mode int
    32  
    33  const (
    34  	Input Mode = iota // The default
    35  	Export
    36  	Def
    37  	Eval
    38  	NumModes
    39  )
    40  
    41  func (m Mode) String() string {
    42  	switch m {
    43  	default:
    44  		return "input"
    45  	case Eval:
    46  		return "eval"
    47  	case Export:
    48  		return "export"
    49  	case Def:
    50  		return "def"
    51  	}
    52  }
    53  
    54  type FileInfo = internal.FileInfo
    55  
    56  // ParseArgs converts a sequence of command line arguments representing
    57  // files into a sequence of build file specifications.
    58  //
    59  // The arguments are of the form
    60  //
    61  //	file* (spec: file+)*
    62  //
    63  // where file is a filename and spec is itself of the form
    64  //
    65  //	tag[=value]('+'tag[=value])*
    66  //
    67  // A file type spec applies to all its following files and until a next spec
    68  // is found.
    69  //
    70  // Examples:
    71  //
    72  //	json: foo.data bar.data json+schema: bar.schema
    73  func ParseArgs(args []string) (files []*build.File, err error) {
    74  	qualifier := ""
    75  	hasFiles := false
    76  
    77  	sc := &scope{}
    78  	for i, s := range args {
    79  		a := strings.Split(s, ":")
    80  		switch {
    81  		case len(a) == 1 || len(a[0]) == 1: // filename
    82  			if s == "" {
    83  				return nil, errors.Newf(token.NoPos, "empty file name")
    84  			}
    85  			f, err := toFile(Input, sc, s)
    86  			if err != nil {
    87  				return nil, err
    88  			}
    89  			files = append(files, f)
    90  			hasFiles = true
    91  
    92  		case len(a) > 2 || a[0] == "":
    93  			return nil, errors.Newf(token.NoPos,
    94  				"unsupported file name %q: may not have ':'", s)
    95  
    96  		case a[1] != "":
    97  			return nil, errors.Newf(token.NoPos, "cannot combine scope with file")
    98  
    99  		default: // scope
   100  			switch {
   101  			case i == len(args)-1:
   102  				qualifier = a[0]
   103  				fallthrough
   104  			case qualifier != "" && !hasFiles:
   105  				return nil, errors.Newf(token.NoPos, "scoped qualifier %q without file", qualifier+":")
   106  			}
   107  			sc, err = parseScope(a[0])
   108  			if err != nil {
   109  				return nil, err
   110  			}
   111  			qualifier = a[0]
   112  			hasFiles = false
   113  		}
   114  	}
   115  
   116  	return files, nil
   117  }
   118  
   119  // DefaultTagsForInterpretation returns any tags that would be set by default
   120  // in the given interpretation in the given mode.
   121  func DefaultTagsForInterpretation(interp build.Interpretation, mode Mode) map[string]bool {
   122  	if interp == "" {
   123  		return nil
   124  	}
   125  
   126  	// This should never fail if called with a legitimate build.Interpretation constant.
   127  	f, err := toFile(mode, &scope{
   128  		topLevel: map[string]bool{
   129  			string(interp): true,
   130  		},
   131  	}, "-")
   132  	if err != nil {
   133  		panic(err)
   134  	}
   135  	return f.BoolTags
   136  }
   137  
   138  // ParseFile parses a single-argument file specifier, such as when a file is
   139  // passed to a command line argument.
   140  //
   141  // Example:
   142  //
   143  //	cue eval -o yaml:foo.data
   144  func ParseFile(s string, mode Mode) (*build.File, error) {
   145  	scope := ""
   146  	file := s
   147  
   148  	if p := strings.LastIndexByte(s, ':'); p >= 0 {
   149  		scope = s[:p]
   150  		file = s[p+1:]
   151  		if scope == "" {
   152  			return nil, errors.Newf(token.NoPos, "unsupported file name %q: may not have ':", s)
   153  		}
   154  	}
   155  
   156  	if file == "" {
   157  		if s != "" {
   158  			return nil, errors.Newf(token.NoPos, "empty file name in %q", s)
   159  		}
   160  		return nil, errors.Newf(token.NoPos, "empty file name")
   161  	}
   162  
   163  	return ParseFileAndType(file, scope, mode)
   164  }
   165  
   166  // ParseFileAndType parses a file and type combo.
   167  func ParseFileAndType(file, scope string, mode Mode) (*build.File, error) {
   168  	sc, err := parseScope(scope)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return toFile(mode, sc, file)
   173  }
   174  
   175  // scope holds attributes that influence encoding and decoding.
   176  // Together with the mode and the file name, they determine
   177  // a number of properties of the encoding process.
   178  type scope struct {
   179  	topLevel         map[string]bool
   180  	subsidiaryBool   map[string]bool
   181  	subsidiaryString map[string]string
   182  }
   183  
   184  func parseScope(scopeStr string) (*scope, error) {
   185  	if scopeStr == "" {
   186  		return &scope{}, nil
   187  	}
   188  	sc := scope{
   189  		topLevel:         make(map[string]bool),
   190  		subsidiaryBool:   make(map[string]bool),
   191  		subsidiaryString: make(map[string]string),
   192  	}
   193  	for _, tag := range strings.Split(scopeStr, "+") {
   194  		tagName, tagVal, hasValue := strings.Cut(tag, "=")
   195  		switch tagTypes[tagName] {
   196  		case TagTopLevel:
   197  			if hasValue {
   198  				return nil, errors.Newf(token.NoPos, "cannot specify value for tag %q", tagName)
   199  			}
   200  			sc.topLevel[tagName] = true
   201  		case TagSubsidiaryBool:
   202  			if hasValue {
   203  				t, err := strconv.ParseBool(tagVal)
   204  				if err != nil {
   205  					return nil, errors.Newf(token.NoPos, "invalid boolean value for tag %q", tagName)
   206  				}
   207  				sc.subsidiaryBool[tagName] = t
   208  			} else {
   209  				sc.subsidiaryBool[tagName] = true
   210  			}
   211  		case TagSubsidiaryString:
   212  			if !hasValue {
   213  				return nil, errors.Newf(token.NoPos, "tag %q must have value (%s=<value>)", tagName, tagName)
   214  			}
   215  			sc.subsidiaryString[tagName] = tagVal
   216  		default:
   217  			return nil, errors.Newf(token.NoPos, "unknown filetype %s", tagName)
   218  		}
   219  	}
   220  	return &sc, nil
   221  }
   222  
   223  // fileExt is like filepath.Ext except we don't treat file names starting with "." as having an extension
   224  // unless there's also another . in the name.
   225  //
   226  // It also treats "-" as a special case, so we treat stdin/stdout as
   227  // a regular file.
   228  func fileExt(f string) string {
   229  	if f == "-" {
   230  		return "-"
   231  	}
   232  	e := filepath.Ext(f)
   233  	if e == "" || e == filepath.Base(f) {
   234  		return ""
   235  	}
   236  	return e
   237  }
   238  
   239  func seqConcat[T any](iters ...iter.Seq[T]) iter.Seq[T] {
   240  	return func(yield func(T) bool) {
   241  		for _, it := range iters {
   242  			for x := range it {
   243  				if !yield(x) {
   244  					return
   245  				}
   246  			}
   247  		}
   248  	}
   249  }