Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode form into custom type #66

Open
yawaramin opened this issue May 31, 2021 · 9 comments
Open

Decode form into custom type #66

yawaramin opened this issue May 31, 2021 · 9 comments

Comments

@yawaramin
Copy link
Contributor

This is a proposal to add a function Dream.form_decode (e.g., name is not super important) to decode a form's fields into a custom type, say e.g. type person = { name : string; id : int }. Fortunately, 'a form_result is generic already which leads me to think you possibly had something like this in mind already.

Justification: currently Dream.form returns an association list of form keys and values. This forces us to pattern match on every form field in order. Sometimes we don't want to handle all form fields, and instead just a few of them. E.g., when writing a Slack slash-command, you set up a POST endpoint that receives a form body with several fields: https://api.slack.com/interactivity/slash-commands#app_command_handling

So we would need to pattern match on token, team_id, etc. to get to the one field we often really want, text. Instead of that, if we could define a form decoder, we could selectively pick out only the fields we want. E.g.:

let slack_form = (* a form decoder *)

match%lwt Dream.form_decode slack_form request with
| `Ok { text } -> (* ... *)
| _ -> Dream.empty `Bad_Request
@aantron
Copy link
Owner

aantron commented May 31, 2021

For this particular use case, would this work?

match%lwt Dream.form request with
| `Ok form ->
  let text = List.assoc "text" form in
  (* ... *)
| _ -> Dream.empty `Bad_Request

(Note: I am willing to consider more generic decoders, of course, just want to ask first!)

@aantron
Copy link
Owner

aantron commented May 31, 2021

(Note that more robust code would probably use List.assoc_opt)

@yawaramin
Copy link
Contributor Author

Yes, that will definitely work. I suppose that reduces this request to more about type-safety ergonomics :-) If I may provide another data point, the Play Framework does something very similar to what I suggested: https://www.playframework.com/documentation/2.8.x/ScalaForms

It has what it calls a 'mapping' which describes how to decode the form into a custom type. It helps to decode and validate the form in one shot. They provide decoding helpers for emails, dates, times, decimal numbers. Very useful.

@tungd
Copy link

tungd commented Jun 1, 2021

From the top of my head there are conformist (used by Sihl) and decoders that implement this mapping functionality, including validation.

I think one of the route we could consider is to make Dream.form returns compatible values with either of these libraries. Or, having helper functions on a separate library like dream-conformist or something like that.

@aantron
Copy link
Owner

aantron commented Jun 3, 2021

Thanks @yawaramin and @tungd. I think separate dream-conformist or dream-decoders is good to get started. I was originally considering creating some kind of deriving_form, too, which might eliminate boilerplate completely, as it would define the schema/decoder automatically, based on the OCaml type.

@thangngoc89
Copy link
Contributor

thangngoc89 commented Jul 17, 2021

I've looked at conformist and I can say that it's a good library.
We need a separate dream-conformist but it would be quite simple:

val to_conformist: (string * string) list -> (string * string list) list

(* Implementation *)
let to_conformist : (string * string) list -> (string * string list) list =
 fun dream_inputs ->
  List.map (fun (field, value) -> (field, [ value ])) dream_inputs

Advance use cases would be look for this pattern:

field.1, field.2, field.3

And turn field's value into a list of string instead of 3 separate field

We can also provide some built-in validators (could be dream-validators). Each validator would have this signature:

val validator: 'a -> string option

See oxidizing/conformist#7 for more details


Update: maybe we don't need dream-conformist because to_conformist doesn't have any dependencies to conformist at all. We could put it in dream-validators

@yawaramin
Copy link
Contributor Author

yawaramin commented Nov 25, 2024

I have something working that looks like this:

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

let user_form =
  let+ name = ensure "Must not be empty" (( <> ) "") required "name" string
  and+ id = ensure "Must be > 0" (( < ) 0) required "id" int
  and+ age =
    ensure "Must be > 16"
      (Option.fold ~none:true ~some:(( < ) 16))
      optional "age" int
  in
  { name; id; age }

let user = validate "user-form" user_form []

With the result:

Error
   (`Assoc
      [("user-form",
        `Assoc
          [("id", `String "Please fill out this field");
           ("name", `String "Please fill out this field")])])

And happy path:

# let user = validate "form-login" form ["name", "Bob"; "id", "16"];;
val user :
  (user,
   [> `Assoc of
        (string * [> `Assoc of (string * [> `String of string ]) list ]) list ])
  result = Ok {name = "Bob"; id = 16; age = None}

Would there be interest in something like this? I can also consider putting it in dream-html as form validation could be loosely said to be related to HTML...

@aantron
Copy link
Owner

aantron commented Nov 25, 2024

I would start with putting it into a downstream library + examples and links from the Dream docs.

@yawaramin
Copy link
Contributor Author

OK, added to dream-html and will publish it soon. Some examples https://yawaramin.github.io/dream-html/dream-html/Dream_html/Form/index.html#examples

tchibanda24 pushed a commit to tchibanda24/dream that referenced this issue Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants