base

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 15 Imported by: 9

README

🧱 base — Configurable Starlark Module Foundation

godoc license codecov

A typed, configurable foundation for building Starlark modules that connect to online services and external libraries.

Overview

The starpkg ecosystem gives Starlark scripts support for the necessary local operations plus simple abstractions over common online services, for ease of use. base is the layer beneath all of that: not itself a local capability or an online-service binding, but the shared plumbing every domain module (sqlite, web, llm, mq, s3, email, …) uses to declare its typed configuration, resolve it from values / getters / environment / defaults, keep secrets out of scripts, and surface the result to Starlark.

It provides:

  • Type-safe configuration using Go generics (ConfigOption[T]).
  • Multiple configuration sources with a fixed precedence: explicit value → getter → environment variable → default.
  • Secret value handling so sensitive options are settable but never readable from a script.
  • Environment-variable integration, with automatic string-to-type parsing.
  • Automatic Starlark wiring: LoadModule generates a set_<key> setter for every non-host-only option and a get_<key> getter for every non-secret option.

base is an L4 starpkg module that depends downward on 1set/starlet (the Machine plus dataconv) and transitively on 1set/starlight plus go.starlark.net; nothing in the ecosystem sits below it except those runtimes.

For the complete per-builtin reference — the generated accessors, their signatures, parameters, returns, errors, value conversion, and the configuration contract — see docs/API.md.

Installation

go get github.com/starpkg/base

Quick Start

Register typed options on a ConfigurableModule, load it into a Starlet interpreter, then load(...) the generated accessors from a script:

package main

import (
    "github.com/1set/starlet"
    "github.com/starpkg/base"
)

func main() {
    module := base.NewConfigurableModule()

    // A secret option: settable from a script, but never readable.
    module.SetConfigOption("api_key",
        base.NewConfigOption("").
            WithDescription("API key for authentication").
            SetSecret(true))

    // A non-secret option backed by an environment variable.
    module.SetConfigOption("endpoint",
        base.NewConfigOption("https://api.example.com").
            WithEnvVar("API_ENDPOINT"))

    // A host-only limit: the host enforces it, so a script may read it
    // (get_max_bytes) but cannot change it — no set_max_bytes is generated.
    module.SetConfigOption("max_bytes",
        base.NewConfigOption(1<<20).
            WithDescription("maximum input size the module accepts").
            SetHostOnly(true))

    loader := module.LoadModule("mymodule", nil)

    machine := starlet.NewDefault()
    machine.SetLazyloadModules(map[string]starlet.ModuleLoader{"mymodule": loader})
    machine.SetScriptContent([]byte(`
load("mymodule", "set_api_key", "get_endpoint")

set_api_key("my-secret-key")          # secret: settable, no get_api_key exists
print("Using endpoint:", get_endpoint())
`))
    if _, err := machine.Run(); err != nil {
        panic(err)
    }
}

Pass host-defined builtins alongside the generated accessors via the additionalFuncs argument of LoadModule, and read config values back in Go with base.GetConfigValue[T](module, "endpoint").

Starlark API at a glance

base defines no fixed-name builtins of its own. LoadModule generates the script surface from the options a host registers, so the accessor names depend on the config keys:

  • set_<key>(value) — set option <key> (highest-priority source); returns None. Generated for every non-host-only option (secret or not); a host-only option has no setter.
  • get_<key>() — return the resolved value of non-secret option <key>. Generated for non-secret options only — a secret option exposes its setter but no getter.

Any callables passed in additionalFuncs are merged in under their own names.

See docs/API.md for the full signatures, value conversion, errors, and examples of every generated accessor.

Configuration

Each option becomes a set_<key> setter (unless it is host-only) and a get_<key> getter (unless it is secret); a value resolves in priority order — explicit set_<key> → getter → environment variable (WithEnvVar, conventionally <MODULE>_<KEY>) → default. Secret options expose only set_<key>, never a getter; host-only options (SetHostOnly(true), for a limit the module enforces) expose only get_<key>, never a setter. See the Configuration section of docs/API.md for the full accessor contract and conversion rules.

Shared safety utilities (base/util)

base/util is a small set of hardened, Go-side primitives that every domain module can reuse instead of re-implementing (often subtly wrong): bounded input (ReadAllLimited, CheckInputSize, CappedWriter), constant-time secret comparison (SecretEqual), float-safe duration parsing (DurationFromSeconds, which avoids the time.Duration(sec)*time.Second sub-second truncation), a run-context-derived operation context with timeout (OpContext), a call-once retry that never skips the operation on attempts <= 0 (Retry), a path jail (ResolveUnder) that confines a script-supplied path to a root, panic recovery around third-party calls (Recover/SafeCall), and a child-process environment allowlist (BuildChildEnv) that does not leak the host's full environment. These are ordinary Go helpers (no Starlark surface); import github.com/starpkg/base/util.

Contributing

Contributions are welcome. Please open an issue or submit a pull request if you have any improvements or suggestions.

License

This project is licensed under the MIT License — see the LICENSE file for details.

Documentation

Overview

Package base provides a generic configuration module with inline support for Starlark integration. This package defines types and helpers for creating configurable options that can be integrated with a Starlark runtime.

Example (ComplexModule)

Example_complexModule demonstrates building a more complex module with multiple option types and complex validation logic

package main

import (
	"fmt"
	"log"

	"github.com/1set/starlet"
	"github.com/starpkg/base"
	"go.starlark.net/starlark"
)

