-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add error-handling exercise version 1
- Loading branch information
Showing
7 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
exercises/practice/error-handling/.docs/instructions.append.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Instructions append | ||
|
||
You are building a tiny web server that queries an even tinier user database. Sounds easy, but in the real world many things can (and often do) go wrong: | ||
|
||
- the connection may be insecure: the URL must start with `"https://"` | ||
- the domain name may be invalid: in this exercise, it should be `"example.com"` | ||
- the page may not be found: only `"https://example.com/"`, `"https://example.com/users/"` and `"https://example.com/users/<userId>"` are allowed | ||
- the `userId` may not be a positive integer | ||
- no user may exist in the database with this `userId` | ||
|
||
When things go wrong, it's important to give the end user a nice and helpful error message so that they can solve the issue. For this, you need to ensure that your code propagates informative errors from the point where the error is detected to the point where the error message is produced. | ||
|
||
Luckily, Roc allows you to carry payload (i.e., data) inside your `Err` tag. It's tempting to carry an error message directly (e.g., `Err "User #42 was not found"`), and this may be fine in some simple cases, but this has several limitations: | ||
|
||
- you might not have enough context inside the function that detects the error to produce a sufficiently helpful error message. | ||
- For example, the `Str.toU64` function can be used to parse all sorts of integers: days, seconds, user IDs, and more. If the error message just says `Could not convert string "0.5" to a positive integer`, the user may not have enough context to solve the issue. | ||
- in many cases your error handling code may need to handle some errors differently than others. However, if the errors only carry string payloads, your error handling code will have to parse that string to know what problem occurred: this is inefficient and it can easily break if the error message is ever tweaked. | ||
- if your website is multilingual, you will need to translate the error message to the user's language. It's going to be much easier to do that if the error payload is machine-friendly data rather than an English string. | ||
|
||
So in this exercise, your errors will instead carry a meaningful tag along with its own helpful payload. For example, if the user is not found, the error will look like `Err (UserNotFound 42)`. | ||
|
||
Okay, let's get started! Here's what you need to do: | ||
|
||
1. Implement `getUser` to return the requested user from the `users` "database" (it's actually just a `Dict`). Make sure the function returns `Err (UserNotFound userId)` in case the user is not found, instead of `Err KeyNotFound`. | ||
2. Implement `parseUserId` to convert the URL's path (such as `"/users/123"`) to a positive integer user ID (`123`). In case of error, return `Err (InvalidUserId userIdStr)`. | ||
3. Implement `getPage`: | ||
- If the URL is `"https://example.com/"`, return `Ok "Home page"` | ||
- If the URL is `"https://example.com/users/"`, return `Ok "Users page"` | ||
- If the URL is `"https://example.com/users/<userId>"`, parse the user ID, load the user with that ID, and return `Ok "<user name>'s page"` | ||
- If the URL prefix is not `"https://"`, return `Err (InsecureConnection url)` | ||
- If the URL domain name is not `"example.com"`, return `Err (InvalidDomain url)` | ||
- If the path is not `/` or `/users/` or `/users/<user id>`, return `Err (PageNotFound path)` | ||
- If the user ID is not a positive integer, return `Err (InvalidUserId userIdStr)` | ||
- If the user does not exist, return `Err (UserNotFound userId)` | ||
4. Implement `errorMessage` to convert the previous errors to translated error messages. The function should at least handle English, but you are encouraged to try handling another language as well. The English error messages should like this: | ||
|
||
- `"Insecure connection (non HTTPS): http://example.com/users/789"` | ||
- `"Invalid domain name: https://google.com/wrong"` | ||
- `"Page not found: /oops"` | ||
- `"User ID is not a positive integer: abc"` | ||
- `"User #42 was not found"` | ||
|
||
Note: instead of displaying an error message to the user when they use HTTP instead of HTTPS, your web server could redirect their browser to HTTPS, and the user would not even notice the error. Since the errors are machine-friendly in Roc, this would be very easy to implement. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Instructions | ||
|
||
Implement various kinds of error handling and resource management. | ||
|
||
An important point of programming is how to handle errors and close resources even if errors occur. | ||
|
||
This exercise requires you to handle various errors. | ||
Because error handling is rather programming language specific you'll have to refer to the tests for your track to see what's exactly required. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
module [getUser, parseUserId, getPage, errorMessage] | ||
|
||
User : { name : Str } | ||
|
||
users : Dict U64 User | ||
users = | ||
Dict.fromList [ | ||
(123, { name: "Alice" }), | ||
(456, { name: "Bob" }), | ||
(789, { name: "Charlie" }), | ||
] | ||
|
||
getUser : U64 -> Result User [UserNotFound U64] | ||
getUser = \userId -> | ||
users |> Dict.get userId |> Result.mapErr \KeyNotFound -> UserNotFound userId | ||
|
||
parseUserId : Str -> Result U64 [InvalidUserId Str] | ||
parseUserId = \path -> | ||
userIdStr = path |> Str.replaceFirst "/users/" "" | ||
userIdStr |> Str.toU64 |> Result.mapErr \InvalidNumStr -> InvalidUserId userIdStr | ||
|
||
parsePath : Str -> Result Str [InvalidDomain Str, InsecureConnection Str] | ||
parsePath = \url -> | ||
prefix = "https://example.com" | ||
if url |> Str.startsWith prefix then | ||
url |> Str.replaceFirst prefix "" |> Ok | ||
else if url |> Str.startsWith "https://" then | ||
Err (InvalidDomain url) | ||
else | ||
Err (InsecureConnection url) | ||
|
||
getPage : Str -> Result Str [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound U64, PageNotFound Str] | ||
getPage = \url -> | ||
when parsePath? url is | ||
"/" -> Ok "Home page" | ||
"/users/" -> Ok "Users page" | ||
userPath if userPath |> Str.startsWith "/users/" -> | ||
userId = parseUserId? userPath | ||
user = getUser? userId | ||
Ok "$(user.name)'s page" | ||
|
||
unknownPath -> Err (PageNotFound unknownPath) | ||
|
||
errorMessage : [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound U64, PageNotFound Str], [English, French] -> Str | ||
errorMessage = \err, language -> | ||
when language is | ||
English -> | ||
when err is | ||
InsecureConnection url -> "Insecure connection (non HTTPS): $(url)" | ||
InvalidDomain url -> "Invalid domain name: $(url)" | ||
InvalidUserId userIdStr -> "User ID is not a positive integer: $(userIdStr)" | ||
UserNotFound userId -> "User #$(userId |> Num.toStr) was not found" | ||
PageNotFound path -> "Page not found: $(path)" | ||
|
||
French -> | ||
when err is | ||
InsecureConnection url -> "Connexion non sécurisée (non HTTPS): $(url)" | ||
InvalidDomain url -> "Ce nom de domaine est incorrect: $(url)" | ||
InvalidUserId userIdStr -> "Cet identifiant utilisateur devrait être un entier positif: $(userIdStr)" | ||
UserNotFound userId -> "L'utilisateur #$(userId |> Num.toStr) est inconnu" | ||
PageNotFound path -> "Cette page est inconnue: $(path)" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"authors": [ | ||
"ageron" | ||
], | ||
"files": { | ||
"solution": [ | ||
"ErrorHandling.roc" | ||
], | ||
"test": [ | ||
"error-handling-test.roc" | ||
], | ||
"example": [ | ||
".meta/Example.roc" | ||
] | ||
}, | ||
"blurb": "Implement various kinds of error handling and resource management." | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
module [getUser, parseUserId, getPage, errorMessage] | ||
|
||
User : { name : Str } | ||
|
||
users : Dict U64 User | ||
users = | ||
Dict.fromList [ | ||
(123, { name: "Alice" }), | ||
(456, { name: "Bob" }), | ||
(789, { name: "Charlie" }), | ||
] | ||
|
||
getUser : U64 -> Result User [UserNotFound Str] | ||
getUser = \userId -> | ||
crash "Please implement the 'getUser' function" | ||
|
||
parseUserId : Str -> Result U64 [InvalidUserId Str] | ||
parseUserId = \path -> | ||
crash "Please implement the 'parseUserId' function" | ||
|
||
getPage : Str -> Result Str _ | ||
getPage = \url -> | ||
crash "Please implement the 'getPage' function" | ||
|
||
errorMessage : _, [English] -> Str | ||
errorMessage = \err, language -> | ||
crash "Please implement the 'errorMessage' function" |
197 changes: 197 additions & 0 deletions
197
exercises/practice/error-handling/error-handling-test.roc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
# File last updated on 2024-09-12 | ||
app [main] { | ||
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", | ||
} | ||
|
||
main = | ||
Task.ok {} | ||
|
||
import ErrorHandling exposing [getUser, parseUserId, getPage, errorMessage] | ||
|
||
## | ||
## getUser should return Ok <user> or Err UserNotFound <userId> | ||
## | ||
|
||
# getUser 123 should return Alice | ||
expect | ||
result = getUser 123 | ||
result == Ok { name: "Alice" } | ||
|
||
# getUser 456 should return Bob | ||
expect | ||
result = getUser 456 | ||
result == Ok { name: "Bob" } | ||
|
||
# getUser 789 should return Charlie | ||
expect | ||
result = getUser 789 | ||
result == Ok { name: "Charlie" } | ||
|
||
# getUser 42 should return an error | ||
expect | ||
result = getUser 42 | ||
result == Err (UserNotFound 42) | ||
|
||
## | ||
## parseUserId should parse a string to a positive integer and return | ||
## Ok <userId> if successful, or Err InvalidUserId <userIdStr> otherwise | ||
## | ||
|
||
# Parsing a valid userId should return Ok <userId> | ||
expect | ||
result = parseUserId "123" | ||
result == Ok 123 | ||
|
||
# Parsing an empty string should fail | ||
expect | ||
result = parseUserId "" | ||
result == Err (InvalidUserId "") | ||
|
||
# Parsing a negative number should fail | ||
expect | ||
result = parseUserId "-123" | ||
result == Err (InvalidUserId "-123") | ||
|
||
# Parsing a fractional number should fail | ||
expect | ||
result = parseUserId "123.456" | ||
result == Err (InvalidUserId "123.456") | ||
|
||
# Parsing a number in scientific format should fail | ||
expect | ||
result = parseUserId "1e03" | ||
result == Err (InvalidUserId "1e03") | ||
|
||
# Parsing a string containing letters should fail | ||
expect | ||
result = parseUserId "abc" | ||
result == Err (InvalidUserId "abc") | ||
|
||
# Parsing a string containing a valid userId followed by junk should fail | ||
expect | ||
result = parseUserId "123 abc" | ||
result == Err (InvalidUserId "123 abc") | ||
|
||
## | ||
## getPage should return Ok with the desired page if it exists | ||
## or it should return the proper Err value if anything fails | ||
## | ||
|
||
# No error for root URL | ||
expect | ||
result = getPage "https://example.com/" | ||
result == Ok "Home page" | ||
|
||
# No error for users URL | ||
expect | ||
result = getPage "https://example.com/users/" | ||
result == Ok "Users page" | ||
|
||
# No error for specific user URL | ||
expect | ||
result = getPage "https://example.com/users/123" | ||
result == Ok "Alice's page" | ||
|
||
# No error for specific user URL | ||
expect | ||
result = getPage "https://example.com/users/456" | ||
result == Ok "Bob's page" | ||
|
||
# No error for specific user URL | ||
expect | ||
result = getPage "https://example.com/users/789" | ||
result == Ok "Charlie's page" | ||
|
||
# Error: insecure connection | ||
expect | ||
result = getPage "http://example.com/users/789" | ||
result == Err (InsecureConnection "http://example.com/users/789") | ||
|
||
# Error: invalid domain name | ||
expect | ||
result = getPage "https://google.com/wrong" | ||
result == Err (InvalidDomain "https://google.com/wrong") | ||
|
||
# Error: page not found | ||
expect | ||
result = getPage "https://example.com/oops" | ||
result == Err (PageNotFound "/oops") | ||
|
||
# Error: invalid userId | ||
expect | ||
result = getPage "https://example.com/users/abc" | ||
result == Err (InvalidUserId "abc") | ||
|
||
# Error: user not found | ||
expect | ||
result = getPage "https://example.com/users/42" | ||
result == Err (UserNotFound 42) | ||
|
||
## | ||
## Handle errors and return a clear message to the user, in the user's language | ||
## Your implementation must at least handle English, but you can handle other | ||
## languages if you want | ||
## | ||
|
||
# No error for root URL: just return the Ok result | ||
expect | ||
pageResult = getPage "https://example.com/" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Ok "Home page" | ||
|
||
# No error for users URL: just return the Ok result | ||
expect | ||
pageResult = getPage "https://example.com/users/" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Ok "Users page" | ||
|
||
# No error for specific user URL: just return the Ok result | ||
expect | ||
pageResult = getPage "https://example.com/users/123" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Ok "Alice's page" | ||
|
||
# No error for specific user URL: just return the Ok result | ||
expect | ||
pageResult = getPage "https://example.com/users/456" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Ok "Bob's page" | ||
|
||
# No error for specific user URL: just return the Ok result | ||
expect | ||
pageResult = getPage "https://example.com/users/789" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Ok "Charlie's page" | ||
|
||
# Error: insecure connection | ||
# Note: instead of displaying an error message, the server could automatically | ||
# redirect the user to the HTTPS URL. This is an example of a recoverable error | ||
# which would be easy to handle because the error payload is machine-friendly | ||
expect | ||
pageResult = getPage "http://example.com/users/789" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Err "Insecure connection (non HTTPS): http://example.com/users/789" | ||
|
||
# Error: invalid domain name | ||
expect | ||
pageResult = getPage "https://google.com/wrong" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Err "Invalid domain name: https://google.com/wrong" | ||
|
||
# Error: page not found | ||
expect | ||
pageResult = getPage "https://example.com/oops" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Err "Page not found: /oops" | ||
|
||
# Error: invalid userId | ||
expect | ||
pageResult = getPage "https://example.com/users/abc" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Err "User ID is not a positive integer: abc" | ||
|
||
# Error: user not found | ||
expect | ||
pageResult = getPage "https://example.com/users/42" | ||
result = pageResult |> Result.mapErr \err -> err |> errorMessage English | ||
result == Err "User #42 was not found" |