Skip to content

robbyt/go-polyscript

Repository files navigation

go-polyscript

Go Reference Go Report Card Coverage License

A Go package providing a unified interface for loading and running various scripting languages and WASM in your app.

Overview

go-polyscript democratizes different scripting engines by abstracting the loading, data handling, runtime, and results handling, allowing for interchangeability of scripting languages. This package provides interfaces and implementations for "engines", "executables", "evaluators" and the final "result". There are several tiers of public APIs, each with increasing complexity and configurability. polyscript.go in the root exposes the most common use cases, but is also the most opiniated.

Features

  • Unified API: Common interfaces and implementations for several scripting languages
  • Flexible Engine Selection: Easily switch between different script engines
  • Thread-safe Data Management: Multiple ways to provide input data to scripts
  • Compilation and Evaluation Separation: Compile once, run multiple times with different inputs
  • Data Preparation and Evaluation Separation: Prepare data in one step/system, evaluate in another

Engines Implemented

  • Risor: A simple scripting language specifically designed for embedding in Go applications
  • Starlark: Google's configuration language (a Python dialect) used in Bazel and many other tools
  • Extism: Pure Go runtime and plugin system for executing WASM

Installation

go get github.com/robbyt/go-polyscript@latest

Quick Start

Using go-polyscript with the Risor scripting engine:

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"

	"github.com/robbyt/go-polyscript"
)

func main() {
	logHandler := slog.NewTextHandler(os.Stdout, nil)

	script := `
		// Script has access to ctx variable passed from Go
		name := ctx.get("name", "Roberto")

		if ctx.get("excited") {
			p := "!"
		} else {
		 	p := "."
		}
		
		message := "Hello, " + name
		if excited {
			message = message + "!"
		}
		
		// Return a map with our result
		{
			"greeting": message,
			"length": len(message)
		}
	`

	inputData := map[string]any{"name": "World"}
	
	evaluator, _ := polyscript.FromRisorStringWithData(
		script,
		inputData,
		logHandler,
	)
	
	ctx := context.Background()
	result, _ := evaluator.Eval(ctx)
	fmt.Printf("Result: %v\n", result.Interface())
}

Working with Data Providers

go-polyscript enables you to send input data using a system called "data providers". There are several built-in providers, and you can implement your own or stack multiple with the CompositeProvider.

StaticProvider

The FromRisorStringWithData function uses a StaticProvider to send the static data map.

inputData := map[string]any{"name": "cats", "excited": true}
evaluator, _ := polyscript.FromRisorStringWithData(script, inputData, logHandler)

However, when using StaticProvider, each evaluation will always use the same input data. If you need to provide dynamic runtime data that varies per evaluation, you can use the ContextProvider.

ContextProvider

The ContextProvider retrieves dynamic data from the context object sent to Eval. This is useful when input data changes at runtime:

evaluator, _ := polyscript.FromRisorString(script, logHandler)

ctx := context.Background()
runtimeData := map[string]any{"name": "Billie Jean", "relationship": false}
enrichedCtx, _ := evaluator.PrepareContext(ctx, runtimeData)

// Execute with the "enriched" context containing the link to the input data
result, _ := evaluator.Eval(enrichedCtx)

Combining Static and Dynamic Runtime Data

This is a common pattern where you want both fixed configuration values and threadsafe per-request data to be available during evaluation:

staticData := map[string]any{
    "appName": "MyApp",
    "version": "1.0",
}

// Create the evaluator with the static data
evaluator, _ := polyscript.FromRisorStringWithData(script, staticData, logHandler)

// For each request, prepare dynamic data
requestData := map[string]any{"userId": 123}
enrichedCtx, _ := evaluator.PrepareContext(context.Background(), requestData)

// Execute with both static and dynamic data available
result, _ := evaluator.Eval(enrichedCtx)

// In scripts, data can be accessed from both locations:
// appName := ctx["appName"]  // Static data: "MyApp"
// userId := ctx["input_data"]["userId"]  // Dynamic data: 123

Architecture

go-polyscript is structured around a few key concepts:

  1. Loader: Loads script content from various sources (disk, io.Reader, strings, http, etc.)
  2. Compiler: Validates and compiles scripts into internal "bytecode"
  3. ExecutableUnit: Compiled script bundle, ready for execution
  4. Engine: A specific implementation of a scripting engine (Risor, Starlark, Extism)
  5. Evaluator: Executes compiled scripts with provided input data
  6. DataProvider: Sends data to the VM prior to evaluation
  7. EvaluatorResponse: The response object returned from all Engines

Note on Data Access Patterns

go-polyscript uses a unified Provider interface to supply data to scripts. The library has standardized on storing dynamic runtime data under the input_data key (previously script_data). For maximum compatibility, scripts should handle two data access patterns:

  1. Top-level access for static data: ctx["config_value"]
  2. Nested access for dynamic data: ctx["input_data"]["user_data"]
  3. HTTP request data access: ctx["input_data"]["request"]["method"] (request objects are always stored under input_data)

See the Data Providers section for more details.

Other Engines

Starlark

Starlark syntax is a deterministic "python like" language designed for complex configuration, not so much for dynamic scripting. It's high performance, but the capabilities of the language are very limited. Read more about it here: Starlark-Go

scriptContent := `
# Starlark has access to ctx variable
name = ctx["name"]
message = "Hello, " + name + "!"

# Create the result dictionary
result = {"greeting": message, "length": len(message)}

# Assign to _ to return the value
_ = result
`

staticData := map[string]any{"name": "World"}
evaluator, err := polyscript.FromStarlarkStringWithData(
    scriptContent,
    staticData,
    logHandler,
)

// Execute with a context
result, err := evaluator.Eval(context.Background())

WASM with Extism

Extism uses the Wazero WASM runtime for providing WASI abstractions, and an easy input/output memory sharing data system. Read more about writing WASM plugins for the Extism/Wazero runtime using the Extism PDK here: extism.org

// Create an Extism evaluator with static data
staticData := map[string]any{"input": "World"}
evaluator, err := polyscript.FromExtismFileWithData(
    "/path/to/module.wasm",
    staticData,
    logHandler,
    "greet",  // entryPoint
)

// Execute with a context
result, err := evaluator.Eval(context.Background())

License

Apache License 2.0

About

One interface, many scripting machines

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages