this repo has no description
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial working proof of concept

+724
+23
forma/.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "26.0.2" 18 + gleam-version: "1.5.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1.15.4" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
forma/.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+24
forma/README.md
··· 1 + # forma 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/forma)](https://hex.pm/packages/forma) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/forma/) 5 + 6 + ```sh 7 + gleam add forma@1 8 + ``` 9 + ```gleam 10 + import forma 11 + 12 + pub fn main() { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/forma>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+20
forma/gleam.toml
··· 1 + name = "forma" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "", repo = "" } 10 + # links = [{ title = "Website", href = "" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = ">= 0.34.0 and < 2.0.0" 17 + justin = ">= 1.0.1 and < 2.0.0" 18 + 19 + [dev-dependencies] 20 + gleeunit = ">= 1.0.0 and < 2.0.0"
+13
forma/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, 6 + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 7 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 8 + ] 9 + 10 + [requirements] 11 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 12 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 13 + justin = { version = ">= 1.0.1 and < 2.0.0" }
+230
forma/src/forma.gleam
··· 1 + // https://docs.djangoproject.com/en/5.0/topics/forms/ 2 + // https://github.com/nakaixo/nakai 3 + // date time handling https://hexdocs.pm/birl/index.html 4 + 5 + // TODO 6 + // - list fields 7 + // - form sets 8 + // - csrf token 9 + // - data order doesn't matter 10 + // - required/option 11 + 12 + import gleam/list 13 + import gleam/option.{type Option, None, Some} 14 + import gleam/result 15 + 16 + pub type HasDecoder 17 + 18 + pub type NoDecoder 19 + 20 + pub type FieldsWithErrors(format) = 21 + List(Field(format)) 22 + 23 + pub opaque type Form(format, output, decoder, has_decoder) { 24 + Form( 25 + fields: List(Field(format)), 26 + parse_with: fn(List(Field(format)), decoder) -> 27 + Result(output, FieldsWithErrors(format)), 28 + decoder: Option(decoder), 29 + ) 30 + } 31 + 32 + pub type Field(format) { 33 + Field( 34 + name: String, 35 + label: String, 36 + help_text: String, 37 + render: fn(Field(format)) -> format, 38 + value: String, 39 + ) 40 + InvalidField( 41 + name: String, 42 + label: String, 43 + help_text: String, 44 + render: fn(Field(format)) -> format, 45 + value: String, 46 + error: String, 47 + ) 48 + } 49 + 50 + pub type Input(output, format) { 51 + Input( 52 + render: fn(Field(format)) -> format, 53 + validate: fn(String) -> Result(output, String), 54 + ) 55 + } 56 + 57 + pub fn new() -> Form(format, a, a, NoDecoder) { 58 + Form([], fn(_, output) { Ok(output) }, None) 59 + } 60 + 61 + pub fn add( 62 + form: Form(format, fn(b) -> c, a, has_decoder), 63 + thing: #(Field(format), fn(String) -> Result(b, String)), 64 + ) -> Form(format, c, a, has_decoder) { 65 + let #(field, validate) = thing 66 + case form { 67 + Form(fields, parse_with, decoder) -> { 68 + // create new form with the new field and update the parse 69 + // function to handle the new details from the type of the 70 + // field 71 + Form( 72 + fields: [field, ..fields], 73 + parse_with: fn(fields, decoder: a) { 74 + let assert [field, ..rest] = fields 75 + case parse_with(rest, decoder), validate(field.value) { 76 + // the form we've already parsed has no errors and the field 77 + // we just parsed has no errors. so we can move on to the next 78 + Ok(next), Ok(value) -> Ok(next(value)) 79 + 80 + // the form already has errors even though this one succeeded. 81 + // so add this to the list and stop anymore parsing 82 + Error(fields), Ok(_value) -> Error([field, ..fields]) 83 + 84 + // form was good so far, but this field errored, so need to 85 + // mark this field as invalid and return all the fields we've got 86 + // so far 87 + Ok(_), Error(error) -> 88 + Error([ 89 + InvalidField( 90 + name: field.name, 91 + label: field.label, 92 + help_text: field.help_text, 93 + render: field.render, 94 + value: field.value, 95 + error: error, 96 + ), 97 + ..rest 98 + ]) 99 + 100 + // form already has errors and this field errored, so add this field 101 + // to the list 102 + Error(fields), Error(error) -> 103 + Error([ 104 + InvalidField( 105 + name: field.name, 106 + label: field.label, 107 + help_text: field.help_text, 108 + render: field.render, 109 + value: field.value, 110 + error: error, 111 + ), 112 + ..fields 113 + ]) 114 + } 115 + }, 116 + decoder:, 117 + ) 118 + } 119 + // FormWithErrors(fields) -> FormWithErrors([field, ..fields]) 120 + } 121 + } 122 + 123 + pub fn data( 124 + form: Form(a, b, format, has_decoder), 125 + input: List(#(String, String)), 126 + ) -> Form(a, b, format, has_decoder) { 127 + case form { 128 + Form(fields, parse_with, decoder) -> { 129 + fields 130 + // we always prepend fields, so reverse to get correct order 131 + // TODO I think we're going to make it so order doesn't matter 132 + |> list.reverse 133 + |> do_add_input_data(input, []) 134 + |> Form(parse_with, decoder) 135 + } 136 + // FormWithErrors(..) -> form 137 + } 138 + } 139 + 140 + fn do_add_input_data( 141 + fields: List(Field(format)), 142 + data: List(#(String, String)), 143 + acc: List(Field(format)), 144 + ) { 145 + case fields, data { 146 + // no more fields, we've return all the fields with data we have accumulated 147 + [], _ -> acc 148 + // no more data! return all the fields we have left plus the ones we accumulated 149 + _, [] -> list.append(fields, acc) 150 + // we have a field and data, and the names match. update field to have data 151 + [Field(name: field_name, ..) as field, ..fields_rest], 152 + [#(data_name, value), ..data_rest] 153 + if field_name == data_name 154 + -> 155 + do_add_input_data(fields_rest, data_rest, [ 156 + Field( 157 + name: field_name, 158 + label: field.label, 159 + help_text: field.help_text, 160 + render: field.render, 161 + value: value, 162 + ), 163 + ..acc 164 + ]) 165 + // at this point we still have fields and data left, but the first 166 + // field and first data don't match. so we decide we've got no data 167 + // for the first field and move on to the next. but we need to add 168 + // this field without data to the accumulator 169 + [field, ..fields_rest], _ -> 170 + do_add_input_data(fields_rest, data, [field, ..acc]) 171 + } 172 + } 173 + 174 + pub fn decodes( 175 + form: Form(format, output, decoder, has_decoder), 176 + decoder: decoder, 177 + ) -> Form(format, output, decoder, HasDecoder) { 178 + // can use let assert here because you can't have errors if you 179 + // haven't tried to parse and decode, which you can't do without 180 + // a decoder 181 + let Form(fields, parse_with, _) = form 182 + 183 + Form(fields, parse_with, Some(decoder)) 184 + } 185 + 186 + pub fn decoded( 187 + form: Form(format, output, decoder, HasDecoder), 188 + ) -> Result(output, Form(format, output, decoder, HasDecoder)) { 189 + let assert Form(fields, parse_with, Some(decoder)) = form 190 + case parse_with(fields, decoder) { 191 + Ok(output) -> Ok(output) 192 + Error(fields) -> Error(Form(fields, parse_with, Some(decoder))) 193 + } 194 + } 195 + 196 + pub fn decode_and_try( 197 + form: Form(format, output, decoder, HasDecoder), 198 + apply fun: fn(output, Form(format, output, decoder, HasDecoder)) -> 199 + Result(c, Form(format, output, decoder, HasDecoder)), 200 + ) -> Result(c, Form(format, output, decoder, HasDecoder)) { 201 + decoded(form) |> result.try(fun(_, form)) 202 + } 203 + 204 + pub fn get_fields(form: Form(format, a, b, has_decoder)) -> List(Field(format)) { 205 + form.fields |> list.reverse 206 + } 207 + 208 + pub fn set_field_error( 209 + form: Form(format, output, decoder, has_decoder), 210 + name: String, 211 + error: String, 212 + ) -> Form(format, output, decoder, has_decoder) { 213 + let updated = 214 + form.fields 215 + |> list.map(fn(field) { 216 + case field.name == name { 217 + True -> 218 + InvalidField( 219 + name: field.name, 220 + label: field.label, 221 + help_text: field.help_text, 222 + render: field.render, 223 + value: field.value, 224 + error: error, 225 + ) 226 + False -> field 227 + } 228 + }) 229 + Form(updated, form.parse_with, form.decoder) 230 + }
+52
forma/src/forma/field.gleam
··· 1 + import forma.{type Field, type Input, Field, Input} 2 + import forma/validation 3 + import forma/widget 4 + import justin 5 + 6 + pub fn text_field() -> Input(String, String) { 7 + Input(widget.text_input, validation.string) 8 + } 9 + 10 + pub fn email_field() -> Input(String, String) { 11 + Input(widget.text_input, validation.email) 12 + } 13 + 14 + pub fn integer_field() -> Input(Int, String) { 15 + Input(widget.text_input, validation.trim |> validation.and(validation.int)) 16 + } 17 + 18 + pub fn new( 19 + name: String, 20 + input: Input(output, format), 21 + ) -> #(Field(format), fn(String) -> Result(output, String)) { 22 + #( 23 + Field(name, justin.sentence_case(name), "", input.render, ""), 24 + input.validate, 25 + ) 26 + } 27 + 28 + pub fn help_text( 29 + thing: #(Field(format), fn(String) -> Result(b, String)), 30 + help_text: String, 31 + ) -> #(Field(format), fn(String) -> Result(b, String)) { 32 + let #(field, validate) = thing 33 + 34 + let field = 35 + Field(field.name, field.label, help_text, field.render, field.value) 36 + 37 + #(field, validate) 38 + } 39 + 40 + pub fn validate( 41 + thing: #(Field(format), fn(String) -> Result(b, String)), 42 + next: fn(b) -> Result(c, String), 43 + ) -> #(Field(format), fn(String) -> Result(c, String)) { 44 + let #(field, previous) = thing 45 + 46 + #(field, fn(str) { 47 + case previous(str) { 48 + Ok(value) -> next(value) 49 + Error(error) -> Error(error) 50 + } 51 + }) 52 + }
+45
forma/src/forma/validation.gleam
··· 1 + import gleam/int 2 + import gleam/result 3 + import gleam/string 4 + 5 + pub fn string(str: String) -> Result(String, String) { 6 + Ok(str) 7 + } 8 + 9 + pub fn email(input: String) -> Result(String, String) { 10 + // TODO verify both parts have at least one character? 11 + case input |> string.trim |> string.split("@") { 12 + [_, _] -> Ok(input) 13 + _ -> Error("Must be an email address") 14 + } 15 + } 16 + 17 + pub fn must_be_longer_than(length: Int) -> fn(String) -> Result(String, String) { 18 + fn(input) { 19 + case string.length(input) > length { 20 + True -> Ok(input) 21 + False -> 22 + Error("Must be longer than " <> int.to_string(length) <> " characters") 23 + } 24 + } 25 + } 26 + 27 + pub fn trim(str: String) -> Result(String, String) { 28 + Ok(string.trim(str)) 29 + } 30 + 31 + pub fn int(str: String) -> Result(Int, String) { 32 + str |> int.parse |> result.replace_error("not an integer") 33 + } 34 + 35 + pub fn and( 36 + previous: fn(a) -> Result(b, String), 37 + next: fn(b) -> Result(c, String), 38 + ) -> fn(a) -> Result(c, String) { 39 + fn(data) { 40 + case previous(data) { 41 + Ok(value) -> next(value) 42 + Error(error) -> Error(error) 43 + } 44 + } 45 + }
+28
forma/src/forma/widget.gleam
··· 1 + import forma 2 + 3 + pub fn checkbox_input(_f, _env) -> String { 4 + "<input type=\"checkbox\">" 5 + } 6 + 7 + pub fn password_input(_f, _env) -> String { 8 + "<input type=\"password\">" 9 + } 10 + 11 + pub fn text_input(f: forma.Field(String)) -> String { 12 + let placeholder = "" 13 + 14 + "<input name=\"" 15 + <> f.name 16 + <> "\" placeholder=\"" 17 + <> placeholder 18 + <> "\" type=\"text\" value=\"" 19 + <> f.value 20 + <> "\">" 21 + } 22 + 23 + // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 24 + // https://til.simonwillison.net/css/resizing-textarea 25 + 26 + pub fn textarea_input(_f, _env) -> String { 27 + "<textarea></textarea>" 28 + }
+285
forma/test/forma_test.gleam
··· 1 + import forma 2 + import forma/field 3 + import gleam/list 4 + import gleeunit 5 + import gleeunit/should 6 + 7 + pub fn main() { 8 + gleeunit.main() 9 + } 10 + 11 + pub fn empty_form_test() { 12 + forma.new() 13 + |> forma.get_fields 14 + |> list.length 15 + |> should.equal(0) 16 + } 17 + 18 + pub fn parse_empty_form_test() { 19 + forma.new() 20 + |> forma.data([]) 21 + |> forma.decodes(1) 22 + |> forma.decoded 23 + |> should.equal(Ok(1)) 24 + 25 + forma.new() 26 + |> forma.data([]) 27 + |> forma.decodes("Hello") 28 + |> forma.decoded 29 + |> should.equal(Ok("Hello")) 30 + } 31 + 32 + pub fn parse_single_field_form_test() { 33 + forma.new() 34 + |> forma.add(field.new("first", field.text_field())) 35 + |> forma.data([#("first", "world")]) 36 + |> forma.decodes(fn(str) { "hello " <> str }) 37 + |> forma.decoded 38 + |> should.equal(Ok("hello world")) 39 + 40 + forma.new() 41 + |> forma.add(field.new("first", field.text_field())) 42 + |> forma.data([]) 43 + |> forma.decodes(fn(str) { "hello " <> str }) 44 + |> forma.decoded 45 + |> should.equal(Ok("hello ")) 46 + } 47 + 48 + pub fn parse_double_field_form_test() { 49 + forma.new() 50 + |> forma.add(field.new("first", field.text_field())) 51 + |> forma.add(field.new("second", field.text_field())) 52 + |> forma.data([#("first", "hello"), #("second", "world")]) 53 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 54 + |> forma.decoded 55 + |> should.equal(Ok("hello world")) 56 + 57 + forma.new() 58 + |> forma.add(field.new("first", field.text_field())) 59 + |> forma.add(field.new("second", field.text_field())) 60 + |> forma.data([#("first", "hello")]) 61 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 62 + |> forma.decoded 63 + |> should.equal(Ok("hello ")) 64 + } 65 + 66 + pub fn parse_double_field_form_extra_data_test() { 67 + forma.new() 68 + |> forma.add(field.new("first", field.text_field())) 69 + |> forma.add(field.new("second", field.text_field())) 70 + |> forma.data([#("first", "1"), #("second", "2")]) 71 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 72 + |> forma.decoded 73 + |> should.equal(Ok("1 2")) 74 + 75 + forma.new() 76 + |> forma.add(field.new("first", field.text_field())) 77 + |> forma.add(field.new("second", field.text_field())) 78 + |> forma.data([#("first", "1"), #("second", "2"), #("second", "3")]) 79 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 80 + |> forma.decoded 81 + |> should.equal(Ok("1 2")) 82 + } 83 + 84 + pub fn parse_double_field_form_missing_data_test() { 85 + forma.new() 86 + |> forma.add(field.new("first", field.text_field())) 87 + |> forma.add(field.new("second", field.text_field())) 88 + |> forma.data([#("second", "1")]) 89 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 90 + |> forma.decoded 91 + |> should.equal(Ok(" 1")) 92 + 93 + forma.new() 94 + |> forma.add(field.new("first", field.text_field())) 95 + |> forma.add(field.new("second", field.text_field())) 96 + |> forma.data([#("first", "1"), #("first", "2")]) 97 + |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 98 + |> forma.decoded 99 + |> should.equal(Ok("1 ")) 100 + } 101 + 102 + pub fn integer_field_test() { 103 + forma.new() 104 + |> forma.add(field.new("first", field.integer_field())) 105 + |> forma.data([#("first", " 1 ")]) 106 + |> forma.decodes(fn(i) { i }) 107 + |> forma.decoded 108 + |> should.equal(Ok(1)) 109 + } 110 + 111 + pub fn can_decodes_in_any_order_test() { 112 + forma.new() 113 + |> forma.decodes(fn(str) { "hello " <> str }) 114 + |> forma.add(field.new("first", field.text_field())) 115 + |> forma.data([#("first", "world")]) 116 + |> forma.decoded 117 + |> should.equal(Ok("hello world")) 118 + 119 + forma.new() 120 + |> forma.add(field.new("first", field.text_field())) 121 + |> forma.data([#("first", "world")]) 122 + |> forma.decodes(fn(str) { "hello " <> str }) 123 + |> forma.decoded 124 + |> should.equal(Ok("hello world")) 125 + } 126 + 127 + pub fn parse_single_field_form_with_error_test() { 128 + let assert Error(f) = 129 + forma.new() 130 + |> forma.add(field.new("first", field.integer_field())) 131 + |> forma.data([#("first", "world")]) 132 + |> forma.decodes(fn(_) { 1 }) 133 + |> forma.decoded 134 + 135 + let assert [field] = forma.get_fields(f) 136 + field |> should_be_field_with_error("not an integer") 137 + } 138 + 139 + pub fn parse_double_field_form_with_error_test() { 140 + let form = 141 + forma.new() 142 + |> forma.add(field.new("a", field.integer_field())) 143 + |> forma.add(field.new("b", field.integer_field())) 144 + |> forma.decodes(fn(_) { fn(_) { 1 } }) 145 + 146 + let assert Error(f) = 147 + form 148 + |> forma.data([#("a", "not a number"), #("b", "2")]) 149 + |> forma.decoded 150 + 151 + let assert [fielda, fieldb] = forma.get_fields(f) 152 + fielda |> should_be_field_with_error("not an integer") 153 + fieldb |> should_be_field_no_error 154 + 155 + let assert Error(f) = 156 + form 157 + |> forma.data([#("a", "1"), #("b", "string")]) 158 + |> forma.decoded 159 + 160 + let assert [fielda, fieldb] = forma.get_fields(f) 161 + fielda |> should_be_field_no_error 162 + fieldb |> should_be_field_with_error("not an integer") 163 + 164 + let assert Error(f) = 165 + form 166 + |> forma.data([#("a", "string"), #("b", "string")]) 167 + |> forma.decoded 168 + 169 + let assert [fielda, fieldb] = forma.get_fields(f) 170 + fielda |> should_be_field_with_error("not an integer") 171 + fieldb |> should_be_field_with_error("not an integer") 172 + } 173 + 174 + pub fn parse_triple_field_form_with_error_test() { 175 + let form = 176 + forma.new() 177 + |> forma.add(field.new("a", field.integer_field())) 178 + |> forma.add(field.new("b", field.integer_field())) 179 + |> forma.add(field.new("c", field.integer_field())) 180 + |> forma.decodes(fn(_) { fn(_) { fn(_) { 1 } } }) 181 + 182 + let assert Error(f) = 183 + form 184 + |> forma.data([#("a", "1"), #("b", "2"), #("c", "string")]) 185 + |> forma.decoded 186 + let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 187 + fielda |> should_be_field_no_error 188 + fieldb |> should_be_field_no_error 189 + fieldc |> should_be_field_with_error("not an integer") 190 + 191 + let assert Error(f) = 192 + form 193 + |> forma.data([#("a", "1"), #("b", "string"), #("c", "string")]) 194 + |> forma.decoded 195 + let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 196 + fielda |> should_be_field_no_error 197 + fieldb |> should_be_field_with_error("not an integer") 198 + fieldc |> should_be_field_with_error("not an integer") 199 + 200 + let assert Error(f) = 201 + form 202 + |> forma.data([#("a", "1"), #("b", "string"), #("c", "3")]) 203 + |> forma.decoded 204 + let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 205 + fielda |> should_be_field_no_error 206 + fieldb |> should_be_field_with_error("not an integer") 207 + fieldc |> should_be_field_no_error 208 + 209 + let assert Error(f) = 210 + form 211 + |> forma.data([#("a", "string"), #("b", "string"), #("c", "3")]) 212 + |> forma.decoded 213 + let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 214 + fielda |> should_be_field_with_error("not an integer") 215 + fieldb |> should_be_field_with_error("not an integer") 216 + fieldc |> should_be_field_no_error 217 + 218 + let assert Error(f) = 219 + form 220 + |> forma.data([#("a", "string"), #("b", "2"), #("c", "3")]) 221 + |> forma.decoded 222 + let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 223 + fielda |> should_be_field_with_error("not an integer") 224 + fieldb |> should_be_field_no_error 225 + fieldc |> should_be_field_no_error 226 + } 227 + 228 + fn should_be_field_no_error(field: forma.Field(String)) { 229 + should.equal( 230 + field, 231 + forma.Field( 232 + name: field.name, 233 + label: field.label, 234 + help_text: field.help_text, 235 + value: field.value, 236 + render: field.render, 237 + ), 238 + ) 239 + } 240 + 241 + fn should_be_field_with_error(field: forma.Field(String), str: String) { 242 + should.equal( 243 + field, 244 + forma.InvalidField( 245 + name: field.name, 246 + label: field.label, 247 + help_text: field.help_text, 248 + value: field.value, 249 + render: field.render, 250 + error: str, 251 + ), 252 + ) 253 + } 254 + 255 + pub fn decoded_and_try_test() { 256 + let f = 257 + forma.new() 258 + |> forma.add(field.new("a", field.integer_field())) 259 + |> forma.add(field.new("b", field.integer_field())) 260 + |> forma.add(field.new("c", field.integer_field())) 261 + |> forma.decodes(fn(_) { fn(_) { fn(_) { 1 } } }) 262 + |> forma.data([#("a", "1"), #("b", "2"), #("c", "3")]) 263 + 264 + // can succeed 265 + forma.decode_and_try(f, fn(_, _) { Ok(3) }) 266 + |> should.equal(Ok(3)) 267 + 268 + // can change type 269 + forma.decode_and_try(f, fn(_, _) { Ok("it worked") }) 270 + |> should.equal(Ok("it worked")) 271 + 272 + // can error 273 + forma.decode_and_try(f, fn(_, form) { Error(form) }) 274 + |> should.equal(Error(f)) 275 + 276 + // can change field 277 + let assert Error(form) = 278 + forma.decode_and_try(f, fn(_, form) { 279 + Error(forma.set_field_error(form, "a", "woops")) 280 + }) 281 + let assert [fielda, fieldb, fieldc] = forma.get_fields(form) 282 + fielda |> should_be_field_with_error("woops") 283 + fieldb |> should_be_field_no_error 284 + fieldc |> should_be_field_no_error 285 + }