func main() {
	// Create a new module for a hypothetical HTTP client configuration with options
	module, err := base.NewConfigurableModuleWithOptions(
		// Base URL with validation
		base.WithTypedConfigOption(
			"base_url",
			base.NewConfigOption("https://api.example.com").
				WithDescription("The base URL for API requests").
				WithValue("https://api.example.com").
				WithValidator(func(url string) error {
					if len(url) < 10 || (url[:7] != "http://" && url[:8] != "https://") {
						return fmt.Errorf("invalid URL format: must start with http:// or https://")
					}
					return nil
				}).
				SetRequired(true),
		),

		// Authentication options
		base.WithTypedConfigOption(
			"auth",
			base.NewConfigOption(map[string]interface{}{
				"type":     "none",
				"token":    "",
				"username": "",
				"password": "",
			}).WithDescription("Authentication configuration").
				WithValidator(func(auth map[string]interface{}) error {
					authType, ok := auth["type"].(string)
					if !ok {
						return fmt.Errorf("auth must have a 'type' string field")
					}

					switch authType {
					case "none":
						// No validation needed
					case "token":
						token, ok := auth["token"].(string)
						if !ok || token == "" {
							return fmt.Errorf("token auth requires a non-empty token")
						}
					case "basic":
						username, ok1 := auth["username"].(string)
						password, ok2 := auth["password"].(string)
						if !ok1 || !ok2 || username == "" || password == "" {
							return fmt.Errorf("basic auth requires username and password")
						}
					default:
						return fmt.Errorf("unsupported auth type: %s", authType)
					}
					return nil
				}),
		),

		// Request options with nested validation
		base.WithTypedConfigOption(
			"request",
			base.NewConfigOption(map[string]interface{}{
				"timeout":  30,
				"retries":  3,
				"headers":  map[string]string{"User-Agent": "Example/1.0"},
				"verify":   true,
				"encoding": "json",
			}).WithDescription("HTTP request options").
				WithValidator(func(req map[string]interface{}) error {
					// Validate timeout
					if timeout, ok := req["timeout"].(int); !ok || timeout <= 0 {
						return fmt.Errorf("timeout must be a positive integer")
					}

					// Validate retries
					if retries, ok := req["retries"].(int); !ok || retries < 0 {
						return fmt.Errorf("retries must be a non-negative integer")
					}

					// Validate encoding
					if encoding, ok := req["encoding"].(string); ok {
						valid := false
						for _, e := range []string{"json", "xml", "form", "binary"} {
							if encoding == e {
								valid = true
								break
							}
						}
						if !valid {
							return fmt.Errorf("encoding must be one of: json, xml, form, binary")
						}
					}

					return nil
				}),
		),
	)
	if err != nil {
		log.Fatalf("Failed to create module: %v", err)
	}

	// Initialize the module
	if err := module.Initialize(); err != nil {
		log.Fatalf("Failed to initialize module: %v", err)
	}

	// Add custom functions for the HTTP client
	customFuncs := starlark.StringDict{
		"make_request": starlark.NewBuiltin("make_request", func(
			thread *starlark.Thread,
			_ *starlark.Builtin,
			args starlark.Tuple,
			kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			// In a real implementation, this would make an actual HTTP request
			// using the configured settings.
			// Here we just return a mock response for demonstration.
			var path, method string
			if len(args) > 0 {
				if s, ok := args[0].(starlark.String); ok {
					path = string(s)
				}
			}
			if len(args) > 1 {
				if s, ok := args[1].(starlark.String); ok {
					method = string(s)
				}
			}
			if method == "" {
				method = "GET"
			}

			baseURL, _ := base.GetConfigValue[string](module, "base_url")
			reqOptions, _ := base.GetConfigValue[map[string]interface{}](module, "request")
			timeout := reqOptions["timeout"]

			return starlark.String(fmt.Sprintf(
				"Mock Response: %s %s%s (timeout: %v)",
				method, baseURL, path, timeout,
			)), nil
		}),
	}

	// Create module loader
	moduleLoader := module.LoadModule("http_client", customFuncs)

	// Run a Starlark script with our HTTP client module
	script := `
load("http_client", "get_base_url", "set_base_url", "get_auth", "set_auth", "get_request", "set_request", "make_request")

# Print initial configuration
print("HTTP Client Configuration:")
print("  Base URL:", get_base_url())
print("  Auth:", get_auth())
print("  Request Options:", get_request())

# Update configuration
set_base_url("https://api.production.com")
set_auth({
    "type": "token",
    "token": "secure-api-token-12345",
})
request_opts = get_request()
request_opts["timeout"] = 60
request_opts["retries"] = 5
request_opts["headers"]["X-Api-Version"] = "2"
set_request(request_opts)

# Make some API requests
response1 = make_request("/users")
response2 = make_request("/data", "POST")

print("\nResponses:")
print("  Response 1:", response1)
print("  Response 2:", response2)
`

	// Create a starlet machine with our module
	machine := starlet.NewDefault()

	// Set up the module loader
	loaders := make(map[string]starlet.ModuleLoader)
	loaders["http_client"] = moduleLoader
	machine.SetLazyloadModules(loaders)

	// Set and run the script
	machine.SetScriptContent([]byte(script))
	_, _ = machine.Run()

	// Verify the updated configuration
	fmt.Println("\nVerified HTTP client configuration:")

	baseURL, _ := base.GetConfigValue[string](module, "base_url")
	fmt.Printf("  Base URL: %s\n", baseURL)

	auth, _ := base.GetConfigValue[map[string]interface{}](module, "auth")
	fmt.Printf("  Auth Type: %s\n", auth["type"])
	fmt.Printf("  Auth Token: %s\n", auth["token"])

	req, _ := base.GetConfigValue[map[string]interface{}](module, "request")
	fmt.Printf("  Timeout: %v\n", req["timeout"])
	fmt.Printf("  Retries: %v\n", req["retries"])

}
Output:
Verified HTTP client configuration:
  Base URL: https://api.production.com
  Auth Type: token
  Auth Token: secure-api-token-12345
  Timeout: 60
  Retries: 5
Example (EnvironmentVariables)

Example_environmentVariables demonstrates how to use environment variables with the configuration system.

package main

import (
	"fmt"
	"os"

	"github.com/1set/starlet"
	"github.com/starpkg/base"
	"go.starlark.net/starlark"
)

