mirror of
https://github.com/lxc/incus.git
synced 2026-02-05 09:46:19 +01:00
247 lines
6.1 KiB
Go
247 lines
6.1 KiB
Go
package scriptlet
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"slices"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
|
||
"go.starlark.net/starlark"
|
||
"go.starlark.net/syntax"
|
||
)
|
||
|
||
// Loader holds the programs for the scriptlet.
|
||
type Loader struct {
|
||
programsMu sync.Mutex
|
||
programs map[string]*starlark.Program
|
||
}
|
||
|
||
// argMismatch represents mismatching arguments in a function.
|
||
type argMismatch struct {
|
||
gotten []string
|
||
expected []string
|
||
}
|
||
|
||
// scriptletFunction represents a possibly optional function in a scriptlet.
|
||
type scriptletFunction struct {
|
||
name string
|
||
optional bool
|
||
}
|
||
|
||
// Declaration is a type alias to make scriptlet declaration easier.
|
||
type Declaration = map[scriptletFunction][]string
|
||
|
||
// NewLoader creates a new Loader.
|
||
func NewLoader() *Loader {
|
||
return &Loader{
|
||
programsMu: sync.Mutex{},
|
||
programs: map[string]*starlark.Program{},
|
||
}
|
||
}
|
||
|
||
// Compile compiles a scriptlet.
|
||
func Compile(programName string, src string, preDeclared []string) (*starlark.Program, error) {
|
||
isPreDeclared := func(name string) bool {
|
||
return slices.Contains(preDeclared, name)
|
||
}
|
||
|
||
// Prepare options.
|
||
opts := syntax.LegacyFileOptions()
|
||
opts.Set = true
|
||
|
||
// Parse, resolve, and compile a Starlark source file.
|
||
_, mod, err := starlark.SourceProgramOptions(opts, programName, src, isPreDeclared)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return mod, nil
|
||
}
|
||
|
||
// Required is a convenience wrapper declaring a required function.
|
||
func Required(name string) scriptletFunction {
|
||
return scriptletFunction{name: name, optional: false}
|
||
}
|
||
|
||
// Optional is a convenience wrapper declaring an optional function.
|
||
func Optional(name string) scriptletFunction {
|
||
return scriptletFunction{name: name, optional: true}
|
||
}
|
||
|
||
// optionalToString converts a Boolean describing optional functions to its string representation.
|
||
func optionalToString(optional bool) string {
|
||
if optional {
|
||
return "optional"
|
||
}
|
||
|
||
return "required"
|
||
}
|
||
|
||
// validateFunction validates a single Starlark function.
|
||
func validateFunction(funv starlark.Value, requiredArgs []string) (bool, bool, *argMismatch) {
|
||
// The function is missing if its name is not found in the globals.
|
||
if funv == nil {
|
||
return true, false, nil
|
||
}
|
||
|
||
// The function is actually not a function if its name is not bound to a function.
|
||
fun, ok := funv.(*starlark.Function)
|
||
if !ok {
|
||
return false, true, nil
|
||
}
|
||
|
||
// Get the function arguments.
|
||
argc := fun.NumParams()
|
||
var args []string
|
||
for i := range argc {
|
||
arg, _ := fun.Param(i)
|
||
args = append(args, arg)
|
||
}
|
||
|
||
// The function is invalid if it does not have the right arguments.
|
||
match := len(args) == len(requiredArgs)
|
||
if match {
|
||
sort.Strings(args)
|
||
sort.Strings(requiredArgs)
|
||
for i := range args {
|
||
if args[i] != requiredArgs[i] {
|
||
match = false
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if !match {
|
||
return false, false, &argMismatch{gotten: args, expected: requiredArgs}
|
||
}
|
||
|
||
return false, false, nil
|
||
}
|
||
|
||
// Validate validates a scriptlet by compiling it and checking the presence of required and optional functions.
|
||
func Validate(compiler func(string, string) (*starlark.Program, error), programName string, src string, scriptletFunctions Declaration) error {
|
||
// Try to compile the program.
|
||
prog, err := compiler(programName, src)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
thread := &starlark.Thread{Name: programName}
|
||
globals, err := prog.Init(thread, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
globals.Freeze()
|
||
|
||
var missingFuns []string
|
||
mistypedFuns := make(map[scriptletFunction]string)
|
||
mismatchingFuns := make(map[scriptletFunction]*argMismatch)
|
||
errorsFound := false
|
||
for fun, requiredArgs := range scriptletFunctions {
|
||
funv := globals[fun.name]
|
||
missing, mistyped, mismatch := validateFunction(funv, requiredArgs)
|
||
|
||
if missing && !fun.optional || mistyped || mismatch != nil {
|
||
errorsFound = true
|
||
if missing {
|
||
missingFuns = append(missingFuns, fun.name)
|
||
} else if mistyped {
|
||
mistypedFuns[fun] = funv.Type()
|
||
} else {
|
||
mismatchingFuns[fun] = mismatch
|
||
}
|
||
}
|
||
}
|
||
|
||
// Return early if everything looks good.
|
||
if !errorsFound {
|
||
return nil
|
||
}
|
||
|
||
errorText := ""
|
||
sentences := 0
|
||
|
||
// String builder to format pretty error messages.
|
||
appendToError := func(text string) {
|
||
var link string
|
||
|
||
switch sentences {
|
||
case 0:
|
||
link = ""
|
||
case 1:
|
||
link = "; additionally, "
|
||
default:
|
||
link = "; finally, "
|
||
}
|
||
|
||
errorText += link
|
||
errorText += text
|
||
sentences++
|
||
}
|
||
|
||
switch len(missingFuns) {
|
||
case 0:
|
||
case 1:
|
||
appendToError(fmt.Sprintf("the function %q is required but has not been found in the scriptlet", missingFuns[0]))
|
||
default:
|
||
appendToError(fmt.Sprintf("the functions %q are required but have not been found in the scriptlet", missingFuns))
|
||
}
|
||
|
||
if len(mistypedFuns) != 0 {
|
||
var parts []string
|
||
for fun, ty := range mistypedFuns {
|
||
parts = append(parts, fmt.Sprintf("%q should define the scriptlet’s %s function of the same name (found a value of type %s instead)", fun.name, optionalToString(fun.optional), ty))
|
||
}
|
||
|
||
appendToError(strings.Join(parts, ", "))
|
||
}
|
||
|
||
if len(mismatchingFuns) != 0 {
|
||
var parts []string
|
||
for fun, args := range mismatchingFuns {
|
||
parts = append(parts, fmt.Sprintf("the %s function %q defines arguments %q (expected %q)", optionalToString(fun.optional), fun.name, args.gotten, args.expected))
|
||
}
|
||
|
||
appendToError(strings.Join(parts, ", "))
|
||
}
|
||
|
||
return errors.New(errorText)
|
||
}
|
||
|
||
// Set compiles a scriptlet into memory. If empty src is provided the current program is deleted.
|
||
func (l *Loader) Set(compiler func(string, string) (*starlark.Program, error), programName string, src string) error {
|
||
if src == "" {
|
||
l.programsMu.Lock()
|
||
delete(l.programs, programName)
|
||
l.programsMu.Unlock()
|
||
} else {
|
||
prog, err := compiler(programName, src)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
l.programsMu.Lock()
|
||
l.programs[programName] = prog
|
||
l.programsMu.Unlock()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Program returns a precompiled scriptlet program.
|
||
func (l *Loader) Program(name string, programName string) (*starlark.Program, *starlark.Thread, error) {
|
||
l.programsMu.Lock()
|
||
prog, found := l.programs[programName]
|
||
l.programsMu.Unlock()
|
||
if !found {
|
||
return nil, nil, fmt.Errorf("%s scriptlet not loaded", name)
|
||
}
|
||
|
||
thread := &starlark.Thread{Name: programName}
|
||
|
||
return prog, thread, nil
|
||
}
|