Feat v0.9.5 typed forms SDK primitive

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>
This commit is contained in:
2026-04-03 16:54:02 +00:00
parent 6b9ce92103
commit 89d64ab2aa
16 changed files with 1415 additions and 323 deletions

63
server/handlers/forms.go Normal file
View File

@@ -0,0 +1,63 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"armature/forms"
)
// FormsHandler provides REST endpoints for the standalone forms system.
type FormsHandler struct{}
// formsValidateRequest is the request body for POST /api/v1/forms/validate.
type formsValidateRequest struct {
Template json.RawMessage `json:"template"`
Data map[string]interface{} `json:"data"`
}
// formsValidateResponse is the response body for POST /api/v1/forms/validate.
type formsValidateResponse struct {
Valid bool `json:"valid"`
Errors []forms.FieldError `json:"errors"`
}
// Validate validates form data against a typed form template.
//
// POST /api/v1/forms/validate
// Body: { "template": {...}, "data": {...} }
// Response: { "valid": true/false, "errors": [...] }
func (h *FormsHandler) Validate(c *gin.Context) {
var req formsValidateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if len(req.Template) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "template is required"})
return
}
tpl := forms.ParseTypedFormTemplate(req.Template)
if tpl == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "template is invalid or empty"})
return
}
if req.Data == nil {
req.Data = make(map[string]interface{})
}
errs := forms.ValidateFormData(tpl, req.Data)
if errs == nil {
errs = []forms.FieldError{}
}
c.JSON(http.StatusOK, formsValidateResponse{
Valid: len(errs) == 0,
Errors: errs,
})
}