Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
reads and construct only via `NewExecutableUnit(...)`. Mid-flight mutation
of `DataProvider` (and the other fields) is no longer possible from outside
the package. Closes [#91](https://github.com/robbyt/go-polyscript/issues/91).
- **Breaking:** Tightened the `platform.EvaluatorResponse` interface:
- `GetScriptExeID() string` → `ScriptExeID() string` (dropped `Get` prefix).
- `GetExecTime() string` → `ExecTime() time.Duration` (no longer stringified
at the boundary).
- Added `AsMap() (map[string]any, error)` returning the result as a typed
map. On type mismatch, the returned error wraps a per-engine sentinel
(`engines/{extism,risor,starlark}/evaluator.ErrAsMapTypeMismatch`) and
includes the actual Go type via `"%T"`, so callers can use
`errors.Is(err, evaluator.ErrAsMapTypeMismatch)` and still read the
concrete type from `err.Error()`. Replaces the
`result.Interface().(map[string]any)` ceremony at 40+ call sites across
the test suite.
Closes [#86](https://github.com/robbyt/go-polyscript/issues/86).

### Deprecated
- The twelve legacy top-level constructors (`FromRisorFile`,
Expand Down
9 changes: 9 additions & 0 deletions engines/extism/evaluator/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package evaluator

import "errors"

// ErrAsMapTypeMismatch is returned by (*execResult).AsMap when the underlying
// value is not a map[string]any. Wrapped with the actual Go type via "%T", so
// callers can use errors.Is(err, ErrAsMapTypeMismatch) and still read the
// concrete type from err.Error().
var ErrAsMapTypeMismatch = errors.New("extism: AsMap expected map[string]any")
4 changes: 2 additions & 2 deletions engines/extism/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ func TestEvaluator_Evaluate(t *testing.T) {
require.NotNil(t, response)

// Verify the response
resultMap, ok := response.Interface().(map[string]any)
require.True(t, ok, "Expected map response")
resultMap, err := response.AsMap()
require.NoError(t, err, "Expected map response")
require.Contains(t, resultMap, "result")
require.Equal(t, "success", resultMap["result"])
require.Contains(t, resultMap, "value")
Expand Down
16 changes: 12 additions & 4 deletions engines/extism/evaluator/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func newEvalResult(
func (r *execResult) String() string {
return fmt.Sprintf(
"execResult{Type: %s, Value: %v, ExecTime: %s, ScriptExeID: %s}",
r.Type(), r.value, r.GetExecTime(), r.GetScriptExeID(),
r.Type(), r.value, r.ExecTime(), r.ScriptExeID(),
)
}

Expand All @@ -66,12 +66,20 @@ func (r *execResult) Type() data.Types {
}
}

func (r *execResult) GetScriptExeID() string {
func (r *execResult) ScriptExeID() string {
return r.scriptExeID
}

func (r *execResult) GetExecTime() string {
return r.execTime.String()
func (r *execResult) ExecTime() time.Duration {
return r.execTime
}

func (r *execResult) AsMap() (map[string]any, error) {
m, ok := r.value.(map[string]any)
if !ok {
return nil, fmt.Errorf("%w, got %T", ErrAsMapTypeMismatch, r.value)
}
return m, nil
}

func (r *execResult) Inspect() string {
Expand Down
39 changes: 35 additions & 4 deletions engines/extism/evaluator/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,11 @@ func TestResponseMethods(t *testing.T) {
handler := slog.NewTextHandler(os.Stdout, nil)
result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)

// Test GetScriptExeID
assert.Equal(t, tt.versionID, result.GetScriptExeID())
// Test ScriptExeID
assert.Equal(t, tt.versionID, result.ScriptExeID())

// Test GetExecTime
assert.Equal(t, tt.execTime.String(), result.GetExecTime())
// Test ExecTime
assert.Equal(t, tt.execTime, result.ExecTime())
})
}
})
Expand Down Expand Up @@ -387,3 +387,34 @@ func TestResponseMethods(t *testing.T) {
}
})
}

func TestExecResultAsMap(t *testing.T) {
t.Parallel()
handler := slog.NewTextHandler(os.Stdout, nil)

t.Run("success", func(t *testing.T) {
m := map[string]any{"k": "v", "n": 42}
result := newEvalResult(handler, m, time.Millisecond, "id-ok")
got, err := result.AsMap()
require.NoError(t, err)
assert.Equal(t, m, got)
})

t.Run("error on string", func(t *testing.T) {
result := newEvalResult(handler, "not a map", time.Millisecond, "id-bad")
got, err := result.AsMap()
require.Error(t, err)
assert.Nil(t, got)
require.ErrorIs(t, err, ErrAsMapTypeMismatch)
assert.Contains(t, err.Error(), "got string")
})

t.Run("error on nil", func(t *testing.T) {
result := newEvalResult(handler, nil, time.Millisecond, "id-nil")
got, err := result.AsMap()
require.Error(t, err)
assert.Nil(t, got)
require.ErrorIs(t, err, ErrAsMapTypeMismatch)
assert.Contains(t, err.Error(), "got <nil>")
})
}
4 changes: 2 additions & 2 deletions engines/extism/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ func TestFromExtismLoader_RunsEndToEnd(t *testing.T) {

res, err := eval.Eval(t.Context())
require.NoError(t, err)
got, ok := res.Interface().(map[string]any)
require.True(t, ok)
got, err := res.AsMap()
require.NoError(t, err)
assert.Equal(t, "Hello, World!", got["greeting"])
}

