Formatter (#51)

Enforce consistent formatting use `dprint`
This commit is contained in:
Luca Palmieri
2024-05-24 17:00:03 +02:00
committed by GitHub
parent 537118574b
commit 99591a715e
157 changed files with 1057 additions and 1044 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

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

View File

@@ -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`

View File

@@ -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:

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View File

@@ -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)

View File

@@ -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/)