wip: currently rewriting the project as a full stack application tangled.org/kacaii.dev/sigo
gleam

:bulb: add missing documentation to error types

+191 -207
+66 -69
src/app/routes/admin/admin_update_user.gleam
··· 33 33 request req: wisp.Request, 34 34 ctx ctx: Context, 35 35 id user_id: String, 36 - ) { 36 + ) -> wisp.Response { 37 37 use <- wisp.require_method(req, http.Put) 38 38 use body <- wisp.require_json(req) 39 39 ··· 46 46 fn handle_body( 47 47 req: wisp.Request, 48 48 ctx: Context, 49 - body: AdminUpdateUserBody, 49 + body: RequestBody, 50 50 user_id: String, 51 51 ) -> wisp.Response { 52 52 case try_update_user(req, ctx, body, user_id) { 53 - Ok(data) -> wisp.json_response(json.to_string(data), 200) 53 + Ok(body) -> wisp.json_response(body, 200) 54 54 Error(err) -> handle_error(req, body, err) 55 55 } 56 + } 57 + 58 + type AdminUpdateUserError { 59 + /// Failed to access the DataBase 60 + DataBaseError(pog.QueryError) 61 + /// User has invalid Uuid format 62 + InvalidUuid(String) 63 + /// Authentication / Authorization failed 64 + AccessError(user.AccessControlError) 65 + /// User not found in the DataBase 66 + UserNotFound(uuid.Uuid) 56 67 } 57 68 58 69 fn handle_error( 59 70 req: wisp.Request, 60 - body: AdminUpdateUserBody, 71 + body: RequestBody, 61 72 err: AdminUpdateUserError, 62 73 ) -> wisp.Response { 63 74 case err { 64 - DataBaseError(err) -> { 65 - case err { 66 - pog.ConstraintViolated(_, _, constraint:) -> { 67 - case constraint { 68 - // Unique Email 69 - "user_account_email_key" -> { 70 - let resp = wisp.response(409) 71 - let body = 72 - wisp.Text("Email já está sendo utilizado: " <> body.email) 75 + AccessError(err) -> user.handle_access_control_error(req, err) 73 76 74 - wisp.set_body(resp, body) 75 - } 77 + InvalidUuid(id) -> wisp.bad_request("Usuário possui Uuid inválido: " <> id) 76 78 77 - // Unique Registration 78 - "user_account_registration_key" -> { 79 - let resp = wisp.response(409) 80 - let body = 81 - wisp.Text( 82 - "Matrícula já está sendo utilizada: " <> body.registration, 83 - ) 79 + UserNotFound(id) -> 80 + wisp.Text("Usuário não encontrado: " <> uuid.to_string(id)) 81 + |> wisp.set_body(wisp.not_found(), _) 84 82 85 - wisp.set_body(resp, body) 86 - } 83 + DataBaseError(err) -> { 84 + case err { 85 + pog.ConstraintViolated(_, _, constraint: "user_account_email_key") -> 86 + wisp.Text("Email já está sendo utilizado: " <> body.email) 87 + |> wisp.set_body(wisp.response(409), _) 87 88 88 - _ -> web.handle_database_error(err) 89 - } 89 + pog.ConstraintViolated( 90 + _, 91 + _, 92 + constraint: "user_account_registration_key", 93 + ) -> { 94 + wisp.Text("Matrícula já está sendo utilizada: " <> body.registration) 95 + |> wisp.set_body(wisp.response(409), _) 90 96 } 97 + 91 98 err -> web.handle_database_error(err) 92 99 } 93 100 } 94 - AccessError(err) -> user.handle_access_control_error(req, err) 95 - InvalidUuid(err) -> 96 - wisp.unprocessable_content() 97 - |> wisp.set_body(wisp.Text("Usuário possui Uuid inválido: " <> err)) 98 - UserNotFound(id) -> 99 - wisp.not_found() 100 - |> wisp.set_body(wisp.Text("Usuário não encontrado: " <> id)) 101 101 } 102 102 } 103 103 104 104 fn try_update_user( 105 105 req: wisp.Request, 106 106 ctx: Context, 107 - body: AdminUpdateUserBody, 107 + body: RequestBody, 108 108 user_id: String, 109 - ) { 109 + ) -> Result(String, AdminUpdateUserError) { 110 110 use _ <- result.try( 111 111 user.check_role_authorization( 112 112 request: req, ··· 135 135 |> result.map_error(DataBaseError), 136 136 ) 137 137 138 - use row <- result.map( 139 - list.first(returned.rows) 140 - |> result.replace_error(UserNotFound(user_id)), 141 - ) 138 + case list.first(returned.rows) { 139 + Error(_) -> Error(UserNotFound(user_uuid)) 140 + Ok(row) -> { 141 + let user_role = case row.user_role { 142 + sql.Admin -> role.Admin 143 + sql.Analyst -> role.Analyst 144 + sql.Captain -> role.Captain 145 + sql.Developer -> role.Developer 146 + sql.Firefighter -> role.Firefighter 147 + sql.Sargeant -> role.Sargeant 148 + } 142 149 143 - let user_role = enum_to_role(row.user_role) 150 + let updated_at_json = 151 + json.float( 152 + row.updated_at 153 + |> timestamp.to_unix_seconds(), 154 + ) 144 155 145 - json.object([ 146 - #("id", json.string(uuid.to_string(row.id))), 147 - #("full_name", json.string(row.full_name)), 148 - #("email", json.string(row.email)), 149 - #("user_role", json.string(role.to_string_pt_br(user_role))), 150 - #("registration", json.string(row.registration)), 151 - #("updated_at", json.float(row.updated_at |> timestamp.to_unix_seconds())), 152 - #("is_active", json.bool(row.is_active)), 153 - ]) 154 - } 155 - 156 - fn enum_to_role(enum: sql.UserRoleEnum) { 157 - case enum { 158 - sql.Admin -> role.Admin 159 - sql.Analyst -> role.Analyst 160 - sql.Captain -> role.Captain 161 - sql.Developer -> role.Developer 162 - sql.Firefighter -> role.Firefighter 163 - sql.Sargeant -> role.Sargeant 156 + json.object([ 157 + #("id", json.string(uuid.to_string(row.id))), 158 + #("full_name", json.string(row.full_name)), 159 + #("email", json.string(row.email)), 160 + #("user_role", json.string(role.to_string_pt_br(user_role))), 161 + #("registration", json.string(row.registration)), 162 + #("updated_at", updated_at_json), 163 + #("is_active", json.bool(row.is_active)), 164 + ]) 165 + |> json.to_string 166 + |> Ok 167 + } 164 168 } 165 169 } 166 170 ··· 182 186 use registration <- decode.field("registration", decode.string) 183 187 use is_active <- decode.field("is_active", decode.bool) 184 188 185 - decode.success(AdminUpdateUserBody( 189 + decode.success(RequestBody( 186 190 full_name:, 187 191 email:, 188 192 user_role:, ··· 191 195 )) 192 196 } 193 197 194 - type AdminUpdateUserBody { 195 - AdminUpdateUserBody( 198 + type RequestBody { 199 + RequestBody( 196 200 full_name: String, 197 201 email: String, 198 202 user_role: role.Role, ··· 200 204 is_active: Bool, 201 205 ) 202 206 } 203 - 204 - type AdminUpdateUserError { 205 - DataBaseError(pog.QueryError) 206 - InvalidUuid(String) 207 - AccessError(user.AccessControlError) 208 - UserNotFound(String) 209 - }
+15 -7
src/app/routes/admin/setup_first_admin.gleam
··· 4 4 import app/web/context.{type Context} 5 5 import argus 6 6 import envoy 7 + import gleam/bool 7 8 import gleam/dynamic/decode 8 9 import gleam/http 9 10 import gleam/list ··· 74 75 wisp.bad_request( 75 76 "O banco de dados precisa estar com a tabela de usuários vazia", 76 77 ) 78 + 77 79 DataBaseReturnedEmptyRow(_) -> 78 80 wisp.internal_server_error() 79 81 |> wisp.set_body(wisp.Text( ··· 85 87 |> wisp.set_body(wisp.Text( 86 88 "Ocorreu um erro ao encriptografar a senha do usuário", 87 89 )) 90 + 88 91 IncorrectRequestToken(_) -> 89 92 wisp.response(403) 90 93 |> wisp.set_body(wisp.Text("Token Inválido")) 94 + 91 95 MissingEnvToken -> 92 96 wisp.internal_server_error() 93 97 |> wisp.set_body(wisp.Text( ··· 114 118 |> result.map_error(DataBaseReturnedEmptyRow), 115 119 ) 116 120 117 - case key == admin_token, row.total { 118 - // Correct token, empty database 119 - True, 0 -> Ok(Nil) 120 - // Invalid token 121 - False, _ -> Error(IncorrectRequestToken(key)) 122 - // Database already have some user 123 - _, _ -> Error(DataBaseNotEmpty) 121 + use <- bool.guard(when: row.total > 0, return: Error(DataBaseNotEmpty)) 122 + case key == admin_token { 123 + True -> Ok(Nil) 124 + False -> Error(IncorrectRequestToken(key)) 124 125 } 125 126 } 126 127 ··· 129 130 decode.success(key) 130 131 } 131 132 133 + /// Setting up default admin can fail 132 134 type SetupAdminError { 135 + /// Submitted the wrong access token 133 136 IncorrectRequestToken(String) 137 + /// Env has not been found 134 138 MissingEnvToken 139 + /// An error occurred while accessing the DataBase 135 140 DataBaseError(pog.QueryError) 141 + /// Failed to count how many users are registered 136 142 DataBaseReturnedEmptyRow(Nil) 143 + /// Database needs to be empty 137 144 DataBaseNotEmpty 145 + /// Failed to hash the admin password 138 146 HashError 139 147 }
+12 -7
src/app/routes/brigade/delete_brigade.gleam
··· 35 35 36 36 fn handle_error(err: DeleteBrigadeError) -> wisp.Response { 37 37 case err { 38 - InvalidBrigadeUuid(id) -> 39 - wisp.bad_request("Equipe possui UUID inválido: " <> id) 40 - UuidNotFound(id) -> wisp.bad_request("Equipe não econtrada: " <> id) 38 + InvalidUuid(id) -> wisp.bad_request("Equipe possui UUID inválido: " <> id) 39 + BrigadeNotFound(id) -> wisp.bad_request("Equipe não econtrada: " <> id) 41 40 DataBaseError(err) -> web.handle_database_error(err) 42 41 } 43 42 } ··· 47 46 id: String, 48 47 ) -> Result(json.Json, DeleteBrigadeError) { 49 48 use brigade_uuid <- result.try( 50 - uuid.from_string(id) |> result.replace_error(InvalidBrigadeUuid(id)), 49 + uuid.from_string(id) 50 + |> result.replace_error(InvalidUuid(id)), 51 51 ) 52 + 52 53 use returned <- result.try( 53 54 sql.delete_brigade_by_id(ctx.db, brigade_uuid) 54 55 |> result.map_error(DataBaseError), 55 56 ) 56 57 use row <- result.map( 57 58 list.first(returned.rows) 58 - |> result.replace_error(UuidNotFound(id)), 59 + |> result.replace_error(BrigadeNotFound(id)), 59 60 ) 60 61 61 62 json.object([ ··· 64 65 ]) 65 66 } 66 67 68 + /// Deleting a brigade can fail 67 69 type DeleteBrigadeError { 68 - InvalidBrigadeUuid(String) 70 + /// Session token has invalid Uuid fornmat 71 + InvalidUuid(String) 72 + /// An error occurred when accessing the DataBase 69 73 DataBaseError(pog.QueryError) 70 - UuidNotFound(String) 74 + /// Brigade not found in the DataBase 75 + BrigadeNotFound(String) 71 76 }
+15 -29
src/app/routes/notification/get_notification_preferences.gleam
··· 1 1 import app/routes/notification/sql 2 2 import app/routes/occurrence/category 3 3 import app/routes/user 4 + import app/web 4 5 import app/web/context.{type Context} 5 6 import gleam/dict 6 7 import gleam/http ··· 29 30 ) -> wisp.Response { 30 31 use <- wisp.require_method(req, http.Get) 31 32 32 - case query_database(req, ctx) { 33 + case try_query_database(req, ctx) { 33 34 Error(err) -> handle_error(err) 34 - Ok(preferences) -> wisp.json_response(json.to_string(preferences), 200) 35 + Ok(body) -> wisp.json_response(body, 200) 35 36 } 36 37 } 37 38 38 39 fn handle_error(err: GetNotificationPreferencesError) -> wisp.Response { 39 40 case err { 40 - AuthenticationFailed(err) -> user.handle_authentication_error(err) 41 - DataBaseReturnedEmptyRow -> 42 - wisp.bad_request("O Banco de Dados não retornou resultados") 43 - DatabaseError(err) -> { 44 - let err_message = case err { 45 - // 46 - //  Connection failed 47 - pog.ConnectionUnavailable -> 48 - "Conexão com o Banco de Dados não disponível" 49 - 50 - //  Took too long 51 - pog.QueryTimeout -> "O Banco de Dados demorou muito para responder" 52 - 53 - // Fallback 54 - _ -> "Ocorreu um erro ao acessar o Banco de Dados" 55 - } 56 - 57 - wisp.internal_server_error() 58 - |> wisp.set_body(wisp.Text(err_message)) 59 - } 41 + AccessControl(err) -> user.handle_authentication_error(err) 42 + DatabaseError(err) -> web.handle_database_error(err) 60 43 } 61 44 } 62 45 63 - fn query_database( 46 + fn try_query_database( 64 47 req: wisp.Request, 65 48 ctx: Context, 66 - ) -> Result(json.Json, GetNotificationPreferencesError) { 49 + ) -> Result(String, GetNotificationPreferencesError) { 67 50 use user_uuid <- result.try( 68 51 user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 69 - |> result.map_error(AuthenticationFailed), 52 + |> result.map_error(AccessControl), 70 53 ) 71 54 72 55 use returned <- result.try( ··· 76 59 77 60 let preferences = { 78 61 use acc, row <- list.fold(returned.rows, dict.new()) 79 - 80 62 let occ_category = case row.notification_type { 81 63 sql.Emergency -> category.MedicEmergency 82 64 sql.Fire -> category.Fire ··· 87 69 dict.insert(acc, occ_category, row.enabled) 88 70 } 89 71 90 - Ok(json.dict(preferences, category.to_string, json.bool)) 72 + json.dict(preferences, category.to_string, json.bool) 73 + |> json.to_string 74 + |> Ok 91 75 } 92 76 77 + /// Querying the user notification preferences can fail 93 78 type GetNotificationPreferencesError { 94 - DataBaseReturnedEmptyRow 95 - AuthenticationFailed(user.AuthenticationError) 79 + /// Authentication failed 80 + AccessControl(user.AuthenticationError) 81 + /// An error occurred while querying the DataBase 96 82 DatabaseError(pog.QueryError) 97 83 }
+49 -56
src/app/routes/notification/update_notification_preferences.gleam
··· 32 32 use <- wisp.require_method(req, http.Put) 33 33 use json_data <- wisp.require_json(req) 34 34 35 - case notification_preferences_decoder(json_data) { 36 - Error(_) -> wisp.bad_request("Solicitação inválida") 37 - Ok(form_data) -> { 38 - case update_preferences(req, ctx, form_data) { 39 - Error(err) -> handle_err(err) 40 - Ok(_) -> { 41 - let data = json.dict(form_data, category.to_string_pt_br, json.bool) 42 - wisp.json_response(json.to_string(data), 200) 43 - } 44 - } 45 - } 35 + case decode.run(json_data, body_decoder()) { 36 + Error(err) -> web.handle_decode_error(err) 37 + Ok(data) -> handle_data(req, ctx, data) 46 38 } 47 39 } 48 40 49 - fn notification_preferences_decoder( 50 - data: decode.Dynamic, 51 - ) -> Result(NotificationPreferences, List(decode.DecodeError)) { 52 - let schema = { 53 - use fire_enabled <- decode.field("incendio", decode.bool) 54 - use emergency_enabled <- decode.field("emergencia", decode.bool) 55 - use traffic <- decode.field("transito", decode.bool) 56 - use other <- decode.field("outros", decode.bool) 41 + type UpdateNotificationPreferencesError { 42 + /// Authentication failed 43 + AccessControl(user.AuthenticationError) 44 + /// Failed to query Database 45 + DataBaseError(pog.QueryError) 46 + } 57 47 58 - decode.success( 59 - dict.from_list([ 60 - #(category.Fire, fire_enabled), 61 - #(category.MedicEmergency, emergency_enabled), 62 - #(category.TrafficAccident, traffic), 63 - #(category.Other, other), 64 - ]), 65 - ) 48 + fn handle_data( 49 + req: wisp.Request, 50 + ctx: Context, 51 + data: dict.Dict(category.Category, Bool), 52 + ) -> wisp.Response { 53 + case try_update(req, ctx, data) { 54 + Error(err) -> handle_error(err) 55 + Ok(_) -> 56 + json.dict(data, category.to_string_pt_br, json.bool) 57 + |> json.to_string 58 + |> wisp.json_response(200) 66 59 } 60 + } 67 61 68 - decode.run(data, schema) 62 + fn body_decoder() -> decode.Decoder(dict.Dict(category.Category, Bool)) { 63 + use fire_enabled <- decode.field("incendio", decode.bool) 64 + use emergency_enabled <- decode.field("emergencia", decode.bool) 65 + use traffic <- decode.field("transito", decode.bool) 66 + use other <- decode.field("outros", decode.bool) 67 + 68 + [ 69 + #(category.Fire, fire_enabled), 70 + #(category.MedicEmergency, emergency_enabled), 71 + #(category.TrafficAccident, traffic), 72 + #(category.Other, other), 73 + ] 74 + |> dict.from_list 75 + |> decode.success 69 76 } 70 77 71 - fn handle_err(err: UpdateNotificationPreferencesError) -> wisp.Response { 78 + fn handle_error(err: UpdateNotificationPreferencesError) -> wisp.Response { 72 79 case err { 73 - AuthenticationFailed(err) -> user.handle_authentication_error(err) 80 + AccessControl(err) -> user.handle_authentication_error(err) 74 81 DataBaseError(err) -> web.handle_database_error(err) 75 82 } 76 83 } 77 84 78 - fn update_preferences( 85 + fn try_update( 79 86 req: wisp.Request, 80 87 ctx: Context, 81 - preferences: NotificationPreferences, 88 + preferences: dict.Dict(category.Category, Bool), 82 89 ) -> Result(List(pog.Returned(Nil)), UpdateNotificationPreferencesError) { 83 90 use user_uuid <- result.try( 84 91 user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 85 - |> result.map_error(AuthenticationFailed), 92 + |> result.map_error(AccessControl), 86 93 ) 87 94 88 - use update_result <- result.try({ 89 - use #(key, value) <- list.try_map(dict.to_list(preferences)) 90 - 91 - // Parse into the SQL enum 92 - let key = case key { 93 - category.Fire -> sql.Fire 94 - category.MedicEmergency -> sql.Emergency 95 - category.Other -> sql.Other 96 - category.TrafficAccident -> sql.Traffic 97 - } 98 - 99 - sql.update_notification_preferences(ctx.db, user_uuid, key, value) 100 - |> result.map_error(DataBaseError) 101 - }) 102 - 103 - Ok(update_result) 104 - } 105 - 106 - type NotificationPreferences = 107 - dict.Dict(category.Category, Bool) 95 + use #(key, value) <- list.try_map(dict.to_list(preferences)) 96 + let key = case key { 97 + category.Fire -> sql.Fire 98 + category.MedicEmergency -> sql.Emergency 99 + category.Other -> sql.Other 100 + category.TrafficAccident -> sql.Traffic 101 + } 108 102 109 - type UpdateNotificationPreferencesError { 110 - AuthenticationFailed(user.AuthenticationError) 111 - DataBaseError(pog.QueryError) 103 + sql.update_notification_preferences(ctx.db, user_uuid, key, value) 104 + |> result.map_error(DataBaseError) 112 105 }
+30 -39
src/app/routes/user/get_crew_members.gleam
··· 2 2 3 3 import app/routes/role 4 4 import app/routes/user/sql 5 + import app/web 5 6 import app/web/context.{type Context} 6 7 import gleam/http 7 8 import gleam/json ··· 45 46 ) -> wisp.Response { 46 47 use <- wisp.require_method(req, http.Get) 47 48 48 - let query_result = query_crew_members(ctx:, user_id:) 49 - 50 - case query_result { 51 - Ok(fellow_members_list) -> 52 - wisp.json_response(json.to_string(fellow_members_list), 200) 53 - 49 + case try_query_database(ctx:, user_id:) { 50 + Ok(body) -> wisp.json_response(body, 200) 54 51 Error(err) -> handle_err(err) 55 52 } 56 53 } 57 54 58 - fn query_crew_members(ctx ctx: Context, user_id user_id: String) { 55 + fn try_query_database( 56 + ctx ctx: Context, 57 + user_id user_id: String, 58 + ) -> Result(String, GetCrewMembersError) { 59 59 use user_uuid <- result.try( 60 60 uuid.from_string(user_id) 61 61 |> result.replace_error(InvalidUUID(user_id)), 62 62 ) 63 + 63 64 use returned <- result.try( 64 65 sql.query_crew_members(ctx.db, user_uuid) 65 - |> result.map_error(DataBaseError), 66 + |> result.map_error(DataBase), 66 67 ) 68 + 67 69 let fellow_members_list = { 68 70 use fellow_brigade_member <- list.map(returned.rows) 69 71 get_crew_members_row_to_json(fellow_brigade_member) 70 72 } 71 73 72 - Ok(json.preprocessed_array(fellow_members_list)) 74 + json.preprocessed_array(fellow_members_list) 75 + |> json.to_string 76 + |> Ok 73 77 } 74 78 75 79 fn get_crew_members_row_to_json(row: sql.QueryCrewMembersRow) -> json.Json { 76 80 let role_name = 77 - row.user_role 78 - |> enum_to_role() 79 - |> role.to_string_pt_br() 81 + role.to_string_pt_br(case row.user_role { 82 + sql.Admin -> role.Admin 83 + sql.Analyst -> role.Analyst 84 + sql.Captain -> role.Captain 85 + sql.Developer -> role.Developer 86 + sql.Firefighter -> role.Firefighter 87 + sql.Sargeant -> role.Sargeant 88 + }) 80 89 81 90 json.object([ 82 91 #("id", json.string(uuid.to_string(row.id))), ··· 86 95 ]) 87 96 } 88 97 89 - fn enum_to_role(user_role: sql.UserRoleEnum) -> role.Role { 90 - case user_role { 91 - sql.Admin -> role.Admin 92 - sql.Analyst -> role.Analyst 93 - sql.Captain -> role.Captain 94 - sql.Developer -> role.Developer 95 - sql.Firefighter -> role.Firefighter 96 - sql.Sargeant -> role.Sargeant 97 - } 98 - } 99 - 100 - fn handle_err(err: GetFellowBrigadeMembersError) { 101 - let error_message = case err { 102 - InvalidUUID(user_id) -> "ID de usuário inválido: " <> user_id 103 - DataBaseError(db_err) -> { 104 - case db_err { 105 - pog.ConnectionUnavailable -> 106 - "Conexão com o Banco de Dados não disponível" 107 - pog.QueryTimeout -> "O Banco de Dados demorou muito para responder" 108 - _ -> "Ocorreu um erro ao realizar a consulta no Banco de Dados" 109 - } 110 - } 98 + fn handle_err(err: GetCrewMembersError) { 99 + case err { 100 + InvalidUUID(id) -> wisp.bad_request("ID de usuário inválido: " <> id) 101 + DataBase(err) -> web.handle_database_error(err) 111 102 } 112 - 113 - wisp.internal_server_error() 114 - |> wisp.set_body(wisp.Text(error_message)) 115 103 } 116 104 117 - type GetFellowBrigadeMembersError { 118 - DataBaseError(pog.QueryError) 105 + /// Finding the user's crew can fail 106 + type GetCrewMembersError { 107 + /// User has invalid Uuid fornmat 119 108 InvalidUUID(String) 109 + /// An error occurred while accessing the DataBase 110 + DataBase(pog.QueryError) 120 111 }
+4
src/app/routes/user/update_user_profile.gleam
··· 119 119 } 120 120 } 121 121 122 + /// Updating an user profile can fail 122 123 type UpdateProfileError { 124 + /// Authentication failed 123 125 AccessError(user.AuthenticationError) 126 + /// An error occurred when accessing the DataBase 124 127 DatabaseError(pog.QueryError) 128 + /// User was not found in the DataBase 125 129 UserNotFound(uuid.Uuid) 126 130 } 127 131