From 94ab52ce96c4fc1afc3583b6c1d5afe5af9f883d Mon Sep 17 00:00:00 2001 From: Mirabellensaft Date: Fri, 10 Feb 2023 17:18:27 +0100 Subject: [PATCH 1/2] finish draft --- .../files-match-result-assignment.adoc | 156 ++++++++++++++---- 1 file changed, 128 insertions(+), 28 deletions(-) diff --git a/assignments/files-match-result-assignment.adoc b/assignments/files-match-result-assignment.adoc index 3fc4938b..9bf4fed9 100644 --- a/assignments/files-match-result-assignment.adoc +++ b/assignments/files-match-result-assignment.adoc @@ -1,14 +1,30 @@ = Exercise: Files, match, and Result :source-language: rust -In this exercise, you will learn +In this exercise you will complete a number of steps to learn about Error Handling. The final result will be a url parser that reads lines from a text file and can distinguish the content between URLs and non-urls. -* how to open a file -* how to handle errors using the `Result`-type. -* how to use the `Option`-type. -* how to do some elementary file processing (counting, reading line by line). -* how to navigate the Rust stdlib documentation -* how to add external dependencies to your project +== In this exercise, you will learn how to + +* handle occuring `Result`-types with `match` for basic error handling. +* when to use the `.unwrap()` method. +* propagate an error with the `?` operator +* return the `Option`-type. +* do some elementary file processing (opening, reading to buffer, counting, reading line by line). +* navigate the Rust stdlib documentation +* add external dependencies to your project + +== Task + +* Manually unwrap the `Result` type that is returned from hte File::open() with a match statement, so that the .unwrap() can be deleted. +* Move this manual unwrap to it's own function. +* Read the content of the file to a buffer and count the lines in a for loop. +* Filter out empty lines and print the non-empty ones. +* Write a function that parses each line and returns Some(url) if the line is a URL, and `None` if it is not. Use the Url crate https://docs.rs/url/2.1.1/url/[Url Type] + + +== Knowledge + +=== Option and Result Both the `Option` and `Result` are similar in way. Both have two variants, and depending on what those variants are, the program may continue in a different way. @@ -24,9 +40,10 @@ In `Ok(t)`, `t` can be the empty tuple or a return value. In `Err(e)`, `e` contains an error message that can be printed. [TIP] -==== Both types can be used with the `match` keyword. The received value is matched on patterns, each leads to the execution of a different expression. +=== how to use `match` +`match` is a way of control flow based on pattern matching. A pattern on the one left evaluates to an expression on the right side. ---- match VALUE { PATTERN => EXPRESSION, @@ -34,6 +51,35 @@ match VALUE { PATTERN => EXPRESSION, } ---- + +other then with if/else, every case has to be handled explicitly, at least with a last catch all arm using a place holder: +---- +match VALUE { + PATTERN => EXPRESSION, + PATTERN => EXPRESSION, + _ => EXPRESSION, +} +---- + +There are different ways to use match: + +The return values of the expression can be bound to a variable: + +---- +let return_value = match VALUE { + // match arms that return a value or panic +} +---- + +In case of a Result, match statements can be used to get to the inner value. +---- +match RESULT { + Ok(T) => EXPRESSION, that uses or returns T + Err(E) => EXPRESSION, +} +---- + +All arms of the match tree have to result in the same type! ==== == Template @@ -47,40 +93,95 @@ git clone https://github.com/ferrous-systems/teaching-material code teaching-material/assignments/files-match-result-assignment/template/ ---- +The template builds but has a runtime error, as the location of the file is wrong. This is intentional. + Your code will use the example data found in https://github.com/ferrous-systems/teaching-material/tree/main/assignments/files-match-result-assignment/template/src/data[files-match-result-assignment/template/src/data]. -== Task 1: Files and Errors +== Step-by-Step Solution +=== Step 1: Unwrapping -* Open the file `"src/data/content.txt"` using https://doc.rust-lang.org/std/fs/struct.File.html#method.open[File::open] and bind the result to the variable `f`. -* Find out what type the variable `f` is. Either your IDE shows you, or you can assign a random type to the variable, and run the program. -* `match` the two possible patterns, `Ok(file)` and `Err(e)` to an an appropriate expression, for example: `println!("File opened")` and `println!("Error: {}", e)` +`File::open` yields a `Result` kind of type, a quick way to get to inner type T is to use the `.unwrap()` method on the `Result`. The cost is that the program panics if the Error variant occurs and the Error can not be propagated. It should only be used when the error does not need to be propagated and would result in a panic anyways. It's often used as a quick fix before implementing proper error handling. +* Check the documentation for the exact type https://doc.rust-lang.org/std/fs/struct.File.html#method.open[File::open] returns. +* implement a manual unwrap using `match` so to get to the inner type. Link the two possible patterns, `Ok(file)` and `Err(e)` to an an appropriate expression, for example: `println!("File opened")` and `println!("Error: {}", e)` +* fix the location of the file so that the error is no longer returned. TIP: IDEs often provide a "quick fix" to roll out all match arms quickly -== Task 2: Reading Files... and Errors +=== Step 2: Moving the unwrapping into a function. +* Implement the following function based on what you wrote in the last step. +[source,rust] +---- +fn unwrap_file(open_result: Result) -> File { + todo! +} +---- +** change `println!("Error: {}", e)` to `panic!("Error: {}", e)` +** to be able to return from the `Ok()` arm, add a `return` statement to return the `File` object. + +* call the function. + +== Task 3: Reading the File content and Error propagation. * import `std::io::prelude::*` -* To use the content of the file, bind the result of the `match` statement to a variable. -** In the other branch of the match statement, change `println!("Error: {}", e)` to `panic!("Error: {}", e)` -* Read the content of the file into a `String`. Use https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string[Read::read_to_string]. -* Print the entire content of the file using `println!` -* Handle all errors (the compiler will warn you if you miss one) -== Task 3: Reading files line by line... and Errors +* Take a look at https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string[Read::read_to_string]. +The method takes in a mutable empty `String`, and writes the content of a file to this buffer. The method returns a `Result`, where the usize is the number of bytes that have been written to the buffer. Handling this Result, will not yield the `String` of file content. For a simple program, handling it with an `.unwrap()` would be sufficient, but for bigger code bases this is not helpful. + +Instead, add the following function to your program. +It takes in a mutable instance of the `File` and returns a `Result`. It creates an empty `String` that serves as buffer. The `.read_to_string()` method is applied on the `File` object. The method takes in the `String` buffer. The method is then followed by the `?` operator. If the method returns an `Error` the function propagates this error. If the method returns the `Ok` value, the funktion returns the `String` wrapped in the `Ok`. + -* Construct a https://doc.rust-lang.org/std/io/struct.BufReader.html[BufReader] around the file -* Print the content of the file line by line. The `lines()`- method returns an Iterator over the file. +[source,rust] +---- +fn content_to_string(mut file: File) -> Result { + let mut content_string = String::new(); + file.read_to_string(&mut content_string)?; + Ok(content_string) +} +---- + +* In `main()`, call the function and bind it to a variable. You can use `.unwrap()` to handle the `Result`. +* Use `println!` to print the content of the `String` + +== Task 4: Counting lines +* add the following imports +---- +use std::io::{ BufReader, BufRead,} +---- +* Take a look at the documentation https://doc.rust-lang.org/std/io/struct.BufReader.html[BufReader]. BufReader is a struct that adds buffering to any reader. It implements the https://doc.rust-lang.org/std/io/trait.BufRead.html#[`BufRead`] trait. In short this means, that methods that are defined for `BufRead` can be used for `BufReader`. For example the https://doc.rust-lang.org/std/io/trait.BufRead.html#method.lines[`lines()`] method. + + +* Construct a `BufReader` around the file. +* The `lines()`- method returns an Iterator over the file's lines. Iterate over the lines with a for loop to count them. * Print the number of lines the file contains. -* `lines` returns the `Result`-Type, use it to get to the actual `String`. +* You don't have to handle the `Result` that is returned from `.lines()`, why? + +[source,rust] +---- + +---- + +== Task 5: Filter out empty lines print the Rest ...and Errors + +* `lines` returns the `Result`-Type, use it with a `match` statement to get to the actual `String`. * Filter out the empty lines, and only print the the others. The https://doc.rust-lang.org/std/string/struct.String.html#method.is_empty[is_empty] method can help you here. -== Task 4: Read URLs from file... and Errors +== Task 6: Read URLs from file and return with Option. + + +* Add `url = "2"` to your `[dependencies]` section in `Cargo.toml` and import `url::Url` in `main.rs`. +* Write a function that parses each line using the https://docs.rs/url/2.1.1/url/[Url Type]. Search the docs for a method for this! + +[source,rust] +---- +fn parse_url(line: String) -> Option { + todo! +} +---- -* Add `url = "2"` to your dependencies section in `Cargo.toml` -* Write a function that parses each line using the https://docs.rs/url/2.1.1/url/[Url Type]: `fn parse_url(line: String) -> Option` ** If a line can be parsed successfully, return `Some(url)`, `None` otherwise ** In the calling context, only print URLs that parse correctly -** Test the `parse_url` function +** Test the `parse_url` == Help @@ -93,6 +194,5 @@ Variables can be typed by using `:` and a type. let my_value: String = String::from("test"); ---- -=== `match` -All arms of the match tree have to result in the same type. + From c1ee2445c40977832b712d662dd99797af196d62 Mon Sep 17 00:00:00 2001 From: Mirabellensaft Date: Fri, 10 Feb 2023 17:19:28 +0100 Subject: [PATCH 2/2] add new cleaned up solution --- .../{template => }/Cargo.toml | 7 +++-- .../{solution/Step2.rs => examples/step_1.rs} | 5 ++-- .../examples/step_2.rs | 17 +++++++++++ .../examples/step_3.rs | 25 +++++++++++++++++ .../{solution/Step4.rs => examples/step_4.rs} | 15 ++++++---- .../examples/step_5.rs | 28 +++++++++++++++++++ .../{solution/Step6.rs => examples/step_6.rs} | 26 +++++++++-------- .../solution/Step3.rs | 16 ----------- .../solution/Step5.rs | 24 ---------------- .../{template => }/src/data/content.txt | 0 .../files-match-result-assignment/src/main.rs | 14 ++++++++++ .../template/Cargo.lock | 7 ----- .../template/src/main.rs | 12 -------- 13 files changed, 114 insertions(+), 82 deletions(-) rename assignments/files-match-result-assignment/{template => }/Cargo.toml (51%) rename assignments/files-match-result-assignment/{solution/Step2.rs => examples/step_1.rs} (55%) create mode 100644 assignments/files-match-result-assignment/examples/step_2.rs create mode 100644 assignments/files-match-result-assignment/examples/step_3.rs rename assignments/files-match-result-assignment/{solution/Step4.rs => examples/step_4.rs} (62%) create mode 100644 assignments/files-match-result-assignment/examples/step_5.rs rename assignments/files-match-result-assignment/{solution/Step6.rs => examples/step_6.rs} (65%) delete mode 100644 assignments/files-match-result-assignment/solution/Step3.rs delete mode 100644 assignments/files-match-result-assignment/solution/Step5.rs rename assignments/files-match-result-assignment/{template => }/src/data/content.txt (100%) create mode 100644 assignments/files-match-result-assignment/src/main.rs delete mode 100644 assignments/files-match-result-assignment/template/Cargo.lock delete mode 100644 assignments/files-match-result-assignment/template/src/main.rs diff --git a/assignments/files-match-result-assignment/template/Cargo.toml b/assignments/files-match-result-assignment/Cargo.toml similarity index 51% rename from assignments/files-match-result-assignment/template/Cargo.toml rename to assignments/files-match-result-assignment/Cargo.toml index 240a544b..f9efabd9 100644 --- a/assignments/files-match-result-assignment/template/Cargo.toml +++ b/assignments/files-match-result-assignment/Cargo.toml @@ -1,9 +1,10 @@ [package] -name = "option-result-assignment" +name = "files_match_result_assignment" version = "0.1.0" -authors = ["Mirabellensaft "] -edition = "2018" +authors = ["Mirabellensaft "] +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +url = "2" \ No newline at end of file diff --git a/assignments/files-match-result-assignment/solution/Step2.rs b/assignments/files-match-result-assignment/examples/step_1.rs similarity index 55% rename from assignments/files-match-result-assignment/solution/Step2.rs rename to assignments/files-match-result-assignment/examples/step_1.rs index 0ad8213d..e36d2993 100644 --- a/assignments/files-match-result-assignment/solution/Step2.rs +++ b/assignments/files-match-result-assignment/examples/step_1.rs @@ -1,11 +1,10 @@ use std::fs::File; fn main() { - - let open_result = File::open("src/data/content.txt"); + let open_result = File::open("src/lib/content.txt"); match open_result { - Ok(file) => println!("File opened!"), + Ok(_file) => println!("File opened"), Err(e) => println!("Problem opening the file: {:?}", e), }; } diff --git a/assignments/files-match-result-assignment/examples/step_2.rs b/assignments/files-match-result-assignment/examples/step_2.rs new file mode 100644 index 00000000..170f476f --- /dev/null +++ b/assignments/files-match-result-assignment/examples/step_2.rs @@ -0,0 +1,17 @@ +use std::fs::File; +use std::io::Error; + +// first function + +fn unwrap_file(open_result: Result) -> File { + match open_result { + Ok(file) => return file, + Err(e) => panic!("Problem opening the file: {:?}", e), + }; +} + +fn main() { + let open_result = File::open("src/lib/content.txt"); + + let _file = unwrap_file(open_result); +} diff --git a/assignments/files-match-result-assignment/examples/step_3.rs b/assignments/files-match-result-assignment/examples/step_3.rs new file mode 100644 index 00000000..654eab0c --- /dev/null +++ b/assignments/files-match-result-assignment/examples/step_3.rs @@ -0,0 +1,25 @@ +use std::fs::File; +use std::io::prelude::*; +use std::io::Error; + +fn unwrap_file(open_result: Result) -> File { + match open_result { + Ok(file) => return file, + Err(e) => panic!("Problem opening the file: {:?}", e), + }; +} + +fn content_to_string(mut file: File) -> Result { + let mut content_string = String::new(); + file.read_to_string(&mut content_string)?; + Ok(content_string) +} + +fn main() { + let open_result = File::open("src/data/content.txt"); + + let file = unwrap_file(open_result); + + let content = content_to_string(file).unwrap(); + println!("{}", content); +} diff --git a/assignments/files-match-result-assignment/solution/Step4.rs b/assignments/files-match-result-assignment/examples/step_4.rs similarity index 62% rename from assignments/files-match-result-assignment/solution/Step4.rs rename to assignments/files-match-result-assignment/examples/step_4.rs index 14118dae..b2e9793c 100644 --- a/assignments/files-match-result-assignment/solution/Step4.rs +++ b/assignments/files-match-result-assignment/examples/step_4.rs @@ -1,14 +1,17 @@ -use std::io::{BufReader, BufRead}; use std::fs::File; +use std::io::{BufRead, BufReader, Error}; -fn main() { +fn unwrap_file(open_result: Result) -> File { + match open_result { + Ok(file) => return file, + Err(e) => panic!("Problem opening the file: {:?}", e), + }; +} +fn main() { let open_result = File::open("src/data/content.txt"); - let file = match open_result { - Ok(file) => file, - Err(e) => panic!("Problem opening the file: {:?}", e), - }; + let file = unwrap_file(open_result); let buf_reader = BufReader::new(file); diff --git a/assignments/files-match-result-assignment/examples/step_5.rs b/assignments/files-match-result-assignment/examples/step_5.rs new file mode 100644 index 00000000..4ccc45e4 --- /dev/null +++ b/assignments/files-match-result-assignment/examples/step_5.rs @@ -0,0 +1,28 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, Error}; + +fn unwrap_file(open_result: Result) -> File { + match open_result { + Ok(file) => return file, + Err(e) => panic!("Problem opening the file: {:?}", e), + }; +} + +fn main() { + let open_result = File::open("src/data/content.txt"); + + let file = unwrap_file(open_result); + + let buf_reader = BufReader::new(file); + + for line in buf_reader.lines() { + match line { + Ok(content) => { + if !content.is_empty() { + println!("{}", content) + } + } + Err(e) => println!("Error reading line {}", e), + } + } +} diff --git a/assignments/files-match-result-assignment/solution/Step6.rs b/assignments/files-match-result-assignment/examples/step_6.rs similarity index 65% rename from assignments/files-match-result-assignment/solution/Step6.rs rename to assignments/files-match-result-assignment/examples/step_6.rs index 4abb5d29..c6885625 100644 --- a/assignments/files-match-result-assignment/solution/Step6.rs +++ b/assignments/files-match-result-assignment/examples/step_6.rs @@ -1,36 +1,40 @@ -use url::Url; -use std::io::{BufReader, BufRead}; use std::fs::File; +use std::io::{BufRead, BufReader, Error}; +use url::Url; fn parse_line(line: String) -> Option { match Url::parse(&line) { Ok(u) => Some(u), - Err(_e) => None + Err(_e) => None, } } -fn main() { - let open_result = File::open("src/lib/content.txt"); - - let file = match open_result { - Ok(file) => file, +fn unwrap_file(open_result: Result) -> File { + match open_result { + Ok(file) => return file, Err(e) => panic!("Problem opening the file: {:?}", e), }; +} + +fn main() { + let open_result = File::open("src/data/content.txt"); + + let file = unwrap_file(open_result); let buf_reader = BufReader::new(file); for line in buf_reader.lines() { - let line = match line { Ok(content) => content, - Err(e) => panic!("Problem reading the file: {:?}", e), + + Err(e) => panic!("Error reading line {}", e), }; let url = parse_line(line); match url { Some(line) => println!("{}", line), - None => continue + None => continue, } } } diff --git a/assignments/files-match-result-assignment/solution/Step3.rs b/assignments/files-match-result-assignment/solution/Step3.rs deleted file mode 100644 index 3a196d45..00000000 --- a/assignments/files-match-result-assignment/solution/Step3.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::io::Read; -use std::fs::File; - -fn main() { - - let open_result = File::open("src/data/content.txt"); - - let mut file = match open_result { - Ok(file) => file, - Err(e) => panic!("Problem opening the file: {:?}", e), - }; - - let mut content_string = String::new(); - file.read_to_string(&mut content_string).unwrap(); - println!("{}", content_string); -} diff --git a/assignments/files-match-result-assignment/solution/Step5.rs b/assignments/files-match-result-assignment/solution/Step5.rs deleted file mode 100644 index a3d6874f..00000000 --- a/assignments/files-match-result-assignment/solution/Step5.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::io::{BufReader, BufRead}; -use std::fs::File; - -fn main() { - - let open_result = File::open("src/data/content.txt"); - - let file = match open_result { - Ok(file) => file, - Err(e) => panic!("Problem opening the file: {:?}", e), - }; - - let buf_reader = BufReader::new(file); - - for line in buf_reader.lines() { - - let line = match line { - Ok(content) => content, - Err(e) => panic!("Problem reading the line: {:?}", e), - }; - - println!("{}", line); - } -} diff --git a/assignments/files-match-result-assignment/template/src/data/content.txt b/assignments/files-match-result-assignment/src/data/content.txt similarity index 100% rename from assignments/files-match-result-assignment/template/src/data/content.txt rename to assignments/files-match-result-assignment/src/data/content.txt diff --git a/assignments/files-match-result-assignment/src/main.rs b/assignments/files-match-result-assignment/src/main.rs new file mode 100644 index 00000000..bd16ed17 --- /dev/null +++ b/assignments/files-match-result-assignment/src/main.rs @@ -0,0 +1,14 @@ +use std::fs::File; + + +fn main() { + let _open_result = File::open("src/lib/content.txt").unwrap(); + // ^^^^^^^^ File::open yields a Result, + // a quick way to get to the File type is to use + // the unwrap() method on the Result. + + + // ... +} + + diff --git a/assignments/files-match-result-assignment/template/Cargo.lock b/assignments/files-match-result-assignment/template/Cargo.lock deleted file mode 100644 index baefc434..00000000 --- a/assignments/files-match-result-assignment/template/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "option-result-assignment" -version = "0.1.0" diff --git a/assignments/files-match-result-assignment/template/src/main.rs b/assignments/files-match-result-assignment/template/src/main.rs deleted file mode 100644 index 8799bfba..00000000 --- a/assignments/files-match-result-assignment/template/src/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::fs::File; - -fn main() { - - let f = File::open("src/data/content.txt"); - - match f { - // substitute this placeholder for the two possible patterns from the result type - // PATTERN => EXPRESSION, - // PATTERN => EXPRESSION, - }; -}