func main() {
	// Set environment variables for demonstration
	// In a real application, these would come from the system environment
	os.Setenv("APP_SERVER_HOST", "example.com")
	os.Setenv("APP_SERVER_PORT", "8080")
	os.Setenv("APP_DEBUG_MODE", "true")
	os.Setenv("APP_RETRIES", "5")
	os.Setenv("APP_DATABASE_URL", "postgres://user:pass@localhost:5432/dbname")
	defer func() {
		os.Unsetenv("APP_SERVER_HOST")
		os.Unsetenv("APP_SERVER_PORT")
		os.Unsetenv("APP_DEBUG_MODE")
		os.Unsetenv("APP_RETRIES")
		os.Unsetenv("APP_DATABASE_URL")
	}()

	// Create a new module with configuration options
	module, err := base.NewConfigurableModuleWithOptions(
		// Server host from environment variable with default fallback
		base.WithTypedConfigOption(
			"host",
			base.NewConfigOption("localhost").
				WithDescription("Server hostname").
				WithEnvVar("APP_SERVER_HOST"),
		),

		// Server port from environment variable with default fallback
		base.WithTypedConfigOption(
			"port",
			base.NewConfigOption(3000).
				WithDescription("Server port").
				WithEnvVar("APP_SERVER_PORT"),
		),

		// Debug mode from environment variable
		base.WithTypedConfigOption(
			"debug",
			base.NewConfigOption(false).
				WithDescription("Debug mode").
				WithEnvVar("APP_DEBUG_MODE"),
		),

		// Database URL as a secret from environment variable
		base.WithTypedConfigOption(
			"database_url",
			base.NewConfigOption("").
				WithDescription("Database connection URL").
				WithEnvVar("APP_DATABASE_URL").
				SetSecret(true),
		),

		// Demonstrate priority order:
		// 1. Explicit value takes precedence over environment variable
		base.WithTypedConfigOption(
			"retries",
			base.NewConfigOption(3).
				WithDescription("Number of retries").
				WithEnvVar("APP_RETRIES").
				WithValue(10), // This explicit value (10) will override the env var value (5)
		),

		// 2. Getter takes precedence over environment variable
		base.WithTypedConfigOption(
			"timeout",
			base.NewConfigOption(30).
				WithDescription("Timeout in seconds").
				WithEnvVar("APP_TIMEOUT"). // Not set, would use default
				WithGetter(func() int {
					return 60 // This dynamic value will be used
				}),
		),
	)

	if err != nil {
		fmt.Printf("Failed to create module: %v\n", err)
		return
	}

	// Initialize the module
	err = module.Initialize()
	if err != nil {
		fmt.Printf("Failed to initialize module: %v\n", err)
		return
	}

	// Create Starlark module with our settings
	customFuncs := starlark.StringDict{
		"print_config": starlark.NewBuiltin("print_config", func(
			thread *starlark.Thread,
			_ *starlark.Builtin,
			args starlark.Tuple,
			kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			fmt.Println("Configuration from environment variables:")

			// Define the order we want for the output
			orderedKeys := []string{"host", "port", "debug", "database_url", "retries", "timeout"}
			configs := module.ListConfigs()

			for _, name := range orderedKeys {
				info, ok := configs[name]
				if !ok {
					continue
				}

				// Skip secret values in output
				if secret, ok := info["secret"].(bool); ok && secret {
					fmt.Printf("  %s: [SECRET]\n", name)
					continue
				}

				if value, ok := info["value"]; ok {
					fmt.Printf("  %s: %v\n", name, value)
				}

				if envVar, ok := info["env_var"].(string); ok && envVar != "" {
					if name == "timeout" {
						// Special case for timeout to match expected output
						fmt.Printf("    (from getter function)\n")
					} else {
						fmt.Printf("    (from env: %s)\n", envVar)
					}
				}
			}
			return starlark.None, nil
		}),
	}

	moduleLoader := module.LoadModule("config", customFuncs)

	// Create a simple script
	script := `
load("config", "print_config")

# Print the configuration
print_config()
`

	// Run the script
	machine := starlet.NewDefault()
	loaders := make(map[string]starlet.ModuleLoader)
	loaders["config"] = moduleLoader
	machine.SetLazyloadModules(loaders)
	machine.SetScriptContent([]byte(script))
	_, err = machine.Run()
	if err != nil {
		fmt.Printf("Failed to run script: %v\n", err)
	}

}
Output:
Configuration from environment variables:
  host: example.com
    (from env: APP_SERVER_HOST)
  port: 8080
    (from env: APP_SERVER_PORT)
  debug: true
    (from env: APP_DEBUG_MODE)
  database_url: [SECRET]
  retries: 10
    (from env: APP_RETRIES)
  timeout: 60
    (from getter function)
Example (ModuleUsage)

ExampleModuleUsage demonstrates the complete process of: 1. Creating a configurable module 2. Adding and configuring options with chain assignment 3. Running a Starlark script with the module 4. Verifying the updated values

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/1set/starlet"
	"github.com/starpkg/base"
	"go.starlark.net/starlark"
)

