package dream-html

  1. Overview
  2. Docs

Module Dream_html.FormSource

Typed, extensible HTML form decoder with error reporting for form field validation failures. Powerful chained decoding functionality–the validation of one field can depend on the values of other decoded fields.

See the bottom of the page for complete examples.

  • since 3.7.0.

Basic type decoders

Sourcetype 'a ty = string -> ('a, string) result

The type of a decoder for a single form field value of type 'a which can successfully decode the field value or fail with an error message key.

In the following type decoders, the minimum and maximum values are all inclusive.

Sourceval bool : bool ty
Sourceval char : ?min:char -> ?max:char -> char ty
Sourceval float : ?min:float -> ?max:float -> float ty
Sourceval int : ?min:int -> ?max:int -> int ty
Sourceval int32 : ?min:int32 -> ?max:int32 -> int32 ty
Sourceval int64 : ?min:int64 -> ?max:int64 -> int64 ty
Sourceval string : ?min_length:int -> ?max_length:int -> string ty
Sourceval unix_tm : ?min:Unix.tm -> ?max:Unix.tm -> Unix.tm ty

This can parse strings with the formats 2024-01-01 or 2024-01-01T00:00:00 into a timestamp.

Note that this is not timezone-aware.

  • since 3.8.0

Forms and fields

Sourcetype 'a t

The type of a form (a form field by itself is also considered a form) which can decode values of type 'a or fail with a list of error message keys.

Sourceval ok : 'a -> 'a t

ok value is a form field that always successfully returns value.

  • since 3.8.0
Sourceval error : string -> string -> 'a t

error name message is a form field that always errors the field name with the message.

These allow adding adding further checks to the entire form using all decoded field values and then attaching more errors to specific fields (or not).

  • since 3.8.0
Sourceval list : ?min_length:int -> ?max_length:int -> 'a ty -> string -> 'a list t

list ?min_length ?max_length ty name is a form field which can decode a list of values which can each be decoded by ty. The list must have at least min_length and at most max_length (inclusive).

Sourceval optional : 'a ty -> string -> 'a option t

optional ty name is a form field which can decode an optional value from the form.

Sourceval required : ?default:'a -> 'a ty -> string -> 'a t

required ?default ty name is a form field which can decode a required value from the form. If at least one value corresponding to the given name does not appear in the form, and if a default value is not specified, the decoding fails with an error.

