formbuilder

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package formbuilder is the GoFastr form-builder plugin: an authoring tool for the framework itself, not another content editor.

Every other plugin in this repo edits content — a document, a diagram, a chart spec, a table view. This one produces a FORM SCHEMA (formbuilder-v1) that GoFastr's own ui.Form renders and that the HOST re-validates on every submit: a form designed in an opaque-origin sandboxed iframe is enforced by the server with no client trust anywhere in the path. That round trip — design in the cage, enforcement in Go — is the entire argument; the demo's live-form route closes it visibly.

The canonical doc is the schema and it is DATA ONLY: {version, fields[]}, never markup (see schema.go — a label containing "<" is refused at save). The Go side validates every save and refuses bad schemas with specific 400 codes: unknown field type, duplicate/empty/invalid name, malformed rule, unknown version, markup. A schema that somehow gets past the frame still gets refused here.

Capabilities: document:read, document:write, theme:read (all always-on; there are no optional grants). pluginhost.Allow is a capability gate, NOT authentication: it passes for anonymous callers, so POST /save must be documented as requiring the host to check the session in its own handler — the demo's WithDevGrantAll skips the gate entirely and MUST NOT survive into a production mount. See docs/formbuilder.md.

Index

Constants

View Source
const (
	Name             = "formbuilder"
	Version          = "0.1.0"
	RoutePrefix      = "/__gofastr/plugin/formbuilder"
	BuilderHTMLURL   = RoutePrefix + "/builder.html"
	BuilderJSURL     = RoutePrefix + "/builder.js"
	BuilderCSSURL    = RoutePrefix + "/builder.css"
	AdapterScriptURL = RoutePrefix + "/adapter.js"
	SaveURL          = RoutePrefix + "/save"
	DemoURL          = "/formbuilder"
	LiveURL          = "/formbuilder/live"
	SchemaVersion    = "formbuilder-v1"
)

Identity and route constants. Both this plugin and host/adapter.js hard-code these exactly (protocol-v1.md §2/§10). The design demo lives at /formbuilder; the live-form proof route at /formbuilder/live.

These mirror the formbuilder row in plugins.json; internal/registry tests pin Name + RoutePrefix against that row, so they MUST NOT drift.

View Source
const (
	ErrBadJSON          = "E_BAD_JSON"
	ErrUnknownVersion   = "E_UNKNOWN_VERSION"
	ErrTooManyFields    = "E_TOO_MANY_FIELDS"
	ErrUnknownFieldType = "E_UNKNOWN_FIELD_TYPE"
	ErrEmptyName        = "E_EMPTY_NAME"
	ErrInvalidName      = "E_INVALID_NAME"
	ErrDuplicateName    = "E_DUPLICATE_NAME"
	ErrLabelTooLong     = "E_LABEL_TOO_LONG"
	ErrHelpTooLong      = "E_HELP_TOO_LONG"
	ErrMarkup           = "E_MARKUP"
	ErrBadSelect        = "E_BAD_SELECT"
	ErrBadRule          = "E_BAD_RULE"
)

Error codes, so tests and docs pin the exact vocabulary.

Variables

View Source
var FieldTypes = []string{"text", "email", "number", "textarea", "select", "checkbox", "date"}

FieldTypes is the closed set of field types a schema may declare. Adding one is a schema change: bump the version and teach render.go + the frame.

Functions

func DefaultCapabilities

func DefaultCapabilities() []string

DefaultCapabilities is the always-on grant set: reading and writing the schema document plus theme bridging. document:write gates POST /save.

func Mount

func Mount(cfg MountConfig) render.HTML

Mount renders the generic mount marker plus the hidden input the adapter syncs on docChanged. Drop it into a form. All interpolated values are HTML-escaped inside pluginhost.MountMarker.

func RenderForm

func RenderForm(action string, doc Doc, values url.Values, errs ui.FieldErrors) render.HTML

RenderForm renders doc as a complete ui.Form posting to action. values pre-fills controls (a re-render after a refused submit keeps what the user typed); errs maps field names to messages and wires the per-field error chrome plus the form-level error summary.

func UIHostOption

func UIHostOption() uihost.Option

UIHostOption injects the platform broker, then this plugin's adapter (the adapter registers with the broker the former defines).

func ValidateDoc

func ValidateDoc(d *Doc) error

ValidateDoc enforces every formbuilder-v1 invariant and NORMALISES the doc in place (version stamped, empty labels defaulted to the name). A doc that returns nil is safe to persist and safe to render.

func ValidateValues

func ValidateValues(doc Doc, vals url.Values) ui.FieldErrors

ValidateValues re-derives every rule from the schema and applies it to the submitted form values, server-side. The browser was not consulted; neither was the frame. Returned errors are ui.FieldErrors so they drop straight into the re-rendered form.

Types

type Doc

type Doc struct {
	// Version is the schema version of the doc itself. Empty is treated as
	// the current version on save (the frame does not always stamp it); any
	// OTHER value is refused with E_UNKNOWN_VERSION — a saved schema has to
	// outlive the plugin that wrote it, so an unknown future version must
	// fail loudly, not silently degrade.
	Version string  `json:"version"`
	Fields  []Field `json:"fields"`
}