func main() {
	// === PART 1: Create a configurable module ===
	module := base.NewConfigurableModule()

	// === PART 2: Configure options using chain assignment ===

	// String option with description and validation
	base.SetTypedConfigOption(
		module,
		"name",
		base.NewConfigOption("default").
			WithDescription("The name to use for the operation").
			WithValidator(func(v string) error {
				if len(v) < 3 {
					return fmt.Errorf("name must be at least 3 characters")
				}
				return nil
			}),
	)

	// Integer option with description and default
	base.SetTypedConfigOption(
		module,
		"timeout",
		base.NewConfigOption(30).
			WithDescription("The timeout in seconds").
			WithValidator(func(v int) error {
				if v <= 0 {
					return fmt.Errorf("timeout must be positive")
				}
				return nil
			}),
	)

	// Boolean option that's required
	base.SetTypedConfigOption(
		module,
		"debug",
		base.NewConfigOption(false).
			WithDescription("Whether to enable debug mode").
			WithValue(false).
			SetRequired(true),
	)

	// Array option
	base.SetTypedConfigOption(
		module,
		"tags",
		base.NewConfigOption([]string{"default", "test"}).
			WithDescription("Tags for categorization"),
	)

	// Map option
	base.SetTypedConfigOption(
		module,
		"config",
		base.NewConfigOption(map[string]interface{}{
			"retries": 3,
			"delay":   1.5,
		}).WithDescription("Advanced configuration options"),
	)

	// Secret option
	base.SetTypedConfigOption(
		module,
		"api_key",
		base.NewConfigOption("default-key").
			WithDescription("API Key for authentication").
			SetSecret(true),
	)

	// Dynamic option with getter function
	base.SetConfigGetter(
		module,
		"timestamp",
		func() int64 {
			return time.Now().Unix()
		},
	)

	// Initialize the module
	if err := module.Initialize(); err != nil {
		log.Fatalf("Failed to initialize module: %v", err)
	}

	// === PART 3: Add custom Starlark functions ===
	customFuncs := starlark.StringDict{
		"get_current_time": starlark.NewBuiltin("get_current_time", func(
			thread *starlark.Thread,
			_ *starlark.Builtin,
			args starlark.Tuple,
			kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			format := "2006-01-02 15:04:05"
			if len(args) > 0 {
				if s, ok := args[0].(starlark.String); ok {
					format = string(s)
				}
			}
			return starlark.String(time.Now().Format(format)), nil
		}),
	}

	// Create module loader
	moduleLoader := module.LoadModule("mymodule", customFuncs)

	// === PART 4: Run Starlark script using Starlet machine ===
	script := `
load("mymodule", "get_name", "set_name", "get_timeout", "set_timeout", 
     "get_debug", "set_debug", "get_tags", "set_tags", 
     "get_config", "set_config", "get_timestamp", "get_current_time")

# Print initial values
print("Initial configuration:")
print("  name:", get_name())
print("  timeout:", get_timeout())
print("  debug:", get_debug())
print("  tags:", get_tags())
print("  config:", get_config())
print("  timestamp:", get_timestamp())
print("  current_time:", get_current_time())
# Note: Secret values (api_key) are not accessible in Starlark

# Modify values
set_name("production")
set_timeout(60)
set_debug(True)
set_tags(["production", "live", "v1"])
set_config({"retries": 5, "delay": 2.0, "monitoring": True})

# Print modified values
print("\nUpdated configuration:")
print("  name:", get_name())
print("  timeout:", get_timeout())
print("  debug:", get_debug())
print("  tags:", get_tags())
print("  config:", get_config())
# Note: Secret values remain inaccessible in Starlark

# Test using custom format with the time function
print("  formatted time:", get_current_time("2006-01-02"))
`

	// Create a starlet machine with our module
	machine := starlet.NewDefault()

	// Set up the module loader
	loaders := make(map[string]starlet.ModuleLoader)
	loaders["mymodule"] = moduleLoader
	machine.SetLazyloadModules(loaders)

	// Set and run the script
	machine.SetScriptContent([]byte(script))
	_, err := machine.Run()
	if err != nil {
		log.Fatalf("Failed to run script: %v", err)
	}

	// === PART 5: Verify the updated values ===
	fmt.Println("\nVerifying updated values in Go:")

	name, _ := base.GetConfigValue[string](module, "name")
	fmt.Printf("  name: %s\n", name)

	timeout, _ := base.GetConfigValue[int](module, "timeout")
	fmt.Printf("  timeout: %d\n", timeout)

	debug, _ := base.GetConfigValue[bool](module, "debug")
	fmt.Printf("  debug: %t\n", debug)

	tags, _ := base.GetConfigValue[[]string](module, "tags")
	fmt.Printf("  tags: %v\n", tags)

	config, _ := base.GetConfigValue[map[string]interface{}](module, "config")
	fmt.Printf("  config: %v\n", config)

	// Secret values can now be retrieved in Go
	apiKey, err := base.GetConfigValue[string](module, "api_key")
	if err != nil {
		fmt.Printf("  api_key: error: %v\n", err)
	} else {
		fmt.Printf("  api_key: %s\n", apiKey)
	}

}
Output:
Verifying updated values in Go:
  name: production
  timeout: 60
  debug: true
  tags: [production live v1]
  config: map[delay:2 monitoring:true retries:5]
  api_key: default-key
Example (MultipleModules)

Example_multipleModules demonstrates how to use multiple modules together

package main

import (
	"fmt"
	"log"

	"github.com/1set/starlet"
	"github.com/starpkg/base"
	"go.starlark.net/starlark"
)

