Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Files match result new #198

Draft
wants to merge 2 commits into
base: training_material_wip
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 128 additions & 28 deletions assignments/files-match-result-assignment.adoc
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,16 +40,46 @@ 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,
PATTERN => EXPRESSION,
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<T,E>, match statements can be used to get to the inner value.
----
match RESULT<T,E> {
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
Expand All @@ -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<T, E>` kind of type, a quick way to get to inner type T is to use the `.unwrap()` method on the `Result<T, E>`. 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, Error>) -> 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<usize, Error>`, 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<String, Error>`. 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<String, Error> {
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<Url> {
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<Url>`
** 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

Expand All @@ -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.

Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[package]
name = "option-result-assignment"
name = "files_match_result_assignment"
version = "0.1.0"
authors = ["Mirabellensaft <[email protected]>"]
edition = "2018"
authors = ["Mirabellensaft <tanks.transfeld@ferrous-systems.com>"]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
url = "2"
Original file line number Diff line number Diff line change
@@ -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),
};
}
17 changes: 17 additions & 0 deletions assignments/files-match-result-assignment/examples/step_2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::fs::File;
use std::io::Error;

// first function

fn unwrap_file(open_result: Result<File, Error>) -> 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);
}
25 changes: 25 additions & 0 deletions assignments/files-match-result-assignment/examples/step_3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::fs::File;
use std::io::prelude::*;
use std::io::Error;

fn unwrap_file(open_result: Result<File, Error>) -> File {
match open_result {
Ok(file) => return file,
Err(e) => panic!("Problem opening the file: {:?}", e),
};
}

fn content_to_string(mut file: File) -> Result<String, Error> {
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);
}
Original file line number Diff line number Diff line change
@@ -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, Error>) -> 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);

Expand Down
28 changes: 28 additions & 0 deletions assignments/files-match-result-assignment/examples/step_5.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::fs::File;
use std::io::{BufRead, BufReader, Error};

fn unwrap_file(open_result: Result<File, Error>) -> 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),
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Url> {
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, Error>) -> 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,
}
}
}
Expand Down
Loading