From 63fb0d43fe2a4ee3117d3f2b7130efba1f93d925 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 21:18:34 -0400 Subject: [PATCH 1/4] add more coverage --- machines/extism/compiler/options_test.go | 563 +++++++++++++++++++++++ 1 file changed, 563 insertions(+) diff --git a/machines/extism/compiler/options_test.go b/machines/extism/compiler/options_test.go index 6ed2762..de99e04 100644 --- a/machines/extism/compiler/options_test.go +++ b/machines/extism/compiler/options_test.go @@ -7,11 +7,574 @@ import ( "testing" extismSDK "github.com/extism/go-sdk" + "github.com/robbyt/go-polyscript/execution/constants" "github.com/robbyt/go-polyscript/machines/extism/compiler/internal/compile" "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero" ) +// TestCompilerOptions_Options tests all compiler option functions +func TestCompilerOptions_Options(t *testing.T) { + t.Parallel() + + // WithEntryPoint tests + t.Run("WithEntryPoint", func(t *testing.T) { + // Success case + t.Run("valid entry point", func(t *testing.T) { + entryPoint := "custom_entrypoint" + + c := &Compiler{} + c.applyDefaults() + opt := WithEntryPoint(entryPoint) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, entryPoint, c.entryPointName) + }) + + // Error case + t.Run("empty entry point", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + emptyOpt := WithEntryPoint("") + err := emptyOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "entry point cannot be empty") + }) + }) + + // GetEntryPointName tests + t.Run("GetEntryPointName", func(t *testing.T) { + t.Run("custom value", func(t *testing.T) { + c := &Compiler{entryPointName: "test_function"} + require.Equal(t, "test_function", c.GetEntryPointName()) + }) + + t.Run("empty value", func(t *testing.T) { + c := &Compiler{entryPointName: ""} + require.Equal(t, "", c.GetEntryPointName()) + }) + + t.Run("with defaults", func(t *testing.T) { + c := &Compiler{entryPointName: ""} + c.applyDefaults() + require.Equal(t, defaultEntryPoint, c.GetEntryPointName()) + }) + }) + + // WithLogHandler tests + t.Run("WithLogHandler", func(t *testing.T) { + // Success case + t.Run("valid handler", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + + c := &Compiler{} + c.applyDefaults() + opt := WithLogHandler(handler) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, handler, c.logHandler) + require.Nil(t, c.logger) // Should clear Logger field + + // Setup logger and ensure it works + c.setupLogger() + c.logger.Info("test message") + require.Contains(t, buf.String(), "test message", "log message should be captured") + }) + + // Error case + t.Run("nil handler", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + nilOpt := WithLogHandler(nil) + err := nilOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "log handler cannot be nil") + }) + }) + + // WithLogger tests + t.Run("WithLogger", func(t *testing.T) { + // Success case + t.Run("valid logger", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + logger := slog.New(handler) + + c := &Compiler{} + c.applyDefaults() + opt := WithLogger(logger) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, logger, c.logger) + require.Nil(t, c.logHandler) // Should clear LogHandler field + + // Setup logger and ensure it works + c.setupLogger() + c.logger.Info("test message") + require.Contains(t, buf.String(), "test message", "log message should be captured") + }) + + // Error case + t.Run("nil logger", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + nilOpt := WithLogger(nil) + err := nilOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "logger cannot be nil") + }) + }) + + // WithWASIEnabled tests + t.Run("WithWASIEnabled", func(t *testing.T) { + t.Run("enable WASI", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{ + EnableWASI: false, + }, + } + + opt := WithWASIEnabled(true) + err := opt(c) + + require.NoError(t, err) + require.True(t, c.options.EnableWASI) + }) + + t.Run("disable WASI", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{ + EnableWASI: true, + }, + } + + opt := WithWASIEnabled(false) + err := opt(c) + + require.NoError(t, err) + require.False(t, c.options.EnableWASI) + }) + + t.Run("nil options initialization", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + + opt := WithWASIEnabled(true) + err := opt(c) + + require.NoError(t, err) + require.NotNil(t, c.options, "options should be initialized") + require.True(t, c.options.EnableWASI) + }) + }) + + // WithRuntimeConfig tests + t.Run("WithRuntimeConfig", func(t *testing.T) { + // Success case + t.Run("valid config", func(t *testing.T) { + runtimeConfig := wazero.NewRuntimeConfig() + c := &Compiler{ + options: &compile.Settings{}, + } + + opt := WithRuntimeConfig(runtimeConfig) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, runtimeConfig, c.options.RuntimeConfig) + }) + + // Error case + t.Run("nil config", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{}, + } + + nilOpt := WithRuntimeConfig(nil) + err := nilOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "runtime config cannot be nil") + }) + + t.Run("initializes options if nil", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + runtimeConfig := wazero.NewRuntimeConfig() + + opt := WithRuntimeConfig(runtimeConfig) + err := opt(c) + + require.NoError(t, err) + require.NotNil(t, c.options) + require.Equal(t, runtimeConfig, c.options.RuntimeConfig) + }) + }) + + // WithHostFunctions tests + t.Run("WithHostFunctions", func(t *testing.T) { + t.Run("single host function", func(t *testing.T) { + testHostFn := extismSDK.NewHostFunctionWithStack( + "test_function", + func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {}, + nil, nil, + ) + testHostFn.SetNamespace("test") + + hostFuncs := []extismSDK.HostFunction{testHostFn} + + c := &Compiler{ + options: &compile.Settings{}, + } + + opt := WithHostFunctions(hostFuncs) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, hostFuncs, c.options.HostFunctions) + require.Len(t, c.options.HostFunctions, 1) + }) + + t.Run("multiple host functions", func(t *testing.T) { + testHostFn1 := extismSDK.NewHostFunctionWithStack( + "test_function1", + func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {}, + nil, nil, + ) + testHostFn1.SetNamespace("test") + + testHostFn2 := extismSDK.NewHostFunctionWithStack( + "test_function2", + func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {}, + nil, nil, + ) + testHostFn2.SetNamespace("test") + + hostFuncs := []extismSDK.HostFunction{testHostFn1, testHostFn2} + + c := &Compiler{ + options: &compile.Settings{}, + } + + opt := WithHostFunctions(hostFuncs) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, hostFuncs, c.options.HostFunctions) + require.Len(t, c.options.HostFunctions, 2) + }) + + t.Run("empty host functions", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{}, + } + + emptyOpt := WithHostFunctions([]extismSDK.HostFunction{}) + err := emptyOpt(c) + + require.NoError(t, err) + require.NotNil(t, c.options.HostFunctions) + require.Empty(t, c.options.HostFunctions) + }) + + t.Run("initializes options if nil", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + + testHostFn := extismSDK.NewHostFunctionWithStack( + "test_function", + func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {}, + nil, nil, + ) + + hostFuncs := []extismSDK.HostFunction{testHostFn} + opt := WithHostFunctions(hostFuncs) + err := opt(c) + + require.NoError(t, err) + require.NotNil(t, c.options) + require.Equal(t, hostFuncs, c.options.HostFunctions) + }) + }) + + // WithContext tests + t.Run("WithContext", func(t *testing.T) { + // Success cases + t.Run("valid context", func(t *testing.T) { + customCtx := context.WithValue( + context.Background(), + constants.EvalData, + "test-value", + ) + + c := &Compiler{} + c.applyDefaults() + opt := WithContext(customCtx) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, customCtx, c.ctx) + require.Equal(t, "test-value", c.ctx.Value(constants.EvalData)) + }) + + t.Run("background context", func(t *testing.T) { + ctx := context.Background() + + c := &Compiler{} + c.applyDefaults() + opt := WithContext(ctx) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, ctx, c.ctx) + }) + + // Error case + t.Run("nil context", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + // Using variable to create nil context to avoid linter issues + var nilContext context.Context + nilOpt := WithContext(nilContext) + err := nilOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "context cannot be nil") + }) + }) +} + +// TestCompilerOptions_SetupLogger tests the setupLogger method +func TestCompilerOptions_SetupLogger(t *testing.T) { + t.Parallel() + + t.Run("with explicit logger", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + logger := slog.New(handler) + + c := &Compiler{logger: logger} + c.setupLogger() + + require.Equal(t, logger, c.logger) + require.Equal(t, handler, c.logHandler, "should extract handler from logger") + + c.logger.Info("test message") + require.Contains(t, buf.String(), "test message") + }) + + t.Run("with explicit handler", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + + c := &Compiler{logHandler: handler} + c.setupLogger() + + require.Equal(t, handler, c.logHandler) + require.NotNil(t, c.logger, "should create logger from handler") + + c.logger.Info("test message") + require.Contains(t, buf.String(), "test message") + }) + + t.Run("with nil logger and handler", func(t *testing.T) { + c := &Compiler{logger: nil, logHandler: nil} + c.applyDefaults() // This will set default handler + c.setupLogger() + + require.NotNil(t, c.logHandler, "handler should be initialized") + require.NotNil(t, c.logger, "logger should be initialized") + }) +} + +// TestCompilerOptions_DefaultsAndValidation tests the defaults and validation functionality +func TestCompilerOptions_DefaultsAndValidation(t *testing.T) { + t.Parallel() + + // Test applyDefaults method + t.Run("applyDefaults", func(t *testing.T) { + t.Run("empty compiler", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + + // Check default values were set correctly + require.NotNil(t, c.logHandler, "default log handler should be created") + require.Equal(t, defaultEntryPoint, c.entryPointName) + require.NotNil(t, c.options, "options should be initialized") + require.True(t, c.options.EnableWASI, "WASI should be enabled by default") + require.NotNil(t, c.options.RuntimeConfig, "runtime config should be initialized") + require.NotNil(t, c.options.HostFunctions, "host functions should be initialized") + require.Empty(t, c.options.HostFunctions, "host functions should be empty by default") + require.NotNil(t, c.ctx, "context should be initialized") + }) + + t.Run("custom values preserved", func(t *testing.T) { + customEntryPoint := "custom_entry" + customCtx := context.WithValue(context.Background(), constants.EvalData, "value") + customConfig := wazero.NewRuntimeConfig() + + // Create a compiler with defaults + c := &Compiler{} + c.applyDefaults() + + // Then set the values that would have been set by options + c.entryPointName = customEntryPoint + c.ctx = customCtx + c.options.RuntimeConfig = customConfig + c.options.EnableWASI = false + + // Check that values are set as expected + require.Equal(t, customEntryPoint, c.entryPointName) + require.Equal(t, customCtx, c.ctx) + require.Equal(t, customConfig, c.options.RuntimeConfig) + require.False(t, c.options.EnableWASI) + }) + + t.Run("logger handling", func(t *testing.T) { + t.Run("with explicit logger", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + logger := slog.New(handler) + + c := &Compiler{logger: logger} + c.applyDefaults() + + require.Equal(t, logger, c.logger, "logger should be preserved") + require.Nil(t, c.logHandler) + }) + + t.Run("with explicit handler", func(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, nil) + + c := &Compiler{logHandler: handler} + c.applyDefaults() + + require.Equal(t, handler, c.logHandler, "handler should be preserved") + require.Nil(t, c.logger, "logger should not be created yet") + }) + + t.Run("with neither", func(t *testing.T) { + c := &Compiler{logHandler: nil, logger: nil} + c.applyDefaults() + + require.NotNil(t, c.logHandler, "default handler should be created") + require.Nil(t, c.logger, "logger should not be created yet") + }) + }) + }) + + // Test validate method + t.Run("validate", func(t *testing.T) { + t.Run("valid compiler", func(t *testing.T) { + c := &Compiler{} + c.applyDefaults() + + err := c.validate() + require.NoError(t, err) + }) + + t.Run("valid custom compiler", func(t *testing.T) { + c := &Compiler{ + entryPointName: "custom", + logHandler: slog.NewTextHandler(bytes.NewBuffer(nil), nil), + ctx: context.Background(), + options: &compile.Settings{ + RuntimeConfig: wazero.NewRuntimeConfig(), + }, + } + + err := c.validate() + require.NoError(t, err) + }) + + // Error cases + t.Run("missing logger and handler", func(t *testing.T) { + c := &Compiler{ + entryPointName: "test", + ctx: context.Background(), + logHandler: nil, + logger: nil, + options: &compile.Settings{ + RuntimeConfig: wazero.NewRuntimeConfig(), + }, + } + + err := c.validate() + require.Error(t, err) + require.Contains(t, err.Error(), "either log handler or logger must be specified") + }) + + t.Run("missing entry point", func(t *testing.T) { + c := &Compiler{ + entryPointName: "", + logHandler: slog.NewTextHandler(bytes.NewBuffer(nil), nil), + ctx: context.Background(), + options: &compile.Settings{ + RuntimeConfig: wazero.NewRuntimeConfig(), + }, + } + + err := c.validate() + require.Error(t, err) + require.Contains(t, err.Error(), "entry point must be specified") + }) + + t.Run("nil options", func(t *testing.T) { + c := &Compiler{ + entryPointName: "test", + logHandler: slog.NewTextHandler(bytes.NewBuffer(nil), nil), + ctx: context.Background(), + options: nil, + } + + err := c.validate() + require.Error(t, err) + require.Contains(t, err.Error(), "runtime config cannot be nil") + }) + + t.Run("nil runtime config", func(t *testing.T) { + c := &Compiler{ + entryPointName: "test", + logHandler: slog.NewTextHandler(bytes.NewBuffer(nil), nil), + ctx: context.Background(), + options: &compile.Settings{ + RuntimeConfig: nil, + }, + } + + err := c.validate() + require.Error(t, err) + require.Contains(t, err.Error(), "runtime config cannot be nil") + }) + + t.Run("nil context", func(t *testing.T) { + c := &Compiler{ + entryPointName: "test", + logHandler: slog.NewTextHandler(bytes.NewBuffer(nil), nil), + ctx: nil, + options: &compile.Settings{ + RuntimeConfig: wazero.NewRuntimeConfig(), + }, + } + + err := c.validate() + require.Error(t, err) + require.Contains(t, err.Error(), "context cannot be nil") + }) + }) +} + // TestCompilerOptions tests all compiler options functionality func TestCompilerOptions(t *testing.T) { t.Parallel() From 3036a862a3ca685b27d849d2e764c1da29b98139 Mon Sep 17 00:00:00 2001 From: RT Date: Fri, 11 Apr 2025 13:00:08 -0400 Subject: [PATCH 2/4] lots of cleanup after removal of the options package --- engine/benchmark_test.go | 80 +-- engine/evaluator_test.go | 83 +-- engine/options/defaults.go | 44 -- engine/options/options.go | 125 ---- engine/options/options_test.go | 126 ---- examples/data-prep/extism/main.go | 38 +- examples/data-prep/extism/main_test.go | 33 +- examples/data-prep/risor/main.go | 29 +- examples/data-prep/starlark/main.go | 29 +- .../multiple-instantiation/extism/main.go | 12 +- examples/multiple-instantiation/risor/main.go | 17 +- .../multiple-instantiation/starlark/main.go | 17 +- examples/simple/extism/main.go | 15 +- examples/simple/risor/main.go | 19 +- examples/simple/starlark/main.go | 18 +- machines/extism/aliases.go | 14 - machines/extism/new.go | 108 +++ machines/new.go | 60 +- machines/new_test.go | 27 - machines/risor/aliases.go | 14 - machines/risor/new.go | 101 +++ machines/starlark/aliases.go | 14 - machines/starlark/new.go | 101 +++ machines/types/gen/templates/new.go.tmpl | 26 +- machines/types/gen/templates/new_test.go.tmpl | 11 - polyscript.go | 450 ++---------- polyscript_test.go | 651 ++++++------------ 27 files changed, 724 insertions(+), 1538 deletions(-) delete mode 100644 engine/options/defaults.go delete mode 100644 engine/options/options.go delete mode 100644 engine/options/options_test.go delete mode 100644 machines/extism/aliases.go create mode 100644 machines/extism/new.go delete mode 100644 machines/risor/aliases.go create mode 100644 machines/risor/new.go delete mode 100644 machines/starlark/aliases.go create mode 100644 machines/starlark/new.go diff --git a/engine/benchmark_test.go b/engine/benchmark_test.go index fa018e2..c666b55 100644 --- a/engine/benchmark_test.go +++ b/engine/benchmark_test.go @@ -31,11 +31,7 @@ import ( "testing" "github.com/robbyt/go-polyscript" - "github.com/robbyt/go-polyscript/engine/options" "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" - starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler" ) // quietHandler is a slog.Handler that discards all logs @@ -63,15 +59,12 @@ func BenchmarkEvaluationPatterns(b *testing.B) { inputData := map[string]any{ "name": "World", } - dataProvider := data.NewStaticProvider(inputData) // Create and evaluate in each iteration (simulating one-time use) - evaluator, err := polyscript.FromRisorString( + evaluator, err := polyscript.FromRisorStringWithData( scriptContent, - options.WithDefaults(), - options.WithDataProvider(dataProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + inputData, + quietHandler, ) if err != nil { b.Fatalf("Failed to create evaluator: %v", err) @@ -87,15 +80,13 @@ func BenchmarkEvaluationPatterns(b *testing.B) { b.Run("CompileOnceRunMany", func(b *testing.B) { // Create evaluator once, outside the loop - dataProvider := data.NewStaticProvider(map[string]any{ + inputData := map[string]any{ "name": "World", - }) - evaluator, err := polyscript.FromRisorString( + } + evaluator, err := polyscript.FromRisorStringWithData( scriptContent, - options.WithDefaults(), - options.WithDataProvider(dataProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + inputData, + quietHandler, ) if err != nil { b.Fatalf("Failed to create evaluator: %v", err) @@ -133,13 +124,11 @@ func BenchmarkDataProviders(b *testing.B) { } b.Run("StaticProvider", func(b *testing.B) { - dataProvider := data.NewStaticProvider(inputData) - evaluator, err := polyscript.FromRisorString( + // Using the *WithData version of the function which sets up a StaticProvider + evaluator, err := polyscript.FromRisorStringWithData( scriptContent, - options.WithDefaults(), - options.WithDataProvider(dataProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + inputData, + quietHandler, ) if err != nil { b.Fatalf("Failed to create evaluator: %v", err) @@ -155,13 +144,10 @@ func BenchmarkDataProviders(b *testing.B) { }) b.Run("ContextProvider", func(b *testing.B) { - dataProvider := data.NewContextProvider(constants.EvalData) + // Using the standard version which uses a ContextProvider evaluator, err := polyscript.FromRisorString( scriptContent, - options.WithDefaults(), - options.WithDataProvider(dataProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + quietHandler, ) if err != nil { b.Fatalf("Failed to create evaluator: %v", err) @@ -179,22 +165,23 @@ func BenchmarkDataProviders(b *testing.B) { }) b.Run("CompositeProvider", func(b *testing.B) { - staticProvider := data.NewStaticProvider(map[string]any{"defaultKey": "value"}) - contextProvider := data.NewContextProvider(constants.EvalData) - compositeProvider := data.NewCompositeProvider(contextProvider, staticProvider) - - evaluator, err := polyscript.FromRisorString( + // For CompositeProvider use case, we can prepare the context separately + staticData := map[string]any{"defaultKey": "value"} + evaluator, err := polyscript.FromRisorStringWithData( scriptContent, - options.WithDefaults(), - options.WithDataProvider(compositeProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + staticData, // Static part + quietHandler, ) if err != nil { b.Fatalf("Failed to create evaluator: %v", err) } - ctx := context.WithValue(context.Background(), constants.EvalData, inputData) + ctx := context.Background() + // Use PrepareContext to add the dynamic part + ctx, err = evaluator.PrepareContext(ctx, inputData) + if err != nil { + b.Fatalf("Failed to prepare context: %v", err) + } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -215,7 +202,6 @@ func BenchmarkVMComparison(b *testing.B) { inputData := map[string]any{ "name": "World", } - staticProvider := data.NewStaticProvider(inputData) // Risor script risorScript := ` @@ -240,12 +226,10 @@ message = "Hello, " + name + "!" // which is more complex to set up in this benchmark template b.Run("RisorVM", func(b *testing.B) { - evaluator, err := polyscript.FromRisorString( + evaluator, err := polyscript.FromRisorStringWithData( risorScript, - options.WithDefaults(), - options.WithDataProvider(staticProvider), - options.WithLogHandler(quietHandler), - risorCompiler.WithGlobals([]string{constants.Ctx}), + inputData, + quietHandler, ) if err != nil { b.Fatalf("Failed to create Risor evaluator: %v", err) @@ -261,12 +245,10 @@ message = "Hello, " + name + "!" }) b.Run("StarlarkVM", func(b *testing.B) { - evaluator, err := polyscript.FromStarlarkString( + evaluator, err := polyscript.FromStarlarkStringWithData( starlarkScript, - options.WithDefaults(), - options.WithDataProvider(staticProvider), - options.WithLogHandler(quietHandler), - starlarkCompiler.WithGlobals([]string{constants.Ctx}), + inputData, + quietHandler, ) if err != nil { b.Fatalf("Failed to create Starlark evaluator: %v", err) diff --git a/engine/evaluator_test.go b/engine/evaluator_test.go index d301532..5dfac64 100644 --- a/engine/evaluator_test.go +++ b/engine/evaluator_test.go @@ -11,11 +11,8 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" "github.com/robbyt/go-polyscript/execution/data" "github.com/robbyt/go-polyscript/machines/mocks" - risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -114,18 +111,16 @@ func TestEvalDataPreparerInterface(t *testing.T) { // Create a logger for testing handler := slog.NewTextHandler(os.Stdout, nil) - // Create a ContextProvider for this test - provider := data.NewContextProvider(constants.EvalData) - // Create an evaluator with PrepareContext capability - evaluator, err := polyscript.FromRisorString(` + // The key name may be different in the new implementation + scriptData := map[string]any{"greeting": "Hello, World!"} + evaluator, err := polyscript.FromRisorStringWithData(` method := ctx["request"]["Method"] -greeting := ctx["input_data"]["greeting"] +greeting := ctx["greeting"] // With new implementation, keys are at top level method + " " + greeting `, - options.WithLogHandler(handler), - options.WithDataProvider(provider), - risorCompiler.WithGlobals([]string{constants.Ctx}), + scriptData, + handler, ) require.NoError(t, err) require.NotNil(t, evaluator) @@ -134,28 +129,12 @@ method + " " + greeting ctx := context.Background() req, err := http.NewRequest("GET", "http://localhost/test", nil) require.NoError(t, err) - scriptData := map[string]any{"greeting": "Hello, World!"} // Use PrepareContext to enrich the context - enrichedCtx, err := evaluator.PrepareContext(ctx, req, scriptData) + enrichedCtx, err := evaluator.PrepareContext(ctx, req) require.NoError(t, err) require.NotNil(t, enrichedCtx) - // Verify data was stored in context - storedData, ok := enrichedCtx.Value(constants.EvalData).(map[string]any) - require.True(t, ok, "Data should be stored in context") - require.NotNil(t, storedData, "Stored data should not be nil") - - // Verify request data - requestData, ok := storedData[constants.Request].(map[string]any) - require.True(t, ok, "Request data should be available") - assert.Equal(t, "GET", requestData["Method"], "Request method should be stored") - - // Verify script data - scriptDataStored, ok := storedData[constants.InputData].(map[string]any) - require.True(t, ok, "input_data should be available") - assert.Equal(t, "Hello, World!", scriptDataStored["greeting"], "Greeting should be stored") - // Test evaluation with the enriched context result, err := evaluator.Eval(enrichedCtx) require.NoError(t, err) @@ -287,49 +266,19 @@ func TestEvaluatorWithPrepErrors(t *testing.T) { // Create a logger for testing handler := slog.NewTextHandler(os.Stdout, nil) - // Test with StaticProvider (which doesn't support adding data) - staticProvider := data.NewStaticProvider(map[string]any{"static": "data"}) - evaluator, err := polyscript.FromRisorString(`ctx["static"]`, - options.WithLogHandler(handler), - options.WithDataProvider(staticProvider), - risorCompiler.WithGlobals([]string{constants.Ctx}), + // Test with StaticProvider (only testing specific error cases) + staticData := map[string]any{"static": "data"} + evaluator, err := polyscript.FromRisorStringWithData(`ctx["static"]`, + staticData, + handler, ) require.NoError(t, err) - // Try to prepare context with StaticProvider + // The context should still be usable ctx := context.Background() - _, err = evaluator.PrepareContext(ctx, map[string]any{"greeting": "Hello"}) - - // Should return error about StaticProvider not supporting runtime data changes - assert.Error(t, err, "Should return error for static provider") - assert.Contains( - t, - err.Error(), - "StaticProvider doesn't support adding data", - "Error should mention static provider limitation", - ) - - // Test with evaluator that has a ContextProvider - contextProvider := data.NewContextProvider(constants.EvalData) - evaluator, err = polyscript.FromRisorString(`ctx["request"]["ID"] || "no id"`, - options.WithLogHandler(handler), - options.WithDataProvider(contextProvider), - risorCompiler.WithGlobals([]string{constants.Ctx}), - ) - require.NoError(t, err) - - // Try to prepare context with unsupported data enrichedCtx, err := evaluator.PrepareContext(ctx, 123) // Integer not supported directly - // Should return error about unsupported data type, but still return the context - assert.Error(t, err, "Should return error for unsupported data type") - assert.Contains( - t, - err.Error(), - "unsupported data type", - "Error should mention unsupported data type", - ) - - // The context should still be usable - assert.NotNil(t, enrichedCtx, "Should still return a context even with error") + // We expect an error because integers aren't directly supported + assert.Error(t, err, "PrepareContext should return an error for integers") + assert.NotNil(t, enrichedCtx, "Should return a context regardless") } diff --git a/engine/options/defaults.go b/engine/options/defaults.go deleted file mode 100644 index 9790a19..0000000 --- a/engine/options/defaults.go +++ /dev/null @@ -1,44 +0,0 @@ -package options - -import ( - "log/slog" - "os" - - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/types" -) - -// DefaultConfig initializes a Config with sensible defaults -func DefaultConfig(machineType types.Type) *Config { - cfg := &Config{} - cfg.SetMachineType(machineType) - cfg.SetHandler(DefaultLoggingHandler()) - cfg.SetDataProvider(DefaultDataProvider()) - return cfg -} - -// DefaultLoggingHandler returns the default logging handler -func DefaultLoggingHandler() slog.Handler { - return slog.NewTextHandler(os.Stdout, nil) -} - -// DefaultDataProvider returns the default data provider -func DefaultDataProvider() data.Provider { - return data.NewContextProvider(constants.EvalData) -} - -// WithDefaults applies default values to any config properties that are nil -func WithDefaults() Option { - return func(c *Config) error { - if c.handler == nil { - c.handler = DefaultLoggingHandler() - } - - if c.dataProvider == nil { - c.dataProvider = DefaultDataProvider() - } - - return nil - } -} diff --git a/engine/options/options.go b/engine/options/options.go deleted file mode 100644 index a537d64..0000000 --- a/engine/options/options.go +++ /dev/null @@ -1,125 +0,0 @@ -package options - -import ( - "errors" - "fmt" - "log/slog" - - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/execution/script/loader" - "github.com/robbyt/go-polyscript/machines/types" -) - -// Config holds all configuration for creating a script engine -type Config struct { - // Logger for the engine - handler slog.Handler - // Type of machine to use (starlark, risor, extism) - machineType types.Type - // Data provider for passing values to the script - dataProvider data.Provider - // Loader for the script content - loader loader.Loader -} - -// Option is a function that modifies Config -type Option func(*Config) error - -// WithLogHandler sets the logger for the script engine -func WithLogHandler(handler slog.Handler) Option { - return func(c *Config) error { - if handler != nil { - c.handler = handler - } - return nil - } -} - -// WithSlog sets the slog logger for the script engine -func WithSlog(logger *slog.Logger) Option { - return func(c *Config) error { - if logger != nil { - c.handler = logger.Handler() - } - return nil - } -} - -// WithDataProvider sets the data provider for the script engine -func WithDataProvider(provider data.Provider) Option { - return func(c *Config) error { - if provider != nil { - c.dataProvider = provider - } - return nil - } -} - -// WithLoader sets the script loader -func WithLoader(l loader.Loader) Option { - return func(c *Config) error { - if l != nil { - c.loader = l - } - return nil - } -} - -// Validate performs basic validation on the common configuration. Machine-specific -// validation is performed in each machine-specific VM package. -func (c *Config) Validate() error { - var errz []error - if c.handler == nil { - errz = append(errz, fmt.Errorf("no logger specified")) - } - if c.machineType == "" { - errz = append(errz, fmt.Errorf("no machine type specified")) - } - if c.dataProvider == nil { - errz = append(errz, fmt.Errorf("no data provider specified")) - } - if c.loader == nil { - errz = append(errz, fmt.Errorf("no loader specified")) - } - return errors.Join(errz...) -} - -// GetHandler returns the configured logger -func (c *Config) GetHandler() slog.Handler { - return c.handler -} - -// SetHandler sets the logger -func (c *Config) SetHandler(handler slog.Handler) { - c.handler = handler -} - -// GetMachineType returns the configured machine type -func (c *Config) GetMachineType() types.Type { - return c.machineType -} - -// SetMachineType sets the machine type -func (c *Config) SetMachineType(machineType types.Type) { - c.machineType = machineType -} - -// GetDataProvider returns the configured data provider -func (c *Config) GetDataProvider() data.Provider { - return c.dataProvider -} - -// SetDataProvider sets the data provider -func (c *Config) SetDataProvider(provider data.Provider) { - c.dataProvider = provider -} - -// GetLoader returns the configured loader -func (c *Config) GetLoader() loader.Loader { - return c.loader -} - -// SetLoader sets the loader -func (c *Config) SetLoader(l loader.Loader) { - c.loader = l -} diff --git a/engine/options/options_test.go b/engine/options/options_test.go deleted file mode 100644 index 451b3d9..0000000 --- a/engine/options/options_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package options - -import ( - "io" - "log/slog" - "net/url" - "os" - "testing" - - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/types" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -// MockLoader is a testify mock implementation of loader.Loader for testing -type MockLoader struct { - mock.Mock -} - -func (m *MockLoader) GetReader() (io.ReadCloser, error) { - args := m.Called() - reader, _ := args.Get(0).(io.ReadCloser) - return reader, args.Error(1) -} - -func (m *MockLoader) GetSourceURL() *url.URL { - args := m.Called() - u, _ := args.Get(0).(*url.URL) - return u -} - -// NewMockLoader creates a pre-configured MockLoader with default expectations -func NewMockLoader() *MockLoader { - mockLoader := new(MockLoader) - - // Set up default expectations - mockLoader.On("GetReader").Return(nil, nil) - - u, err := url.Parse("file:///mock") - if err != nil { - panic(err) // This should never happen with a valid URL string - } - mockLoader.On("GetSourceURL").Return(u) - - return mockLoader -} - -func TestWithOptions(t *testing.T) { - t.Parallel() - // Create test config - cfg := &Config{ - machineType: types.Starlark, - } - - // Create test options - testHandler := slog.NewTextHandler(os.Stdout, nil) - testDataProvider := data.NewStaticProvider(map[string]any{"test": "value"}) - testLoader := NewMockLoader() - - // Create and apply options - loggerOpt := WithLogHandler(testHandler) - dataProviderOpt := WithDataProvider(testDataProvider) - loaderOpt := WithLoader(testLoader) - - // Apply options - err := loggerOpt(cfg) - require.NoError(t, err) - err = dataProviderOpt(cfg) - require.NoError(t, err) - err = loaderOpt(cfg) - require.NoError(t, err) - - // Verify config was updated correctly - require.Equal(t, testHandler, cfg.handler) - require.Equal(t, testDataProvider, cfg.dataProvider) - require.Equal(t, testLoader, cfg.loader) -} - -func TestConfigValidation(t *testing.T) { - t.Parallel() - // Test with missing loader - cfg1 := &Config{ - machineType: types.Starlark, - } - err := cfg1.Validate() - require.Error(t, err) - require.Contains(t, err.Error(), "no loader specified") - - // Test with missing machine type - cfg2 := &Config{ - loader: NewMockLoader(), - } - err = cfg2.Validate() - require.Error(t, err) - require.Contains(t, err.Error(), "no machine type specified") - - // Test with valid config - cfg3 := &Config{ - machineType: types.Starlark, - loader: NewMockLoader(), - handler: slog.NewTextHandler(os.Stdout, nil), - dataProvider: data.NewStaticProvider(map[string]any{"test": "value"}), - } - err = cfg3.Validate() - require.NoError(t, err) -} - -func TestConfigGetters(t *testing.T) { - t.Parallel() - testHandler := slog.NewTextHandler(os.Stdout, nil) - testDataProvider := data.NewStaticProvider(map[string]any{"test": "value"}) - testLoader := NewMockLoader() - - cfg := &Config{ - handler: testHandler, - machineType: types.Starlark, - dataProvider: testDataProvider, - loader: testLoader, - } - - require.Equal(t, testHandler, cfg.GetHandler()) - require.Equal(t, types.Starlark, cfg.GetMachineType()) - require.Equal(t, testDataProvider, cfg.GetDataProvider()) - require.Equal(t, testLoader, cfg.GetLoader()) -} diff --git a/examples/data-prep/extism/main.go b/examples/data-prep/extism/main.go index 2b11269..fa60687 100644 --- a/examples/data-prep/extism/main.go +++ b/examples/data-prep/extism/main.go @@ -11,39 +11,14 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/extism/compiler" ) // ExtismEvaluator is a type alias to make testing cleaner type ExtismEvaluator = engine.EvaluatorWithPrep -// createExtismEvaluator creates a new Extism evaluator with the given WASM file and logger. -// Sets up a CompositeProvider that combines static and dynamic data providers. -func createExtismEvaluator( - logger *slog.Logger, - wasmFilePath string, - staticData map[string]any, -) (ExtismEvaluator, error) { - // The static provider enables access to the static data map - staticProvider := data.NewStaticProvider(staticData) - - // This context provider enables each request to add different dynamic data - dynamicProvider := data.NewContextProvider(constants.EvalData) - - // Composite provider handles static data first, then dynamic data - compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) - - // Create evaluator using the functional options pattern - return polyscript.FromExtismFile( - wasmFilePath, - options.WithLogHandler(logger.Handler()), - options.WithDataProvider(compositeProvider), - compiler.WithEntryPoint("greet"), - ) -} +const ( + EntryPointFuncName = "greet" // Entry point in the WASM module +) // prepareRuntimeData adds dynamic runtime data to the context. // Returns the enriched context or an error. @@ -198,7 +173,12 @@ func run() error { } // Create evaluator with static and dynamic data providers - evaluator, err := createExtismEvaluator(logger, wasmFilePath, staticData) + evaluator, err := polyscript.FromExtismFileWithData( + wasmFilePath, + staticData, + logger.Handler(), + EntryPointFuncName, + ) if err != nil { return fmt.Errorf("failed to create evaluator: %w", err) } diff --git a/examples/data-prep/extism/main_test.go b/examples/data-prep/extism/main_test.go index c53b21b..4c30efa 100644 --- a/examples/data-prep/extism/main_test.go +++ b/examples/data-prep/extism/main_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/robbyt/go-polyscript" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -46,7 +47,12 @@ func TestDemonstrateDataPrepAndEval(t *testing.T) { staticData := getTestStaticData() // Create evaluator - evaluator, err := createExtismEvaluator(logger, wasmFilePath, staticData) + evaluator, err := polyscript.FromExtismFileWithData( + wasmFilePath, + staticData, + logger.Handler(), + EntryPointFuncName, + ) if err != nil { t.Errorf("Failed to create evaluator: %v", err) return @@ -77,7 +83,12 @@ func TestPrepareRuntimeData(t *testing.T) { staticData := getTestStaticData() // Create evaluator - evaluator, err := createExtismEvaluator(logger, wasmFilePath, staticData) + evaluator, err := polyscript.FromExtismFileWithData( + wasmFilePath, + staticData, + logger.Handler(), + EntryPointFuncName, + ) require.NoError(t, err, "Failed to create evaluator") require.NotNil(t, evaluator, "Evaluator should not be nil") @@ -106,7 +117,12 @@ func TestEvalAndExtractResult(t *testing.T) { staticData := getTestStaticData() // Create evaluator - evaluator, err := createExtismEvaluator(logger, wasmFilePath, staticData) + evaluator, err := polyscript.FromExtismFileWithData( + wasmFilePath, + staticData, + logger.Handler(), + EntryPointFuncName, + ) require.NoError(t, err, "Failed to create evaluator") require.NotNil(t, evaluator, "Evaluator should not be nil") @@ -121,7 +137,7 @@ func TestEvalAndExtractResult(t *testing.T) { assert.NotNil(t, result, "Result should not be nil") } -func TestCreateExtismEvaluator(t *testing.T) { +func TestFromExtismFileWithData(t *testing.T) { // Create a test logger handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, @@ -138,8 +154,13 @@ func TestCreateExtismEvaluator(t *testing.T) { // Get static test data staticData := getTestStaticData() - // Test createExtismEvaluator function - evaluator, err := createExtismEvaluator(logger, wasmFilePath, staticData) + // Test FromExtismFileWithData function + evaluator, err := polyscript.FromExtismFileWithData( + wasmFilePath, + staticData, + logger.Handler(), + EntryPointFuncName, + ) assert.NoError(t, err, "Should create evaluator without error") assert.NotNil(t, evaluator, "Evaluator should not be nil") } diff --git a/examples/data-prep/risor/main.go b/examples/data-prep/risor/main.go index f8642bb..de9796c 100644 --- a/examples/data-prep/risor/main.go +++ b/examples/data-prep/risor/main.go @@ -10,10 +10,6 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/risor/compiler" ) // RisorEvaluator is a type alias to make testing cleaner @@ -23,31 +19,18 @@ type RisorEvaluator = engine.EvaluatorWithPrep var risorScript string // createRisorEvaluator creates a new Risor evaluator with the given script and logger. -// Sets up a CompositeProvider that combines static and dynamic data providers. +// Uses the simplified interface that automatically sets up static and dynamic data providers. func createRisorEvaluator( logger *slog.Logger, scriptContent string, staticData map[string]any, ) (RisorEvaluator, error) { - // Define globals that will be available to the script - globals := []string{constants.Ctx} - - // The static provider enables access to the static data map - staticProvider := data.NewStaticProvider(staticData) - - // This context provider enables each request to add different dynamic data - dynamicProvider := data.NewContextProvider(constants.EvalData) - - // Composite provider handles static data first, then dynamic data - compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) - - // Create evaluator using the functional options pattern - return polyscript.FromRisorString( + // Create evaluator using the new simplified interface + // This automatically sets up a composite provider with both static and dynamic data + return polyscript.FromRisorStringWithData( scriptContent, - options.WithDefaults(), - options.WithLogHandler(logger.Handler()), - options.WithDataProvider(compositeProvider), - compiler.WithGlobals(globals), + staticData, + logger.Handler(), ) } diff --git a/examples/data-prep/starlark/main.go b/examples/data-prep/starlark/main.go index dcca9a2..b047ff5 100644 --- a/examples/data-prep/starlark/main.go +++ b/examples/data-prep/starlark/main.go @@ -12,10 +12,6 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/starlark/compiler" ) // StarlarkEvaluator is a type alias to make testing cleaner @@ -25,31 +21,18 @@ type StarlarkEvaluator = engine.EvaluatorWithPrep var starlarkScript string // createStarlarkEvaluator creates a new Starlark evaluator with the given script and logger. -// Sets up a CompositeProvider that combines static and dynamic data providers. +// Uses the simplified interface that automatically sets up static and dynamic data providers. func createStarlarkEvaluator( logger *slog.Logger, scriptContent string, staticData map[string]any, ) (StarlarkEvaluator, error) { - // Define globals that will be available to the script - globals := []string{constants.Ctx} - - // the static provider enables access to the static data map - staticProvider := data.NewStaticProvider(staticData) - - // this context provider enables each request to add different dynamic data - dynamicProvider := data.NewContextProvider(constants.EvalData) - - // Composite provider handles static data first, then dynamic data - compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) - - // Create evaluator using the functional options pattern - return polyscript.FromStarlarkString( + // Create evaluator using the new simplified interface + // This automatically sets up a composite provider with both static and dynamic data + return polyscript.FromStarlarkStringWithData( scriptContent, - options.WithDefaults(), - options.WithLogHandler(logger.Handler()), - options.WithDataProvider(compositeProvider), - compiler.WithGlobals(globals), + staticData, + logger.Handler(), ) } diff --git a/examples/multiple-instantiation/extism/main.go b/examples/multiple-instantiation/extism/main.go index f929778..f083d6d 100644 --- a/examples/multiple-instantiation/extism/main.go +++ b/examples/multiple-instantiation/extism/main.go @@ -10,10 +10,7 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/extism/compiler" ) // ExtismEvaluator is a type alias to make testing cleaner @@ -59,15 +56,12 @@ func createEvaluator(handler slog.Handler) (ExtismEvaluator, error) { return nil, err } - // Create the context provider for runtime data - dataProvider := data.NewContextProvider(constants.EvalData) - // Create the evaluator + // Uses the simpler interface with dynamic data only via context evaluator, err := polyscript.FromExtismFile( wasmFilePath, - options.WithLogHandler(handler), - options.WithDataProvider(dataProvider), - compiler.WithEntryPoint("greet"), + handler, + "greet", // entry point ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/examples/multiple-instantiation/risor/main.go b/examples/multiple-instantiation/risor/main.go index f18ddf0..b50b8d9 100644 --- a/examples/multiple-instantiation/risor/main.go +++ b/examples/multiple-instantiation/risor/main.go @@ -9,10 +9,7 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/risor/compiler" ) // RisorEvaluator is a type alias to make testing cleaner @@ -28,19 +25,11 @@ func createEvaluator(handler slog.Handler) (RisorEvaluator, error) { } logger := slog.New(handler) - // Define globals that will be available to the script - globals := []string{constants.Ctx} - - // Create a context provider for runtime data - ctxProvider := data.NewContextProvider(constants.EvalData) - - // Create evaluator using the functional options pattern + // Create evaluator using the new simplified interface + // This provides a dynamic context provider automatically evaluator, err := polyscript.FromRisorString( risorScript, - options.WithDefaults(), - options.WithLogHandler(handler), - options.WithDataProvider(ctxProvider), - compiler.WithGlobals(globals), + handler, ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/examples/multiple-instantiation/starlark/main.go b/examples/multiple-instantiation/starlark/main.go index 8820b48..2524815 100644 --- a/examples/multiple-instantiation/starlark/main.go +++ b/examples/multiple-instantiation/starlark/main.go @@ -9,10 +9,7 @@ import ( "github.com/robbyt/go-polyscript" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/starlark/compiler" ) // StarlarkEvaluator is a type alias to make testing cleaner @@ -28,19 +25,11 @@ func createEvaluator(handler slog.Handler) (StarlarkEvaluator, error) { } logger := slog.New(handler) - // Define globals that will be available to the script - globals := []string{constants.Ctx} - - // Create a context provider for runtime data - ctxProvider := data.NewContextProvider(constants.EvalData) - - // Create evaluator using the functional options pattern + // Create evaluator using the new simplified interface + // This provides a dynamic context provider automatically evaluator, err := polyscript.FromStarlarkString( starlarkScript, - options.WithDefaults(), - options.WithLogHandler(handler), - options.WithDataProvider(ctxProvider), - compiler.WithGlobals(globals), + handler, ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/examples/simple/extism/main.go b/examples/simple/extism/main.go index 14244ed..ecc709e 100644 --- a/examples/simple/extism/main.go +++ b/examples/simple/extism/main.go @@ -9,9 +9,6 @@ import ( "time" "github.com/robbyt/go-polyscript" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/extism/compiler" ) // findWasmFile searches for the Extism WASM file in various likely locations @@ -58,15 +55,13 @@ func runExtismExample(handler slog.Handler) (map[string]any, error) { inputData := map[string]any{ "input": "World", } - dataProvider := data.NewStaticProvider(inputData) - // Create evaluator using the functional options pattern - evaluator, err := polyscript.FromExtismFile( + // Create evaluator using the new simplified interface + evaluator, err := polyscript.FromExtismFileWithData( wasmFilePath, - options.WithDefaults(), - options.WithLogHandler(handler), - options.WithDataProvider(dataProvider), - compiler.WithEntryPoint("greet"), + inputData, + handler, + "greet", // entry point ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/examples/simple/risor/main.go b/examples/simple/risor/main.go index f3a7a6a..3f35b41 100644 --- a/examples/simple/risor/main.go +++ b/examples/simple/risor/main.go @@ -8,10 +8,6 @@ import ( "os" "github.com/robbyt/go-polyscript" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/risor/compiler" ) //go:embed testdata/script.risor @@ -24,22 +20,17 @@ func runRisorExample(handler slog.Handler) (map[string]any, error) { } logger := slog.New(handler) - // Define globals that will be available to the script - globals := []string{constants.Ctx} - // Create input data input := map[string]any{ "name": "World", } - dataProvider := data.NewStaticProvider(input) - // Create evaluator using the functional options pattern - evaluator, err := polyscript.FromRisorString( + // Create evaluator using the new simplified interface + // With data pattern now automatically includes what was previously set via globals + evaluator, err := polyscript.FromRisorStringWithData( risorScript, - options.WithDefaults(), // Add defaults option to ensure all required fields are set - options.WithLogHandler(handler), - options.WithDataProvider(dataProvider), - compiler.WithGlobals(globals), + input, + handler, ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/examples/simple/starlark/main.go b/examples/simple/starlark/main.go index bf8206e..e313713 100644 --- a/examples/simple/starlark/main.go +++ b/examples/simple/starlark/main.go @@ -8,10 +8,6 @@ import ( "os" "github.com/robbyt/go-polyscript" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/machines/starlark/compiler" ) //go:embed testdata/script.star @@ -24,22 +20,16 @@ func runStarlarkExample(handler slog.Handler) (map[string]any, error) { } logger := slog.New(handler) - // Define globals that will be available to the script - globals := []string{constants.Ctx} - // Create input data input := map[string]any{ "name": "World", } - dataProvider := data.NewStaticProvider(input) - // Create evaluator using the functional options pattern - evaluator, err := polyscript.FromStarlarkString( + // Create evaluator using the new simplified interface + evaluator, err := polyscript.FromStarlarkStringWithData( starlarkScript, - options.WithDefaults(), - options.WithLogHandler(handler), - options.WithDataProvider(dataProvider), - compiler.WithGlobals(globals), + input, + handler, ) if err != nil { logger.Error("Failed to create evaluator", "error", err) diff --git a/machines/extism/aliases.go b/machines/extism/aliases.go deleted file mode 100644 index f0b907f..0000000 --- a/machines/extism/aliases.go +++ /dev/null @@ -1,14 +0,0 @@ -package extism - -import ( - "github.com/robbyt/go-polyscript/machines/extism/compiler" - "github.com/robbyt/go-polyscript/machines/extism/evaluator" -) - -type BytecodeEvaluator = evaluator.BytecodeEvaluator - -var NewBytecodeEvaluator = evaluator.NewBytecodeEvaluator - -type Compiler = compiler.Compiler - -var NewCompiler = compiler.NewCompiler diff --git a/machines/extism/new.go b/machines/extism/new.go new file mode 100644 index 0000000..9922610 --- /dev/null +++ b/machines/extism/new.go @@ -0,0 +1,108 @@ +package extism + +import ( + "fmt" + "log/slog" + + "github.com/robbyt/go-polyscript/execution/constants" + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/extism/compiler" + "github.com/robbyt/go-polyscript/machines/extism/evaluator" +) + +// FromExtismLoader creates an Extism evaluator from a loader with dynamic data only (ContextProvider) +// +// Input parameters: +// - l: loader implementation for loading the WASM content +// - logHandler: logger handler for logging +// - entryPoint: entry point for the WASM module (which function to call in the WASM file) +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromExtismLoader( + logHandler slog.Handler, + ldr loader.Loader, + entryPoint string, +) (*evaluator.BytecodeEvaluator, error) { + return NewEvaluator( + logHandler, + ldr, + data.NewContextProvider(constants.EvalData), + entryPoint, + ) +} + +// FromExtismLoaderWithData creates an Extism evaluator with both static and dynamic data capabilities. +// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// +// Input parameters: +// - l: loader implementation for loading the WASM content +// - staticData: map of initial static data to be passed to the WASM module +// - logHandler: logger handler for logging +// - entryPoint: entry point for the WASM module (which function to call in the WASM file) +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromExtismLoaderWithData( + logHandler slog.Handler, + ldr loader.Loader, + staticData map[string]any, + entryPoint string, +) (*evaluator.BytecodeEvaluator, error) { + // Create a composite provider with the static, and dynamic data loader + staticProvider := data.NewStaticProvider(staticData) + dynamicProvider := data.NewContextProvider(constants.EvalData) + compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) + + // Create the evaluator + return NewEvaluator( + logHandler, + ldr, + compositeProvider, + entryPoint, + ) +} + +// NewCompiler creates a new Extism compiler using the functional options pattern. +// See the extismMachine package for available compiler options. Returns a compiler, +// which implements the script.Compiler interface. +func NewCompiler(opts ...compiler.FunctionalOption) (*compiler.Compiler, error) { + return compiler.NewCompiler(opts...) +} + +// NewEvaluator creates an full Extism evaluator with bytecode loaded, and ready for execution. +// Returns a BytecodeEvaluator, which implements the engine.EvaluatorWithPrep interface. +func NewEvaluator( + logHandler slog.Handler, + ldr loader.Loader, + dataProvider data.Provider, + entryPoint string, +) (*evaluator.BytecodeEvaluator, error) { + // Create compiler with the entry point option + compiler, err := NewCompiler(compiler.WithEntryPoint(entryPoint)) + if err != nil { + return nil, fmt.Errorf("failed to create Extism compiler: %w", err) + } + + // Create executable unit ID from source URL + execUnitID := "" + sourceURL := ldr.GetSourceURL() + if sourceURL != nil { + execUnitID = sourceURL.String() + } + + // Create executable unit (to compile and prepare the script) + execUnit, err := script.NewExecutableUnit( + logHandler, + execUnitID, + ldr, + compiler, + dataProvider, + ) + if err != nil { + return nil, err + } + + // BytecodeEvaluator already implements the EvaluatorWithPrep interface + return evaluator.NewBytecodeEvaluator(logHandler, execUnit), nil +} diff --git a/machines/new.go b/machines/new.go index 1d30511..4713a93 100644 --- a/machines/new.go +++ b/machines/new.go @@ -9,12 +9,12 @@ import ( "github.com/robbyt/go-polyscript/engine" "github.com/robbyt/go-polyscript/execution/script" - extismMachine "github.com/robbyt/go-polyscript/machines/extism" extismCompiler "github.com/robbyt/go-polyscript/machines/extism/compiler" - risorMachine "github.com/robbyt/go-polyscript/machines/risor" + extismEvaluator "github.com/robbyt/go-polyscript/machines/extism/evaluator" risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" - starlarkMachine "github.com/robbyt/go-polyscript/machines/starlark" + risorEvaluator "github.com/robbyt/go-polyscript/machines/risor/evaluator" starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler" + starlarkEvaluator "github.com/robbyt/go-polyscript/machines/starlark/evaluator" machineTypes "github.com/robbyt/go-polyscript/machines/types" ) @@ -30,13 +30,13 @@ func NewEvaluator(handler slog.Handler, ver *script.ExecutableUnit) (engine.Eval switch ver.GetMachineType() { case machineTypes.Risor: // Risor VM: https://github.com/risor-io/risor - return risorMachine.NewBytecodeEvaluator(handler, ver), nil + return risorEvaluator.NewBytecodeEvaluator(handler, ver), nil case machineTypes.Starlark: // Starlark VM: https://github.com/google/starlark-go - return starlarkMachine.NewBytecodeEvaluator(handler, ver), nil + return starlarkEvaluator.NewBytecodeEvaluator(handler, ver), nil case machineTypes.Extism: // Extism WASM VM: https://extism.org/ - return extismMachine.NewBytecodeEvaluator(handler, ver), nil + return extismEvaluator.NewBytecodeEvaluator(handler, ver), nil default: return nil, fmt.Errorf("%w: %s", machineTypes.ErrInvalidMachineType, ver.GetMachineType()) } @@ -64,7 +64,11 @@ func NewCompiler(opts ...any) (script.Compiler, error) { } if allMatch && len(risorOpts) > 0 { - return NewRisorCompiler(risorOpts...) + compiler, err := risorCompiler.NewCompiler(risorOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create Risor compiler: %w", err) + } + return compiler, nil } } @@ -83,7 +87,11 @@ func NewCompiler(opts ...any) (script.Compiler, error) { } if allMatch && len(starlarkOpts) > 0 { - return NewStarlarkCompiler(starlarkOpts...) + compiler, err := starlarkCompiler.NewCompiler(starlarkOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create Starlark compiler: %w", err) + } + return compiler, nil } } @@ -102,39 +110,13 @@ func NewCompiler(opts ...any) (script.Compiler, error) { } if allMatch && len(extismOpts) > 0 { - return NewExtismCompiler(extismOpts...) + compiler, err := extismCompiler.NewCompiler(extismOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create Extism compiler: %w", err) + } + return compiler, nil } } return nil, fmt.Errorf("unable to determine compiler type from provided options") } - -// NewRisorCompiler creates a new Risor compiler using the functional options pattern. -// See the risorMachine package for available compiler options. -func NewRisorCompiler(opts ...risorCompiler.FunctionalOption) (script.Compiler, error) { - compiler, err := risorMachine.NewCompiler(opts...) - if err != nil { - return nil, fmt.Errorf("failed to create Risor compiler: %w", err) - } - return compiler, nil -} - -// NewStarlarkCompiler creates a new Starlark compiler using the functional options pattern. -// See the starlarkMachine package for available compiler options. -func NewStarlarkCompiler(opts ...starlarkCompiler.FunctionalOption) (script.Compiler, error) { - compiler, err := starlarkMachine.NewCompiler(opts...) - if err != nil { - return nil, fmt.Errorf("failed to create Starlark compiler: %w", err) - } - return compiler, nil -} - -// NewExtismCompiler creates a new Extism compiler using the functional options pattern. -// See the extismMachine package for available compiler options. -func NewExtismCompiler(opts ...extismCompiler.FunctionalOption) (script.Compiler, error) { - compiler, err := extismMachine.NewCompiler(opts...) - if err != nil { - return nil, fmt.Errorf("failed to create Extism compiler: %w", err) - } - return compiler, nil -} diff --git a/machines/new_test.go b/machines/new_test.go index 56f9785..e040dee 100644 --- a/machines/new_test.go +++ b/machines/new_test.go @@ -10,11 +10,8 @@ import ( "github.com/robbyt/go-polyscript/execution/data" "github.com/robbyt/go-polyscript/execution/script" _ "github.com/robbyt/go-polyscript/machines/extism" - extismCompiler "github.com/robbyt/go-polyscript/machines/extism/compiler" _ "github.com/robbyt/go-polyscript/machines/risor" - risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" _ "github.com/robbyt/go-polyscript/machines/starlark" - starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler" machineTypes "github.com/robbyt/go-polyscript/machines/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -82,30 +79,6 @@ func TestNewEvaluator(t *testing.T) { } } -func TestNewRisorCompiler(t *testing.T) { - t.Run("success", func(t *testing.T) { - compiler, err := NewRisorCompiler(risorCompiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) - require.NoError(t, err) - require.NotNil(t, compiler) - }) -} - -func TestNewStarlarkCompiler(t *testing.T) { - t.Run("success", func(t *testing.T) { - compiler, err := NewStarlarkCompiler(starlarkCompiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) - require.NoError(t, err) - require.NotNil(t, compiler) - }) -} - -func TestNewExtismCompiler(t *testing.T) { - t.Run("success", func(t *testing.T) { - compiler, err := NewExtismCompiler(extismCompiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) - require.NoError(t, err) - require.NotNil(t, compiler) - }) -} - type mockExecutableContent struct { mock.Mock } diff --git a/machines/risor/aliases.go b/machines/risor/aliases.go deleted file mode 100644 index bddbf6d..0000000 --- a/machines/risor/aliases.go +++ /dev/null @@ -1,14 +0,0 @@ -package risor - -import ( - "github.com/robbyt/go-polyscript/machines/risor/compiler" - "github.com/robbyt/go-polyscript/machines/risor/evaluator" -) - -type BytecodeEvaluator = evaluator.BytecodeEvaluator - -var NewBytecodeEvaluator = evaluator.NewBytecodeEvaluator - -type Compiler = compiler.Compiler - -var NewCompiler = compiler.NewCompiler diff --git a/machines/risor/new.go b/machines/risor/new.go new file mode 100644 index 0000000..203b8fb --- /dev/null +++ b/machines/risor/new.go @@ -0,0 +1,101 @@ +package risor + +import ( + "fmt" + "log/slog" + + "github.com/robbyt/go-polyscript/execution/constants" + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/risor/compiler" + "github.com/robbyt/go-polyscript/machines/risor/evaluator" +) + +// FromRisorLoader creates a Risor evaluator from a loader with dynamic data only (ContextProvider) +// +// Input parameters: +// - logHandler: logger handler for logging +// - ldr: loader implementation for loading the Risor script content +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromRisorLoader( + logHandler slog.Handler, + ldr loader.Loader, +) (*evaluator.BytecodeEvaluator, error) { + return NewEvaluator( + logHandler, + ldr, + data.NewContextProvider(constants.EvalData), + ) +} + +// FromRisorLoaderWithData creates a Risor evaluator with both static and dynamic data capabilities. +// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// +// Input parameters: +// - logHandler: logger handler for logging +// - ldr: loader implementation for loading the Risor script content +// - staticData: map of initial static data to be passed to the script +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromRisorLoaderWithData( + logHandler slog.Handler, + ldr loader.Loader, + staticData map[string]any, +) (*evaluator.BytecodeEvaluator, error) { + // Create a composite provider with the static, and dynamic data loader + staticProvider := data.NewStaticProvider(staticData) + dynamicProvider := data.NewContextProvider(constants.EvalData) + compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) + + // Create the evaluator + return NewEvaluator( + logHandler, + ldr, + compositeProvider, + ) +} + +// NewCompiler creates a new Risor compiler using the functional options pattern. +// See the risorMachine package for available compiler options. Returns a compiler, +// which implements the script.Compiler interface. +func NewCompiler(opts ...compiler.FunctionalOption) (*compiler.Compiler, error) { + return compiler.NewCompiler(opts...) +} + +// NewEvaluator creates a full Risor evaluator with bytecode loaded, and ready for execution. +// Returns a BytecodeEvaluator, which implements the engine.EvaluatorWithPrep interface. +func NewEvaluator( + logHandler slog.Handler, + ldr loader.Loader, + dataProvider data.Provider, +) (*evaluator.BytecodeEvaluator, error) { + // Create compiler with the context global option + compiler, err := NewCompiler(compiler.WithCtxGlobal()) + if err != nil { + return nil, fmt.Errorf("failed to create Risor compiler: %w", err) + } + + // Create executable unit ID from source URL + execUnitID := "" + sourceURL := ldr.GetSourceURL() + if sourceURL != nil { + execUnitID = sourceURL.String() + } + + // Create executable unit (to compile and prepare the script) + execUnit, err := script.NewExecutableUnit( + logHandler, + execUnitID, + ldr, + compiler, + dataProvider, + ) + if err != nil { + return nil, err + } + + // BytecodeEvaluator already implements the EvaluatorWithPrep interface + return evaluator.NewBytecodeEvaluator(logHandler, execUnit), nil +} diff --git a/machines/starlark/aliases.go b/machines/starlark/aliases.go deleted file mode 100644 index e7bb6a2..0000000 --- a/machines/starlark/aliases.go +++ /dev/null @@ -1,14 +0,0 @@ -package starlark - -import ( - "github.com/robbyt/go-polyscript/machines/starlark/compiler" - "github.com/robbyt/go-polyscript/machines/starlark/evaluator" -) - -type BytecodeEvaluator = evaluator.BytecodeEvaluator - -var NewBytecodeEvaluator = evaluator.NewBytecodeEvaluator - -type Compiler = compiler.Compiler - -var NewCompiler = compiler.NewCompiler diff --git a/machines/starlark/new.go b/machines/starlark/new.go new file mode 100644 index 0000000..462ca56 --- /dev/null +++ b/machines/starlark/new.go @@ -0,0 +1,101 @@ +package starlark + +import ( + "fmt" + "log/slog" + + "github.com/robbyt/go-polyscript/execution/constants" + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/starlark/compiler" + "github.com/robbyt/go-polyscript/machines/starlark/evaluator" +) + +// FromStarlarkLoader creates a Starlark evaluator from a loader with dynamic data only (ContextProvider) +// +// Input parameters: +// - logHandler: logger handler for logging +// - ldr: loader implementation for loading the Starlark script content +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromStarlarkLoader( + logHandler slog.Handler, + ldr loader.Loader, +) (*evaluator.BytecodeEvaluator, error) { + return NewEvaluator( + logHandler, + ldr, + data.NewContextProvider(constants.EvalData), + ) +} + +// FromStarlarkLoaderWithData creates a Starlark evaluator with both static and dynamic data capabilities. +// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// +// Input parameters: +// - logHandler: logger handler for logging +// - ldr: loader implementation for loading the Starlark script content +// - staticData: map of initial static data to be passed to the script +// +// Returns an evaluator, which implements the engine.EvaluatorWithPrep interface. +func FromStarlarkLoaderWithData( + logHandler slog.Handler, + ldr loader.Loader, + staticData map[string]any, +) (*evaluator.BytecodeEvaluator, error) { + // Create a composite provider with the static, and dynamic data loader + staticProvider := data.NewStaticProvider(staticData) + dynamicProvider := data.NewContextProvider(constants.EvalData) + compositeProvider := data.NewCompositeProvider(staticProvider, dynamicProvider) + + // Create the evaluator + return NewEvaluator( + logHandler, + ldr, + compositeProvider, + ) +} + +// NewCompiler creates a new Starlark compiler using the functional options pattern. +// See the starlarkMachine package for available compiler options. Returns a compiler, +// which implements the script.Compiler interface. +func NewCompiler(opts ...compiler.FunctionalOption) (*compiler.Compiler, error) { + return compiler.NewCompiler(opts...) +} + +// NewEvaluator creates a full Starlark evaluator with bytecode loaded, and ready for execution. +// Returns a BytecodeEvaluator, which implements the engine.EvaluatorWithPrep interface. +func NewEvaluator( + logHandler slog.Handler, + ldr loader.Loader, + dataProvider data.Provider, +) (*evaluator.BytecodeEvaluator, error) { + // Create compiler with the context global option + compiler, err := NewCompiler(compiler.WithGlobals([]string{constants.Ctx})) + if err != nil { + return nil, fmt.Errorf("failed to create Starlark compiler: %w", err) + } + + // Create executable unit ID from source URL + execUnitID := "" + sourceURL := ldr.GetSourceURL() + if sourceURL != nil { + execUnitID = sourceURL.String() + } + + // Create executable unit (to compile and prepare the script) + execUnit, err := script.NewExecutableUnit( + logHandler, + execUnitID, + ldr, + compiler, + dataProvider, + ) + if err != nil { + return nil, err + } + + // BytecodeEvaluator already implements the EvaluatorWithPrep interface + return evaluator.NewBytecodeEvaluator(logHandler, execUnit), nil +} diff --git a/machines/types/gen/templates/new.go.tmpl b/machines/types/gen/templates/new.go.tmpl index dbffd16..02523e5 100644 --- a/machines/types/gen/templates/new.go.tmpl +++ b/machines/types/gen/templates/new.go.tmpl @@ -10,10 +10,10 @@ import ( "github.com/robbyt/go-polyscript/engine" "github.com/robbyt/go-polyscript/execution/script" {{- range .Types}} - {{.Value}}Machine "github.com/robbyt/go-polyscript/machines/{{.Value}}" + {{.Value}}Evaluator "github.com/robbyt/go-polyscript/machines/{{.Value}}/evaluator" {{.Value}}Compiler "github.com/robbyt/go-polyscript/machines/{{.Value}}/compiler" {{- end}} - + machineTypes "github.com/robbyt/go-polyscript/machines/types" ) @@ -29,7 +29,7 @@ func NewEvaluator(handler slog.Handler, ver *script.ExecutableUnit) (engine.Eval {{- range .Types}} case machineTypes.{{.Name}}: // {{.Description}} - return {{.Value}}Machine.NewBytecodeEvaluator(handler, ver), nil + return {{.Value}}Evaluator.NewBytecodeEvaluator(handler, ver), nil {{- end}} default: return nil, fmt.Errorf("%w: %s", machineTypes.ErrInvalidMachineType, ver.GetMachineType()) @@ -59,22 +59,14 @@ func NewCompiler(opts ...any) (script.Compiler, error) { } if allMatch && len({{.Value}}Opts) > 0 { - return New{{.Name}}Compiler({{.Value}}Opts...) + compiler, err := {{.Value}}Compiler.NewCompiler({{.Value}}Opts...) + if err != nil { + return nil, fmt.Errorf("failed to create {{.Name}} compiler: %w", err) + } + return compiler, nil } } {{end}} return nil, fmt.Errorf("unable to determine compiler type from provided options") -} - -{{range .Types}} -// New{{.Name}}Compiler creates a new {{.Name}} compiler using the functional options pattern. -// See the {{.Value}}Machine package for available compiler options. -func New{{.Name}}Compiler(opts ...{{.Value}}Compiler.FunctionalOption) (script.Compiler, error) { - compiler, err := {{.Value}}Machine.NewCompiler(opts...) - if err != nil { - return nil, fmt.Errorf("failed to create {{.Name}} compiler: %w", err) - } - return compiler, nil -} -{{end}} \ No newline at end of file +} \ No newline at end of file diff --git a/machines/types/gen/templates/new_test.go.tmpl b/machines/types/gen/templates/new_test.go.tmpl index 097ce76..644ced3 100644 --- a/machines/types/gen/templates/new_test.go.tmpl +++ b/machines/types/gen/templates/new_test.go.tmpl @@ -11,7 +11,6 @@ import ( "github.com/robbyt/go-polyscript/execution/script" {{- range .Types}} _ "github.com/robbyt/go-polyscript/machines/{{.Value}}" - {{.Value}}Compiler "github.com/robbyt/go-polyscript/machines/{{.Value}}/compiler" {{- end}} machineTypes "github.com/robbyt/go-polyscript/machines/types" "github.com/stretchr/testify/mock" @@ -70,16 +69,6 @@ func TestNewEvaluator(t *testing.T) { } } -{{range .Types}} -func TestNew{{.Name}}Compiler(t *testing.T) { - t.Run("success", func(t *testing.T) { - compiler, err := New{{.Name}}Compiler({{.Value}}Compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) - require.NoError(t, err) - require.NotNil(t, compiler) - }) -} -{{end}} - type mockExecutableContent struct { mock.Mock } diff --git a/polyscript.go b/polyscript.go index 3ef39f4..2306cdc 100644 --- a/polyscript.go +++ b/polyscript.go @@ -1,25 +1,27 @@ package polyscript import ( - "fmt" "log/slog" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" - "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" - "github.com/robbyt/go-polyscript/execution/script" "github.com/robbyt/go-polyscript/execution/script/loader" - "github.com/robbyt/go-polyscript/machines" - extismCompiler "github.com/robbyt/go-polyscript/machines/extism/compiler" - risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" - starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler" - "github.com/robbyt/go-polyscript/machines/types" + extismMachine "github.com/robbyt/go-polyscript/machines/extism" + risorMachine "github.com/robbyt/go-polyscript/machines/risor" + starlarkMachine "github.com/robbyt/go-polyscript/machines/starlark" ) // FromExtismFile creates an Extism evaluator from a WASM file -func FromExtismFile(filePath string, opts ...any) (engine.EvaluatorWithPrep, error) { - return fromFileLoader(types.Extism, filePath, opts...) +func FromExtismFile( + filePath string, + logHandler slog.Handler, + entryPoint string, +) (engine.EvaluatorWithPrep, error) { + l, err := loader.NewFromDisk(filePath) + if err != nil { + return nil, err + } + + return extismMachine.FromExtismLoader(logHandler, l, entryPoint) } // FromExtismFileWithData creates an Extism evaluator with both static and dynamic data capabilities. @@ -41,24 +43,17 @@ func FromExtismFileWithData( return nil, err } - // Create an evaluator with specific options - cfg := options.DefaultConfig(types.Extism) - cfg.SetHandler(logHandler) - cfg.SetLoader(l) - - // Set the data provider - cfg.SetDataProvider(buildCompositeDataProvider(staticData)) - - // Create an evaluator using our custom createExtismEvaluator function - return createExtismEvaluator( - cfg, - []extismCompiler.FunctionalOption{extismCompiler.WithEntryPoint(entryPoint)}, - ) + return extismMachine.FromExtismLoaderWithData(logHandler, l, staticData, entryPoint) } // FromRisorFile creates a Risor evaluator from a .risor file -func FromRisorFile(filePath string, opts ...any) (engine.EvaluatorWithPrep, error) { - return fromFileLoader(types.Risor, filePath, opts...) +func FromRisorFile(filePath string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { + l, err := loader.NewFromDisk(filePath) + if err != nil { + return nil, err + } + + return risorMachine.FromRisorLoader(logHandler, l) } // FromRisorFileWithData creates an Risor evaluator with both static and dynamic data capabilities. @@ -78,24 +73,17 @@ func FromRisorFileWithData( return nil, err } - // Create an evaluator with specific options - cfg := options.DefaultConfig(types.Risor) - cfg.SetHandler(logHandler) - cfg.SetLoader(l) - - // Set the data provider - cfg.SetDataProvider(buildCompositeDataProvider(staticData)) - - // Create an evaluator using our custom createRisorEvaluator function - return createRisorEvaluator( - cfg, - []risorCompiler.FunctionalOption{risorCompiler.WithGlobals([]string{constants.Ctx})}, - ) + return risorMachine.FromRisorLoaderWithData(logHandler, l, staticData) } // FromRisorString creates a Risor evaluator from a script string -func FromRisorString(content string, opts ...any) (engine.EvaluatorWithPrep, error) { - return fromStringLoader(types.Risor, content, opts...) +func FromRisorString(content string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { + l, err := loader.NewFromString(content) + if err != nil { + return nil, err + } + + return risorMachine.FromRisorLoader(logHandler, l) } // FromRisorStringWithData creates a Risor evaluator with both static and dynamic data capabilities @@ -115,24 +103,17 @@ func FromRisorStringWithData( return nil, err } - // Create an evaluator with specific options - cfg := options.DefaultConfig(types.Risor) - cfg.SetHandler(logHandler) - cfg.SetLoader(l) - - // Set the data provider - cfg.SetDataProvider(buildCompositeDataProvider(staticData)) - - // Create an evaluator using our custom createRisorEvaluator function - return createRisorEvaluator( - cfg, - []risorCompiler.FunctionalOption{risorCompiler.WithGlobals([]string{constants.Ctx})}, - ) + return risorMachine.FromRisorLoaderWithData(logHandler, l, staticData) } // FromStarlarkFile creates a Starlark evaluator from a .star file -func FromStarlarkFile(filePath string, opts ...any) (engine.EvaluatorWithPrep, error) { - return fromFileLoader(types.Starlark, filePath, opts...) +func FromStarlarkFile(filePath string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { + l, err := loader.NewFromDisk(filePath) + if err != nil { + return nil, err + } + + return starlarkMachine.FromStarlarkLoader(logHandler, l) } // FromStarlarkFileWithData creates a Starlark evaluator with both static and dynamic data capabilities. @@ -152,24 +133,17 @@ func FromStarlarkFileWithData( return nil, err } - // Create an evaluator with specific options - cfg := options.DefaultConfig(types.Starlark) - cfg.SetHandler(logHandler) - cfg.SetLoader(l) - - // Set the data provider - cfg.SetDataProvider(buildCompositeDataProvider(staticData)) - - // Create an evaluator using our custom createStarlarkEvaluator function - return createStarlarkEvaluator( - cfg, - []starlarkCompiler.FunctionalOption{starlarkCompiler.WithGlobals([]string{constants.Ctx})}, - ) + return starlarkMachine.FromStarlarkLoaderWithData(logHandler, l, staticData) } // FromStarlarkString creates a Starlark evaluator from a script string -func FromStarlarkString(content string, opts ...any) (engine.EvaluatorWithPrep, error) { - return fromStringLoader(types.Starlark, content, opts...) +func FromStarlarkString(content string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { + l, err := loader.NewFromString(content) + if err != nil { + return nil, err + } + + return starlarkMachine.FromStarlarkLoader(logHandler, l) } // FromStarlarkStringWithData creates a Starlark evaluator with both static and dynamic data @@ -190,335 +164,5 @@ func FromStarlarkStringWithData( return nil, err } - // Create an evaluator with specific options - cfg := options.DefaultConfig(types.Starlark) - cfg.SetHandler(logHandler) - cfg.SetLoader(l) - - // Set the data provider - cfg.SetDataProvider(buildCompositeDataProvider(staticData)) - - // Create an evaluator using our custom createStarlarkEvaluator function - return createStarlarkEvaluator( - cfg, - []starlarkCompiler.FunctionalOption{starlarkCompiler.WithGlobals([]string{constants.Ctx})}, - ) -} - -// fromFileLoader creates an evaluator from a file path using the specified machine type -func fromFileLoader( - machineType types.Type, - filePath string, - opts ...any, -) (engine.EvaluatorWithPrep, error) { - // Create a file loader - l, err := loader.NewFromDisk(filePath) - if err != nil { - return nil, err - } - - // Combine options, adding the loader - allOpts := append([]any{options.WithLoader(l)}, opts...) - - return NewEvaluator(machineType, allOpts...) -} - -// fromStringLoader creates an evaluator from a string content using the specified machine type -func fromStringLoader( - machineType types.Type, - content string, - opts ...any, -) (engine.EvaluatorWithPrep, error) { - if machineType == types.Extism { - return nil, fmt.Errorf("extism does not currently support string loaders") - } - - // Create a string loader - l, err := loader.NewFromString(content) - if err != nil { - return nil, err - } - - // Combine options, adding the loader - allOpts := append([]any{options.WithLoader(l)}, opts...) - - return NewEvaluator(machineType, allOpts...) -} - -// NewEvaluator creates a new evaluator for the specified machine type. -// -// This function initializes a configuration with machine-specific defaults, -// applies the provided options to customize the configuration, validates the -// resulting configuration, and then creates an evaluator using the finalized -// configuration. -// -// Parameters: -// - machineType: The type of machine (e.g., Extism, Risor, Starlark) for which -// the evaluator is being created. -// - opts: A variadic list of options to customize the evaluator's configuration. -// These can be either engine.Option or machine-specific options. -// -// Returns: -// - engine.EvaluatorWithPrep: The created evaluator, which includes preparation -// capabilities for runtime data. -// - error: An error if the configuration is invalid or if the evaluator creation -// fails. -// -// Example usage: -// -// evaluator, err := NewEvaluator(types.Risor, options.WithLoader(loader), risor.WithCtxGlobal()) -// if err != nil { -// log.Fatalf("Failed to create evaluator: %v", err) -// } -func NewEvaluator( - machineType types.Type, - opts ...any, -) (engine.EvaluatorWithPrep, error) { - switch machineType { - case types.Extism: - return NewExtismEvaluator(opts...) - case types.Risor: - return NewRisorEvaluator(opts...) - case types.Starlark: - return NewStarlarkEvaluator(opts...) - default: - return nil, fmt.Errorf("unsupported machine type: %s", machineType) - } -} - -// NewExtismEvaluator creates a new evaluator for Extism WASM -func NewExtismEvaluator(opts ...any) (engine.EvaluatorWithPrep, error) { - // First create a config with the engine options - cfg := options.DefaultConfig(types.Extism) - - // Separate engine options from machine options - var engineOpts []options.Option - var machineOpts []extismCompiler.FunctionalOption - - for _, opt := range opts { - switch o := opt.(type) { - case options.Option: - engineOpts = append(engineOpts, o) - case extismCompiler.FunctionalOption: - machineOpts = append(machineOpts, o) - default: - return nil, fmt.Errorf("unsupported option type: %T", opt) - } - } - - // Apply all engine options - for _, opt := range engineOpts { - if err := opt(cfg); err != nil { - return nil, fmt.Errorf("error applying option: %w", err) - } - } - - // Validate configuration - if err := cfg.Validate(); err != nil { - return nil, err - } - - // Create the evaluator using our helper - return createExtismEvaluator(cfg, machineOpts) -} - -// createExtismEvaluator creates an Extism evaluator with the given options -func createExtismEvaluator( - cfg *options.Config, - compilerOptions []extismCompiler.FunctionalOption, -) (engine.EvaluatorWithPrep, error) { - // Create compiler using machine-specific factory function - compiler, err := machines.NewExtismCompiler(compilerOptions...) - if err != nil { - return nil, fmt.Errorf("failed to create Extism compiler: %w", err) - } - - // Create executable unit ID from source URL - execUnitID := "" - sourceURL := cfg.GetLoader().GetSourceURL() - if sourceURL != nil { - execUnitID = sourceURL.String() - } - - // Create executable unit (to compile and prepare the script) - execUnit, err := script.NewExecutableUnit( - cfg.GetHandler(), - execUnitID, - cfg.GetLoader(), - compiler, - cfg.GetDataProvider(), - ) - if err != nil { - return nil, err - } - - // Create the machine-specific evaluator - machineEvaluator, err := machines.NewEvaluator(cfg.GetHandler(), execUnit) - if err != nil { - return nil, err - } - - // Wrap the evaluator to store the executable unit - return NewEvaluatorWrapper(machineEvaluator, execUnit), nil -} - -// NewRisorEvaluator creates a new evaluator for Risor scripts -func NewRisorEvaluator(opts ...any) (engine.EvaluatorWithPrep, error) { - // First create a config with the engine options - cfg := options.DefaultConfig(types.Risor) - - // Separate engine options from machine options - var engineOpts []options.Option - var machineOpts []risorCompiler.FunctionalOption - - for _, opt := range opts { - switch o := opt.(type) { - case options.Option: - engineOpts = append(engineOpts, o) - case risorCompiler.FunctionalOption: - machineOpts = append(machineOpts, o) - default: - return nil, fmt.Errorf("unsupported option type: %T", opt) - } - } - - // Apply all engine options - for _, opt := range engineOpts { - if err := opt(cfg); err != nil { - return nil, fmt.Errorf("error applying option: %w", err) - } - } - - // Validate configuration - if err := cfg.Validate(); err != nil { - return nil, err - } - - // Create the evaluator using our helper - return createRisorEvaluator(cfg, machineOpts) -} - -// createRisorEvaluator creates a Risor evaluator with the given options -func createRisorEvaluator( - cfg *options.Config, - compilerOptions []risorCompiler.FunctionalOption, -) (engine.EvaluatorWithPrep, error) { - // Create compiler using machine-specific factory function - compiler, err := machines.NewRisorCompiler(compilerOptions...) - if err != nil { - return nil, fmt.Errorf("failed to create Risor compiler: %w", err) - } - - // Create executable unit ID from source URL - execUnitID := "" - sourceURL := cfg.GetLoader().GetSourceURL() - if sourceURL != nil { - execUnitID = sourceURL.String() - } - - // Create executable unit (to compile and prepare the script) - execUnit, err := script.NewExecutableUnit( - cfg.GetHandler(), - execUnitID, - cfg.GetLoader(), - compiler, - cfg.GetDataProvider(), - ) - if err != nil { - return nil, err - } - - // Create the machine-specific evaluator - machineEvaluator, err := machines.NewEvaluator(cfg.GetHandler(), execUnit) - if err != nil { - return nil, err - } - - // Wrap the evaluator to store the executable unit - return NewEvaluatorWrapper(machineEvaluator, execUnit), nil -} - -// NewStarlarkEvaluator creates a new evaluator for Starlark scripts -func NewStarlarkEvaluator(opts ...any) (engine.EvaluatorWithPrep, error) { - // First create a config with the engine options - cfg := options.DefaultConfig(types.Starlark) - - // Separate engine options from machine options - var engineOpts []options.Option - var machineOpts []starlarkCompiler.FunctionalOption - - for _, opt := range opts { - switch o := opt.(type) { - case options.Option: - engineOpts = append(engineOpts, o) - case starlarkCompiler.FunctionalOption: - machineOpts = append(machineOpts, o) - default: - return nil, fmt.Errorf("unsupported option type: %T", opt) - } - } - - // Apply all engine options - for _, opt := range engineOpts { - if err := opt(cfg); err != nil { - return nil, fmt.Errorf("error applying option: %w", err) - } - } - - // Validate configuration - if err := cfg.Validate(); err != nil { - return nil, err - } - - // Create the evaluator using our helper - return createStarlarkEvaluator(cfg, machineOpts) -} - -// createStarlarkEvaluator creates a Starlark evaluator with the given options -func createStarlarkEvaluator( - cfg *options.Config, - compilerOptions []starlarkCompiler.FunctionalOption, -) (engine.EvaluatorWithPrep, error) { - // Create compiler using machine-specific factory function - compiler, err := machines.NewStarlarkCompiler(compilerOptions...) - if err != nil { - return nil, fmt.Errorf("failed to create Starlark compiler: %w", err) - } - - // Create executable unit ID from source URL - execUnitID := "" - sourceURL := cfg.GetLoader().GetSourceURL() - if sourceURL != nil { - execUnitID = sourceURL.String() - } - - // Create executable unit (to compile and prepare the script) - execUnit, err := script.NewExecutableUnit( - cfg.GetHandler(), - execUnitID, - cfg.GetLoader(), - compiler, - cfg.GetDataProvider(), - ) - if err != nil { - return nil, err - } - - // Create the machine-specific evaluator - machineEvaluator, err := machines.NewEvaluator(cfg.GetHandler(), execUnit) - if err != nil { - return nil, err - } - - // Wrap the evaluator to store the executable unit - return NewEvaluatorWrapper(machineEvaluator, execUnit), nil -} - -// buildCompositeDataProvider creates a composite data provider from a static data map -func buildCompositeDataProvider( - staticData map[string]any, -) data.Provider { - staticProvider := data.NewStaticProvider(staticData) - dynamicProvider := data.NewContextProvider(constants.EvalData) - return data.NewCompositeProvider(staticProvider, dynamicProvider) + return starlarkMachine.FromStarlarkLoaderWithData(logHandler, l, staticData) } diff --git a/polyscript_test.go b/polyscript_test.go index cc42878..e41c7d4 100644 --- a/polyscript_test.go +++ b/polyscript_test.go @@ -14,14 +14,9 @@ import ( "testing" "github.com/robbyt/go-polyscript/engine" - "github.com/robbyt/go-polyscript/engine/options" "github.com/robbyt/go-polyscript/execution/constants" - "github.com/robbyt/go-polyscript/execution/data" "github.com/robbyt/go-polyscript/execution/script/loader" - extismCompiler "github.com/robbyt/go-polyscript/machines/extism/compiler" "github.com/robbyt/go-polyscript/machines/mocks" - risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler" - starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler" "github.com/robbyt/go-polyscript/machines/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -36,14 +31,6 @@ func getLogger() slog.Handler { return slog.NewTextHandler(os.Stdout, nil) } -// withCompositeProvider creates a composite provider with static data -func withCompositeProvider(staticData map[string]any) any { - return options.WithDataProvider(data.NewCompositeProvider( - data.NewStaticProvider(staticData), - data.NewContextProvider(constants.Ctx), - )) -} - // mockPreparer implements engine.EvalDataPreparer for testing type mockPreparer struct { mock.Mock @@ -113,153 +100,27 @@ func TestMachineEvaluators(t *testing.T) { name string content string machineType types.Type - creator func(opts ...any) (engine.EvaluatorWithPrep, error) - options []any + creator func(string, slog.Handler) (engine.EvaluatorWithPrep, error) }{ { - name: "NewStarlarkEvaluator", + name: "FromStarlarkString", content: `print("Hello, World!")`, machineType: types.Starlark, - creator: NewStarlarkEvaluator, - options: []any{ - options.WithDefaults(), - }, + creator: FromStarlarkString, }, { - name: "NewRisorEvaluator", + name: "FromRisorString", content: `print("Hello, World!")`, machineType: types.Risor, - creator: NewRisorEvaluator, - options: []any{ - options.WithDefaults(), - }, - }, - } - - for _, tc := range tests { - tc := tc // Capture for parallel execution - t.Run(tc.name, func(t *testing.T) { - // Create a loader - l, err := loader.NewFromString(tc.content) - require.NoError(t, err) - - // Combine options with loader - opts := append( - []any{ - options.WithLoader(l), - options.WithLogHandler(getLogger()), - }, - tc.options..., - ) - - // Create evaluator - evaluator, err := tc.creator(opts...) - require.NoError(t, err) - require.NotNil(t, evaluator) - }) - } -} - -func TestNewEvaluator(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - machineType types.Type - options []any - expectError bool - errorMsg string - }{ - { - name: "Valid Starlark", - machineType: types.Starlark, - options: []any{ - options.WithLoader(func() loader.Loader { - l, err := loader.NewFromString("print('test')") - require.NoError(t, err) - return l - }()), - options.WithLogHandler(getLogger()), - }, - expectError: false, - }, - { - name: "Valid Risor", - machineType: types.Risor, - options: []any{ - options.WithLoader(func() loader.Loader { - l, err := loader.NewFromString("print('test')") - require.NoError(t, err) - return l - }()), - options.WithLogHandler(getLogger()), - }, - expectError: false, - }, - { - name: "No Loader", - machineType: types.Starlark, - options: []any{ - options.WithLogHandler(getLogger()), - }, - expectError: true, - errorMsg: "no loader specified", - }, - { - name: "Invalid Option", - machineType: types.Starlark, - options: []any{ - options.WithLoader(func() loader.Loader { - l, err := loader.NewFromString("print('test')") - require.NoError(t, err) - return l - }()), - func(cfg *options.Config) error { - return errors.New("invalid option") - }, - }, - expectError: true, - errorMsg: "unsupported option type", - }, - { - name: "Option Type Test", - machineType: types.Risor, - options: []any{ - options.WithLoader(func() loader.Loader { - l, err := loader.NewFromString("print('test')") - require.NoError(t, err) - return l - }()), - }, - expectError: false, + creator: FromRisorString, }, } for _, tc := range tests { tc := tc // Capture for parallel execution t.Run(tc.name, func(t *testing.T) { - var evaluator engine.EvaluatorWithPrep - var err error - - switch tc.machineType { - case types.Starlark: - evaluator, err = NewStarlarkEvaluator(tc.options...) - case types.Risor: - evaluator, err = NewRisorEvaluator(tc.options...) - case types.Extism: - evaluator, err = NewExtismEvaluator(tc.options...) - default: - t.Fatalf("unsupported machine type: %s", tc.machineType) - } - - if tc.expectError { - require.Error(t, err) - if tc.errorMsg != "" { - assert.Contains(t, err.Error(), tc.errorMsg) - } - return - } - + // Create evaluator directly with content and logger + evaluator, err := tc.creator(tc.content, getLogger()) require.NoError(t, err) require.NotNil(t, evaluator) }) @@ -272,36 +133,36 @@ func TestFromStringLoaders(t *testing.T) { tests := []struct { name string content string - creator func(content string, opts ...any) (engine.EvaluatorWithPrep, error) - options []any + creator func(string, slog.Handler) (engine.EvaluatorWithPrep, error) + logHandler slog.Handler expectError bool }{ { name: "FromStarlarkString - Valid", content: `print("Hello, World!")`, creator: FromStarlarkString, - options: []any{starlarkCompiler.WithGlobals([]string{"ctx"})}, + logHandler: getLogger(), expectError: false, }, { name: "FromRisorString - Valid", content: `print("Hello, World!")`, creator: FromRisorString, - options: []any{risorCompiler.WithGlobals([]string{"ctx"})}, + logHandler: getLogger(), expectError: false, }, { name: "FromStarlarkString - Empty", content: "", creator: FromStarlarkString, - options: []any{}, + logHandler: getLogger(), expectError: true, }, { name: "FromRisorString - Empty", content: "", creator: FromRisorString, - options: []any{}, + logHandler: getLogger(), expectError: true, }, } @@ -309,7 +170,7 @@ func TestFromStringLoaders(t *testing.T) { for _, tc := range tests { tc := tc // Capture for parallel execution t.Run(tc.name, func(t *testing.T) { - evaluator, err := tc.creator(tc.content, tc.options...) + evaluator, err := tc.creator(tc.content, tc.logHandler) if tc.expectError { require.Error(t, err) @@ -320,20 +181,6 @@ func TestFromStringLoaders(t *testing.T) { require.NotNil(t, evaluator) }) } - - // Skip the Extism string loader test - covered by design - - // Test invalid option in string loader - t.Run("FromRisorString - Invalid Option", func(t *testing.T) { - _, err := FromRisorString( - "print('test')", - func(cfg *options.Config) error { - return errors.New("invalid option test") - }, - ) - require.Error(t, err) - assert.Contains(t, err.Error(), "unsupported option type") - }) } func TestFromFileLoaders(t *testing.T) { @@ -360,90 +207,90 @@ _ = result` err = os.WriteFile(starlarkPath, []byte(starlarkContent), 0o644) require.NoError(t, err) - tests := []struct { - name string - loaderFunc func(string, ...any) (engine.EvaluatorWithPrep, error) - filePath string - options []any - expectError bool - }{ - { - name: "FromExtismFile - Valid", - loaderFunc: FromExtismFile, - filePath: wasmPath, - options: []any{ - options.WithLogHandler(getLogger()), - extismCompiler.WithEntryPoint("greet"), - options.WithDataProvider(data.NewStaticProvider(map[string]any{ - "input": "Test User", // Required for WASM execution - })), - }, - expectError: false, - }, - { - name: "FromExtismFile - Invalid Path", - loaderFunc: FromExtismFile, - filePath: "non-existent-file.wasm", - options: []any{}, - expectError: true, - }, - { - name: "FromRisorFile - Valid", - loaderFunc: FromRisorFile, - filePath: risorPath, - options: []any{ - options.WithLogHandler(getLogger()), - risorCompiler.WithGlobals([]string{"ctx"}), - }, - expectError: false, - }, - { - name: "FromRisorFile - Invalid Path", - loaderFunc: FromRisorFile, - filePath: "non-existent-file.risor", - options: []any{}, - expectError: true, - }, - { - name: "FromStarlarkFile - Valid", - loaderFunc: FromStarlarkFile, - filePath: starlarkPath, - options: []any{ - options.WithLogHandler(getLogger()), - starlarkCompiler.WithGlobals([]string{"ctx"}), - }, - expectError: false, - }, - { - name: "FromStarlarkFile - Invalid Path", - loaderFunc: FromStarlarkFile, - filePath: "non-existent-file.star", - options: []any{}, - expectError: true, - }, - } + // Setup the logger handler + logHandler := getLogger() - for _, tc := range tests { - tc := tc // Capture for parallel execution - t.Run(tc.name, func(t *testing.T) { - evaluator, err := tc.loaderFunc(tc.filePath, tc.options...) + t.Run("FromExtismFile - Valid", func(t *testing.T) { + evaluator, err := FromExtismFile(wasmPath, logHandler, "greet") + require.NoError(t, err) + require.NotNil(t, evaluator) - if tc.expectError { - require.Error(t, err) - return - } + // For Extism, we need to test with correct input data + // Create a context with the input data directly + ctx := context.Background() + ctx = context.WithValue(ctx, constants.EvalData, map[string]any{ + "input": "Test User", + }) - require.NoError(t, err) - require.NotNil(t, evaluator) + // Evaluate with the context containing input data + result, err := evaluator.Eval(ctx) + require.NoError(t, err) + require.NotNil(t, result) + }) - // For valid evaluators, test basic execution only for non-Extism types - if !tc.expectError && tc.name != "FromExtismFile - Valid" { - result, evalErr := evaluator.Eval(context.Background()) - require.NoError(t, evalErr) - require.NotNil(t, result) - } - }) - } + t.Run("FromExtismFile - Invalid Path", func(t *testing.T) { + _, err := FromExtismFile("non-existent-file.wasm", logHandler, "greet") + require.Error(t, err) + }) + + t.Run("FromExtismFileWithData - Valid", func(t *testing.T) { + staticData := map[string]any{ + "input": "Test User", // Required for WASM execution + } + evaluator, err := FromExtismFileWithData(wasmPath, staticData, logHandler, "greet") + require.NoError(t, err) + require.NotNil(t, evaluator) + }) + + t.Run("FromRisorFile - Valid", func(t *testing.T) { + evaluator, err := FromRisorFile(risorPath, logHandler) + require.NoError(t, err) + require.NotNil(t, evaluator) + + // Basic execution + result, err := evaluator.Eval(context.Background()) + require.NoError(t, err) + require.NotNil(t, result) + }) + + t.Run("FromRisorFile - Invalid Path", func(t *testing.T) { + _, err := FromRisorFile("non-existent-file.risor", logHandler) + require.Error(t, err) + }) + + t.Run("FromRisorFileWithData - Valid", func(t *testing.T) { + staticData := map[string]any{ + "test_key": "test_value", + } + evaluator, err := FromRisorFileWithData(risorPath, staticData, logHandler) + require.NoError(t, err) + require.NotNil(t, evaluator) + }) + + t.Run("FromStarlarkFile - Valid", func(t *testing.T) { + evaluator, err := FromStarlarkFile(starlarkPath, logHandler) + require.NoError(t, err) + require.NotNil(t, evaluator) + + // Basic execution + result, err := evaluator.Eval(context.Background()) + require.NoError(t, err) + require.NotNil(t, result) + }) + + t.Run("FromStarlarkFile - Invalid Path", func(t *testing.T) { + _, err := FromStarlarkFile("non-existent-file.star", logHandler) + require.Error(t, err) + }) + + t.Run("FromStarlarkFileWithData - Valid", func(t *testing.T) { + staticData := map[string]any{ + "test_key": "test_value", + } + evaluator, err := FromStarlarkFileWithData(starlarkPath, staticData, logHandler) + require.NoError(t, err) + require.NotNil(t, evaluator) + }) } func TestDataProviders(t *testing.T) { @@ -456,13 +303,11 @@ func TestDataProviders(t *testing.T) { // Create static data staticData := map[string]any{ "static_key": "static_value", - } - - // Create an evaluator with composite provider - evaluator, err := FromStarlarkString( + } // Create an evaluator with composite provider + evaluator, err := FromStarlarkStringWithData( script, - withCompositeProvider(staticData), - starlarkCompiler.WithGlobals([]string{constants.Ctx}), + staticData, + getLogger(), ) require.NoError(t, err) require.NotNil(t, evaluator) @@ -485,20 +330,18 @@ func TestEvalHelpers(t *testing.T) { t.Run("PrepareAndEval", func(t *testing.T) { // Create a simple Risor evaluator script := ` - name := ctx["input_data"]["name"] - { - "message": "Hello, " + name + "!", - "length": len(name) - } - ` + name := ctx["input_data"]["name"] + { + "message": "Hello, " + name + "!", + "length": len(name) + } + ` // Create an evaluator with the CompositeProvider - evaluator, err := FromRisorString( + evaluator, err := FromRisorStringWithData( script, - options.WithDefaults(), - options.WithLogHandler(getLogger()), - withCompositeProvider(map[string]any{}), - risorCompiler.WithGlobals([]string{constants.Ctx}), + map[string]any{}, + getLogger(), ) require.NoError(t, err) @@ -569,8 +412,10 @@ func TestEvalHelpers(t *testing.T) { data := map[string]any{"name": "World"} // Mock PrepareContext to succeed - //nolint - enrichedCtx := context.WithValue(ctx, "test-key", "test-value") + // Define a type for context keys to avoid linting warnings + type contextKey string + testKey := contextKey("test-key") + enrichedCtx := context.WithValue(ctx, testKey, "test-value") mockPrepCtx.On("PrepareContext", ctx, []any{data}).Return(enrichedCtx, nil) // Mock Eval to fail @@ -598,18 +443,17 @@ func TestEvalHelpers(t *testing.T) { t.Run("EvalAndExtractMap", func(t *testing.T) { // Create a simple Risor evaluator script := ` - { - "message": "Hello, Static!", - "length": 12 - } - ` + { + "message": "Hello, Static!", + "length": 12 + } + ` // Create an evaluator - evaluator, err := FromRisorString( + evaluator, err := FromRisorStringWithData( script, - options.WithDefaults(), - options.WithLogHandler(getLogger()), - risorCompiler.WithGlobals([]string{constants.Ctx}), + map[string]any{}, + getLogger(), ) require.NoError(t, err) @@ -636,9 +480,7 @@ func TestEvalHelpers(t *testing.T) { nilScript := `nil` nilEvaluator, err := FromRisorString( nilScript, - options.WithDefaults(), - options.WithLogHandler(getLogger()), - risorCompiler.WithGlobals([]string{constants.Ctx}), + getLogger(), ) require.NoError(t, err) @@ -650,9 +492,7 @@ func TestEvalHelpers(t *testing.T) { numScript := `42` numEvaluator, err := FromRisorString( numScript, - options.WithDefaults(), - options.WithLogHandler(getLogger()), - risorCompiler.WithGlobals([]string{constants.Ctx}), + getLogger(), ) require.NoError(t, err) @@ -678,7 +518,7 @@ func TestEvalHelpers(t *testing.T) { }) } -func TestMachineWithData(t *testing.T) { +func TestDataIntegrationScenarios(t *testing.T) { t.Parallel() // Create a temporary directory for test files @@ -694,8 +534,8 @@ func TestMachineWithData(t *testing.T) { // Create a basic Risor script that uses context risorFileContent := `// Get data from context { - "message": "Hello, " + ctx["input_data"]["name"] + " (v" + ctx["app_version"] + ")", - "timeout": ctx["config"]["timeout"] + "message": "Hello, " + ctx["input_data"]["name"] + " (v" + ctx["app_version"] + ")", + "timeout": ctx["config"]["timeout"] }` err = os.WriteFile(risorPath, []byte(risorFileContent), 0o644) require.NoError(t, err) @@ -718,24 +558,28 @@ _ = result` }, } - t.Run("FromRisorStringWithData", func(t *testing.T) { + t.Run("RisorWithData", func(t *testing.T) { // Test script risorScript := ` - // Access static data - version := ctx["app_version"] - timeout := ctx["config"]["timeout"] - - // Access dynamic data - name := ctx["input_data"]["name"] - - { - "message": "Hello, " + name + " (v" + version + ")", - "timeout": timeout - } - ` - - // Create evaluator - risorEval, err := FromRisorStringWithData(risorScript, staticData, getLogger()) + // Access static data + version := ctx["app_version"] + timeout := ctx["config"]["timeout"] + + // Access dynamic data + name := ctx["input_data"]["name"] + + { + "message": "Hello, " + name + " (v" + version + ")", + "timeout": timeout + } + ` + + // Create evaluator with static data + risorEval, err := FromRisorStringWithData( + risorScript, + staticData, + getLogger(), + ) require.NoError(t, err) // Test with dynamic data @@ -764,10 +608,10 @@ _ = result` } }) - t.Run("FromStarlarkStringWithData", func(t *testing.T) { - // Create evaluator - starlarkEval, err := FromStarlarkStringWithData( - starlarkFileContent, + t.Run("StarlarkWithData", func(t *testing.T) { + // Create evaluator with static data + starlarkEval, err := FromStarlarkFileWithData( + starlarkPath, staticData, getLogger(), ) @@ -792,13 +636,17 @@ _ = result` assert.Equal(t, int64(30), starlarkTimeout, "timeout should be 30") }) - t.Run("FromExtismFileWithData", func(t *testing.T) { + t.Run("ExtismWithData", func(t *testing.T) { // Create evaluator with static data that includes input + staticDataWithInput := map[string]any{ + "input": "Test User", + } + extismEval, err := FromExtismFileWithData( wasmPath, - map[string]any{"input": "Test User"}, + staticDataWithInput, getLogger(), - "greet", // entry point + "greet", ) require.NoError(t, err) require.NotNil(t, extismEval) @@ -816,11 +664,17 @@ _ = result` assert.Equal(t, "Hello, Test User!", resultMap["greeting"]) // Test evaluator with no input (should fail) + // Create a copy of staticData without input field + configOnlyData := map[string]any{ + "app_version": staticData["app_version"], + "config": staticData["config"], + } + extismEvalNoInput, err := FromExtismFileWithData( wasmPath, - staticData, // Only static config data, no input + configOnlyData, getLogger(), - "greet", // entry point + "greet", ) require.NoError(t, err) require.NotNil(t, extismEvalNoInput) @@ -831,100 +685,7 @@ _ = result` }) } -func TestFileWithDataFunctions(t *testing.T) { - t.Parallel() - - // Create temporary test files - tmpDir := t.TempDir() - - // Create test files to use for testing - risorPath := filepath.Join(tmpDir, "test.risor") - risorContent := `{ "message": "Hello from Risor!" }` - err := os.WriteFile(risorPath, []byte(risorContent), 0o644) - require.NoError(t, err) - - starlarkPath := filepath.Join(tmpDir, "test.star") - starlarkContent := `result = {"message": "Hello from Starlark!"}\n_ = result` - err = os.WriteFile(starlarkPath, []byte(starlarkContent), 0o644) - require.NoError(t, err) - - // Test FromRisorFileWithData - t.Run("FromRisorFileWithData", func(t *testing.T) { - logger := getLogger() - staticData := map[string]any{"test": "data"} - - // This just needs to call the function, even if execution would fail later - _, err := FromRisorFileWithData(risorPath, staticData, logger) - // We don't assert on the result since we just want to cover the function - _ = err - }) - - // Test FromStarlarkFileWithData - t.Run("FromStarlarkFileWithData", func(t *testing.T) { - logger := getLogger() - staticData := map[string]any{"test": "data"} - - // This just needs to call the function, even if execution would fail later - _, err := FromStarlarkFileWithData(starlarkPath, staticData, logger) - // We don't assert on the result since we just want to cover the function - _ = err - }) -} - -func TestFromStringLoader(t *testing.T) { - t.Parallel() - - // Test the Extism string loader error case directly - t.Run("ExtismStringNotSupported", func(t *testing.T) { - // We can't call it directly, so we'll make our own version - // that's similar to what FromExtismString would look like - // if it existed, but just enough to test the error branch - content := "test" - l, err := loader.NewFromString(content) - require.NoError(t, err) - - // Create the options with the string loader - opts := []any{options.WithLoader(l)} - - // Create Extism evaluator, which should fail - _, err = NewExtismEvaluator(opts...) - // We just want to make sure it errors out - require.Error(t, err) - }) -} - -func TestCreateEvaluatorEdgeCases2(t *testing.T) { - t.Parallel() - - // Test a case where source URL is nil - t.Run("NilSourceURL", func(t *testing.T) { - // Create a minimal mock loader with nil URL - mockLoader := &mockLoader{} - - // Create an evaluator with this loader - _, err := NewRisorEvaluator( - options.WithLoader(mockLoader), - options.WithDefaults(), - ) - - // Because we specified risorCompiler.WithGlobals, we'll get compiler options error - require.Error(t, err) - }) -} - -// mockLoader is a simple implementation of loader.Loader that's just enough to test -// the nil source URL case -type mockLoader struct{} - -func (m *mockLoader) GetReader() (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("return 0")), nil -} - -func (m *mockLoader) GetSourceURL() *url.URL { - return nil -} - -func TestNewExtismEvaluator(t *testing.T) { +func TestFromExtismFile(t *testing.T) { t.Parallel() // Create a temporary directory for the WASM file @@ -938,21 +699,12 @@ func TestNewExtismEvaluator(t *testing.T) { // Create a logger handler handler := getLogger() - // Create an evaluator with file loader - evaluator, err := NewExtismEvaluator( - options.WithDefaults(), - options.WithLoader( - func() loader.Loader { - loader, err := loader.NewFromDisk(wasmPath) - require.NoError(t, err) - return loader - }(), - ), - options.WithLogHandler(handler), - options.WithDataProvider(data.NewStaticProvider(map[string]any{ - "input": "Test User", // Put the input directly at the top level - })), - extismCompiler.WithEntryPoint("greet"), + // Create an evaluator with file loader and static data + evaluator, err := FromExtismFileWithData( + wasmPath, + map[string]any{"input": "Test User"}, + handler, + "greet", ) require.NoError(t, err) @@ -976,24 +728,59 @@ func TestNewExtismEvaluator(t *testing.T) { func TestCreateEvaluatorEdgeCases(t *testing.T) { t.Parallel() - // Test validation error in newEvaluator - t.Run("Configuration Validation Error", func(t *testing.T) { - // Try to create an evaluator without a loader - _, err := NewRisorEvaluator() + // Test error with empty script content + t.Run("Empty Script Content Error", func(t *testing.T) { + // Try to create an evaluator with empty script + _, err := FromRisorString("", getLogger()) require.Error(t, err) - assert.Contains(t, err.Error(), "no loader specified") + assert.Contains(t, err.Error(), "content is empty") }) - // Test option application error - t.Run("Option Error", func(t *testing.T) { - // Create an invalid option that returns an error - invalidOption := func(cfg *options.Config) error { - return errors.New("custom invalid option error") - } + // Test invalid path error + t.Run("Invalid Path Error", func(t *testing.T) { + // Try to create an evaluator with non-existent file + _, err := FromRisorFile("/path/does/not/exist.risor", getLogger()) + require.Error(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + }) + + // Test with invalid script content + t.Run("InvalidScriptTest", func(t *testing.T) { + // Try to create an evaluator with invalid script content + _, err := FromRisorString("this is not valid risor code }{", getLogger()) - // This should fail when applying the option - _, err := NewRisorEvaluator(invalidOption) + // Should return an error when trying to compile invalid code require.Error(t, err) - assert.Contains(t, err.Error(), "unsupported option type") + assert.Contains(t, err.Error(), "compile") + }) +} + +// MockStringLoader is a simple implementation of loader.Loader using a string +type MockStringLoader struct{} + +func (m *MockStringLoader) GetReader() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("return 0")), nil +} + +func (m *MockStringLoader) GetSourceURL() *url.URL { + return nil +} + +func TestFromStringLoader(t *testing.T) { + t.Parallel() + + // Test the Extism string loader error case directly + t.Run("ExtismStringNotSupported", func(t *testing.T) { + // Just test if a hypothetical FromExtismString would have issues + // For now, we'll simulate this by testing if we can create a string loader + content := "test" + l, err := loader.NewFromString(content) + require.NoError(t, err) + + // Since we know Extism is for WASM modules, string content + // would not be valid WASM, so this would fail. + // Just verify our loader was created correctly + require.NotNil(t, l) + require.NotNil(t, l.GetSourceURL()) }) } From 54bca159c9d32cce6fd80fe4db4d5408df8e8cc4 Mon Sep 17 00:00:00 2001 From: RT Date: Fri, 11 Apr 2025 13:36:38 -0400 Subject: [PATCH 3/4] adjust polyscript.go docs --- polyscript.go | 142 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/polyscript.go b/polyscript.go index 2306cdc..8801e98 100644 --- a/polyscript.go +++ b/polyscript.go @@ -1,3 +1,16 @@ +// Package polyscript provides a unified interface for executing scripts in different language runtimes. +// +// This package supports these "machine" types: +// - Extism: WebAssembly modules +// - Risor: Risor scripting language +// - Starlark: Starlark configuration language +// +// For each script machine, there are two main patterns available: +// 1. Basic execution: Load and execute scripts without external data +// 2. With data preparation: Provide initial static data, and thread-safe dynamic runtime data +// +// All functions in this package return a common engine.EvaluatorWithPrep interface. For direct +// access to the underlying machine, use the specific machine's methods. package polyscript import ( @@ -10,7 +23,12 @@ import ( starlarkMachine "github.com/robbyt/go-polyscript/machines/starlark" ) -// FromExtismFile creates an Extism evaluator from a WASM file +// FromExtismFile creates an Extism evaluator from a WASM file. +// +// Example: +// +// be, err := FromExtismFile("path/to/module.wasm", slog.Default().Handler(), "process") +// result, err := be.Eval(context.Background()) func FromExtismFile( filePath string, logHandler slog.Handler, @@ -25,13 +43,16 @@ func FromExtismFile( } // FromExtismFileWithData creates an Extism evaluator with both static and dynamic data capabilities. -// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// To add runtime data, use the PrepareContext method on the evaluator to add data to the context. +// +// Example: // -// Input parameters: -// - filePath: path to the WASM file -// - staticData: map of initial static data to be passed to the WASM module -// - logHandler: logger handler for logging -// - entryPoint: entry point for the WASM module (which function to call in the WASM file) +// staticData := map[string]any{"config": "value"} +// be, err := FromExtismFileWithData("path/to/module.wasm", staticData, slog.Default().Handler(), "process") +// +// runtimeData := map[string]any{"request": req} +// ctx, err = be.PrepareContext(context.Background(), runtimeData) +// result, err := be.Eval(ctx) func FromExtismFileWithData( filePath string, staticData map[string]any, @@ -46,8 +67,16 @@ func FromExtismFileWithData( return extismMachine.FromExtismLoaderWithData(logHandler, l, staticData, entryPoint) } -// FromRisorFile creates a Risor evaluator from a .risor file -func FromRisorFile(filePath string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { +// FromRisorFile creates a Risor evaluator from a .risor file. +// +// Example: +// +// be, _ := FromRisorFile("path/to/script.risor", slog.Default().Handler()) +// result, err := be.Eval(context.Background()) +func FromRisorFile( + filePath string, + logHandler slog.Handler, +) (engine.EvaluatorWithPrep, error) { l, err := loader.NewFromDisk(filePath) if err != nil { return nil, err @@ -56,13 +85,17 @@ func FromRisorFile(filePath string, logHandler slog.Handler) (engine.EvaluatorWi return risorMachine.FromRisorLoader(logHandler, l) } -// FromRisorFileWithData creates an Risor evaluator with both static and dynamic data capabilities. -// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// FromRisorFileWithData creates a Risor evaluator with both static and dynamic data capabilities. +// To add runtime data, use the PrepareContext method on the evaluator to add data to the context. // -// Input parameters: -// - filePath: path to the .risor script file -// - staticData: map of initial static data to be passed to the script -// - logHandler: logger handler for logging +// Example: +// +// staticData := map[string]any{"config": "value"} +// be, err := FromRisorFileWithData("path/to/script.risor", staticData, slog.Default().Handler()) +// +// runtimeData := map[string]any{"request": req} +// ctx, err = be.PrepareContext(context.Background(), runtimeData) +// result, err := be.Eval(ctx) func FromRisorFileWithData( filePath string, staticData map[string]any, @@ -76,8 +109,17 @@ func FromRisorFileWithData( return risorMachine.FromRisorLoaderWithData(logHandler, l, staticData) } -// FromRisorString creates a Risor evaluator from a script string -func FromRisorString(content string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { +// FromRisorString creates a Risor evaluator from a script string. +// +// Example: +// +// script := `return "Hello, world!"` +// be, err := FromRisorString(script, slog.Default().Handler()) +// result, err := be.Eval(context.Background()) +func FromRisorString( + content string, + logHandler slog.Handler, +) (engine.EvaluatorWithPrep, error) { l, err := loader.NewFromString(content) if err != nil { return nil, err @@ -86,13 +128,18 @@ func FromRisorString(content string, logHandler slog.Handler) (engine.EvaluatorW return risorMachine.FromRisorLoader(logHandler, l) } -// FromRisorStringWithData creates a Risor evaluator with both static and dynamic data capabilities -// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// FromRisorStringWithData creates a Risor evaluator with both static and dynamic data capabilities. +// To add runtime data, use the PrepareContext method on the evaluator to add data to the context. +// +// Example: +// +// script := `return config + " and " + request.field` +// staticData := map[string]any{"config": "static value"} +// be, err := FromRisorStringWithData(script, staticData, slog.Default().Handler()) // -// Input parameters: -// - script: the Risor script as a string -// - staticData: map of initial static data to be passed to the script -// - logHandler: logger handler for logging +// runtimeData := map[string]any{"request": map[string]string{"field": "dynamic value"}} +// ctx, err = be.PrepareContext(context.Background(), runtimeData) +// result, err := be.Eval(ctx) func FromRisorStringWithData( script string, staticData map[string]any, @@ -106,8 +153,16 @@ func FromRisorStringWithData( return risorMachine.FromRisorLoaderWithData(logHandler, l, staticData) } -// FromStarlarkFile creates a Starlark evaluator from a .star file -func FromStarlarkFile(filePath string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { +// FromStarlarkFile creates a Starlark evaluator from a .star file. +// +// Example: +// +// be, err := FromStarlarkFile("path/to/script.star", slog.Default().Handler()) +// result, err := be.Eval(context.Background()) +func FromStarlarkFile( + filePath string, + logHandler slog.Handler, +) (engine.EvaluatorWithPrep, error) { l, err := loader.NewFromDisk(filePath) if err != nil { return nil, err @@ -117,12 +172,16 @@ func FromStarlarkFile(filePath string, logHandler slog.Handler) (engine.Evaluato } // FromStarlarkFileWithData creates a Starlark evaluator with both static and dynamic data capabilities. -// To add runtime data, use the `PrepareContext` method on the evaluator to add data to the context. +// To add runtime data, use the PrepareContext method on the evaluator to add data to the context. +// +// Example: // -// Input parameters: -// - filePath: path to the .star script file -// - staticData: map of initial static data to be passed to the script -// - logHandler: logger handler for logging +// staticData := map[string]any{"constants": map[string]string{"version": "1.0"}} +// be, err := FromStarlarkFileWithData("path/to/script.star", staticData, slog.Default().Handler()) +// +// runtimeData := map[string]any{"input": userInput} +// ctx, err = be.PrepareContext(context.Background(), runtimeData) +// result, err := be.Eval(ctx) func FromStarlarkFileWithData( filePath string, staticData map[string]any, @@ -136,7 +195,13 @@ func FromStarlarkFileWithData( return starlarkMachine.FromStarlarkLoaderWithData(logHandler, l, staticData) } -// FromStarlarkString creates a Starlark evaluator from a script string +// FromStarlarkString creates a Starlark evaluator from a script string. +// +// Example: +// +// script := `def main(): return "Hello from Starlark"` +// be, err := FromStarlarkString(script, slog.Default().Handler()) +// result, err := be.Eval(context.Background()) func FromStarlarkString(content string, logHandler slog.Handler) (engine.EvaluatorWithPrep, error) { l, err := loader.NewFromString(content) if err != nil { @@ -147,13 +212,18 @@ func FromStarlarkString(content string, logHandler slog.Handler) (engine.Evaluat } // FromStarlarkStringWithData creates a Starlark evaluator with both static and dynamic data -// capabilities. To add runtime data, use the `PrepareContext` method on the evaluator to add data +// capabilities. To add runtime data, use the PrepareContext method on the evaluator to add data // to the context. // -// Input parameters: -// - script: the Starlark script as a string -// - staticData: map of initial static data to be passed to the script -// - logHandler: logger handler for logging +// Example: +// +// script := `def main(): return constants.greeting + " " + user.name` +// staticData := map[string]any{"constants": map[string]string{"greeting": "Hello"}} +// be, err := FromStarlarkStringWithData(script, staticData, slog.Default().Handler()) +// +// runtimeData := map[string]any{"user": map[string]string{"name": "World"}} +// ctx, err = be.PrepareContext(context.Background(), runtimeData) +// result, err := be.Eval(ctx) func FromStarlarkStringWithData( script string, staticData map[string]any, From 2e0f30e6d5397ffdc39d91d1481d1c2ec0df5d2c Mon Sep 17 00:00:00 2001 From: RT Date: Fri, 11 Apr 2025 15:26:59 -0400 Subject: [PATCH 4/4] add tests for the machine's top-level new.go files --- execution/script/loader/mock.go | 42 ++++ execution/script/loader/mock_test.go | 14 ++ machines/extism/new.go | 5 + machines/extism/new_test.go | 347 +++++++++++++++++++++++++++ machines/risor/new.go | 5 + machines/risor/new_test.go | 286 ++++++++++++++++++++++ machines/starlark/new.go | 5 + machines/starlark/new_test.go | 311 ++++++++++++++++++++++++ 8 files changed, 1015 insertions(+) create mode 100644 execution/script/loader/mock.go create mode 100644 execution/script/loader/mock_test.go create mode 100644 machines/extism/new_test.go create mode 100644 machines/risor/new_test.go create mode 100644 machines/starlark/new_test.go diff --git a/execution/script/loader/mock.go b/execution/script/loader/mock.go new file mode 100644 index 0000000..caa3490 --- /dev/null +++ b/execution/script/loader/mock.go @@ -0,0 +1,42 @@ +package loader + +import ( + "bytes" + "io" + "net/url" + + "github.com/stretchr/testify/mock" +) + +// MockLoader implements the loader.Loader interface for testing +type MockLoader struct { + mock.Mock +} + +func (m *MockLoader) GetSourceURL() *url.URL { + args := m.Called() + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*url.URL) +} + +func (m *MockLoader) GetReader() (io.ReadCloser, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(io.ReadCloser), args.Error(1) +} + +func (m *MockLoader) Close() error { + args := m.Called() + return args.Error(0) +} + +// Helper method to easily create a mock with content +func NewMockLoaderWithContent(content []byte) *MockLoader { + m := new(MockLoader) + m.On("GetReader").Return(io.NopCloser(bytes.NewReader(content)), nil) + return m +} diff --git a/execution/script/loader/mock_test.go b/execution/script/loader/mock_test.go new file mode 100644 index 0000000..ceb7e2f --- /dev/null +++ b/execution/script/loader/mock_test.go @@ -0,0 +1,14 @@ +package loader + +import ( + "testing" +) + +// TestMockLoaderImplementsLoaderInterface ensures that MockLoader correctly implements the Loader interface +func TestMockLoaderImplementsLoaderInterface(t *testing.T) { + // This is a compile-time check to ensure MockLoader implements Loader interface + var _ Loader = (*MockLoader)(nil) + + // No need for further testing as the mock implementation is handled by testify/mock + // and will be tested indirectly when used in other tests +} diff --git a/machines/extism/new.go b/machines/extism/new.go index 9922610..8c5a3d2 100644 --- a/machines/extism/new.go +++ b/machines/extism/new.go @@ -78,6 +78,11 @@ func NewEvaluator( dataProvider data.Provider, entryPoint string, ) (*evaluator.BytecodeEvaluator, error) { + // Validate provider is not nil + if dataProvider == nil { + return nil, fmt.Errorf("provider is nil") + } + // Create compiler with the entry point option compiler, err := NewCompiler(compiler.WithEntryPoint(entryPoint)) if err != nil { diff --git a/machines/extism/new_test.go b/machines/extism/new_test.go new file mode 100644 index 0000000..c4fb4be --- /dev/null +++ b/machines/extism/new_test.go @@ -0,0 +1,347 @@ +package extism + +import ( + "bytes" + "fmt" + "io" + "log/slog" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/extism/compiler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getTestWasmBytes returns the test WASM bytes from the examples directory +func getTestWasmBytes(t *testing.T) []byte { + t.Helper() + // Find the main.wasm file in the examples directory + wasmPath := filepath.Join("..", "..", "examples", "testdata", "main.wasm") + bytes, err := os.ReadFile(wasmPath) + require.NoError(t, err, "Failed to read test WASM file") + require.NotEmpty(t, bytes, "Test WASM file is empty") + return bytes +} + +func setupMockLoader(t *testing.T) *loader.MockLoader { + t.Helper() + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-wasm-file.wasm") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + + // Create a reader that will call Close on the mock loader when it's closed + wasmBytes := getTestWasmBytes(t) + reader := io.NopCloser(bytes.NewReader(wasmBytes)) + mockLoader.On("GetReader").Return(reader, nil) + + // We don't expect Close to be called directly on the loader, + // it seems the code doesn't call it directly + return mockLoader +} + +func setupErrorMockLoader(t *testing.T) *loader.MockLoader { + t.Helper() + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-wasm-file.wasm") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load WASM")) + // Don't expect Close for error case + return mockLoader +} + +func TestFromExtismLoader(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupMockLoader(t) + + // Execute + evaluator, err := FromExtismLoader(handler, mockLoader, "greet") + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + assert.Equal(t, "extism.BytecodeEvaluator", evaluator.String()) + mockLoader.AssertExpectations(t) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupErrorMockLoader(t) + + // Execute + evaluator, err := FromExtismLoader(handler, mockLoader, "greet") + + // Verify + require.Error(t, err) + require.Nil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("nil URL in loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockLoader.On("GetSourceURL").Return(nil) + mockLoader.On("GetReader").Return(io.NopCloser(bytes.NewReader(getTestWasmBytes(t))), nil) + // Don't expect Close - loader.Close() is not called by the code + + // Execute + evaluator, err := FromExtismLoader(handler, mockLoader, "greet") + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + mockLoader.AssertExpectations(t) + }) +} + +func TestFromExtismLoaderWithData(t *testing.T) { + t.Parallel() + + t.Run("success with static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupMockLoader(t) + + staticData := map[string]any{ + "version": "1.0.0", + "config": map[string]any{ + "timeout": 30, + "retry": true, + }, + } + + // Execute + evaluator, err := FromExtismLoaderWithData(handler, mockLoader, staticData, "greet") + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("empty static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupMockLoader(t) + + // Execute + evaluator, err := FromExtismLoaderWithData(handler, mockLoader, map[string]any{}, "greet") + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupErrorMockLoader(t) + + staticData := map[string]any{"version": "1.0.0"} + + // Execute + evaluator, err := FromExtismLoaderWithData(handler, mockLoader, staticData, "greet") + + // Verify + require.Error(t, err) + require.Nil(t, evaluator) + mockLoader.AssertExpectations(t) + }) +} + +func TestNewCompiler(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Execute + comp, err := NewCompiler(compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) + + t.Run("with multiple options", func(t *testing.T) { + // Execute + comp, err := NewCompiler( + compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + compiler.WithEntryPoint("process"), + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) +} + +func TestNewEvaluator(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupMockLoader(t) + provider := data.NewContextProvider("test_key") + + // Execute + evalInstance, err := NewEvaluator( + handler, + mockLoader, + provider, + "greet", + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "extism.BytecodeEvaluator", evalInstance.String()) + mockLoader.AssertExpectations(t) + }) + + t.Run("with nil URL", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockLoader.On("GetSourceURL").Return(nil) + mockLoader.On("GetReader").Return(io.NopCloser(bytes.NewReader(getTestWasmBytes(t))), nil) + // Don't expect Close - loader.Close() is not called by the code + + provider := data.NewContextProvider("test_key") + + // Execute + evaluator, err := NewEvaluator( + handler, + mockLoader, + provider, + "greet", + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("with nil handler", func(t *testing.T) { + // Setup + mockLoader := setupMockLoader(t) + provider := data.NewContextProvider("test_key") + + // Execute + evaluator, err := NewEvaluator( + nil, + mockLoader, + provider, + "greet", + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("loader error", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := setupErrorMockLoader(t) + provider := data.NewContextProvider("test_key") + + // Execute + evaluator, err := NewEvaluator( + handler, + mockLoader, + provider, + "greet", + ) + + // Verify + require.Error(t, err) + require.Nil(t, evaluator) + mockLoader.AssertExpectations(t) + }) + + t.Run("nil provider", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + // Don't use setupMockLoader for this test since it won't be used + mockLoader := new(loader.MockLoader) + + // Execute + evaluator, err := NewEvaluator( + handler, + mockLoader, + nil, + "greet", + ) + + // Verify + require.Error(t, err) + require.Nil(t, evaluator) + require.Contains(t, err.Error(), "provider is nil") + }) +} + +func TestDiskLoaderIntegration(t *testing.T) { + t.Run("create from disk loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + + // Create a temporary directory + tmpDir := t.TempDir() + + // Get WASM bytes for test + wasmBytes := getTestWasmBytes(t) + + // Create a temporary file in the temporary directory + tempFilePath := fmt.Sprintf("%s/test.wasm", tmpDir) + err := os.WriteFile(tempFilePath, wasmBytes, 0o644) + require.NoError(t, err) + + // Create a disk loader for the temporary file + diskLoader, err := loader.NewFromDisk(tempFilePath) + require.NoError(t, err) + require.NotNil(t, diskLoader) + + provider := data.NewContextProvider("test_key") + + // Execute + evaluator, err := NewEvaluator( + handler, + diskLoader, + provider, + "greet", + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evaluator) + assert.Equal(t, "extism.BytecodeEvaluator", evaluator.String()) + + // Verify the disk loader has correct path + fileURL := diskLoader.GetSourceURL() + require.NotNil(t, fileURL) + assert.Contains(t, fileURL.String(), "test.wasm") + + // Verify content was loaded correctly + reader, err := diskLoader.GetReader() + require.NoError(t, err) + content, err := io.ReadAll(reader) + require.NoError(t, err) + assert.NotEmpty(t, content) + assert.Equal(t, wasmBytes, content) + err = reader.Close() // Close the reader when done + require.NoError(t, err, "Failed to close reader") + }) +} diff --git a/machines/risor/new.go b/machines/risor/new.go index 203b8fb..f9e9370 100644 --- a/machines/risor/new.go +++ b/machines/risor/new.go @@ -71,6 +71,11 @@ func NewEvaluator( ldr loader.Loader, dataProvider data.Provider, ) (*evaluator.BytecodeEvaluator, error) { + // Validate provider is not nil + if dataProvider == nil { + return nil, fmt.Errorf("provider is nil") + } + // Create compiler with the context global option compiler, err := NewCompiler(compiler.WithCtxGlobal()) if err != nil { diff --git a/machines/risor/new_test.go b/machines/risor/new_test.go new file mode 100644 index 0000000..6b6f5aa --- /dev/null +++ b/machines/risor/new_test.go @@ -0,0 +1,286 @@ +package risor + +import ( + "fmt" + "io" + "log/slog" + "net/url" + "os" + "testing" + + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/risor/compiler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testRisorScript = ` +// Simple Risor script that uses built-in print function +print("Hello from Risor") + +// Define and call a simple function +func greet(name) { + return "Hello, " + name +} + +greet("World") +` + +// Helper function to create a string loader with test script +func createTestLoader(t *testing.T) *loader.FromString { + t.Helper() + stringLoader, err := loader.NewFromString(testRisorScript) + require.NoError(t, err) + require.NotNil(t, stringLoader) + return stringLoader +} + +func TestFromRisorLoader(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := FromRisorLoader(handler, stringLoader) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "risor.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup - create a mock loader that will return an error + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-risor-file.risor") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load script")) + + // Execute + evalInstance, err := FromRisorLoader(handler, mockLoader) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load script") + mockLoader.AssertExpectations(t) + }) +} + +func TestFromRisorLoaderWithData(t *testing.T) { + t.Parallel() + + t.Run("success with static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + staticData := map[string]any{ + "version": "1.0.0", + "config": map[string]any{ + "timeout": 30, + "retry": true, + }, + } + + // Execute + evalInstance, err := FromRisorLoaderWithData(handler, stringLoader, staticData) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + }) + + t.Run("empty static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := FromRisorLoaderWithData(handler, stringLoader, map[string]any{}) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-risor-file.risor") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load script")) + staticData := map[string]any{"version": "1.0.0"} + + // Execute + evalInstance, err := FromRisorLoaderWithData(handler, mockLoader, staticData) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load script") + mockLoader.AssertExpectations(t) + }) +} + +func TestNewCompiler(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Execute + comp, err := NewCompiler(compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) + + t.Run("with multiple options", func(t *testing.T) { + // Execute + comp, err := NewCompiler( + compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + compiler.WithCtxGlobal(), + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) +} + +func TestNewEvaluator(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + provider := data.NewContextProvider("test_key") + + // Execute + evalInstance, err := NewEvaluator( + handler, + stringLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "risor.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("with nil handler", func(t *testing.T) { + // Setup + stringLoader := createTestLoader(t) + provider := data.NewContextProvider("test_key") + + // Execute + evalInstance, err := NewEvaluator( + nil, + stringLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + }) + + t.Run("loader error", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-risor-file.risor") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load content")) + provider := data.NewContextProvider("test_key") + + // Execute + evalInstance, err := NewEvaluator( + handler, + mockLoader, + provider, + ) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load content") + mockLoader.AssertExpectations(t) + }) + + t.Run("nil provider", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := NewEvaluator( + handler, + stringLoader, + nil, + ) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + require.Contains(t, err.Error(), "provider is nil") + }) +} + +func TestDiskLoaderIntegration(t *testing.T) { + t.Run("create from disk loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + + // write test script to tmp file, load it + tmpDir := t.TempDir() + tempFilePath := fmt.Sprintf("%s/test.risor", tmpDir) + err := os.WriteFile(tempFilePath, []byte(testRisorScript), 0o644) + require.NoError(t, err) + + // Create a disk loader for the temporary file + diskLoader, err := loader.NewFromDisk(tempFilePath) + require.NoError(t, err) + require.NotNil(t, diskLoader) + + provider := data.NewContextProvider("test_key") + + // Execute + evalInstance, err := NewEvaluator( + handler, + diskLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "risor.BytecodeEvaluator", evalInstance.String()) + + // Verify the disk loader has correct path + fileURL := diskLoader.GetSourceURL() + require.NotNil(t, fileURL) + assert.Contains(t, fileURL.String(), "test.risor") + + // Verify content was loaded correctly + reader, err := diskLoader.GetReader() + require.NoError(t, err) + content, err := io.ReadAll(reader) + require.NoError(t, err) + assert.NotEmpty(t, content) + assert.Equal(t, testRisorScript, string(content)) + + // Properly close the reader when done + err = reader.Close() + require.NoError(t, err, "Failed to close reader") + }) +} diff --git a/machines/starlark/new.go b/machines/starlark/new.go index 462ca56..ef691b2 100644 --- a/machines/starlark/new.go +++ b/machines/starlark/new.go @@ -71,6 +71,11 @@ func NewEvaluator( ldr loader.Loader, dataProvider data.Provider, ) (*evaluator.BytecodeEvaluator, error) { + // Validate provider is not nil + if dataProvider == nil { + return nil, fmt.Errorf("provider is nil") + } + // Create compiler with the context global option compiler, err := NewCompiler(compiler.WithGlobals([]string{constants.Ctx})) if err != nil { diff --git a/machines/starlark/new_test.go b/machines/starlark/new_test.go new file mode 100644 index 0000000..fb0d616 --- /dev/null +++ b/machines/starlark/new_test.go @@ -0,0 +1,311 @@ +package starlark + +import ( + "fmt" + "io" + "log/slog" + "net/url" + "os" + "testing" + + "github.com/robbyt/go-polyscript/execution/constants" + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/execution/script/loader" + "github.com/robbyt/go-polyscript/machines/starlark/compiler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testStarlarkScript = ` +# Simple Starlark script that prints a message +print("Hello from Starlark") + +# Define and call a simple function +def greet(name): + return "Hello, " + name + +result = greet("World") +` + +// Helper function to create a string loader with test script +func createTestLoader(t *testing.T) *loader.FromString { + t.Helper() + stringLoader, err := loader.NewFromString(testStarlarkScript) + require.NoError(t, err) + require.NotNil(t, stringLoader) + return stringLoader +} + +func TestFromStarlarkLoader(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := FromStarlarkLoader(handler, stringLoader) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup - create a mock loader that will return an error + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-starlark-file.star") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load script")) + + // Execute + evalInstance, err := FromStarlarkLoader(handler, mockLoader) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load script") + mockLoader.AssertExpectations(t) + }) +} + +func TestFromStarlarkLoaderWithData(t *testing.T) { + t.Parallel() + + t.Run("success with static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + staticData := map[string]any{ + "version": "1.0.0", + "config": map[string]any{ + "timeout": 30, + "retry": true, + }, + } + + // Execute + evalInstance, err := FromStarlarkLoaderWithData(handler, stringLoader, staticData) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("empty static data", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := FromStarlarkLoaderWithData(handler, stringLoader, map[string]any{}) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("error from loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-starlark-file.star") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load script")) + staticData := map[string]any{"version": "1.0.0"} + + // Execute + evalInstance, err := FromStarlarkLoaderWithData(handler, mockLoader, staticData) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load script") + mockLoader.AssertExpectations(t) + }) +} + +func TestNewCompiler(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Execute + comp, err := NewCompiler(compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil))) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) + + t.Run("with multiple options", func(t *testing.T) { + // Execute + comp, err := NewCompiler( + compiler.WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + compiler.WithGlobals([]string{"data", "context"}), + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, comp) + }) +} + +func TestNewEvaluator(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + provider := data.NewContextProvider(constants.EvalData) + + // Execute + evalInstance, err := NewEvaluator( + handler, + stringLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("with nil handler", func(t *testing.T) { + // Setup + stringLoader := createTestLoader(t) + provider := data.NewContextProvider(constants.EvalData) + + // Execute + evalInstance, err := NewEvaluator( + nil, + stringLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + }) + + t.Run("loader error", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + mockLoader := new(loader.MockLoader) + mockURL, err := url.Parse("file:///test-starlark-file.star") + require.NoError(t, err, "Failed to parse URL") + mockLoader.On("GetSourceURL").Return(mockURL) + mockLoader.On("GetReader").Return(nil, fmt.Errorf("failed to load content")) + provider := data.NewContextProvider(constants.EvalData) + + // Execute + evalInstance, err := NewEvaluator( + handler, + mockLoader, + provider, + ) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + assert.Contains(t, err.Error(), "failed to load content") + mockLoader.AssertExpectations(t) + }) + + t.Run("nil provider", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + stringLoader := createTestLoader(t) + + // Execute + evalInstance, err := NewEvaluator( + handler, + stringLoader, + nil, + ) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + require.Contains(t, err.Error(), "provider is nil") + }) + + t.Run("invalid script syntax", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + invalidScript := `this is { not valid } Starlark syntax` + invalidLoader, err := loader.NewFromString(invalidScript) + require.NoError(t, err) + provider := data.NewContextProvider(constants.EvalData) + + // Execute + evalInstance, err := NewEvaluator( + handler, + invalidLoader, + provider, + ) + + // Verify + require.Error(t, err) + require.Nil(t, evalInstance) + // Update error message check to match actual Starlark error message + assert.Contains(t, err.Error(), "illegal token") + }) +} + +func TestDiskLoaderIntegration(t *testing.T) { + t.Run("create from disk loader", func(t *testing.T) { + // Setup + handler := slog.NewTextHandler(os.Stdout, nil) + + // write test script to tmp file, load it + tmpDir := t.TempDir() + tempFilePath := fmt.Sprintf("%s/test.star", tmpDir) + err := os.WriteFile(tempFilePath, []byte(testStarlarkScript), 0o644) + require.NoError(t, err) + + // Create a disk loader for the temporary file + diskLoader, err := loader.NewFromDisk(tempFilePath) + require.NoError(t, err) + require.NotNil(t, diskLoader) + + provider := data.NewContextProvider(constants.EvalData) + + // Execute + evalInstance, err := NewEvaluator( + handler, + diskLoader, + provider, + ) + + // Verify + require.NoError(t, err) + require.NotNil(t, evalInstance) + assert.Equal(t, "starlark.BytecodeEvaluator", evalInstance.String()) + + // Verify the disk loader has correct path + fileURL := diskLoader.GetSourceURL() + require.NotNil(t, fileURL) + assert.Contains(t, fileURL.String(), "test.star") + + // Verify content was loaded correctly + reader, err := diskLoader.GetReader() + require.NoError(t, err) + content, err := io.ReadAll(reader) + require.NoError(t, err) + assert.NotEmpty(t, content) + assert.Equal(t, testStarlarkScript, string(content)) + + // Properly close the reader when done + err = reader.Close() + require.NoError(t, err, "Failed to close reader") + }) +}