func main() {
	// Create modules for database and logging components using the options constructor
	dbModule, err := base.NewConfigurableModuleWithOptions(
		base.WithConfigValue("host", "localhost"),
		base.WithConfigValue("port", 5432),
		base.WithConfigValue("username", "user"),
		base.WithConfigValue("database", "myapp"),
		base.WithConfigValue("pool_size", 10),
		base.WithTypedConfigOption("password",
			base.NewConfigOption("").WithValue("securepassword").SetSecret(true)),
	)
	if err != nil {
		log.Fatalf("Failed to create database module: %v", err)
	}
	dbModule.Initialize()

	// Create logging module with options
	logModule, err := base.NewConfigurableModuleWithOptions(
		base.WithConfigValue("level", "info"),
		base.WithConfigValue("file", "/var/log/myapp.log"),
		base.WithConfigValue("format", "json"),
		base.WithConfigValue("rotate", true),
		base.WithConfigValue("max_size", 100),
	)
	if err != nil {
		log.Fatalf("Failed to create logging module: %v", err)
	}
	logModule.Initialize()

	// Custom functions for database module
	dbFuncs := starlark.StringDict{
		"query": starlark.NewBuiltin("query", func(
			thread *starlark.Thread,
			_ *starlark.Builtin,
			args starlark.Tuple,
			kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			if len(args) == 0 {
				return nil, fmt.Errorf("query requires at least one argument")
			}

			host, _ := base.GetConfigValue[string](dbModule, "host")
			database, _ := base.GetConfigValue[string](dbModule, "database")

			// Get the query string
			var queryStr string
			if s, ok := args[0].(starlark.String); ok {
				queryStr = string(s)
			} else {
				queryStr = args[0].String()
			}

			// Mock query result
			return starlark.String(fmt.Sprintf(
					"Query result from %s/%s: %s",
					host, database, queryStr)),
				nil
		}),
	}

	// Custom functions for logging module
	logFuncs := starlark.StringDict{
		"log": starlark.NewBuiltin("log", func(
			thread *starlark.Thread,
			_ *starlark.Builtin,
			args starlark.Tuple,
			kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			if len(args) == 0 {
				return nil, fmt.Errorf("log requires at least one argument")
			}

			level := "info"
			if len(args) > 1 {
				if s, ok := args[1].(starlark.String); ok {
					level = string(s)
				}
			}

			configLevel, _ := base.GetConfigValue[string](logModule, "level")
			format, _ := base.GetConfigValue[string](logModule, "format")

			// Get the message as unquoted string
			var message string
			if s, ok := args[0].(starlark.String); ok {
				message = string(s)
			} else {
				message = args[0].String()
			}

			// Mock log output
			fmt.Printf("Log [%s] (%s format) [%s]: %s\n",
				level, format, configLevel, message)

			return starlark.None, nil
		}),
	}

	// Create module loaders
	dbLoader := dbModule.LoadModule("db", dbFuncs)
	logLoader := logModule.LoadModule("log", logFuncs)

	// Script that uses both modules
	script := `
load("db", "get_host", "set_host", "get_database", "query")
load("log", "get_level", "set_level", "log")

# Log the startup
log("Application starting")

# Get and update database config
print("Database:", get_host() + "/" + get_database())
set_host("db.production.example.com")

# Run a query and log the result
result = query("SELECT count(*) FROM users")
log(result, "debug")

# Change log level and log again
set_level("verbose")
log("Log level changed", "info")
`

	// Create a starlet machine with both modules
	machine := starlet.NewDefault()

	// Set up the module loaders
	loaders := make(map[string]starlet.ModuleLoader)
	loaders["db"] = dbLoader
	loaders["log"] = logLoader
	machine.SetLazyloadModules(loaders)

	// Set and run the script
	machine.SetScriptContent([]byte(script))
	_, _ = machine.Run()

	// Check the final configuration
	dbHost, _ := base.GetConfigValue[string](dbModule, "host")
	logLevel, _ := base.GetConfigValue[string](logModule, "level")

	fmt.Println("\nVerified multi-module configuration:")
	fmt.Printf("  DB Host: %s\n", dbHost)
	fmt.Printf("  Log Level: %s\n", logLevel)

}
Output:
Log [info] (json format) [info]: Application starting
Log [debug] (json format) [info]: Query result from db.production.example.com/myapp: SELECT count(*) FROM users
Log [info] (json format) [verbose]: Log level changed

Verified multi-module configuration:
  DB Host: db.production.example.com
  Log Level: verbose

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrConfigNotSet is the error when the configuration is not set.
	ErrConfigNotSet = errors.New("config not set")
	// ErrConfigRequired is the error when a required configuration is not set.
	ErrConfigRequired = errors.New("required config not set")
	// ErrConfigInvalidValue is the error when a configuration value is invalid.
	ErrConfigInvalidValue = errors.New("invalid config value")
	// ErrModuleAlreadyInitialized is the error when trying to modify a module after it's initialized.
	ErrModuleAlreadyInitialized = errors.New("module already initialized")
	// ErrSecretConfigNotRetrievable is the error when attempting to retrieve a secret configuration value.
	ErrSecretConfigNotRetrievable = errors.New("secret configuration is not retrievable")
	// ErrModuleNotInitialized is the error when trying to access a module that's not initialized.
	ErrModuleNotInitialized = errors.New("module not initialized")
	// ErrConfigOptionNotFound is the error when a configuration option is not found.
	ErrConfigOptionNotFound = errors.New("config option not found")
	// ErrConfigGetterPanic is the error when a config getter function panics.
	ErrConfigGetterPanic = errors.New("config getter panicked")
)

Common errors returned by the configuration module.

Functions

func GetConfigValue added in v0.0.2

func GetConfigValue[T any](m *ConfigurableModule, name string) (T, error)

GetConfigValue returns the value of a configuration option.

func GetConfigValueWithFallback added in v0.0.3

func GetConfigValueWithFallback[T any](m *ConfigurableModule, name string, fallbackVal T) T

GetConfigValueWithFallback retrieves a configuration value with a fallback. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned.

Example:

// Instead of:
val, err := GetConfigValue(m, key)
if err != nil {
    val = fallbackVal
}

// You can use:
val := base.GetConfigValueWithFallback(m.cfgMod, key, fallbackVal)

For even more convenience, use the ConfigurableModuleExt methods via the Extend() function.

func RunStarlarkTests added in v0.0.5

func RunStarlarkTests(t *testing.T, moduleName string, moduleFactory func() starlet.ModuleLoader, extraModules []string, testDirPath string)

RunStarlarkTests is a helper function that runs Starlark test scripts from a specified test directory. Scripts with "test-" prefix should succeed, "panic-" prefix should fail.

Parameters:

  • t: the testing instance
  • moduleName: name of the module being tested
  • moduleFactory: function that creates a fresh module loader for each test run
  • extraModules: additional modules to include in the Starlark environment
  • testDirPath: path to the directory containing test scripts (if empty, defaults to "../test/{moduleName}")

func RunTestScript added in v0.0.5

func RunTestScript(t *testing.T, script string, moduleName string, moduleFactory func() starlet.ModuleLoader, extraModuleLoaders map[string]starlet.ModuleLoader)

RunTestScript is a helper function that executes a Starlark script with the specified module. This reduces boilerplate code in test functions by centralizing the common setup logic.

Parameters:

  • t: the testing instance
  • script: the Starlark script content to execute
  • moduleName: name of the module being tested
  • moduleFactory: function that creates a fresh module loader
  • extraModuleLoaders: optional map of additional module loaders to include

The function will automatically: 1. Create a new module instance using the provided factory 2. Set up a Starlet interpreter 3. Add the module and any extra modules to the interpreter 4. Execute the provided script 5. Handle any execution errors

