@@ -1,7 +1,7 @@
|
||||
# Modelling A Ticket, pt. 2
|
||||
|
||||
The `Ticket` struct we worked on in the previous chapters is a good start,
|
||||
but it still screams "I'm a beginner Rustacean!".
|
||||
The `Ticket` struct we worked on in the previous chapters is a good start,
|
||||
but it still screams "I'm a beginner Rustacean!".
|
||||
|
||||
We'll use this chapter to refine our Rust domain modelling skills.
|
||||
We'll need to introduce a few more concepts along the way:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Enumerations
|
||||
|
||||
Based on the validation logic you wrote [in a previous chapter](../03_ticket_v1/02_validation.md),
|
||||
there are only a few valid statuses for a ticket: `To-Do`, `InProgress` and `Done`.
|
||||
This is not obvious if we look at the `status` field in the `Ticket` struct or at the type of the `status`
|
||||
Based on the validation logic you wrote [in a previous chapter](../03_ticket_v1/02_validation.md),
|
||||
there are only a few valid statuses for a ticket: `To-Do`, `InProgress` and `Done`.\
|
||||
This is not obvious if we look at the `status` field in the `Ticket` struct or at the type of the `status`
|
||||
parameter in the `new` method:
|
||||
|
||||
```rust
|
||||
@@ -29,7 +29,7 @@ We can do better than that with **enumerations**.
|
||||
|
||||
## `enum`
|
||||
|
||||
An enumeration is a type that can have a fixed set of values, called **variants**.
|
||||
An enumeration is a type that can have a fixed set of values, called **variants**.\
|
||||
In Rust, you define an enumeration using the `enum` keyword:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `match`
|
||||
|
||||
You may be wondering—what can you actually **do** with an enum?
|
||||
You may be wondering—what can you actually **do** with an enum?\
|
||||
The most common operation is to **match** on it.
|
||||
|
||||
```rust
|
||||
@@ -22,13 +22,13 @@ impl Status {
|
||||
}
|
||||
```
|
||||
|
||||
A `match` statement that lets you compare a Rust value against a series of **patterns**.
|
||||
A `match` statement that lets you compare a Rust value against a series of **patterns**.\
|
||||
You can think of it as a type-level `if`. If `status` is a `Done` variant, execute the first block;
|
||||
if it's a `InProgress` or `ToDo` variant, execute the second block.
|
||||
|
||||
## Exhaustiveness
|
||||
|
||||
There's one key detail here: `match` is **exhaustive**. You must handle all enum variants.
|
||||
There's one key detail here: `match` is **exhaustive**. You must handle all enum variants.\
|
||||
If you forget to handle a variant, Rust will stop you **at compile-time** with an error.
|
||||
|
||||
E.g. if we forget to handle the `ToDo` variant:
|
||||
@@ -50,7 +50,7 @@ error[E0004]: non-exhaustive patterns: `ToDo` not covered
|
||||
| ^^^^^^^^^^^^ pattern `ToDo` not covered
|
||||
```
|
||||
|
||||
This is a big deal!
|
||||
This is a big deal!\
|
||||
Codebases evolve over time—you might add a new status down the line, e.g. `Blocked`. The Rust compiler
|
||||
will emit an error for every single `match` statement that's missing logic for the new variant.
|
||||
That's why Rust developers often sing the praises of "compiler-driven refactoring"—the compiler tells you
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Variants can hold data
|
||||
# Variants can hold data
|
||||
|
||||
```rust
|
||||
enum Status {
|
||||
@@ -8,17 +8,17 @@ enum Status {
|
||||
}
|
||||
```
|
||||
|
||||
Our `Status` enum is what's usually called a **C-style enum**.
|
||||
Each variant is a simple label, a bit like a named constant. You can find this kind of enum in many programming
|
||||
Our `Status` enum is what's usually called a **C-style enum**.\
|
||||
Each variant is a simple label, a bit like a named constant. You can find this kind of enum in many programming
|
||||
languages, like C, C++, Java, C#, Python, etc.
|
||||
|
||||
Rust enums can go further though. We can **attach data to each variant**.
|
||||
|
||||
## Variants
|
||||
|
||||
Let's say that we want to store the name of the person who's currently working on a ticket.
|
||||
We would only have this information if the ticket is in progress. It wouldn't be there for a to-do ticket or
|
||||
a done ticket.
|
||||
Let's say that we want to store the name of the person who's currently working on a ticket.\
|
||||
We would only have this information if the ticket is in progress. It wouldn't be there for a to-do ticket or
|
||||
a done ticket.
|
||||
We can model this by attaching a `String` field to the `InProgress` variant:
|
||||
|
||||
```rust
|
||||
@@ -31,7 +31,7 @@ enum Status {
|
||||
}
|
||||
```
|
||||
|
||||
`InProgress` is now a **struct-like variant**.
|
||||
`InProgress` is now a **struct-like variant**.\
|
||||
The syntax mirrors, in fact, the one we used to define a struct—it's just "inlined" inside the enum, as a variant.
|
||||
|
||||
## Accessing variant data
|
||||
@@ -55,7 +55,7 @@ error[E0609]: no field `assigned_to` on type `Status`
|
||||
| ^^^^^^^^^^^ unknown field
|
||||
```
|
||||
|
||||
`assigned_to` is **variant-specific**, it's not available on all `Status` instances.
|
||||
`assigned_to` is **variant-specific**, it's not available on all `Status` instances.\
|
||||
To access `assigned_to`, we need to use **pattern matching**:
|
||||
|
||||
```rust
|
||||
@@ -71,9 +71,9 @@ match status {
|
||||
|
||||
## Bindings
|
||||
|
||||
In the match pattern `Status::InProgress { assigned_to }`, `assigned_to` is a **binding**.
|
||||
We're **destructuring** the `Status::InProgress` variant and binding the `assigned_to` field to
|
||||
a new variable, also named `assigned_to`.
|
||||
In the match pattern `Status::InProgress { assigned_to }`, `assigned_to` is a **binding**.\
|
||||
We're **destructuring** the `Status::InProgress` variant and binding the `assigned_to` field to
|
||||
a new variable, also named `assigned_to`.\
|
||||
If we wanted, we could bind the field to a different variable name:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -15,14 +15,14 @@ impl Ticket {
|
||||
}
|
||||
```
|
||||
|
||||
You only care about the `Status::InProgress` variant.
|
||||
You only care about the `Status::InProgress` variant.
|
||||
Do you really need to match on all the other variants?
|
||||
|
||||
New constructs to the rescue!
|
||||
|
||||
## `if let`
|
||||
|
||||
The `if let` construct allows you to match on a single variant of an enum,
|
||||
The `if let` construct allows you to match on a single variant of an enum,
|
||||
without having to handle all the other variants.
|
||||
|
||||
Here's how you can use `if let` to simplify the `assigned_to` method:
|
||||
@@ -61,8 +61,8 @@ as the code that precedes it.
|
||||
|
||||
## Style
|
||||
|
||||
Both `if let` and `let/else` are idiomatic Rust constructs.
|
||||
Use them as you see fit to improve the readability of your code,
|
||||
Both `if let` and `let/else` are idiomatic Rust constructs.\
|
||||
Use them as you see fit to improve the readability of your code,
|
||||
but don't overdo it: `match` is always there when you need it.
|
||||
|
||||
## References
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Nullability
|
||||
|
||||
Our implementation of the `assigned` method is fairly blunt: panicking for to-do and done tickets is far from ideal.
|
||||
Our implementation of the `assigned` method is fairly blunt: panicking for to-do and done tickets is far from ideal.\
|
||||
We can do better using **Rust's `Option` type**.
|
||||
|
||||
## `Option`
|
||||
|
||||
`Option` is a Rust type that represents **nullable values**.
|
||||
`Option` is a Rust type that represents **nullable values**.\
|
||||
It is an enum, defined in Rust's standard library:
|
||||
|
||||
```rust
|
||||
@@ -15,10 +15,10 @@ enum Option<T> {
|
||||
}
|
||||
```
|
||||
|
||||
`Option` encodes the idea that a value might be present (`Some(T)`) or absent (`None`).
|
||||
`Option` encodes the idea that a value might be present (`Some(T)`) or absent (`None`).\
|
||||
It also forces you to **explicitly handle both cases**. You'll get a compiler error if you are working with
|
||||
a nullable value and you forget to handle the `None` case.
|
||||
This is a significant improvement over "implicit" nullability in other languages, where you can forget to check
|
||||
a nullable value and you forget to handle the `None` case.\
|
||||
This is a significant improvement over "implicit" nullability in other languages, where you can forget to check
|
||||
for `null` and thus trigger a runtime error.
|
||||
|
||||
## `Option`'s definition
|
||||
@@ -27,11 +27,11 @@ for `null` and thus trigger a runtime error.
|
||||
|
||||
### Tuple-like variants
|
||||
|
||||
`Option` has two variants: `Some(T)` and `None`.
|
||||
`Some` is a **tuple-like variant**: it's a variant that holds **unnamed fields**.
|
||||
`Option` has two variants: `Some(T)` and `None`.\
|
||||
`Some` is a **tuple-like variant**: it's a variant that holds **unnamed fields**.
|
||||
|
||||
Tuple-like variants are often used when there is a single field to store, especially when we're looking at a
|
||||
"wrapper" type like `Option`.
|
||||
Tuple-like variants are often used when there is a single field to store, especially when we're looking at a
|
||||
"wrapper" type like `Option`.
|
||||
|
||||
### Tuple-like structs
|
||||
|
||||
@@ -51,7 +51,7 @@ let y = point.1;
|
||||
|
||||
### Tuples
|
||||
|
||||
It's weird say that something is tuple-like when we haven't seen tuples yet!
|
||||
It's weird say that something is tuple-like when we haven't seen tuples yet!\
|
||||
Tuples are another example of a primitive Rust type.
|
||||
They group together a fixed number of values with (potentially different) types:
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ impl Ticket {
|
||||
}
|
||||
```
|
||||
|
||||
As soon as one of the checks fails, the function panics.
|
||||
This is not ideal, as it doesn't give the caller a chance to **handle the error**.
|
||||
As soon as one of the checks fails, the function panics.
|
||||
This is not ideal, as it doesn't give the caller a chance to **handle the error**.
|
||||
|
||||
It's time to introduce the `Result` type, Rust's primary mechanism for error handling.
|
||||
|
||||
@@ -52,22 +52,22 @@ Both `Ok` and `Err` are generic, allowing you to specify your own types for the
|
||||
|
||||
## No exceptions
|
||||
|
||||
Recoverable errors in Rust are **represented as values**.
|
||||
Recoverable errors in Rust are **represented as values**.\
|
||||
They're just an instance of a type, being passed around and manipulated like any other value.
|
||||
This is a significant difference from other languages, such as Python or C#, where **exceptions** are used to signal errors.
|
||||
|
||||
Exceptions create a separate control flow path that can be hard to reason about.
|
||||
Exceptions create a separate control flow path that can be hard to reason about.\
|
||||
You don't know, just by looking at a function's signature, if it can throw an exception or not.
|
||||
You don't know, just by looking at a function's signature, **which** exception types it can throw.
|
||||
You don't know, just by looking at a function's signature, **which** exception types it can throw.\
|
||||
You must either read the function's documentation or look at its implementation to find out.
|
||||
|
||||
Exception handling logic has very poor locality: the code that throws the exception is far removed from the code
|
||||
Exception handling logic has very poor locality: the code that throws the exception is far removed from the code
|
||||
that catches it, and there's no direct link between the two.
|
||||
|
||||
## Fallibility is encoded in the type system
|
||||
|
||||
Rust, with `Result`, forces you to **encode fallibility in the function's signature**.
|
||||
If a function can fail (and you want the caller to have a shot at handling the error), it must return a `Result`.
|
||||
Rust, with `Result`, forces you to **encode fallibility in the function's signature**.\
|
||||
If a function can fail (and you want the caller to have a shot at handling the error), it must return a `Result`.
|
||||
|
||||
```rust
|
||||
// Just by looking at the signature, you know that this function can fail.
|
||||
@@ -77,7 +77,7 @@ fn parse_int(s: &str) -> Result<i32, ParseIntError> {
|
||||
}
|
||||
```
|
||||
|
||||
That's the big advantage of `Result`: it makes fallibility explicit.
|
||||
That's the big advantage of `Result`: it makes fallibility explicit.
|
||||
|
||||
Keep in mind, though, that panics exist. They aren't tracked by the type system, just like exceptions in other languages.
|
||||
But they're meant for **unrecoverable errors** and should be used sparingly.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Unwrapping
|
||||
|
||||
`Ticket::new` now returns a `Result` instead of panicking on invalid inputs.
|
||||
`Ticket::new` now returns a `Result` instead of panicking on invalid inputs.\
|
||||
What does this mean for the caller?
|
||||
|
||||
## Failures can't be (implicitly) ignored
|
||||
|
||||
Unlike exceptions, Rust's `Result` forces you to **handle errors at the call site**.
|
||||
Unlike exceptions, Rust's `Result` forces you to **handle errors at the call site**.\
|
||||
If you call a function that returns a `Result`, Rust won't allow you to implicitly ignore the error case.
|
||||
|
||||
```rust
|
||||
@@ -30,7 +30,7 @@ When you call a function that returns a `Result`, you have two key options:
|
||||
let number = parse_int("42").unwrap();
|
||||
// `expect` lets you specify a custom panic message.
|
||||
let number = parse_int("42").expect("Failed to parse integer");
|
||||
```
|
||||
```
|
||||
- Destructure the `Result` using a `match` expression to deal with the error case explicitly.
|
||||
```rust
|
||||
match parse_int("42") {
|
||||
@@ -41,4 +41,4 @@ When you call a function that returns a `Result`, you have two key options:
|
||||
|
||||
## References
|
||||
|
||||
- The exercise for this section is located in `exercises/05_ticket_v2/07_unwrap`
|
||||
- The exercise for this section is located in `exercises/05_ticket_v2/07_unwrap`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Error enums
|
||||
|
||||
Your solution to the previous exercise may have felt awkward: matching on strings is not ideal!
|
||||
Your solution to the previous exercise may have felt awkward: matching on strings is not ideal!\
|
||||
A colleague might rework the error messages returned by `Ticket::new` (e.g. to improve readability) and,
|
||||
all of a sudden, your calling code would break.
|
||||
|
||||
@@ -22,7 +22,7 @@ enum U32ParseError {
|
||||
```
|
||||
|
||||
Using an error enum, you're encoding the different error cases in the type system—they become part of the
|
||||
signature of the fallible function.
|
||||
signature of the fallible function.\
|
||||
This simplifies error handling for the caller, as they can use a `match` expression to react to the different
|
||||
error cases:
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## Error reporting
|
||||
|
||||
In the previous exercise you had to destructure the `InvalidTitle` variant to extract the error message and
|
||||
pass it to the `panic!` macro.
|
||||
In the previous exercise you had to destructure the `InvalidTitle` variant to extract the error message and
|
||||
pass it to the `panic!` macro.\
|
||||
This is a (rudimentary) example of **error reporting**: transforming an error type into a representation that can be
|
||||
shown to a user, a service operator, or a developer.
|
||||
|
||||
@@ -13,7 +13,7 @@ That's why Rust provides the `std::error::Error` trait.
|
||||
|
||||
## The `Error` trait
|
||||
|
||||
There are no constraints on the type of the `Err` variant in a `Result`, but it's a good practice to use a type
|
||||
There are no constraints on the type of the `Err` variant in a `Result`, but it's a good practice to use a type
|
||||
that implements the `Error` trait.
|
||||
`Error` is the cornerstone of Rust's error handling story:
|
||||
|
||||
@@ -31,7 +31,7 @@ implement `Debug` and `Display`.
|
||||
We've already encountered the `Debug` trait in [a previous exercise](../04_traits/04_derive.md)—it's the trait used by
|
||||
`assert_eq!` to display the values of the variables it's comparing when the assertion fails.
|
||||
|
||||
From a "mechanical" perspective, `Display` and `Debug` are identical—they encode how a type should be converted
|
||||
From a "mechanical" perspective, `Display` and `Debug` are identical—they encode how a type should be converted
|
||||
into a string-like representation:
|
||||
|
||||
```rust
|
||||
@@ -46,8 +46,8 @@ pub trait Display {
|
||||
}
|
||||
```
|
||||
|
||||
The difference is in their *purpose*: `Display` returns a representation that's meant for "end-users",
|
||||
while `Debug` provides a low-level representation that's more suitable to developers and service operators.
|
||||
The difference is in their _purpose_: `Display` returns a representation that's meant for "end-users",
|
||||
while `Debug` provides a low-level representation that's more suitable to developers and service operators.\
|
||||
That's why `Debug` can be automatically implemented using the `#[derive(Debug)]` attribute, while `Display`
|
||||
**requires** a manual implementation.
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
# Libraries and binaries
|
||||
|
||||
It took a bit of code to implement the `Error` trait for `TicketNewError`, didn't it?
|
||||
It took a bit of code to implement the `Error` trait for `TicketNewError`, didn't it?\
|
||||
A manual `Display` implementation, plus an `Error` impl block.
|
||||
|
||||
We can remove some of the boilerplate by using [`thiserror`](https://docs.rs/thiserror/latest/thiserror/),
|
||||
a Rust crate that provides a **procedural macro** to simplify the creation of custom error types.
|
||||
But we're getting ahead of ourselves: `thiserror` is a third-party crate, it'd be our first dependency!
|
||||
We can remove some of the boilerplate by using [`thiserror`](https://docs.rs/thiserror/latest/thiserror/),
|
||||
a Rust crate that provides a **procedural macro** to simplify the creation of custom error types.\
|
||||
But we're getting ahead of ourselves: `thiserror` is a third-party crate, it'd be our first dependency!
|
||||
|
||||
Let's take a step back to talk about Rust's packaging system before we dive into dependencies.
|
||||
|
||||
## What is a package?
|
||||
|
||||
A Rust package is defined by the `[package]` section in a `Cargo.toml` file, also known as its **manifest**.
|
||||
A Rust package is defined by the `[package]` section in a `Cargo.toml` file, also known as its **manifest**.
|
||||
Within `[package]` you can set the package's metadata, such as its name and version.
|
||||
|
||||
Go check the `Cargo.toml` file in the directory of this section's exercise!
|
||||
|
||||
## What is a crate?
|
||||
|
||||
Inside a package, you can have one or more **crates**, also known as **targets**.
|
||||
Inside a package, you can have one or more **crates**, also known as **targets**.\
|
||||
The two most common crate types are **binary crates** and **library crates**.
|
||||
|
||||
### Binaries
|
||||
|
||||
A binary is a program that can be compiled to an **executable file**.
|
||||
A binary is a program that can be compiled to an **executable file**.\
|
||||
It must include a function named `main`—the program's entry point. `main` is invoked when the program is executed.
|
||||
|
||||
### Libraries
|
||||
|
||||
Libraries, on the other hand, are not executable on their own. You can't _run_ a library,
|
||||
but you can _import its code_ from another package that depends on it.
|
||||
A library groups together code (i.e. functions, types, etc.) that can be leveraged by other packages as a **dependency**.
|
||||
Libraries, on the other hand, are not executable on their own. You can't _run_ a library,
|
||||
but you can _import its code_ from another package that depends on it.\
|
||||
A library groups together code (i.e. functions, types, etc.) that can be leveraged by other packages as a **dependency**.
|
||||
|
||||
All the exercises you've solved so far have been structured as libraries, with a test suite attached to them.
|
||||
|
||||
@@ -55,7 +55,7 @@ You can use `cargo` to scaffold a new package:
|
||||
cargo new my-binary
|
||||
```
|
||||
|
||||
This will create a new folder, `my-binary`, containing a new Rust package with the same name and a single
|
||||
This will create a new folder, `my-binary`, containing a new Rust package with the same name and a single
|
||||
binary crate inside. If you want to create a library crate instead, you can use the `--lib` flag:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dependencies
|
||||
|
||||
A package can depend on other packages by listing them in the `[dependencies]` section of its `Cargo.toml` file.
|
||||
A package can depend on other packages by listing them in the `[dependencies]` section of its `Cargo.toml` file.\
|
||||
The most common way to specify a dependency is by providing its name and version:
|
||||
|
||||
```toml
|
||||
@@ -8,7 +8,7 @@ The most common way to specify a dependency is by providing its name and version
|
||||
thiserror = "1"
|
||||
```
|
||||
|
||||
This will add `thiserror` as a dependency to your package, with a **minimum** version of `1.0.0`.
|
||||
This will add `thiserror` as a dependency to your package, with a **minimum** version of `1.0.0`.
|
||||
`thiserror` will be pulled from [crates.io](https://crates.io), Rust's official package registry.
|
||||
When you run `cargo build`, `cargo` will go through a few stages:
|
||||
|
||||
@@ -17,10 +17,10 @@ When you run `cargo build`, `cargo` will go through a few stages:
|
||||
- Compiling your project (your own code and the dependencies)
|
||||
|
||||
Dependency resolution is skipped if your project has a `Cargo.lock` file and your manifest files are unchanged.
|
||||
A lockfile is automatically generated by `cargo` after a successful round of dependency resolution: it contains
|
||||
the exact versions of all dependencies used in your project, and is used to ensure that the same versions are
|
||||
A lockfile is automatically generated by `cargo` after a successful round of dependency resolution: it contains
|
||||
the exact versions of all dependencies used in your project, and is used to ensure that the same versions are
|
||||
consistently used across different builds (e.g. in CI). If you're working on a project with multiple developers,
|
||||
you should commit the `Cargo.lock` file to your version control system.
|
||||
you should commit the `Cargo.lock` file to your version control system.
|
||||
|
||||
You can use `cargo update` to update the `Cargo.lock` file with the latest (compatible) versions of all your dependencies.
|
||||
|
||||
@@ -43,7 +43,7 @@ details on where you can get dependencies from and how to specify them in your `
|
||||
## Dev dependencies
|
||||
|
||||
You can also specify dependencies that are only needed for development—i.e. they only get pulled in when you're
|
||||
running `cargo test`.
|
||||
running `cargo test`.\
|
||||
They go in the `[dev-dependencies]` section of your `Cargo.toml` file:
|
||||
|
||||
```toml
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# `thiserror`
|
||||
|
||||
That was a bit of detour, wasn't it? But a necessary one!
|
||||
That was a bit of detour, wasn't it? But a necessary one!\
|
||||
Let's get back on track now: custom error types and `thiserror`.
|
||||
|
||||
## Custom error types
|
||||
|
||||
We've seen how to implement the `Error` trait "manually" for a custom error type.
|
||||
We've seen how to implement the `Error` trait "manually" for a custom error type.\
|
||||
Imagine that you have to do this for most error types in your codebase. That's a lot of boilerplate, isn't it?
|
||||
|
||||
We can remove some of the boilerplate by using [`thiserror`](https://docs.rs/thiserror/latest/thiserror/),
|
||||
@@ -23,12 +23,12 @@ enum TicketNewError {
|
||||
|
||||
## You can write your own macros
|
||||
|
||||
All the `derive` macros we've seen so far were provided by the Rust standard library.
|
||||
All the `derive` macros we've seen so far were provided by the Rust standard library.\
|
||||
`thiserror::Error` is the first example of a **third-party** `derive` macro.
|
||||
|
||||
`derive` macros are a subset of **procedural macros**, a way to generate Rust code at compile time.
|
||||
`derive` macros are a subset of **procedural macros**, a way to generate Rust code at compile time.
|
||||
We won't get into the details of how to write a procedural macro in this course, but it's important
|
||||
to know that you can write your own!
|
||||
to know that you can write your own!\
|
||||
A topic to approach in a more advanced Rust course.
|
||||
|
||||
## Custom syntax
|
||||
@@ -39,7 +39,7 @@ In the case of `thiserror`, we have:
|
||||
- `#[derive(thiserror::Error)]`: this is the syntax to derive the `Error` trait for a custom error type, helped by `thiserror`.
|
||||
- `#[error("{0}")]`: this is the syntax to define a `Display` implementation for each variant of the custom error type.
|
||||
`{0}` is replaced by the zero-th field of the variant (`String`, in this case) when the error is displayed.
|
||||
|
||||
|
||||
## References
|
||||
|
||||
- The exercise for this section is located in `exercises/05_ticket_v2/12_thiserror`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# `TryFrom` and `TryInto`
|
||||
|
||||
In the previous chapter we looked at the [`From` and `Into` traits](../04_traits/09_from.md),
|
||||
Rust's idiomatic interfaces for **infallible** type conversions.
|
||||
In the previous chapter we looked at the [`From` and `Into` traits](../04_traits/09_from.md),
|
||||
Rust's idiomatic interfaces for **infallible** type conversions.\
|
||||
But what if the conversion is not guaranteed to succeed?
|
||||
|
||||
We now know enough about errors to discuss the **fallible** counterparts of `From` and `Into`:
|
||||
We now know enough about errors to discuss the **fallible** counterparts of `From` and `Into`:
|
||||
`TryFrom` and `TryInto`.
|
||||
|
||||
## `TryFrom` and `TryInto`
|
||||
@@ -23,7 +23,7 @@ pub trait TryInto<T>: Sized {
|
||||
}
|
||||
```
|
||||
|
||||
The main difference between `From`/`Into` and `TryFrom`/`TryInto` is that the latter return a `Result` type.
|
||||
The main difference between `From`/`Into` and `TryFrom`/`TryInto` is that the latter return a `Result` type.\
|
||||
This allows the conversion to fail, returning an error instead of panicking.
|
||||
|
||||
## `Self::Error`
|
||||
@@ -36,7 +36,7 @@ being attempted.
|
||||
|
||||
## Duality
|
||||
|
||||
Just like `From` and `Into`, `TryFrom` and `TryInto` are dual traits.
|
||||
Just like `From` and `Into`, `TryFrom` and `TryInto` are dual traits.\
|
||||
If you implement `TryFrom` for a type, you get `TryInto` for free.
|
||||
|
||||
## References
|
||||
|
||||
@@ -11,15 +11,15 @@ pub trait Error: Debug + Display {
|
||||
}
|
||||
```
|
||||
|
||||
The `source` method is a way to access the **error cause**, if any.
|
||||
The `source` method is a way to access the **error cause**, if any.\
|
||||
Errors are often chained, meaning that one error is the cause of another: you have a high-level error (e.g.
|
||||
cannot connect to the database) that is caused by a lower-level error (e.g. can't resolve the database hostname).
|
||||
The `source` method allows you to "walk" the full chain of errors, often used when capturing error context in logs.
|
||||
|
||||
## Implementing `source`
|
||||
|
||||
The `Error` trait provides a default implementation that always returns `None` (i.e. no underlying cause). That's why
|
||||
you didn't have to care about `source` in the previous exercises.
|
||||
The `Error` trait provides a default implementation that always returns `None` (i.e. no underlying cause). That's why
|
||||
you didn't have to care about `source` in the previous exercises.\
|
||||
You can override this default implementation to provide a cause for your error type.
|
||||
|
||||
```rust
|
||||
@@ -48,14 +48,14 @@ We then override the `source` method to return this source when called.
|
||||
|
||||
## `&(dyn Error + 'static)`
|
||||
|
||||
What's this `&(dyn Error + 'static)` type?
|
||||
What's this `&(dyn Error + 'static)` type?\
|
||||
Let's unpack it:
|
||||
|
||||
- `dyn Error` is a **trait object**. It's a way to refer to any type that implements the `Error` trait.
|
||||
- `'static` is a special **lifetime specifier**.
|
||||
`'static` implies that the reference is valid for "as long as we need it", i.e. the entire program execution.
|
||||
|
||||
Combined: `&(dyn Error + 'static)` is a reference to a trait object that implements the `Error` trait
|
||||
Combined: `&(dyn Error + 'static)` is a reference to a trait object that implements the `Error` trait
|
||||
and is valid for the entire program execution.
|
||||
|
||||
Don't worry too much about either of these concepts for now. We'll cover them in more detail in future chapters.
|
||||
@@ -75,7 +75,7 @@ Don't worry too much about either of these concepts for now. We'll cover them in
|
||||
source: std::io::Error
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
- A field annotated with the `#[source]` attribute will automatically be used as the source of the error.
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
@@ -88,8 +88,8 @@ Don't worry too much about either of these concepts for now. We'll cover them in
|
||||
inner: std::io::Error
|
||||
}
|
||||
}
|
||||
```
|
||||
- A field annotated with the `#[from]` attribute will automatically be used as the source of the error **and**
|
||||
```
|
||||
- A field annotated with the `#[from]` attribute will automatically be used as the source of the error **and**
|
||||
`thiserror` will automatically generate a `From` implementation to convert the annotated type into your error type.
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
@@ -102,11 +102,11 @@ Don't worry too much about either of these concepts for now. We'll cover them in
|
||||
inner: std::io::Error
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## The `?` operator
|
||||
|
||||
The `?` operator is a shorthand for propagating errors.
|
||||
The `?` operator is a shorthand for propagating errors.\
|
||||
When used in a function that returns a `Result`, it will return early with an error if the `Result` is `Err`.
|
||||
|
||||
For example:
|
||||
@@ -145,7 +145,7 @@ fn read_file() -> Result<String, std::io::Error> {
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `?` operator to shorten your error handling code significantly.
|
||||
You can use the `?` operator to shorten your error handling code significantly.\
|
||||
In particular, the `?` operator will automatically convert the error type of the fallible operation into the error type
|
||||
of the function, if a conversion is possible (i.e. if there is a suitable `From` implementation)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Wrapping up
|
||||
|
||||
When it comes to domain modelling, the devil is in the details.
|
||||
When it comes to domain modelling, the devil is in the details.\
|
||||
Rust offers a wide range of tools to help you represent the constraints of your domain directly in the type system,
|
||||
but it takes some practice to get it right and write code that looks idiomatic.
|
||||
|
||||
Let's close the chapter with one final refinement of our `Ticket` model.
|
||||
We'll introduce a new type for each of the fields in `Ticket` to encapsulate the respective constraints.
|
||||
Every time someone accesses a `Ticket` field, they'll get back a value that's guaranteed to be valid—i.e. a
|
||||
Let's close the chapter with one final refinement of our `Ticket` model.\
|
||||
We'll introduce a new type for each of the fields in `Ticket` to encapsulate the respective constraints.\
|
||||
Every time someone accesses a `Ticket` field, they'll get back a value that's guaranteed to be valid—i.e. a
|
||||
`TicketTitle` instead of a `String`. They won't have to worry about the title being empty elsewhere in the code:
|
||||
as long as they have a `TicketTitle`, they know it's valid **by construction**.
|
||||
as long as they have a `TicketTitle`, they know it's valid **by construction**.
|
||||
|
||||
This is just an example of how you can use Rust's type system to make your code safer and more expressive.
|
||||
|
||||
@@ -19,4 +19,4 @@ This is just an example of how you can use Rust's type system to make your code
|
||||
## Further reading
|
||||
|
||||
- [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
|
||||
- [Using types to guarantee domain invariants](https://www.lpalmieri.com/posts/2020-12-11-zero-to-production-6-domain-modelling/)
|
||||
- [Using types to guarantee domain invariants](https://www.lpalmieri.com/posts/2020-12-11-zero-to-production-6-domain-modelling/)
|
||||
|
||||
Reference in New Issue
Block a user