···11+# forma
22+33+[](https://hex.pm/packages/forma)
44+[](https://hexdocs.pm/forma/)
55+66+```sh
77+gleam add forma@1
88+```
99+```gleam
1010+import forma
1111+1212+pub fn main() {
1313+ // TODO: An example of the project in use
1414+}
1515+```
1616+1717+Further documentation can be found at <https://hexdocs.pm/forma>.
1818+1919+## Development
2020+2121+```sh
2222+gleam run # Run the project
2323+gleam test # Run the tests
2424+```
+20
forma/gleam.toml
···11+name = "forma"
22+version = "1.0.0"
33+44+# Fill out these fields if you intend to generate HTML documentation or publish
55+# your project to the Hex package manager.
66+#
77+# description = ""
88+# licences = ["Apache-2.0"]
99+# repository = { type = "github", user = "", repo = "" }
1010+# links = [{ title = "Website", href = "" }]
1111+#
1212+# For a full reference of all the available options, you can have a look at
1313+# https://gleam.run/writing-gleam/gleam-toml/.
1414+1515+[dependencies]
1616+gleam_stdlib = ">= 0.34.0 and < 2.0.0"
1717+justin = ">= 1.0.1 and < 2.0.0"
1818+1919+[dev-dependencies]
2020+gleeunit = ">= 1.0.0 and < 2.0.0"
+13
forma/manifest.toml
···11+# This file was generated by Gleam
22+# You typically do not need to edit this file
33+44+packages = [
55+ { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" },
66+ { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
77+ { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
88+]
99+1010+[requirements]
1111+gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
1212+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
1313+justin = { version = ">= 1.0.1 and < 2.0.0" }
+230
forma/src/forma.gleam
···11+// https://docs.djangoproject.com/en/5.0/topics/forms/
22+// https://github.com/nakaixo/nakai
33+// date time handling https://hexdocs.pm/birl/index.html
44+55+// TODO
66+// - list fields
77+// - form sets
88+// - csrf token
99+// - data order doesn't matter
1010+// - required/option
1111+1212+import gleam/list
1313+import gleam/option.{type Option, None, Some}
1414+import gleam/result
1515+1616+pub type HasDecoder
1717+1818+pub type NoDecoder
1919+2020+pub type FieldsWithErrors(format) =
2121+ List(Field(format))
2222+2323+pub opaque type Form(format, output, decoder, has_decoder) {
2424+ Form(
2525+ fields: List(Field(format)),
2626+ parse_with: fn(List(Field(format)), decoder) ->
2727+ Result(output, FieldsWithErrors(format)),
2828+ decoder: Option(decoder),
2929+ )
3030+}
3131+3232+pub type Field(format) {
3333+ Field(
3434+ name: String,
3535+ label: String,
3636+ help_text: String,
3737+ render: fn(Field(format)) -> format,
3838+ value: String,
3939+ )
4040+ InvalidField(
4141+ name: String,
4242+ label: String,
4343+ help_text: String,
4444+ render: fn(Field(format)) -> format,
4545+ value: String,
4646+ error: String,
4747+ )
4848+}
4949+5050+pub type Input(output, format) {
5151+ Input(
5252+ render: fn(Field(format)) -> format,
5353+ validate: fn(String) -> Result(output, String),
5454+ )
5555+}
5656+5757+pub fn new() -> Form(format, a, a, NoDecoder) {
5858+ Form([], fn(_, output) { Ok(output) }, None)
5959+}
6060+6161+pub fn add(
6262+ form: Form(format, fn(b) -> c, a, has_decoder),
6363+ thing: #(Field(format), fn(String) -> Result(b, String)),
6464+) -> Form(format, c, a, has_decoder) {
6565+ let #(field, validate) = thing
6666+ case form {
6767+ Form(fields, parse_with, decoder) -> {
6868+ // create new form with the new field and update the parse
6969+ // function to handle the new details from the type of the
7070+ // field
7171+ Form(
7272+ fields: [field, ..fields],
7373+ parse_with: fn(fields, decoder: a) {
7474+ let assert [field, ..rest] = fields
7575+ case parse_with(rest, decoder), validate(field.value) {
7676+ // the form we've already parsed has no errors and the field
7777+ // we just parsed has no errors. so we can move on to the next
7878+ Ok(next), Ok(value) -> Ok(next(value))
7979+8080+ // the form already has errors even though this one succeeded.
8181+ // so add this to the list and stop anymore parsing
8282+ Error(fields), Ok(_value) -> Error([field, ..fields])
8383+8484+ // form was good so far, but this field errored, so need to
8585+ // mark this field as invalid and return all the fields we've got
8686+ // so far
8787+ Ok(_), Error(error) ->
8888+ Error([
8989+ InvalidField(
9090+ name: field.name,
9191+ label: field.label,
9292+ help_text: field.help_text,
9393+ render: field.render,
9494+ value: field.value,
9595+ error: error,
9696+ ),
9797+ ..rest
9898+ ])
9999+100100+ // form already has errors and this field errored, so add this field
101101+ // to the list
102102+ Error(fields), Error(error) ->
103103+ Error([
104104+ InvalidField(
105105+ name: field.name,
106106+ label: field.label,
107107+ help_text: field.help_text,
108108+ render: field.render,
109109+ value: field.value,
110110+ error: error,
111111+ ),
112112+ ..fields
113113+ ])
114114+ }
115115+ },
116116+ decoder:,
117117+ )
118118+ }
119119+ // FormWithErrors(fields) -> FormWithErrors([field, ..fields])
120120+ }
121121+}
122122+123123+pub fn data(
124124+ form: Form(a, b, format, has_decoder),
125125+ input: List(#(String, String)),
126126+) -> Form(a, b, format, has_decoder) {
127127+ case form {
128128+ Form(fields, parse_with, decoder) -> {
129129+ fields
130130+ // we always prepend fields, so reverse to get correct order
131131+ // TODO I think we're going to make it so order doesn't matter
132132+ |> list.reverse
133133+ |> do_add_input_data(input, [])
134134+ |> Form(parse_with, decoder)
135135+ }
136136+ // FormWithErrors(..) -> form
137137+ }
138138+}
139139+140140+fn do_add_input_data(
141141+ fields: List(Field(format)),
142142+ data: List(#(String, String)),
143143+ acc: List(Field(format)),
144144+) {
145145+ case fields, data {
146146+ // no more fields, we've return all the fields with data we have accumulated
147147+ [], _ -> acc
148148+ // no more data! return all the fields we have left plus the ones we accumulated
149149+ _, [] -> list.append(fields, acc)
150150+ // we have a field and data, and the names match. update field to have data
151151+ [Field(name: field_name, ..) as field, ..fields_rest],
152152+ [#(data_name, value), ..data_rest]
153153+ if field_name == data_name
154154+ ->
155155+ do_add_input_data(fields_rest, data_rest, [
156156+ Field(
157157+ name: field_name,
158158+ label: field.label,
159159+ help_text: field.help_text,
160160+ render: field.render,
161161+ value: value,
162162+ ),
163163+ ..acc
164164+ ])
165165+ // at this point we still have fields and data left, but the first
166166+ // field and first data don't match. so we decide we've got no data
167167+ // for the first field and move on to the next. but we need to add
168168+ // this field without data to the accumulator
169169+ [field, ..fields_rest], _ ->
170170+ do_add_input_data(fields_rest, data, [field, ..acc])
171171+ }
172172+}
173173+174174+pub fn decodes(
175175+ form: Form(format, output, decoder, has_decoder),
176176+ decoder: decoder,
177177+) -> Form(format, output, decoder, HasDecoder) {
178178+ // can use let assert here because you can't have errors if you
179179+ // haven't tried to parse and decode, which you can't do without
180180+ // a decoder
181181+ let Form(fields, parse_with, _) = form
182182+183183+ Form(fields, parse_with, Some(decoder))
184184+}
185185+186186+pub fn decoded(
187187+ form: Form(format, output, decoder, HasDecoder),
188188+) -> Result(output, Form(format, output, decoder, HasDecoder)) {
189189+ let assert Form(fields, parse_with, Some(decoder)) = form
190190+ case parse_with(fields, decoder) {
191191+ Ok(output) -> Ok(output)
192192+ Error(fields) -> Error(Form(fields, parse_with, Some(decoder)))
193193+ }
194194+}
195195+196196+pub fn decode_and_try(
197197+ form: Form(format, output, decoder, HasDecoder),
198198+ apply fun: fn(output, Form(format, output, decoder, HasDecoder)) ->
199199+ Result(c, Form(format, output, decoder, HasDecoder)),
200200+) -> Result(c, Form(format, output, decoder, HasDecoder)) {
201201+ decoded(form) |> result.try(fun(_, form))
202202+}
203203+204204+pub fn get_fields(form: Form(format, a, b, has_decoder)) -> List(Field(format)) {
205205+ form.fields |> list.reverse
206206+}
207207+208208+pub fn set_field_error(
209209+ form: Form(format, output, decoder, has_decoder),
210210+ name: String,
211211+ error: String,
212212+) -> Form(format, output, decoder, has_decoder) {
213213+ let updated =
214214+ form.fields
215215+ |> list.map(fn(field) {
216216+ case field.name == name {
217217+ True ->
218218+ InvalidField(
219219+ name: field.name,
220220+ label: field.label,
221221+ help_text: field.help_text,
222222+ render: field.render,
223223+ value: field.value,
224224+ error: error,
225225+ )
226226+ False -> field
227227+ }
228228+ })
229229+ Form(updated, form.parse_with, form.decoder)
230230+}