Feat v0.9.5 typed forms sdk (#79)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 3m6s
CI/CD / build-and-deploy (push) Successful in 29s
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 3m6s
CI/CD / build-and-deploy (push) Successful in 29s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #79.
This commit is contained in:
86
server/sandbox/forms_module.go
Normal file
86
server/sandbox/forms_module.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package sandbox
|
||||
|
||||
// forms_module.go
|
||||
//
|
||||
// Starlark forms module — validates form data against typed form templates.
|
||||
// Gated by the forms.validate extension permission.
|
||||
//
|
||||
// Starlark API:
|
||||
// result = forms.validate(template, data)
|
||||
// # result["valid"] → True/False
|
||||
// # result["errors"] → [{"key": "...", "message": "..."}, ...]
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go.starlark.net/starlark"
|
||||
"go.starlark.net/starlarkstruct"
|
||||
|
||||
"armature/forms"
|
||||
)
|
||||
|
||||
// BuildFormsModule creates the "forms" Starlark module.
|
||||
// Requires the forms.validate permission.
|
||||
func BuildFormsModule(_ context.Context) *starlarkstruct.Module {
|
||||
return MakeModule("forms", starlark.StringDict{
|
||||
"validate": starlark.NewBuiltin("forms.validate", formsValidateBuiltin()),
|
||||
})
|
||||
}
|
||||
|
||||
func formsValidateBuiltin() func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
|
||||
return func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var templateVal, dataVal starlark.Value
|
||||
if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &templateVal, &dataVal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert template dict → Go map → JSON → TypedFormTemplate
|
||||
templateDict, ok := templateVal.(*starlark.Dict)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("forms.validate: template must be a dict, got %s", templateVal.Type())
|
||||
}
|
||||
templateMap := DictToMap(templateDict)
|
||||
templateJSON, err := json.Marshal(templateMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forms.validate: failed to marshal template: %w", err)
|
||||
}
|
||||
|
||||
tpl := forms.ParseTypedFormTemplate(json.RawMessage(templateJSON))
|
||||
if tpl == nil {
|
||||
return nil, fmt.Errorf("forms.validate: template is invalid or empty")
|
||||
}
|
||||
|
||||
// Convert data dict → Go map
|
||||
dataDict, ok := dataVal.(*starlark.Dict)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("forms.validate: data must be a dict, got %s", dataVal.Type())
|
||||
}
|
||||
dataMap := DictToMap(dataDict)
|
||||
|
||||
// Coerce to map[string]interface{}
|
||||
data := make(map[string]interface{}, len(dataMap))
|
||||
for k, v := range dataMap {
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
// Validate
|
||||
errs := forms.ValidateFormData(tpl, data)
|
||||
|
||||
// Build result dict
|
||||
errList := make([]starlark.Value, len(errs))
|
||||
for i, e := range errs {
|
||||
d := starlark.NewDict(2)
|
||||
_ = d.SetKey(starlark.String("key"), starlark.String(e.Key))
|
||||
_ = d.SetKey(starlark.String("message"), starlark.String(e.Message))
|
||||
errList[i] = d
|
||||
}
|
||||
|
||||
result := starlark.NewDict(2)
|
||||
_ = result.SetKey(starlark.String("valid"), starlark.Bool(len(errs) == 0))
|
||||
_ = result.SetKey(starlark.String("errors"), starlark.NewList(errList))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
116
server/sandbox/forms_module_test.go
Normal file
116
server/sandbox/forms_module_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
func runFormsScript(t *testing.T, script string) starlark.StringDict {
|
||||
t.Helper()
|
||||
mod := BuildFormsModule(context.Background())
|
||||
predeclared := starlark.StringDict{"forms": mod}
|
||||
globals, err := starlark.ExecFile(&starlark.Thread{Name: "test"}, "test.star", script, predeclared)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return globals
|
||||
}
|
||||
|
||||
func TestFormsModule_ValidateValid(t *testing.T) {
|
||||
globals := runFormsScript(t, `
|
||||
tpl = {
|
||||
"fields": [
|
||||
{"key": "name", "type": "text", "label": "Name", "required": True},
|
||||
{"key": "email", "type": "email", "label": "Email"},
|
||||
]
|
||||
}
|
||||
result = forms.validate(tpl, {"name": "Alice", "email": "alice@example.com"})
|
||||
`)
|
||||
result := globals["result"].(*starlark.Dict)
|
||||
valid, _, _ := result.Get(starlark.String("valid"))
|
||||
if valid != starlark.True {
|
||||
t.Fatalf("expected valid=True, got %v", valid)
|
||||
}
|
||||
errors, _, _ := result.Get(starlark.String("errors"))
|
||||
errList := errors.(*starlark.List)
|
||||
if errList.Len() != 0 {
|
||||
t.Fatalf("expected 0 errors, got %d", errList.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormsModule_ValidateRequired(t *testing.T) {
|
||||
globals := runFormsScript(t, `
|
||||
tpl = {
|
||||
"fields": [
|
||||
{"key": "name", "type": "text", "label": "Name", "required": True},
|
||||
]
|
||||
}
|
||||
result = forms.validate(tpl, {})
|
||||
`)
|
||||
result := globals["result"].(*starlark.Dict)
|
||||
valid, _, _ := result.Get(starlark.String("valid"))
|
||||
if valid != starlark.False {
|
||||
t.Fatalf("expected valid=False, got %v", valid)
|
||||
}
|
||||
errors, _, _ := result.Get(starlark.String("errors"))
|
||||
errList := errors.(*starlark.List)
|
||||
if errList.Len() != 1 {
|
||||
t.Fatalf("expected 1 error, got %d", errList.Len())
|
||||
}
|
||||
errDict := errList.Index(0).(*starlark.Dict)
|
||||
key, _, _ := errDict.Get(starlark.String("key"))
|
||||
if key.(starlark.String).GoString() != "name" {
|
||||
t.Fatalf("expected error key 'name', got %v", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormsModule_ValidateTypeErrors(t *testing.T) {
|
||||
globals := runFormsScript(t, `
|
||||
tpl = {
|
||||
"fields": [
|
||||
{"key": "age", "type": "number", "label": "Age", "validation": {"min": 0, "max": 150}},
|
||||
{"key": "email", "type": "email", "label": "Email"},
|
||||
]
|
||||
}
|
||||
result = forms.validate(tpl, {"age": -5, "email": "bad"})
|
||||
`)
|
||||
result := globals["result"].(*starlark.Dict)
|
||||
valid, _, _ := result.Get(starlark.String("valid"))
|
||||
if valid != starlark.False {
|
||||
t.Fatalf("expected valid=False, got %v", valid)
|
||||
}
|
||||
errors, _, _ := result.Get(starlark.String("errors"))
|
||||
errList := errors.(*starlark.List)
|
||||
if errList.Len() != 2 {
|
||||
t.Fatalf("expected 2 errors, got %d", errList.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormsModule_ValidateCondition(t *testing.T) {
|
||||
globals := runFormsScript(t, `
|
||||
tpl = {
|
||||
"fields": [
|
||||
{"key": "type", "type": "text", "label": "Type"},
|
||||
{"key": "details", "type": "text", "label": "Details", "required": True,
|
||||
"condition": {"when": "type", "op": "eq", "value": "other"}},
|
||||
]
|
||||
}
|
||||
# Condition not met — details not required
|
||||
result1 = forms.validate(tpl, {"type": "normal"})
|
||||
# Condition met — details required
|
||||
result2 = forms.validate(tpl, {"type": "other"})
|
||||
`)
|
||||
result1 := globals["result1"].(*starlark.Dict)
|
||||
valid1, _, _ := result1.Get(starlark.String("valid"))
|
||||
if valid1 != starlark.True {
|
||||
t.Fatalf("result1: expected valid=True, got %v", valid1)
|
||||
}
|
||||
|
||||
result2 := globals["result2"].(*starlark.Dict)
|
||||
valid2, _, _ := result2.Get(starlark.String("valid"))
|
||||
if valid2 != starlark.False {
|
||||
t.Fatalf("result2: expected valid=False, got %v", valid2)
|
||||
}
|
||||
}
|
||||
@@ -417,6 +417,9 @@ func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, m
|
||||
})
|
||||
}
|
||||
|
||||
case models.ExtPermFormValidate:
|
||||
modules["forms"] = BuildFormsModule(ctx)
|
||||
|
||||
case models.ExtPermBatchExec:
|
||||
hasBatchExec = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user