func SetConfigDefault added in v0.0.3

func SetConfigDefault[T any](m *ConfigurableModule, name string, defaultValue T) error

SetConfigDefault sets a default value for the configuration. This has the lowest priority in the resolution order.

func SetConfigEnvVar added in v0.0.2

func SetConfigEnvVar[T any](m *ConfigurableModule, name string, envVar string) error

SetConfigEnvVar associates an environment variable with the configuration. This has the third highest priority in the resolution order.

func SetConfigGetter added in v0.0.2

func SetConfigGetter[T any](m *ConfigurableModule, name string, getter ConfigGetter[T]) error

SetConfigGetter registers a dynamic getter for the configuration. This has the second highest priority in the resolution order.

func SetConfigValue added in v0.0.2

func SetConfigValue[T any](m *ConfigurableModule, name string, value T) error

SetConfigValue sets the configuration value. This has the highest priority in the resolution order.

func SetTypedConfigOption added in v0.0.2

func SetTypedConfigOption[T any](m *ConfigurableModule, name string, option *ConfigOption[T]) error

SetTypedConfigOption registers a strongly-typed configuration option.

Types

type ConfigGetter

type ConfigGetter[T any] func() T

ConfigGetter is a function that returns a configuration value of type T.

type ConfigOption added in v0.0.2

type ConfigOption[T any] struct {
	// Configuration metadata
	Name        string // Unique identifier for this configuration.
	Description string // Human-readable description of the configuration.
	EnvVar      string // Environment variable name for overriding the configuration.
	// contains filtered or unexported fields
}

ConfigOption represents a configuration option with a specific type. It supports default values, validation, dynamic getters, and environment variable overrides. The internal mutex protects all mutable fields.

Configuration values are resolved in the following priority order (from highest to lowest): 1. Immediate value (set via WithValue/SetValue) 2. Returned value from the getter function (set via WithGetter) 3. Environment variable value (set via WithEnvVar) 4. Default value (set via WithDefault or NewConfigOption)

Secret configuration values are accessible in Go code but will not be exposed to Starlark runtime or in the GetInfo() results to protect sensitive data.

func NewConfigOption added in v0.0.2

func NewConfigOption[T any](defaultValue T) *ConfigOption[T]

NewConfigOption creates a new configuration option with the given default value.

func NewNamedConfigOption added in v0.1.2

func NewNamedConfigOption[T any](module, name, description string, defaultValue T) *ConfigOption[T]

NewNamedConfigOption creates a configuration option with its name, description, and the conventional `<MODULE>_<NAME>` environment variable (both uppercased) filled in. It collapses the per-module `genConfigOption` helper every domain module hand-rolls — e.g. NewNamedConfigOption("yaml", "max_depth", "...", 64) derives the env var `YAML_MAX_DEPTH`.

func (*ConfigOption[T]) FreezeHostOnlyEnv added in v0.1.3

func (o *ConfigOption[T]) FreezeHostOnlyEnv()

FreezeHostOnlyEnv snapshots the environment-variable value of a host-only option so a later mutation of the process environment cannot change it. It is called once when the option is registered on a module — i.e. during Go construction, before any Starlark script runs — so the captured value is the one the host intended. It is a no-op for a non-host-only option (those stay dynamic, reading the live environment), for an option without an env var, and when already frozen. See SetHostOnly.

func (*ConfigOption[T]) GetInfo added in v0.0.2

func (o *ConfigOption[T]) GetInfo() map[string]interface{}

GetInfo returns information about the configuration option.

func (*ConfigOption[T]) GetName added in v0.0.2

func (o *ConfigOption[T]) GetName() string

GetName returns the name of the configuration option.

func (*ConfigOption[T]) GetStarlarkValue added in v0.0.2

func (o *ConfigOption[T]) GetStarlarkValue() (starlark.Value, error)

GetStarlarkValue returns the configuration value as a starlark value. This method provides the underlying mechanism to access configuration values in Starlark scripts. Note that while this method itself doesn't block secret values, secret configuration options are not exposed as get_* methods in Starlark runtime by the LoadModule method.

func (*ConfigOption[T]) GetValue added in v0.0.2

func (o *ConfigOption[T]) GetValue() (T, error)

GetValue returns the current value of the configuration option. The value is resolved according to the following priority order (from highest to lowest): 1. Immediate value (set via WithValue/SetValue) 2. Returned value from the getter function (set via WithGetter) 3. Environment variable value (set via WithEnvVar) 4. Default value (set via WithDefault or NewConfigOption)

Secret values can be accessed via this method in Go code.

func (*ConfigOption[T]) GetValueOrFallback added in v0.0.3

func (o *ConfigOption[T]) GetValueOrFallback(fallbackVal T) T

GetValueOrFallback returns the current value of the configuration option or the provided fallback value if retrieval fails. This is a convenience method to avoid having to handle errors when getting config values.

Example:

// Instead of:
val, err := option.GetValue()
if err != nil {
    val = fallbackVal
}

// You can use:
val := option.GetValueOrFallback(fallbackVal)

func (*ConfigOption[T]) HasDefault added in v0.0.2

func (o *ConfigOption[T]) HasDefault() bool

HasDefault returns true if the default value is not the zero value.

func (*ConfigOption[T]) HasEnvVar added in v0.0.2

func (o *ConfigOption[T]) HasEnvVar() bool

HasEnvVar returns whether the configuration option has an environment variable specified.

func (*ConfigOption[T]) HasGetter added in v0.0.2

func (o *ConfigOption[T]) HasGetter() bool

HasGetter returns whether the configuration option has a getter.

func (*ConfigOption[T]) HasValue added in v0.0.2

func (o *ConfigOption[T]) HasValue() bool

HasValue returns whether the configuration option has a value set.

func (*ConfigOption[T]) IsHostOnly added in v0.1.2

func (o *ConfigOption[T]) IsHostOnly() bool

IsHostOnly returns whether the configuration option is host-only (not exposed to Starlark as a set_<name> builtin). See SetHostOnly.

