Extract typed form system from models/workflow.go into standalone server/forms/ package. Any package can now declare and validate forms, not just workflow stages. - New server/forms/ package (types + validation + condition evaluator) - REST POST /api/v1/forms/validate endpoint - Starlark forms.validate(template, data) module - Frontend sw.forms.render(), .validate(), .validateRemote() - Manifest form_template accepted at package level - 16 new tests (12 forms + 4 Starlark module) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
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
|
|
}
|
|
}
|