howdy/form
HTML forms, submitted as application/x-www-form-urlencoded or as
multipart/form-data, the encoding a form with a file input needs.
The body can be read only once, so read turns it into a Form and
every field is then taken from that value. Field problems are returned
as validate errors rather than responses, so a page can be rendered
again with its errors and the values the user typed.
A local variable named form would shadow this module, so call the
value something else, such as fields.
fn signup(fields: Form) -> validate.Result(Signup) {
use email <- form.string(fields, "email", [validate.email()])
use age <- form.int(fields, "age", [validate.min(13)])
use newsletter <- form.checkbox(fields, "newsletter")
validate.ok(Signup(email:, age:, newsletter:))
}
fn create(ctx: Context) {
use fields <- form.read(ctx)
case signup(fields) {
Ok(signup) -> ...
Error(errors) ->
controller.html(ctx, signup_page(fields, errors))
|> controller.with_status(422)
}
}
Browsers submit an empty input as name=. string keeps the empty
string, so pair it with validate.not_empty(); int treats it as
missing; the optional_* helpers treat it as None. A singular field
submitted more than once is an error. Any other content type gets a
415.
Files
A form takes no files unless its limits accept them, so an endpoint that does not expect uploads cannot be made to write them to disk. Each file is written to a temporary file as it arrives rather than held in memory, and deleted once the continuation returns, so store it or move it before then.
fn change_avatar(ctx: Context) {
let limits =
form.limits() |> form.accept_files(max_files: 1, max_file_size: 5_000_000)
use fields <- form.read_with(ctx, limits)
let avatar = {
use avatar <- form.file(fields, "avatar", [
form.file_type(["image/png", "image/jpeg"]),
])
validate.ok(avatar)
}
case avatar {
Ok(avatar) -> ... // copy avatar.path somewhere lasting
Error(errors) -> ...
}
}
The file name and content type are what the browser claimed. Check the content itself before relying on either, and never use the name as a path. A file input left empty is not submitted as a file.
Form posts are not subject to CORS preflight, so cookie-authenticated
forms need CSRF protection. Use howdy/csrf, or the Origin check
howdy_auth already applies to routes behind its guard.
Types
A file submitted with a form, written to a temporary file.
pub type UploadedFile {
UploadedFile(
file_name: String,
content_type: String,
size: Int,
path: String,
)
}
Constructors
-
UploadedFile( file_name: String, content_type: String, size: Int, path: String, )Arguments
- file_name
-
The name the browser gave, without any directories. Do not use it as a path.
- content_type
-
The content type the browser claimed, or
application/octet-stream. - size
-
The size in bytes.
- path
-
Where the file is. It is deleted once the continuation given to
read_withreturns.
Values
pub fn accept_files(
limits: Limits,
max_files max_files: Int,
max_file_size max_file_size: Int,
) -> Limits
Accept up to max_files files of up to max_file_size bytes each. A
larger file, or one too many, fails the whole form with a 400. Check
smaller limits with validation rules such as max_size instead, to show
the user an error beside the field.
pub fn all(form: Form, name: String) -> List(String)
Every value submitted for name, in order. An absent field gives [].
pub fn all_files(form: Form, name: String) -> List(UploadedFile)
Every file submitted for name, in order. An absent field gives [].
pub fn checkbox(
form: Form,
name: String,
next: fn(Bool) -> Result(b, List(service.FieldError)),
) -> Result(b, List(service.FieldError))
Whether a checkbox was ticked. Browsers leave unticked checkboxes out of
the submission, so this is True when the field is present at all.
pub fn error(
errors: List(service.FieldError),
name: String,
) -> option.Option(problem.Problem)
The problem with name among validation errors, if it has one. Put it
into words with problem.message.
pub fn fields(form: Form) -> List(#(String, String))
Every field but the files, in the order submitted.
pub fn file(
form: Form,
name: String,
rules: List(
fn(UploadedFile) -> Result(UploadedFile, problem.Problem),
),
next: fn(UploadedFile) -> Result(b, List(service.FieldError)),
) -> Result(b, List(service.FieldError))
A required file, checked with rules.
pub fn file_type(
allowed: List(String),
) -> fn(UploadedFile) -> Result(UploadedFile, problem.Problem)
A file whose claimed content type is one of allowed, such as
"application/pdf", or matches a pattern such as "image/*". The type
comes from the browser, so check the content too before relying on it.
pub fn files(
form: Form,
name: String,
rules: List(
fn(UploadedFile) -> Result(UploadedFile, problem.Problem),
),
next: fn(List(UploadedFile)) -> Result(
b,
List(service.FieldError),
),
) -> Result(b, List(service.FieldError))
Every file of a field that may hold several, such as an input with
multiple, each checked with rules.
pub fn from_fields(fields: List(#(String, String))) -> Form
A form made of fields, for calling a validator directly in a test.
pub fn get(form: Form, name: String) -> Result(String, Nil)
The first value submitted for name.
pub fn get_file(
form: Form,
name: String,
) -> Result(UploadedFile, Nil)
The first file submitted for name.
pub fn int(
form: Form,
name: String,
rules: List(fn(Int) -> Result(Int, problem.Problem)),
next: fn(Int) -> Result(b, List(service.FieldError)),
) -> Result(b, List(service.FieldError))
A required integer, checked with rules. An empty value is missing.
pub fn limits() -> Limits
The limits read uses: up to body.default_limit bytes of fields, and
no files.
pub fn max_fields_size(limits: Limits, bytes: Int) -> Limits
The most bytes of everything but file contents: the whole body of a urlencoded form, or the fields, part headers and boundaries of a multipart one.
pub fn max_size(
bytes: Int,
) -> fn(UploadedFile) -> Result(UploadedFile, problem.Problem)
A file of at most bytes bytes.
pub fn max_total_file_size(limits: Limits, bytes: Int) -> Limits
The most bytes all the files together may have. By default it is
max_files times max_file_size.
pub fn optional_file(
form: Form,
name: String,
rules: List(
fn(UploadedFile) -> Result(UploadedFile, problem.Problem),
),
next: fn(option.Option(UploadedFile)) -> Result(
b,
List(service.FieldError),
),
) -> Result(b, List(service.FieldError))
An optional file, checked with rules when given.
pub fn optional_int(
form: Form,
name: String,
rules: List(fn(Int) -> Result(Int, problem.Problem)),
next: fn(option.Option(Int)) -> Result(
b,
List(service.FieldError),
),
) -> Result(b, List(service.FieldError))
An optional integer, checked with rules when given. An absent or empty
value is None.
pub fn optional_string(
form: Form,
name: String,
rules: List(fn(String) -> Result(String, problem.Problem)),
next: fn(option.Option(String)) -> Result(
b,
List(service.FieldError),
),
) -> Result(b, List(service.FieldError))
An optional string, checked with rules when given. An absent or empty
value is None, as is one that rules leave empty, such as by trimming.
pub fn read(
ctx: context.Context(guarded),
next: fn(Form) -> response.Response(content.Content),
) -> response.Response(content.Content)
Read the request body as a form, with the default limits: no files. A
wrong content type returns 415; a body that is too large or badly
encoded returns 400. The continuation never runs in either case.
pub fn read_with(
ctx: context.Context(guarded),
limits: Limits,
next: fn(Form) -> response.Response(content.Content),
) -> response.Response(content.Content)
Like read but with the limits given, such as to accept files. Any
uploaded files are deleted once next returns, even if it crashes.
pub fn read_with_limit(
ctx: context.Context(guarded),
limit: Int,
next: fn(Form) -> response.Response(content.Content),
) -> response.Response(content.Content)
Like read but with a custom size limit in bytes.
pub fn string(
form: Form,
name: String,
rules: List(fn(String) -> Result(String, problem.Problem)),
next: fn(String) -> Result(b, List(service.FieldError)),
) -> Result(b, List(service.FieldError))
A required string, checked with rules. An empty value is kept.
pub fn strings(
form: Form,
name: String,
next: fn(List(String)) -> Result(b, List(service.FieldError)),
) -> Result(b, List(service.FieldError))
Every value of a field that may repeat, such as a multi-select.
pub fn temporary_directory(
limits: Limits,
directory: String,
) -> Limits
Where uploaded files are written while a request uses them. By default
the directory named by TMPDIR, TMP or TEMP, else /tmp. It is
created if missing.
pub fn uploaded_files(
form: Form,
) -> List(#(String, UploadedFile))
Every file with the name of its field, in the order submitted.
pub fn validated(
ctx: context.Context(guarded),
validator: fn(Form) -> Result(a, List(service.FieldError)),
next: fn(a) -> response.Response(content.Content),
) -> response.Response(content.Content)
Read a form and validate it, for endpoints that answer with JSON. A
validation failure returns 422 with every field error, as
body.validated does. Pages that render their errors should use read.
pub fn validated_with(
ctx: context.Context(guarded),
limits: Limits,
validator: fn(Form) -> Result(a, List(service.FieldError)),
next: fn(a) -> response.Response(content.Content),
) -> response.Response(content.Content)
Like validated but with the limits given, such as to accept files.
pub fn value(form: Form, name: String) -> String
The first value submitted for name, or "". Use it to fill an input
back in when rendering a form again. Escape it like any other user input.
pub fn with_files(
form: Form,
files: List(#(String, UploadedFile)),
) -> Form
Add files to a form, for calling a validator directly in a test.