Sourceval multiple : int -> (int -> 'a t) -> 'a list t

multiple n form is a form which can decode a list of nested form values. It tries to decode exactly n values and fails if it can't find all of them. Assumes that the items are 0-indexed. See Multiple structured values for a complete example.

  • since 3.10.0
Sourceval ensure : string -> ('b -> bool) -> ('a ty -> string -> 'b t) -> 'a ty -> string -> 'b t

ensure message condition field ty name is a form field which imposes an additional condition on top of the existing field. If the condition fails, the result is an error message. It is suggested that the message be a translation key so that the application can be localized to different languages.

Form decoders

Sourceval (let*) : 'a t -> ('a -> 'b t) -> 'b t

let* start_date = required unix_tm "start-date" decodes a form field and allows accessing it in the subsequent decoders. Eg:

  let* start_date = required unix_tm "start-date" in
  let+ end_date = required (unix_tm ~min:start_date) "end-date" in
  ...

However, note that let* uses a 'fail-fast' decoding strategy. If there is a decoding error, it immediately returns the error without decoding the subsequent fields. (Which makes sense if you think about the example above.) So, in order to ensure complete error reporting for all fields, you would need to use let+ and and+.

  • since 3.8.0
Sourceval (let+) : 'a t -> ('a -> 'b) -> 'b t

let+ email = required string "email" decodes a form field named email as a string.

Sourceval (and+) : 'a t -> 'b t -> ('a * 'b) t

and+ password = required string "password" continues decoding in an existing form declaration and decodes a form field password as a string.

Sourceval (or) : 'a t -> 'a t -> 'a t

form1 or form2 is form1 if it succeeds, else form2.

Sourceval validate : 'a t -> (string * string) list -> ('a, (string * string) list) result

validate form values is a result of validating the given form's values. It may be either some value of type 'a or a list of form field names and the corresponding error message keys.

Sourceval pp_error : (string * string) list Fmt.t

pp_error is a helper pretty-printer for debugging/troubleshooting form validation errors.

Error keys

When errors are reported, the following keys are used instead of English strings. These keys can be used for localizing the error messages. The suggested default English translations are given below.

These keys are modelled after Play Framework.

Sourceval error_expected_bool : string

Please enter true or false.

Sourceval error_expected_char : string

Please enter a single character.

Sourceval error_expected_single : string

Please enter a single value.

Sourceval error_expected_int : string

Please enter a valid integer.

Sourceval error_expected_int32 : string

Please enter a valid 32-bit integer.

Sourceval error_expected_int64 : string

Please enter a valid 64-bit integer.

Sourceval error_expected_number : string

Please enter a valid number.

Sourceval error_expected_time : string

Please enter a valid date or date-time.

Sourceval error_length : string

Please enter a value of the expected length.

Sourceval error_range : string

Please enter a value in the expected range.

Sourceval error_required : string

Please enter a value.

Examples

Basic functionality

  type user =
    { name : string;
      age : int option
    }

  open Dream_html.Form

  let user_form =
    let+ name = required string "name"
    and+ age = optional (int ~min:16) "age" in
    (* Thanks, Australia! *)
    { name; age }

  let dream_form = ["age", "42"; "name", "Bob"]
  let user_result = validate user_form dream_form

Result: Ok { name = "Bob"; age = Some 42 }

Sad path:

  validate user_form ["age", "none"]

Result: Error [("age", "error.expected.int"); ("name", "error.required")]

Repeated values

  type plan =
    { id : string;
      features : string list
    }

  let plan_form =
    let+ id = required string "id"
    and+ features = list string "features" in
    { id; features } validate plan_form ["id", "foo"]

Result: Ok {id = "foo"; features = []}

  validate plan_form ["id", "foo"; "features", "f1"; "features", "f2"]

Result: Ok {id = "foo"; features = ["f1"; "f2"]}

Note that the names can be anything, eg "features[]" if you prefer.

Constrained field values

  let plan_form =
    let+ id =
      ensure "error.expected.nonempty" (( <> ) "") required string "id"
    and+ features = list string "features" in
    { id; features } validate plan_form ["id", ""]

Result: Error [("id", "error.expected.nonempty")]

Complex validation rules

Using chained validation rules where some fields depend on others:

  type req =
    { id : string;
      years : int option;
      months : int option;
      weeks : int option;
      days : int option
    }

  let req_form =
    let+ id = required string "id" (* Both id... *)
    and+ days, weeks, months, years =
      (* ...and period are required *)
      let* days = optional int "days" in
      let* weeks = optional int "weeks" in
      let* months = optional int "months" in
      let* years = optional int "years" in
      match days, weeks, months, years with
      | None, None, None, None -> error "years" "Please enter a period"
      (* Only one period component is required *)
      | _ -> ok (days, weeks, months, years)
    in
    { id; days; weeks; months; years } validate req []

Result: Error [("years", "Please enter a period"); ("id", "error.required")]

Multiple structured values

Suppose you have the following form data submitted:

  item-count: 2
  item[0].id: abc
  item[0].qty: 1
  item[1].id: def
  item[1].qty: 10
  item[1].discount: 25

And you want to decode it into the following types:

  type item =
    { id : string;
      qty : int;
      discount : int
    }

  type invoice =
    { item_count : int;
      items : item list
    }

First create the indexed invoice item decoder and invoice decoder:

  let item n =
    let nth name = "item[" ^ string_of_int n ^ "]." ^ name in
    let+ id = required string (nth "id")
    and+ qty = required int (nth "qty")
    and+ discount = required ~default:0 int (nth "discount") in
    { id; qty; discount }

  let invoice =
    let* item_count = required int "item-count" in
    let+ items = multiple item_count item in
    { item_count; items }

Try it:

  validate invoice
    [ "item[0].id", "abc";
      "item[0].qty", "1";
      "item[1].id", "def";
      "item[1].qty", "10";
      "item[1].discount", "25";
      "item-count", "2" ]

Result:

  Ok
    { item_count = 2;
      items =
        [ { id = "def"; qty = 10; discount = 25 };
          { id = "abc"; qty = 1; discount = 0 } ]
    }

Validation error:

  validate invoice
    [ "item[0].qty", "1";
      "item[1].id", "def";
      "item[1].discount", "25";
      "item-count", "2" ]
    Error
    [item [0].id, error.required; item [1].qty, error.required]
OCaml

Innovation. Community. Security.