Expand Down
88 changes: 44 additions & 44 deletions engines/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ let tags = ctx["tags"]
require.NoError(t, err)

// Verify results
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "risor", resultMap["engine"])
assert.Equal(t, "Integration Test", resultMap["name"])
Expand Down Expand Up @@ -135,8 +135,8 @@ _ = result
require.NoError(t, err)

// Verify results
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "starlark", resultMap["engine"])
assert.Equal(t, "Integration Test", resultMap["name"])
Expand Down Expand Up @@ -175,8 +175,8 @@ _ = result
require.NoError(t, err)

// Verify results - the greet function returns {"greeting": "Hello, <input>!"}
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -239,8 +239,8 @@ let action = ctx["action"]
require.NoError(t, err)

// Verify both static and dynamic data are accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app"]) // Static data
assert.Equal(t, "user123", resultMap["user"]) // Dynamic data
Expand Down Expand Up @@ -282,8 +282,8 @@ _ = result
require.NoError(t, err)

// Verify both static and dynamic data are accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app"]) // Static data
assert.Equal(t, "user123", resultMap["user"]) // Dynamic data
Expand Down Expand Up @@ -323,8 +323,8 @@ _ = result
require.NoError(t, err)

// Verify the WASM module processed the data
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -364,8 +364,8 @@ let debug = ctx["config"]["debug"]
result, err := evaluator.Eval(t.Context())
require.NoError(t, err)

resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "World", resultMap["name"])
assert.Equal(t, true, resultMap["debug"])
Expand All @@ -390,8 +390,8 @@ _ = result
result, err := evaluator.Eval(t.Context())
require.NoError(t, err)

resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "World", resultMap["name"])
assert.Equal(t, true, resultMap["debug"])
Expand All @@ -416,8 +416,8 @@ _ = result
result, err := evaluator.Eval(t.Context())
require.NoError(t, err)

resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

// The greet function returns {"greeting": "Hello, <input>!"}
greeting, exists := resultMap["greeting"]
Expand Down Expand Up @@ -476,8 +476,8 @@ let user_data = ctx["user_data"]
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

config, exists := resultMap["config"].(map[string]any)
require.True(t, exists)
Expand Down Expand Up @@ -534,8 +534,8 @@ _ = result
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

config, exists := resultMap["config"].(map[string]any)
require.True(t, exists)
Expand Down Expand Up @@ -576,8 +576,8 @@ _ = result
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -624,8 +624,8 @@ let max_retries = ctx["constants"]["max_retries"]
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app_name"])
assert.Equal(t, "1.0.0", resultMap["version"])
Expand Down Expand Up @@ -661,8 +661,8 @@ _ = result
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app_name"])
assert.Equal(t, "1.0.0", resultMap["version"])
Expand Down Expand Up @@ -692,8 +692,8 @@ _ = result
require.NoError(t, err)

// Verify
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -752,8 +752,8 @@ let request_id = ctx["request_id"]
require.NoError(t, err)

// Verify both static and dynamic data are accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app_name"]) // Static data
assert.Equal(t, "1.0.0", resultMap["version"]) // Static data
Expand Down Expand Up @@ -799,8 +799,8 @@ _ = result
require.NoError(t, err)

// Verify both static and dynamic data are accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "TestApp", resultMap["app_name"]) // Static data
assert.Equal(t, "1.0.0", resultMap["version"]) // Static data
Expand Down Expand Up @@ -845,8 +845,8 @@ _ = result
require.NoError(t, err)

// Verify dynamic data overrode static data
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -914,8 +914,8 @@ let content_type = ctx["request"]["Headers"]["Content-Type"][0]
require.NoError(t, err)

// Verify HTTP request data is accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "POST", resultMap["method"])
assert.Equal(t, "/api/test", resultMap["path"])
Expand Down Expand Up @@ -963,8 +963,8 @@ _ = result
require.NoError(t, err)

// Verify HTTP request data is accessible
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, "POST", resultMap["method"])
assert.Equal(t, "/api/test", resultMap["path"])
Expand Down Expand Up @@ -998,8 +998,8 @@ _ = result
require.NoError(t, err)

// Verify the WASM module processed the request data
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

greeting, exists := resultMap["greeting"]
require.True(t, exists)
Expand Down Expand Up @@ -1048,8 +1048,8 @@ let config_data = ctx["config"]
require.NoError(t, err)

// Verify explicit key wrapping worked
resultMap, ok := result.Interface().(map[string]any)
require.True(t, ok)
resultMap, err := result.AsMap()
require.NoError(t, err)

assert.Equal(t, true, resultMap["has_request"])
assert.Equal(t, true, resultMap["has_user"])
Expand Down
9 changes: 9 additions & 0 deletions engines/risor/evaluator/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package evaluator

import "errors"

// ErrAsMapTypeMismatch is returned by (*execResult).AsMap when the underlying
// value is not a map[string]any. Wrapped with the actual Go type via "%T", so
// callers can use errors.Is(err, ErrAsMapTypeMismatch) and still read the
// concrete type from err.Error().
var ErrAsMapTypeMismatch = errors.New("risor: AsMap expected map[string]any")
Loading