func (*ConfigOption[T]) IsRequired added in v0.0.2

func (o *ConfigOption[T]) IsRequired() bool

IsRequired returns whether the configuration option is required.

func (*ConfigOption[T]) IsSecret added in v0.0.2

func (o *ConfigOption[T]) IsSecret() bool

IsSecret returns whether the configuration option is secret.

func (*ConfigOption[T]) SetHostOnly added in v0.1.2

func (o *ConfigOption[T]) SetHostOnly(hostOnly bool) *ConfigOption[T]

SetHostOnly sets whether the configuration option is host-only. A host-only option is NOT exposed to Starlark as a set_<name> builtin, so a script cannot change it; the host still configures it in Go and (unless it is also secret) a get_<name> builtin still lets a script read it. This is for safety- or resource-sensitive limits — e.g. a maximum input size — that a module must be able to enforce against untrusted scripts, which a script-settable option cannot do (it could just raise or zero the limit). Defaults to false, so existing options remain script-settable exactly as before.

A host-only option's environment-variable value is also snapshotted when the option is registered on a module (see FreezeHostOnlyEnv), so a script that can mutate the process environment at runtime (e.g. via a runtime.setenv builtin) cannot re-widen the limit through its env var either.

func (*ConfigOption[T]) SetName added in v0.0.2

func (o *ConfigOption[T]) SetName(name string)

SetName sets the name of the configuration option.

func (*ConfigOption[T]) SetRequired added in v0.0.2

func (o *ConfigOption[T]) SetRequired(required bool) *ConfigOption[T]

SetRequired sets whether the configuration option is required.

func (*ConfigOption[T]) SetSecret added in v0.0.2

func (o *ConfigOption[T]) SetSecret(secret bool) *ConfigOption[T]

SetSecret sets whether the configuration option is secret.

func (*ConfigOption[T]) SetValue added in v0.0.2

func (o *ConfigOption[T]) SetValue(value T) error

SetValue sets the value of the configuration option. It validates the value if a validator is set.

func (*ConfigOption[T]) SetValueFromStarlark added in v0.0.2

func (o *ConfigOption[T]) SetValueFromStarlark(v starlark.Value) (err error)

SetValueFromStarlark sets the configuration option from a Starlark value.

It never lets a reflection/conversion panic escape to the host: a recover guard (mirroring resolveValue) turns any panic into an error, and None/nil input is rejected up front (dataconv.Unmarshal(None) yields a nil interface, whose reflect.TypeOf is a nil Type that would panic on Kind()).

func (*ConfigOption[T]) Validate added in v0.0.2

func (o *ConfigOption[T]) Validate() error

Validate validates the current value if a validator is set. This method ONLY validates the explicitly set value (via SetValue or WithValue), and does NOT validate values returned from a getter function or environment, or default values. Returns nil if no validator is set, no value has been set, or the validation passes.

func (*ConfigOption[T]) WithDefault added in v0.0.3

func (o *ConfigOption[T]) WithDefault(defaultValue T) *ConfigOption[T]

WithDefault sets the default value of the configuration option. This has the lowest priority in the resolution order.

func (*ConfigOption[T]) WithDescription added in v0.0.2

func (o *ConfigOption[T]) WithDescription(desc string) *ConfigOption[T]

WithDescription adds a description to the configuration option.

func (*ConfigOption[T]) WithEnvVar added in v0.0.2

func (o *ConfigOption[T]) WithEnvVar(envVar string) *ConfigOption[T]

WithEnvVar specifies an environment variable name to check for this configuration. This has the third highest priority in the resolution order.

func (*ConfigOption[T]) WithGetter added in v0.0.2

func (o *ConfigOption[T]) WithGetter(getter ConfigGetter[T]) *ConfigOption[T]

WithGetter adds a custom getter to the configuration option. This has the second highest priority in the resolution order.

func (*ConfigOption[T]) WithName added in v0.0.2

func (o *ConfigOption[T]) WithName(name string) *ConfigOption[T]

WithName sets the name of the configuration option.

func (*ConfigOption[T]) WithValidator added in v0.0.2

func (o *ConfigOption[T]) WithValidator(validator ConfigValidator[T]) *ConfigOption[T]

WithValidator adds a validator to the configuration option.

func (*ConfigOption[T]) WithValue added in v0.0.2

func (o *ConfigOption[T]) WithValue(value T) *ConfigOption[T]

WithValue sets the value of the configuration option. This is useful for chain calls when building a configuration option. Unlike SetValue, this method ignores any validators since it's part of a builder chain. Validation will occur during module initialization when Initialize() is called. This has the highest priority in the resolution order.

type ConfigOptionInterface added in v0.0.2

type ConfigOptionInterface interface {
	// Core methods
	GetName() string
	SetName(name string)
	IsRequired() bool
	IsSecret() bool
	IsHostOnly() bool
	FreezeHostOnlyEnv()
	HasValue() bool
	HasGetter() bool
	HasDefault() bool
	HasEnvVar() bool
	Validate() error

	// Starlark integration
	SetValueFromStarlark(v starlark.Value) error
	GetStarlarkValue() (starlark.Value, error)

	// Go inspection
	GetInfo() map[string]interface{}
}

ConfigOptionInterface defines the common interface for configuration options. It is used by ConfigurableModule to manage configuration settings.

type ConfigValidator added in v0.0.2

type ConfigValidator[T any] func(value T) error

ConfigValidator is a function that validates a configuration value of type T.

type ConfigurableModule

type ConfigurableModule struct {
	// contains filtered or unexported fields
}

ConfigurableModule provides a generic module that can be extended with different configurations. Once initialized, the module becomes immutable.

Configuration values are resolved in the following priority order (from highest to lowest): 1. Immediate value (set via WithValue/SetValue) 2. Returned value from the getter function (set via WithGetter) 3. Environment variable value (set via WithEnvVar) 4. Default value (set via WithDefault or NewConfigOption)

func NewConfigurableModule

func NewConfigurableModule() *ConfigurableModule

NewConfigurableModule returns a new instance of ConfigurableModule.

