CARLOS rastrillo docs

๐Ÿค– form

amadan.net/rastrillo/rastrillo/form

Framework-independent form helpers: field parsing with validation, a field-error map, and money. Generated and hand-written handlers use the same ones. Forms is the guide.

Parse

func Parse(r *http.Request, fields ...Field) *Parsed

Reads and validates every declared field in one pass.

It reads through r.PostFormValue, which parses the form on first use. If you want a body-size cap or a 400 on a malformed body, wrap r.Body and call r.ParseForm first, as the generated actions do.

Field and Kind

type Field struct {
	Name     string
	Kind     Kind
	Required bool
	Location *time.Location // Date and DateTime only; nil means time.UTC
}

Kind is how the submitted text is read. The zero value is Text, so a plain form.Field{Name: "Title"} does the common thing.

Constant Read with Behaviour
Text (the zero Kind) Parsed.String Trimmed; value and echo are both the trimmed text
Textarea Parsed.String Raw, whitespace preserved; only the required check trims
Money Parsed.Cents Parsed by ParseCents; echo keeps the raw text
Date Parsed.Date time.ParseInLocation on 2006-01-02 in Location
Time Parsed.Time time.Parse on 15:04 โ€” a clock reading, no location
DateTime Parsed.DateTime time.ParseInLocation on 2006-01-02T15:04 in Location

Required on Money, Date, Time and DateTime is checked against the raw text rather than the parsed value, so a present but unparseable value reports the parse error instead of the required message. For Money that also makes "0" a present, valid zero where "" is required-blank; a date has no such pair, because "0" is not a date and reports rastrillo.ui.date_invalid.

The three date kinds report catalog keys rather than English: rastrillo.ui.date_invalid for an unparseable value, rastrillo.ui.field_required for a blank required one. The renderer resolves them through T. See Dates and times.

Parsed

type Parsed struct{ /* unexported */ }

Parse's result: values, the echo map, and the errors.

Method Returns
Parsed.OK true when every field validated
Parsed.Errors the Errors map, empty rather than nil when OK
Parsed.String a Text or Textarea field's value
Parsed.Cents a Money field's value in integer cents
Parsed.Date a Date field's time.Time
Parsed.DateTime a DateTime field's time.Time
Parsed.Time a Time field's (h, m int, ok bool) clock reading
Parsed.Echo every field's raw text, for seeding a re-render

Dates and times

func (p *Parsed) Date(name string) time.Time
func (p *Parsed) DateTime(name string) time.Time
func (p *Parsed) Time(name string) (h, m int, ok bool)

Each kind parses one exact wire format and nothing looser โ€” the format the matching <input> posts, so the browser has already normalised whatever the person typed.

Date and DateTime parse in Field.Location, defaulting to time.UTC. Time is a bare clock reading and ignores it.

Date and DateTime return the zero time.Time for an unknown name, an empty optional field, and one that failed to parse alike, so check OK (or Errors) rather than IsZero to tell those apart from a real midnight-UTC submission. Time says the same thing through ok, and reports 0, 0 rather than half a reading when it is false.

An empty optional field is never an error. An unparseable one echoes back exactly as typed, so nothing is retyped over one bad field.

Range

func Range(p *Parsed, start, end string)

Adds rastrillo.ui.date_end_before_start on end when its instant precedes start's. It is a separate call because the check spans two fields, and Parse is one-declaration-per-field by design.

p := form.Parse(r,
	form.Field{Name: "starts", Kind: form.DateTime, Required: true},
	form.Field{Name: "ends", Kind: form.DateTime, Required: true})
form.Range(p, "starts", "ends")

Both fields must have been declared as Date or DateTime; either kind works, and they can be mixed. Equal instants are fine. If either side is blank or failed to parse, Range adds nothing at all โ€” Parse already recorded that field's own problem, and a second message on top of it would only be noise.

Range compares the fields that actually parsed, not the ones that are non-zero, so a genuinely submitted zero date takes part like any other instant instead of being mistaken for unset.

The daylight-saving corner

time.ParseInLocation resolves a wall-clock time that does not exist โ€” the hour a spring-forward skips โ€” using the offset in force before the transition, which means a time inside the skipped hour and the real time it collapses onto land on the same instant. An app scheduling across a transition should store the zone alongside the value and say so on screen, rather than trusting that two distinct readings stay distinct.

Errors

type Errors map[string]string

Field name to message โ€” the shape a template renders beside each input. Errors.Any tells you whether there is at least one.

Humanize

func Humanize(name string) string

Turns a field name into the words a message uses, so a required title reports "Title is required". Exported so your own validation messages can match the generated ones.

Money

Money is int64 cents throughout. Never a float.

ParseCents

func ParseCents(s string) (int64, error)

Parses a decimal amount into cents. Strict on purpose: at most two decimal places, no $, no sign character at all, both halves ASCII digits. An empty string parses to zero, because a required money field has already rejected blankness on the raw text before this runs.

The strictness earns its keep. Handing each half to strconv.ParseInt accepts its own +/-, so "12.-5" would quietly parse to a different magnitude than its digits suggest instead of being rejected as the not-an-amount it is.

Error

type Error struct {
    Key string // a rastrillo.ui.* catalog key
    Msg string // the English, and what Error() returns
}

func (e *Error) Error() string

Every error ParseCents returns is one of these. It names the catalog key for its message as well as carrying the message, so a caller that has a translator can render it in the reader's language:

cents, err := form.ParseCents(r.FormValue("price"))
var fe *form.Error
if errors.As(err, &fe) {
    fields["price"] = t(fe.Key) // or fe.Error() for the English
}

The key rather than a translated string, because this package imports nothing from rastrillo and has no request in reach. A package-level translator hook would be worse than none: the locale is per request, and a global would hand one request's language to another's error. A caller that ignores Key and prints the error gets English, which is the same three-step fallback the rest of the framework walks.

FormatCents and FormatCentsPlain

func FormatCents(cents int64) string      // "$12.34" โ€” for display
func FormatCentsPlain(cents int64) string // "12.34"  โ€” for seeding a form field

Using the wrong one is a real bug. Seed with the plain one: a browser may resubmit an untouched field completely unchanged, so the seed has to be exactly what ParseCents accepts back, and ParseCents rejects a leading $. Seed with FormatCents and resubmitting an unmodified money field always fails.

Both write the sign once against the absolute value. Formatting a negative directly produces "$-1.-50", because Go's / and % both truncate toward zero.

FormatCents writes a dollar sign and knows no other currency. Nothing in the framework stores a currency, so there is nothing for it to read. Format your own money anywhere that is not what you want, and store the currency beside the amount when you do โ€” a reader's locale decides how a price is written, never which currency it is in.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.