cuelang.org/go@v0.10.1/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 "path/filepath" 19 "strings" 20 21 "cuelang.org/go/cue" 22 "cuelang.org/go/cue/build" 23 "cuelang.org/go/cue/errors" 24 "cuelang.org/go/cue/token" 25 ) 26 27 // Mode indicate the base mode of operation and indicates a different set of 28 // defaults. 29 type Mode int 30 31 const ( 32 Input Mode = iota // The default 33 Export 34 Def 35 Eval 36 ) 37 38 func (m Mode) String() string { 39 switch m { 40 default: 41 return "input" 42 case Eval: 43 return "eval" 44 case Export: 45 return "export" 46 case Def: 47 return "def" 48 } 49 } 50 51 // FileInfo defines the parsing plan for a file. 52 type FileInfo struct { 53 *build.File 54 55 Definitions bool `json:"definitions"` // include/allow definition fields 56 Data bool `json:"data"` // include/allow regular fields 57 Optional bool `json:"optional"` // include/allow definition fields 58 Constraints bool `json:"constraints"` // include/allow constraints 59 References bool `json:"references"` // don't resolve/allow references 60 Cycles bool `json:"cycles"` // cycles are permitted 61 KeepDefaults bool `json:"keepDefaults"` // select/allow default values 62 Incomplete bool `json:"incomplete"` // permit incomplete values 63 Imports bool `json:"imports"` // don't expand/allow imports 64 Stream bool `json:"stream"` // permit streaming 65 Docs bool `json:"docs"` // show/allow docs 66 Attributes bool `json:"attributes"` // include/allow attributes 67 } 68 69 // TODO(mvdan): the funcs below make use of typesValue concurrently, 70 // even though we clearly document that cue.Values are not safe for concurrent use. 71 // It seems to be OK in practice, as otherwise we would run into `go test -race` failures. 72 73 // FromFile return detailed file info for a given build file. 74 // Encoding must be specified. 75 // TODO: mode should probably not be necessary here. 76 func FromFile(b *build.File, mode Mode) (*FileInfo, error) { 77 // Handle common case. This allows certain test cases to be analyzed in 78 // isolation without interference from evaluating these files. 79 if mode == Input && 80 b.Encoding == build.CUE && 81 b.Form == "" && 82 b.Interpretation == "" { 83 return &FileInfo{ 84 File: b, 85 86 Definitions: true, 87 Data: true, 88 Optional: true, 89 Constraints: true, 90 References: true, 91 Cycles: true, 92 KeepDefaults: true, 93 Incomplete: true, 94 Imports: true, 95 Stream: true, 96 Docs: true, 97 Attributes: true, 98 }, nil 99 } 100 101 typesInit() 102 modeVal := typesValue.LookupPath(cue.MakePath(cue.Str("modes"), cue.Str(mode.String()))) 103 fileVal := modeVal.LookupPath(cue.MakePath(cue.Str("FileInfo"))) 104 fileVal = fileVal.FillPath(cue.Path{}, b) 105 106 if b.Encoding == "" { 107 ext := modeVal.LookupPath(cue.MakePath(cue.Str("extensions"), cue.Str(fileExt(b.Filename)))) 108 if ext.Exists() { 109 fileVal = fileVal.Unify(ext) 110 } 111 } 112 var errs errors.Error 113 114 interpretation, _ := fileVal.LookupPath(cue.MakePath(cue.Str("interpretation"))).String() 115 if b.Form != "" { 116 fileVal, errs = unifyWith(errs, fileVal, typesValue, "forms", string(b.Form)) 117 // may leave some encoding-dependent options open in data mode. 118 } else if interpretation != "" { 119 // always sets schema form. 120 fileVal, errs = unifyWith(errs, fileVal, typesValue, "interpretations", interpretation) 121 } 122 if interpretation == "" { 123 s, err := fileVal.LookupPath(cue.MakePath(cue.Str("encoding"))).String() 124 if err != nil { 125 return nil, err 126 } 127 fileVal, errs = unifyWith(errs, fileVal, modeVal, "encodings", s) 128 } 129 130 fi := &FileInfo{} 131 if err := fileVal.Decode(fi); err != nil { 132 return nil, errors.Wrapf(err, token.NoPos, "could not parse arguments") 133 } 134 return fi, errs 135 } 136 137 // unifyWith returns the equivalent of `v1 & v2[field][value]`. 138 func unifyWith(errs errors.Error, v1, v2 cue.Value, field, value string) (cue.Value, errors.Error) { 139 v1 = v1.Unify(v2.LookupPath(cue.MakePath(cue.Str(field), cue.Str(value)))) 140 if err := v1.Err(); err != nil { 141 errs = errors.Append(errs, 142 errors.Newf(token.NoPos, "unknown %s %s", field, value)) 143 } 144 return v1, errs 145 } 146 147 // ParseArgs converts a sequence of command line arguments representing 148 // files into a sequence of build file specifications. 149 // 150 // The arguments are of the form 151 // 152 // file* (spec: file+)* 153 // 154 // where file is a filename and spec is itself of the form 155 // 156 // tag[=value]('+'tag[=value])* 157 // 158 // A file type spec applies to all its following files and until a next spec 159 // is found. 160 // 161 // Examples: 162 // 163 // json: foo.data bar.data json+schema: bar.schema 164 func ParseArgs(args []string) (files []*build.File, err error) { 165 typesInit() 166 var modeVal, fileVal cue.Value 167 168 qualifier := "" 169 hasFiles := false 170 171 for i, s := range args { 172 a := strings.Split(s, ":") 173 switch { 174 case len(a) == 1 || len(a[0]) == 1: // filename 175 if !fileVal.Exists() { 176 if len(a) == 1 && strings.HasSuffix(a[0], ".cue") { 177 // Handle majority case. 178 f := *fileForCUE 179 f.Filename = a[0] 180 files = append(files, &f) 181 hasFiles = true 182 continue 183 } 184 185 modeVal, fileVal, err = parseType("", Input) 186 if err != nil { 187 return nil, err 188 } 189 } 190 if s == "" { 191 return nil, errors.Newf(token.NoPos, "empty file name") 192 } 193 f, err := toFile(modeVal, fileVal, s) 194 if err != nil { 195 return nil, err 196 } 197 files = append(files, f) 198 hasFiles = true 199 200 case len(a) > 2 || a[0] == "": 201 return nil, errors.Newf(token.NoPos, 202 "unsupported file name %q: may not have ':'", s) 203 204 case a[1] != "": 205 return nil, errors.Newf(token.NoPos, "cannot combine scope with file") 206 207 default: // scope 208 switch { 209 case i == len(args)-1: 210 qualifier = a[0] 211 fallthrough 212 case qualifier != "" && !hasFiles: 213 return nil, errors.Newf(token.NoPos, "scoped qualifier %q without file", qualifier+":") 214 } 215 modeVal, fileVal, err = parseType(a[0], Input) 216 if err != nil { 217 return nil, err 218 } 219 qualifier = a[0] 220 hasFiles = false 221 } 222 } 223 224 return files, nil 225 } 226 227 // ParseFile parses a single-argument file specifier, such as when a file is 228 // passed to a command line argument. 229 // 230 // Example: 231 // 232 // cue eval -o yaml:foo.data 233 func ParseFile(s string, mode Mode) (*build.File, error) { 234 scope := "" 235 file := s 236 237 if p := strings.LastIndexByte(s, ':'); p >= 0 { 238 scope = s[:p] 239 file = s[p+1:] 240 if scope == "" { 241 return nil, errors.Newf(token.NoPos, "unsupported file name %q: may not have ':", s) 242 } 243 } 244 245 if file == "" { 246 if s != "" { 247 return nil, errors.Newf(token.NoPos, "empty file name in %q", s) 248 } 249 return nil, errors.Newf(token.NoPos, "empty file name") 250 } 251 252 return ParseFileAndType(file, scope, mode) 253 } 254 255 // ParseFileAndType parses a file and type combo. 256 func ParseFileAndType(file, scope string, mode Mode) (*build.File, error) { 257 // Quickly discard files which we aren't interested in. 258 // These cases are very common when loading `./...` in a large repository. 259 typesInit() 260 if scope == "" && file != "-" { 261 ext := fileExt(file) 262 if ext == "" { 263 return nil, errors.Newf(token.NoPos, "no encoding specified for file %q", file) 264 } 265 f, ok := fileForExt[ext] 266 if !ok { 267 return nil, errors.Newf(token.NoPos, "unknown file extension %s", ext) 268 } 269 if mode == Input { 270 f1 := *f 271 f1.Filename = file 272 return &f1, nil 273 } 274 } 275 modeVal, fileVal, err := parseType(scope, mode) 276 if err != nil { 277 return nil, err 278 } 279 return toFile(modeVal, fileVal, file) 280 } 281 282 func hasEncoding(v cue.Value) bool { 283 enc := v.LookupPath(cue.MakePath(cue.Str("encoding"))) 284 d, _ := enc.Default() 285 return d.IsConcrete() 286 } 287 288 func toFile(modeVal, fileVal cue.Value, filename string) (*build.File, error) { 289 if !hasEncoding(fileVal) { 290 if filename == "-" { 291 fileVal = fileVal.Unify(modeVal.LookupPath(cue.MakePath(cue.Str("Default")))) 292 } else if ext := fileExt(filename); ext != "" { 293 extFile := modeVal.LookupPath(cue.MakePath(cue.Str("extensions"), cue.Str(ext))) 294 fileVal = fileVal.Unify(extFile) 295 if err := fileVal.Err(); err != nil { 296 return nil, errors.Newf(token.NoPos, "unknown file extension %s", ext) 297 } 298 } else { 299 return nil, errors.Newf(token.NoPos, "no encoding specified for file %q", filename) 300 } 301 } 302 303 // Note that the filename is only filled in the Go value, and not the CUE value. 304 // This makes no difference to the logic, but saves a non-trivial amount of evaluator work. 305 f := &build.File{Filename: filename} 306 if err := fileVal.Decode(&f); err != nil { 307 return nil, errors.Wrapf(err, token.NoPos, 308 "could not determine file type") 309 } 310 return f, nil 311 } 312 313 func parseType(scope string, mode Mode) (modeVal, fileVal cue.Value, _ error) { 314 modeVal = typesValue.LookupPath(cue.MakePath(cue.Str("modes"), cue.Str(mode.String()))) 315 fileVal = modeVal.LookupPath(cue.MakePath(cue.Str("File"))) 316 317 if scope != "" { 318 for _, tag := range strings.Split(scope, "+") { 319 tagName, tagVal, ok := strings.Cut(tag, "=") 320 if ok { 321 fileVal = fileVal.FillPath(cue.MakePath(cue.Str("tags"), cue.Str(tagName)), tagVal) 322 } else { 323 info := typesValue.LookupPath(cue.MakePath(cue.Str("tags"), cue.Str(tag))) 324 if !info.Exists() { 325 return cue.Value{}, cue.Value{}, errors.Newf(token.NoPos, "unknown filetype %s", tag) 326 } 327 fileVal = fileVal.Unify(info) 328 } 329 } 330 } 331 332 return modeVal, fileVal, nil 333 } 334 335 // fileExt is like filepath.Ext except we don't treat file names starting with "." as having an extension 336 // unless there's also another . in the name. 337 func fileExt(f string) string { 338 e := filepath.Ext(f) 339 if e == "" || e == filepath.Base(f) { 340 return "" 341 } 342 return e 343 }