๐Ÿ‘ฉโ€๐Ÿš’ Firefighters API written in Gleam!
gleam

:boom: use json instead of forms

-1
gleam.toml
··· 21 21 mist = ">= 5.0.3 and < 6.0.0" 22 22 gleam_otp = ">= 1.1.0 and < 2.0.0" 23 23 gleam_json = ">= 3.0.2 and < 4.0.0" 24 - formal = ">= 3.0.0 and < 4.0.0" 25 24 envoy = ">= 1.0.2 and < 2.0.0" 26 25 gleam_http = ">= 4.1.1 and < 5.0.0" 27 26 youid = ">= 1.5.1 and < 2.0.0"
-2
manifest.toml
··· 11 11 { name = "eval", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "eval", source = "hex", outer_checksum = "264DAF4B49DF807F303CA4A4E4EBC012070429E40BE384C58FE094C4958F9BDA" }, 12 12 { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 13 13 { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 14 - { name = "formal", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "formal", source = "hex", outer_checksum = "686D0C4C9CB36397DBAC2EC8C6ED9BD0F81B2DF2E88F75A7DB75F56768DDD8FC" }, 15 14 { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 16 15 { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, 17 16 { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, ··· 60 59 argv = { version = ">= 1.0.2 and < 2.0.0" } 61 60 cors_builder = { version = ">= 2.0.6 and < 3.0.0" } 62 61 envoy = { version = ">= 1.0.2 and < 2.0.0" } 63 - formal = { version = ">= 3.0.0 and < 4.0.0" } 64 62 gleam_community_ansi = { version = ">= 1.4.3 and < 2.0.0" } 65 63 gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" } 66 64 gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
+22 -48
src/app/domain/user/signup.gleam
··· 12 12 import app/web 13 13 import app/web/context.{type Context} 14 14 import argus 15 - import formal/form 15 + import gleam/dynamic/decode 16 16 import gleam/http 17 17 import gleam/json 18 18 import gleam/list ··· 32 32 ctx ctx: Context, 33 33 ) -> wisp.Response { 34 34 use <- wisp.require_method(req, http.Post) 35 - use form_data <- wisp.require_form(req) 36 - let form_result = 37 - signup_form() 38 - |> form.add_values(form_data.values) 39 - |> form.run 35 + use body <- wisp.require_json(req) 40 36 41 - case form_result { 42 - Error(_) -> wisp.unprocessable_content() 37 + case decode.run(body, sign_up_decoder()) { 38 + Error(err) -> web.handle_decode_error(err) 43 39 Ok(body) -> handle_body(req, body, ctx) 44 40 } 45 41 } ··· 118 114 phone_number: String, 119 115 email: String, 120 116 password: String, 117 + confirm_password: String, 121 118 user_role: String, 122 119 ) 123 120 } 124 121 125 - /// ๓ฑ A form that decodes the `SignUp` value. 126 - fn signup_form() -> form.Form(SignUp) { 127 - form.new({ 128 - use name <- form.field("nome", { 129 - form.parse_string |> form.check_not_empty() 130 - }) 131 - 132 - use registration <- form.field("matricula", { 133 - form.parse_string |> form.check_not_empty() 134 - }) 135 - 136 - use phone_number <- form.field("telefone", { 137 - form.parse_phone_number |> form.check_not_empty() 138 - }) 139 - 140 - use email <- form.field("email", { 141 - form.parse_email |> form.check_not_empty() 142 - }) 143 - 144 - use password <- form.field("senha", { 145 - form.parse_string |> form.check_not_empty() 146 - }) 147 - 148 - use _ <- form.field("confirma_senha", { 149 - form.parse_string |> form.check_confirms(password) 150 - }) 151 - 152 - use user_role <- form.field("cargo", { 153 - form.parse_string |> form.check_not_empty() 154 - }) 155 - 156 - form.success(SignUp( 157 - name:, 158 - registration:, 159 - phone_number:, 160 - email:, 161 - password:, 162 - user_role:, 163 - )) 164 - }) 122 + fn sign_up_decoder() -> decode.Decoder(SignUp) { 123 + use name <- decode.field("nome", decode.string) 124 + use registration <- decode.field("matricula", decode.string) 125 + use phone_number <- decode.field("telefone", decode.string) 126 + use email <- decode.field("email", decode.string) 127 + use password <- decode.field("senha", decode.string) 128 + use confirm_password <- decode.field("confirma_senha", decode.string) 129 + use user_role <- decode.field("cargo", decode.string) 130 + decode.success(SignUp( 131 + name:, 132 + registration:, 133 + phone_number:, 134 + email:, 135 + password:, 136 + confirm_password:, 137 + user_role:, 138 + )) 165 139 } 166 140 167 141 fn handle_database_error(err: pog.QueryError) -> wisp.Response {
+23 -44
src/app/domain/user/update_user_password.gleam
··· 3 3 import app/web 4 4 import app/web/context.{type Context} 5 5 import argus 6 - import formal/form 7 6 import gleam/bool 7 + import gleam/dynamic/decode 8 8 import gleam/http 9 9 import gleam/list 10 10 import gleam/result ··· 23 23 ctx ctx: Context, 24 24 ) -> wisp.Response { 25 25 use <- wisp.require_method(req, http.Put) 26 - use form_data <- wisp.require_form(req) 27 - let form_result = 28 - update_password_form() 29 - |> form.add_values(form_data.values) 30 - |> form.run() 26 + use body <- wisp.require_json(req) 31 27 32 - case form_result { 28 + case decode.run(body, request_body_decoder()) { 33 29 Ok(form_data) -> handle_form_data(request: req, ctx:, form_data:) 34 - Error(_) -> { 35 - let resp = wisp.unprocessable_content() 36 - 37 - "Formulรกrio invรกlido" 38 - |> wisp.Text 39 - |> wisp.set_body(resp, _) 40 - } 30 + Error(err) -> web.handle_decode_error(err) 41 31 } 42 32 } 43 33 44 34 fn handle_form_data( 45 - request request: wisp.Request, 35 + request req: wisp.Request, 46 36 ctx ctx: Context, 47 37 form_data form_data: RequestBody, 48 38 ) -> wisp.Response { 49 - case update_user_password(request:, ctx:, form_data:) { 39 + case update_user_password(request: req, ctx:, form_data:) { 50 40 Error(err) -> handle_error(err) 51 41 Ok(_) -> { 52 42 let resp = wisp.ok() ··· 85 75 } 86 76 87 77 fn update_user_password( 88 - request request: wisp.Request, 78 + request req: wisp.Request, 89 79 ctx ctx: Context, 90 80 form_data form_data: RequestBody, 91 81 ) -> Result(pog.Returned(Nil), UpdatePasswordError) { 92 82 use user_uuid <- result.try( 93 - user.extract_uuid(request:, cookie_name: user.uuid_cookie_name) 83 + user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 94 84 |> result.map_error(AccessControl), 95 85 ) 96 86 ··· 149 139 row.password_hash 150 140 } 151 141 152 - fn update_password_form() -> form.Form(RequestBody) { 153 - form.new({ 154 - use current_password <- form.field("senhaAtual", { 155 - form.parse_string |> form.check_not_empty() 156 - }) 157 - use new_password <- form.field("novaSenha", { 158 - form.parse_string 159 - |> form.check_not_empty() 160 - |> form.check(fn(value) { 161 - case value == current_password { 162 - False -> Ok(value) 163 - True -> Error("Nova senha deve ser diferente da atual") 164 - } 165 - }) 166 - }) 167 - 168 - use _ <- form.field("confirmarSenha", { 169 - form.parse_string |> form.check_confirms(new_password) 170 - }) 171 - 172 - // Success! 173 - RequestBody(current_password:, new_password:) 174 - |> form.success() 175 - }) 142 + type RequestBody { 143 + RequestBody( 144 + current_password: String, 145 + new_password: String, 146 + confirm_password: String, 147 + ) 176 148 } 177 149 178 - type RequestBody { 179 - RequestBody(current_password: String, new_password: String) 150 + fn request_body_decoder() -> decode.Decoder(RequestBody) { 151 + use current_password <- decode.field("senhaAtual", decode.string) 152 + use new_password <- decode.field("novaSenha", decode.string) 153 + use confirm_password <- decode.field("confirmarSenha", decode.string) 154 + decode.success(RequestBody( 155 + current_password:, 156 + new_password:, 157 + confirm_password:, 158 + )) 180 159 } 181 160 182 161 /// ๎™” Updating an user's password can fail
+55 -45
test/user_test.gleam
··· 60 60 let dummy_password = wisp.random_string(10) 61 61 let req = 62 62 simulate.browser_request(http.Post, "/admin/signup") 63 - |> simulate.form_body([ 64 - #("nome", wisp.random_string(10)), 65 - #("matricula", int.random(111) |> int.to_string), 66 - #("telefone", int.random(9_999_999_999) |> int.to_string), 67 - #("email", wisp.random_string(12) <> "@email.com"), 68 - #("senha", dummy_password), 69 - #("confirma_senha", dummy_password), 70 - #("cargo", role.to_string_pt_br(dummy.random_role())), 71 - ]) 63 + |> simulate.json_body( 64 + json.object([ 65 + #("nome", wisp.random_string(10) |> json.string), 66 + #("matricula", int.random(111) |> int.to_string |> json.string), 67 + #("telefone", int.random(9_999_999_999) |> int.to_string |> json.string), 68 + #("email", { wisp.random_string(12) <> "@email.com" } |> json.string), 69 + #("senha", dummy_password |> json.string), 70 + #("confirma_senha", dummy_password |> json.string), 71 + #("cargo", dummy.random_role() |> role.to_string_pt_br() |> json.string), 72 + ]), 73 + ) 72 74 73 75 let resp = http_router.handle_request(req, ctx) 74 76 assert resp.status == 401 as "Endpoint access should be restricted" ··· 102 104 103 105 let req = 104 106 simulate.browser_request(http.Post, "/admin/signup") 105 - |> simulate.form_body([ 106 - #("nome", wisp.random_string(10)), 107 - #("matricula", taken_registration), 108 - #("telefone", int.random(9_999_999_999) |> int.to_string), 109 - #("email", wisp.random_string(5) <> "@email.com"), 110 - #("senha", dummy_pswd), 111 - #("confirma_senha", dummy_pswd), 112 - #("cargo", role.to_string_pt_br(dummy.random_role())), 113 - ]) 107 + |> simulate.json_body( 108 + json.object([ 109 + #("nome", wisp.random_string(10) |> json.string), 110 + #("matricula", taken_registration |> json.string), 111 + #("telefone", int.random(9_999_999_999) |> int.to_string |> json.string), 112 + #("email", { wisp.random_string(12) <> "@email.com" } |> json.string), 113 + #("senha", dummy_pswd |> json.string), 114 + #("confirma_senha", dummy_pswd |> json.string), 115 + #("cargo", dummy.random_role() |> role.to_string_pt_br() |> json.string), 116 + ]), 117 + ) 114 118 115 119 let with_auth = app_test.with_authorization(next: req) 116 120 let resp = http_router.handle_request(with_auth, ctx) ··· 126 130 127 131 let req = 128 132 simulate.browser_request(http.Post, "/admin/signup") 129 - |> simulate.form_body([ 130 - #("nome", wisp.random_string(10)), 131 - #("matricula", int.random(111) |> int.to_string), 132 - #("telefone", int.random(9_999_999_999) |> int.to_string), 133 - #("email", taken_email), 134 - #("senha", dummy_pswd), 135 - #("confirma_senha", dummy_pswd), 136 - #("cargo", role.to_string_pt_br(dummy.random_role())), 137 - ]) 133 + |> simulate.json_body( 134 + json.object([ 135 + #("nome", wisp.random_string(10) |> json.string), 136 + #("matricula", int.random(111) |> int.to_string |> json.string), 137 + #("telefone", int.random(9_999_999_999) |> int.to_string |> json.string), 138 + #("email", taken_email |> json.string), 139 + #("senha", dummy_pswd |> json.string), 140 + #("confirma_senha", dummy_pswd |> json.string), 141 + #("cargo", dummy.random_role() |> role.to_string_pt_br() |> json.string), 142 + ]), 143 + ) 138 144 139 145 let with_auth = app_test.with_authorization(next: req) 140 146 let resp = http_router.handle_request(with_auth, ctx) ··· 149 155 150 156 let req = 151 157 simulate.browser_request(http.Post, "/admin/signup") 152 - |> simulate.form_body([ 153 - #("nome", wisp.random_string(10)), 154 - #("matricula", int.random(111) |> int.to_string), 155 - #("telefone", taken_phone), 156 - #("email", wisp.random_string(5) <> "@email.com"), 157 - #("senha", dummy_pswd), 158 - #("confirma_senha", dummy_pswd), 159 - #("cargo", role.to_string_pt_br(dummy.random_role())), 160 - ]) 158 + |> simulate.json_body( 159 + json.object([ 160 + #("nome", wisp.random_string(10) |> json.string), 161 + #("matricula", int.random(111) |> int.to_string |> json.string), 162 + #("telefone", taken_phone |> json.string), 163 + #("email", { wisp.random_string(12) <> "@email.com" } |> json.string), 164 + #("senha", dummy_pswd |> json.string), 165 + #("confirma_senha", dummy_pswd |> json.string), 166 + #("cargo", dummy.random_role() |> role.to_string_pt_br() |> json.string), 167 + ]), 168 + ) 161 169 162 170 let with_auth = app_test.with_authorization(next: req) 163 171 let resp = http_router.handle_request(with_auth, ctx) ··· 209 217 210 218 let new_user_req = 211 219 simulate.browser_request(http.Post, signup_path) 212 - |> simulate.form_body([ 213 - #("nome", wisp.random_string(10)), 214 - #("matricula", dummy_registration), 215 - #("telefone", dummy_phone), 216 - #("email", dummy_email), 217 - #("senha", dummy_password), 218 - #("confirma_senha", dummy_password), 219 - #("cargo", role.to_string_pt_br(role.Firefighter)), 220 - ]) 220 + |> simulate.json_body( 221 + json.object([ 222 + #("nome", wisp.random_string(10) |> json.string), 223 + #("matricula", dummy_registration |> json.string), 224 + #("telefone", dummy_phone |> json.string), 225 + #("email", dummy_email |> json.string), 226 + #("senha", dummy_password |> json.string), 227 + #("confirma_senha", dummy_password |> json.string), 228 + #("cargo", role.to_string_pt_br(role.Firefighter) |> json.string), 229 + ]), 230 + ) 221 231 222 232 let with_auth = app_test.with_authorization(new_user_req) 223 233 let new_user_resp = http_router.handle_request(with_auth, ctx)