1set/starlet

View on GitHub
module.go

Summary

Maintainability
A
1 hr
Test Coverage
package starlet

import (
    "errors"
    "io"
    "io/fs"
    "sort"
    "strings"

    "github.com/1set/starlight/convert"
    "go.starlark.net/starlark"
    "go.starlark.net/starlarkstruct"
)

// ModuleLoader is a function that loads a Starlark module and returns the module's string dict.
type ModuleLoader func() (starlark.StringDict, error)

// NamedModuleLoader is a function that loads a Starlark module with the given name and returns the module's string dict.
// If the module is not found, it returns nil as the first and second return value.
type NamedModuleLoader func(string) (starlark.StringDict, error)

// ModuleLoaderList is a list of Starlark module loaders, usually used to load a list of modules in order.
type ModuleLoaderList []ModuleLoader

// Clone returns a copy of the list.
func (l ModuleLoaderList) Clone() []ModuleLoader {
    return append([]ModuleLoader{}, l...)
}

// LoadAll loads all modules in the list into the given StringDict.
// It returns an error as second return value if any module fails to load.
func (l ModuleLoaderList) LoadAll(d starlark.StringDict) error {
    if d == nil {
        return errorStarletErrorf(`load`, "cannot load modules into nil dict")
    }
    for _, ld := range l {
        if ld == nil {
            return errorStarletErrorf(`load`, "nil module loader")
        }
        m, err := ld()
        if err != nil {
            return errorStarletError(`load`, err)
        }
        if m != nil {
            for k, v := range m {
                d[k] = v
            }
        }
    }
    return nil
}

// MakeBuiltinModuleLoaderList creates a list of module loaders from a list of module names.
// It returns an error as second return value if any module is not found.
func MakeBuiltinModuleLoaderList(names ...string) (ModuleLoaderList, error) {
    ld := make(ModuleLoaderList, len(names))
    for i, name := range names {
        ld[i] = allBuiltinModules[name]
        if ld[i] == nil {
            return ld, errorStarletErrorf(`make`, "module not found: %s", name)
        }
    }
    return ld, nil
}

// ModuleLoaderMap is a map of Starlark module loaders, usually used to load a map of modules by name.
type ModuleLoaderMap map[string]ModuleLoader

// Clone returns a copy of the map.
func (m ModuleLoaderMap) Clone() ModuleLoaderMap {
    clone := make(map[string]ModuleLoader, len(m))
    for k, v := range m {
        clone[k] = v
    }
    return clone
}

// Keys returns the keys of the map, sorted in ascending order of the keys.
func (m ModuleLoaderMap) Keys() []string {
    keys := make([]string, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    return keys
}

// Values returns the elements of the map, sorted in ascending order of the keys.
func (m ModuleLoaderMap) Values() []ModuleLoader {
    keys := m.Keys()
    values := make([]ModuleLoader, 0, len(keys))
    for _, k := range keys {
        values = append(values, m[k])
    }
    return values
}

// Merge merges the given map into the map. It does nothing if the current map is nil.
func (m ModuleLoaderMap) Merge(other ModuleLoaderMap) {
    if m == nil {
        return
    }
    for k := range other {
        m[k] = other[k]
    }
}

// GetLazyLoader returns a lazy loader that loads the module with the given name.
// It returns an error as second return value if the module is found but fails to load.
// Otherwise, the first return value is nil if the module is not found.
// Note that the loader is usually used by the Starlark thread, so that the errors should not be wrapped.
func (m ModuleLoaderMap) GetLazyLoader() NamedModuleLoader {
    return func(s string) (starlark.StringDict, error) {
        // if the map or the name is empty, just return nil to indicate not found
        if m == nil || s == "" {
            return nil, nil
        }
        // attempt to find the module
        ld, ok := m[s]
        if !ok {
            // not found
            return nil, nil
        } else if ld == nil {
            // found but nil
            return nil, errors.New("nil module loader")
        }
        // try to load it
        d, err := ld()
        if err != nil {
            // failed to load
            return nil, err
        }
        // extract all members of module from dict like `{name: module}` or `{name: struct}`
        if len(d) == 1 {
            m, found := d[s]
            if found {
                if mm, ok := m.(*starlarkstruct.Module); ok && mm != nil {
                    return mm.Members, nil
                } else if sm, ok := m.(*starlarkstruct.Struct); ok && sm != nil {
                    sd := make(starlark.StringDict)
                    sm.ToStringDict(sd)
                    return sd, nil
                }
            }
        }
        // otherwise, just return the dict
        return d, nil
    }
}

// MakeBuiltinModuleLoaderMap creates a map of module loaders from a list of module names.
// It returns an error as second return value if any module is not found.
func MakeBuiltinModuleLoaderMap(names ...string) (ModuleLoaderMap, error) {
    ld := make(ModuleLoaderMap, len(names))
    for _, name := range names {
        ld[name] = allBuiltinModules[name]
        if ld[name] == nil {
            return ld, errorStarletErrorf(`make`, "module not found: %s", name)
        }
    }
    return ld, nil
}

// MakeModuleLoaderFromStringDict creates a module loader from the given string dict.
func MakeModuleLoaderFromStringDict(d starlark.StringDict) ModuleLoader {
    return func() (starlark.StringDict, error) {
        return d, nil
    }
}

// MakeModuleLoaderFromMap creates a module loader from the given map, it converts the map to a string dict when loading.
func MakeModuleLoaderFromMap(m StringAnyMap) ModuleLoader {
    return func() (starlark.StringDict, error) {
        dict, err := convert.MakeStringDict(m)
        if err != nil {
            return nil, err
        }
        return dict, nil
    }
}

// MakeModuleLoaderFromString creates a module loader from the given source code.
func MakeModuleLoaderFromString(name, source string, predeclared starlark.StringDict) ModuleLoader {
    return func() (starlark.StringDict, error) {
        if name == "" {
            name = "load.star"
        }
        return starlark.ExecFile(&starlark.Thread{}, name, source, predeclared)
    }
}

// MakeModuleLoaderFromReader creates a module loader from the given IO reader.
func MakeModuleLoaderFromReader(name string, rd io.Reader, predeclared starlark.StringDict) ModuleLoader {
    return func() (starlark.StringDict, error) {
        if name == "" {
            name = "load.star"
        }
        return starlark.ExecFile(&starlark.Thread{}, name, rd, predeclared)
    }
}

// MakeModuleLoaderFromFile creates a module loader from the given file.
func MakeModuleLoaderFromFile(name string, fileSys fs.FS, predeclared starlark.StringDict) ModuleLoader {
    return func() (starlark.StringDict, error) {
        // read file content
        b, err := readScriptFile(name, fileSys)
        if err != nil {
            return nil, err
        }
        // execute file
        return starlark.ExecFile(&starlark.Thread{}, name, b, predeclared)
    }
}

// readScriptFile reads a script file from the given file system.
// No need to wrap errors because they are usually used by the Starlark thread.
func readScriptFile(name string, fileSys fs.FS) ([]byte, error) {
    // precondition checks
    if name == "" {
        return nil, errors.New("no file name given")
    }
    if fileSys == nil {
        return nil, errors.New("no file system given")
    }

    // if file name does not end with ".star", append it
    if !strings.HasSuffix(name, ".star") {
        name += ".star"
    }

    // open file
    f, err := fileSys.Open(name)
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = f.Close()
    }()

    // read
    return io.ReadAll(f)
}