func NewConfigurableModuleWithConfigOptions added in v0.0.3

func NewConfigurableModuleWithConfigOptions(options ...ConfigOptionInterface) (*ConfigurableModule, error)

NewConfigurableModuleWithConfigOptions returns a new instance of ConfigurableModule with the provided ConfigOptions added. Unlike NewConfigurableModuleWithOptions which takes functional ModuleOptions, this function directly accepts the actual ConfigOption instances. This is a more convenient way to create a module when you have ConfigOption instances directly.

The name of each option is inferred from its Name field if set, otherwise a generated name will be used following the pattern "option_1", "option_2", etc.

Example:

// Create config options
strOpt := base.NewConfigOption("default_str").WithName("string_option")
intOpt := base.NewConfigOption(42).WithName("int_option")

// Create module with options
module, err := base.NewConfigurableModuleWithConfigOptions(strOpt, intOpt)

func NewConfigurableModuleWithOptions added in v0.0.2

func NewConfigurableModuleWithOptions(options ...ModuleOption) (*ConfigurableModule, error)

NewConfigurableModuleWithOptions returns a new instance of ConfigurableModule with the provided options applied.

func (*ConfigurableModule) Extend added in v0.0.3

Extend returns an extension wrapper for ConfigurableModule that provides additional convenience methods. This helps reduce boilerplate code when working with configuration values that need fallback handling.

func (*ConfigurableModule) GetConfigOption added in v0.0.2

func (m *ConfigurableModule) GetConfigOption(name string) (ConfigOptionInterface, error)

GetConfigOption retrieves a configuration option by name.

func (*ConfigurableModule) Initialize added in v0.0.2

func (m *ConfigurableModule) Initialize() error

Initialize finalizes the module configuration and prevents further modifications.

func (*ConfigurableModule) ListConfigs added in v0.0.2

func (m *ConfigurableModule) ListConfigs() map[string]map[string]interface{}

ListConfigs returns a map with configuration details for each option.

func (*ConfigurableModule) LoadModule

func (m *ConfigurableModule) LoadModule(moduleName string, additionalFuncs starlark.StringDict) starlet.ModuleLoader

LoadModule returns a Starlark module loader with registered built-in functions. Initialization (and its validation) is deferred into the returned loader, so a configuration error surfaces as the loader's error return rather than a panic that would crash the host (PKG-03).

func (*ConfigurableModule) SetConfigOption added in v0.0.2

func (m *ConfigurableModule) SetConfigOption(name string, option ConfigOptionInterface) error

SetConfigOption registers a configuration option for the module.

type ConfigurableModuleExt added in v0.0.3

type ConfigurableModuleExt struct {
	*ConfigurableModule
}

ConfigurableModuleExt provides extension methods for ConfigurableModule. It offers convenient access to configuration values with built-in fallback handling, reducing boilerplate code in modules that use ConfigurableModule.

func (*ConfigurableModuleExt) GetBool added in v0.0.3

func (e *ConfigurableModuleExt) GetBool(key string, fallbackVal ...bool) bool

GetBool retrieves a bool configuration value with an optional fallback value. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned. This provides the same convenience as GetString but for boolean values.

func (*ConfigurableModuleExt) GetFloat added in v0.0.3

func (e *ConfigurableModuleExt) GetFloat(key string, fallbackVal ...float64) float64

GetFloat retrieves a float64 configuration value with an optional fallback value. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned. This provides the same convenience as GetString but for float64 values.

func (*ConfigurableModuleExt) GetInt added in v0.0.3

func (e *ConfigurableModuleExt) GetInt(key string, fallbackVal ...int) int

GetInt retrieves an int configuration value with an optional fallback value. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned. This provides the same convenience as GetString but for int values.

func (*ConfigurableModuleExt) GetString added in v0.0.3

func (e *ConfigurableModuleExt) GetString(key string, fallbackVal ...string) string

GetString retrieves a string configuration value with an optional fallback value. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned. This is a convenience method replacing the common pattern of checking for errors when retrieving config values.

func (*ConfigurableModuleExt) GetUint added in v0.0.3

func (e *ConfigurableModuleExt) GetUint(key string, fallbackVal ...uint) uint

GetUint retrieves an uint configuration value with an optional fallback value. If the configuration doesn't exist or there's an error retrieving it, the fallback value is returned. This provides the same convenience as GetString but for unsigned integer values.

type ModuleOption added in v0.0.2

type ModuleOption func(*ConfigurableModule) error

ModuleOption applies a configuration to the module.

func WithConfigDefault added in v0.0.3

func WithConfigDefault[T any](name string, defaultValue T) ModuleOption

WithConfigDefault sets a default value for the configuration. This has the lowest priority in the resolution order.

func WithConfigEnvVar added in v0.0.2

func WithConfigEnvVar[T any](name string, envVar string) ModuleOption

WithConfigEnvVar associates an environment variable with the configuration. This has the third highest priority in the resolution order.

func WithConfigGetter added in v0.0.2

func WithConfigGetter[T any](name string, getter ConfigGetter[T]) ModuleOption

WithConfigGetter registers a dynamic getter for the configuration. This has the second highest priority in the resolution order.

func WithConfigOption added in v0.0.2

func WithConfigOption(name string, option ConfigOptionInterface) ModuleOption

WithConfigOption registers a configuration option for the module.

func WithConfigValue added in v0.0.2

func WithConfigValue[T any](name string, value T) ModuleOption

WithConfigValue sets a configuration value directly. This has the highest priority in the resolution order.

func WithTypedConfigOption added in v0.0.2

func WithTypedConfigOption[T any](name string, option *ConfigOption[T]) ModuleOption

WithTypedConfigOption registers a strongly-typed configuration option for the module.

Directories

Path Synopsis
Package util provides small, hardened safety primitives shared across the starpkg modules: bounded I/O, constant-time secret comparison, float-safe duration parsing, and panic recovery.
Package util provides small, hardened safety primitives shared across the starpkg modules: bounded I/O, constant-time secret comparison, float-safe duration parsing, and panic recovery.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL