cue-lang / cue

The home of the CUE language! Validate and define text-based and dynamic configuration
https://cuelang.org
Apache License 2.0
5.14k stars 294 forks source link

Provide a quick way to load and build files #2162

Open mvdan opened 1 year ago

mvdan commented 1 year ago

Is your feature request related to a problem? Please describe.

When writing Go code which uses the CUE APIs, it's common to need to load some CUE configuration. That can be done from strings or bytes rather easily, such as:

ctx := cuecontext.New()
val := ctx.CompileBytes(valBytes)
if err := val.Err(); err != nil {
    handle(err)
}

However, in some cases we want the Go program to load CUE from files on the filesystem, akin to cue eval foobar.cue, without embedding it as a static string in the binary. For example, see https://github.com/cue-lang/cue/discussions/2155, where a user wanted to mimic cue vet schema.cue data.json via the Go API.

Currently, I resort to this rather verbose and repetitive bit of code:

valBytes, err := os.ReadFile("schema.cue")
if err != nil {
    handle(err)
}
ctx := cuecontext.New()
val := ctx.CompileBytes(valBytes, cue.Filename("schema.cue"))
if err := val.Err(); err != nil {
    handle(err)
}

I don't particularly like it - it's quite a bit of typing, I need to handle two errors, and I need to repeat the filename twice to properly include the filename in any CUE errors.

@rogpeppe has a similar approach via the cue/load package:

inst = load.Instances([]string{"schema.cue"}, nil)[0]
if err := inst.Err(); err != nil {
    handle(err)
}
ctx := cuecontext.New()
val := ctx.BuildInstance(inst)
if err := val.Err(); err != nil {
    handle(err)
}

It doesn't repeat the filename, but it still handles errors twice, and the call to Instances is somewhat quirky with []string{} and [0]. It's also not obvious to me what is the difference between

Describe the solution you'd like

A quick way to load and build/compile a number of CUE files, akin to what cmd/cue does. For example, as a strawman proposal for loading a single file:

ctx := cuecontext.New()
val := ctx.CompileFile("schema.cue")
if err := val.Err(); err != nil {
    panic(err)
}

It's probably not ideal given that we already have BuildFile, but it gets my point across. We could also consider a similar addition to cue/load, which would take either a file or a directory, such as:

val := load.Compile("schema.cue")
if err := val.Err(); err != nil {
    panic(err)
}

Describe alternatives you've considered

An alternative would be to use the AST route, parsing first and then using BuildFile, but that's even more verbose.

Additional context

I struggle with our Go APIs due to this especially when writing examples or one-off programs. I suspect these problems aren't as noticeable for bigger or more complex Go programs, because they will often need to load and compile CUE files in a particular manner. An extra five lines of verbosity is also not noteworthy for a program of 500+ lines, but for a quick 30-line example, it does matter.

mvdan commented 1 year ago

Thinking outloud for a second, we could also take Roger's example and rethink the API slightly. For example, add load.Instance to load a single instance, and check the Err() method only once:

inst = load.Instance("schema.cue", nil)
ctx := cuecontext.New()
val := ctx.BuildInstance(inst)
if err := val.Err(); err != nil {
    handle(err)
}