Doc is the canonical formbuilder-v1 document: the form schema as pure data.

func (Doc) RuleCount

func (d Doc) RuleCount() int

RuleCount is the number of enforceable constraints in the doc (required flags plus explicit rules) — the number the demo's proof strip and the save verdict report.

type Field

type Field struct {
	Type     string `json:"type"`
	Name     string `json:"name"`
	Label    string `json:"label"`
	Required bool   `json:"required"`
	Help     string `json:"help"`
	// Options is the closed value set; select fields only.
	Options []string `json:"options,omitempty"`
	Rules   Rules    `json:"rules,omitempty"`
}

Field is one designed form field.

type MountConfig

type MountConfig struct {
	// DocID is the persistence key for the schema doc. Defaults to "demo".
	DocID string
	// Field is the hidden input name the adapter mirrors the schema JSON
	// into, so a normal form POST round-trips it. Defaults to
	// "formbuilder_doc".
	Field string
	// Doc is optional initial schema JSON server-rendered into the marker
	// (data-fui-plugin-doc) for reload round-trip.
	Doc string
	// MinHeight is the initial iframe height. Defaults to 560px.
	MinHeight string
}

MountConfig configures Mount.

type Option

type Option func(*Plugin)

Option configures a Plugin.

func WithCapabilities

func WithCapabilities(caps ...string) Option

WithCapabilities overrides the grant set advertised to the builder. Default: DefaultCapabilities. Grants are matched with the framework's wildcard scope grammar at runtime.

func WithDemoDoc

func WithDemoDoc(doc Doc) Option

WithDemoDoc sets the schema the demo page mounts when nothing has been saved yet — the starting canvas the visitor edits.

func WithDemoPage

func WithDemoPage() Option

WithDemoPage registers the self-contained themed demo page at DemoURL AND the live-form proof route at LiveURL (GET renders the saved schema through ui.Form; POST validates in Go and answers).

func WithDemoRoute

func WithDemoRoute(path string) Option

WithDemoRoute overrides where WithDemoPage mounts the design demo (default DemoURL, "/formbuilder"). The live route stays at LiveURL.

func WithDevGrantAll

func WithDevGrantAll() Option

WithDevGrantAll short-circuits the capability gate (demo / tests only). It bypasses the gate on POST /save; the route still fails closed on an unwired handler.

func WithSaveHandler

func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option

WithSaveHandler overrides the schema persistence hook. The default stores the validated canonical doc JSON in an in-memory map keyed by DocID. This is the host's chance to authorize the write against the real session: pluginhost.Allow is a capability gate, not authentication.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the form-builder plugin. It implements framework.Plugin and mirrors the mermaid/datagrid shape: opaque-origin sandboxed iframe, protocol v1 over postMessage, go:embed'd frame bundle, capability gate, one host-side RPC route (/save). The difference is what the doc IS: not content to display but a schema the server consumes and enforces.

func New

func New(opts ...Option) *Plugin

New constructs the plugin.

func (*Plugin) Capabilities

func (p *Plugin) Capabilities() []string

Capabilities returns the grant set this plugin advertises to the builder.

func (*Plugin) Init

func (p *Plugin) Init(app *framework.App) error

Init registers every asset and RPC route on the app's router.

func (*Plugin) LoadDoc

func (p *Plugin) LoadDoc(ctx context.Context, docID string) (docJSON string, ok bool)

LoadDoc returns the last-saved schema JSON for docID (demo round-trip). ok is false when the doc has never been saved.

func (*Plugin) Manifest

func (p *Plugin) Manifest() pluginhost.Manifest

func (*Plugin) Name

func (p *Plugin) Name() string

type Rules

type Rules struct {
	MinLength *float64 `json:"minLength,omitempty"`
	MaxLength *float64 `json:"maxLength,omitempty"`
	Min       *float64 `json:"min,omitempty"`
	Max       *float64 `json:"max,omitempty"`
	// Pattern is a regexp, compiled with Go's RE2 at save time. The server
	// re-compiles it on every validation, so the frame and the server cannot
	// disagree about what it means.
	Pattern string `json:"pattern,omitempty"`
}

Rules are the validation constraints. Length/range bounds ride as *float64 (not *int) so a hostile "minLength": 2.5 decodes and is REFUSED as a bad rule, rather than dying as a JSON unmarshal error that reads as a transport problem.

type SaveRequest

type SaveRequest struct {
	DocID         string
	Doc           Doc
	DocJSON       string
	SchemaVersion string
}

SaveRequest is the schema persist signal handed to the save handler.

type SchemaError

type SchemaError struct {
	Code    string
	Message string
}

SchemaError is a validation refusal: a stable machine-readable code plus a human sentence. The code is what tests (and the frame's status line) branch on; the message is what a person reads.

func (*SchemaError) Error

func (e *SchemaError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL