Chapter 17: Rust’s most popular crates
The chapter introduces widely used external crates as practical additions to Rust and briefly states what blanket trait implementations make possible.
After this chapter you will be able to
- Practical value of popular crates. Explain why the chapter treats these external crates as useful additions to Rust, and name some tasks they help with.
- Serde and Rust type mapping. Explain why serde is useful when working with JSON: JSON has a small set of data types, but a Rust program may need to represent a JSON value with a particular Rust type.
- Struct based JSON conversion. Describe how the section's example uses a struct with serde's Serialize and/or Deserialize attributes, and explain which direction each trait converts. State how the example reads a JSON request into NewUserRequest.
- Instant and elapsed time. Explain how Instant and Duration work together to measure how long an operation takes, using either subtraction or .elapsed().
- System time and epoch. Distinguish SystemTime from Instant and describe what UNIX_EPOCH provides when expressing system time.
- Timezone-free Naive types. Describe what Naive means in chrono and name the simple types the section introduces.
- Creating Naive date-times. Describe the creation methods introduced for Naive values and how a NaiveDate becomes a NaiveDateTime.
- Chrono date method failure. Explain why chrono deprecated date methods such as from_ymd() and what the _opt versions indicate.
- External method source checks. Justify briefly checking the source of methods in external crates for possible panics, while recognizing why an author might allow a panic.
- Chrono date-time conversion path. Trace the example’s conversion from seconds since the Unix epoch to a time-zone-free date and time, then to UTC and an offset-based time zone, and back to a time-zone-free value.
- UTC event timestamp conversion. Describe why the final example converts incoming event timestamps from UTC to the Korea/Japan time zone and what it does with the converted events.
- Parallel iterator methods. Explain how Rayon lets you use parallel versions of familiar iterator methods without spawning threads yourself.
- Measured speedup and hardware. Describe how the section compares Rayon with ordinary iterator code and why the result can vary.
- Anyhow and thiserror roles. Distinguish why a developer might use Anyhow for an easy-to-use single error type and later use thiserror to make defining their own error types less tedious.
- One return type for errors. Explain why the example cannot use std::io::Error as its return type and how Anyhow provides a single error type for the different failures.
- Deriving a library error type. Explain how #[derive(Error)] turns an error enum into a type that implements std::error::Error, and how #[error] gives its variants messages shown by Display but not Debug.
- Automatic conversions into error variants. Describe how #[from] can automatically implement From for specified other error types so they can be brought into a custom error enum.
- Blanket implementations and bounds. Explain how implementing a trait for a generic type T can cover other people’s types, and how a trait bound limits which types receive the implementation.
- External error conversion example. Describe how the section uses a blanket implementation for Error types to put errors from external crates into its own SystemError enum.
- Run-time static initialization. Explain that lazy_static and once_cell allow static variables that cannot be initialized with a const fn to be initialized at run time instead of compile time. Mention that Rust 1.63 made const-fn initialization possible for some cases that previously needed one of these crates.
- Runtime Initialization of Statics. Explain why the proposed global Logger cannot be initialized as an ordinary static and how lazy_static addresses that limitation.
- Lazy Static Declaration. Describe how the section declares a lazy static and what it says about the term static ref.
- OnceCell initialization. Describe how to create an empty OnceCell for a type and initialize its value with .set().
- Delayed value flexibility. Explain why OnceCell is useful when a value becomes available later, and describe where it can be placed.
What it builds on
- Crates and Cargo.toml (chapter 16) · Introduces external crates and how they fit into a Rust project.
- Traits: The basics (chapter 7) · Introduces traits and their methods.
- Structs (chapter 4) · Using a struct as a Rust type of your own.
- Enums (chapter 4) · What a Rust enum is.
- Spawning threads (chapter 11) · The distinction between a thread and a program that can run multiple threads.
- Option (chapter 5) · Recognizing Option as the type shown for the example results.
- The [src] button (chapter 13) · Finding a method’s source through documentation.
- Iterators (chapter 8) · Familiarity with iterators and their methods
Section by section
Introduction
The chapter introduces widely used external crates as practical additions to Rust and briefly states what blanket trait implementations make possible.
- Practical value of popular crates: Explain why the chapter treats these external crates as useful additions to Rust, and name some tasks they help with.
Why it matters: It gives you a reason to recognize these crates before learning how to use each one. - Blanket implementation capability: State what blanket trait implementations let you do with types owned by others, without describing how to implement one.
Why it matters: This is the capability to listen for when the reader later encounters the implementation details.
Key terms: external crates, standard library, serde, chrono, rayon, anyhow, blanket trait implementations, trait methods
17.1 serde
This section explains how serde converts between Rust types and data formats such as JSON, using a struct and a JSON request as its main example.
- Serde and Rust type mapping: Explain why serde is useful when working with JSON: JSON has a small set of data types, but a Rust program may need to represent a JSON value with a particular Rust type.
Why it matters: It explains why converting JSON into your own Rust types takes more than simply reading its keys and values. - Struct based JSON conversion: Describe how the section's example uses a struct with serde's Serialize and/or Deserialize attributes, and explain which direction each trait converts. State how the example reads a JSON request into NewUserRequest.
Why it matters: The two conversion directions tell you which serde capability a request-reading or response-writing type needs. - Format specific serde crates: Distinguish serde from the additional crate used for a particular data format, such as serde_json for JSON or serde_yaml for YAML.
Why it matters: It helps you identify the crate needed for the format your program actually reads or writes. - Serde serialization customization: Describe the section's example of changing how an enum is serialized by adding a serde rename_all attribute.
Why it matters: It shows that the output format can be adjusted with an attribute rather than changing the Rust enum itself.
Key terms: serde, JSON, Rust type, Serialize, Deserialize, NewUserRequest, serde_json, from_str(), serde_yaml, serde data model
17.2 Time in the standard library
The section explains how Rust’s standard library measures elapsed time, represents system time, and pauses threads, while showing its limits for working with dates.
- Instant and elapsed time: Explain how Instant and Duration work together to measure how long an operation takes, using either subtraction or .elapsed().
Why it matters: It helps you choose a way to time an operation without mistaking an Instant for today’s date. - System time and epoch: Distinguish SystemTime from Instant and describe what UNIX_EPOCH provides when expressing system time.
Why it matters: The distinction helps you use elapsed-time measurements and system-clock measurements for different purposes. - Standard library date limits: Explain why std::time alone does not cover the date-handling needs mentioned in this section.
Why it matters: It explains why the book turns to a crate after introducing the standard library’s time types. - Sleeping with a Duration: Describe how to pause a thread with std::thread::sleep() and what happens when there are no other threads.
Why it matters: It shows another practical use of Duration besides measuring elapsed time.
Key terms: Instant, Instant::now(), Duration, .elapsed(), SystemTime, UNIX_EPOCH, monotonic, std::time, chrono, human-readable date
17.3 chrono
The section explains why time-related types can be complex, then introduces chrono’s timezone-free Naive types and basic ways to create them.
- Sources of time complexity: Explain why working with time can be complex, using the section’s astronomical and historical examples.
Why it matters: It explains why a time library needs to represent more than a simple clock reading. - Timezone-free Naive types: Describe what Naive means in chrono and name the simple types the section introduces.
Why it matters: It lets you recognize what information a Naive date or time does—and does not—carry. - Creating Naive date-times: Describe the creation methods introduced for Naive values and how a NaiveDate becomes a NaiveDateTime.
Why it matters: It connects a date to a time of day using the chrono methods shown in the section.
Key terms: time zones, calendars, chrono, Naive, NaiveDate, NaiveDateTime, time zone, from_, _opt, Option<NaiveDate>
17.3.1 Checking the code inside external crates
This section uses chrono’s date methods to show why checking an external crate’s source can reveal possible panics.
- Chrono date method failure: Explain why chrono deprecated date methods such as from_ymd() and what the _opt versions indicate.
Why it matters: A convenient method name can hide a panic that its Option-returning counterpart lets you handle. - External method source checks: Justify briefly checking the source of methods in external crates for possible panics, while recognizing why an author might allow a panic.
Why it matters: Checking a method’s source helps you spot failure behavior before you rely on it.
Key terms: chrono, from_ymd(), from_ymd_opt(), _opt, Option, .expect(), source, external crates, panics
17.3.2 Back to chrono
This section illustrates how to convert between standard-library time values and chrono date-time types, then applies a UTC-to-local-time conversion to timestamped events.
- Chrono date-time conversion path: Trace the example’s conversion from seconds since the Unix epoch to a time-zone-free date and time, then to UTC and an offset-based time zone, and back to a time-zone-free value.
Why it matters: Chrono work often involves finding the right conversion between time representations. - UTC event timestamp conversion: Describe why the final example converts incoming event timestamps from UTC to the Korea/Japan time zone and what it does with the converted events.
Why it matters: It shows a practical reason to convert time zones: presenting received events in the time zone their users need.
Key terms: SystemTime, UNIX_EPOCH, NaiveDateTime, DateTime<Utc>, FixedOffset, DateTime<FixedOffset>, UTC timestamp, Korea/Japan time zone, KoreaJapanUserEvent
17.4 Rayon
Rayon makes familiar iterator work parallel, but its speed benefit depends on the machine, the workload, and compiler optimization.
- Parallel iterator methods: Explain how Rayon lets you use parallel versions of familiar iterator methods without spawning threads yourself.
Why it matters: This is the basic change that makes Rayon easy to try in existing iterator code. - Measured speedup and hardware: Describe how the section compares Rayon with ordinary iterator code and why the result can vary.
Why it matters: Measuring your own workload is more reliable than assuming that more threads mean less time. - Release mode speed reversal: Explain why the simple example can run much faster without Rayon in release mode.
Why it matters: This example shows why a parallel version can lose badly even when it uses more threads.
Key terms: Rayon, .par_iter(), parallel, thread::spawn(), available_parallelism(), Instant, .elapsed(), microseconds, Release, optimization
17.5 Anyhow and thiserror
The section explains why a Rust developer might move from temporary panics to Anyhow or thiserror for error handling.
- Anyhow and thiserror roles: Distinguish why a developer might use Anyhow for an easy-to-use single error type and later use thiserror to make defining their own error types less tedious.
Why it matters: The distinction helps you choose which crate fits the kind of error handling you want.
Key terms: Anyhow, thiserror, .unwrap(), .expect(), Box<dyn Error>
17.5.1 Anyhow
This section shows how Anyhow handles several possible error types in one return type, how to add context to an error, and when a separately defined error type is needed.
- One return type for errors: Explain why the example cannot use std::io::Error as its return type and how Anyhow provides a single error type for the different failures.
Why it matters: It explains why Anyhow is useful when a function can fail in several ways. - Creating and clarifying Anyhow errors: Describe what the anyhow! macro creates and how .with_context() improves an error message.
Why it matters: A single error type is more useful when its messages also make failures understandable. - Anyhow and defined error types: Distinguish Anyhow’s single error type in the example from defining an error type that implements std::error::Error.
Why it matters: It marks the distinction between using Anyhow to handle errors and defining your own error type.
Key terms: Anyhow, anyhow::Error, std::io::Error, question mark operator, anyhow!, .with_context(), Display, std::error::Error, derive(Error), thiserror
17.5.2 thiserror
This section shows how thiserror helps define a library error enum with displayed messages and conversions from other error types.
- Deriving a library error type: Explain how #[derive(Error)] turns an error enum into a type that implements std::error::Error, and how #[error] gives its variants messages shown by Display but not Debug.
Why it matters: It lets a library define its own proper error type without writing all of those implementations by hand. - Automatic conversions into error variants: Describe how #[from] can automatically implement From for specified other error types so they can be brought into a custom error enum.
Why it matters: It helps a library bring errors from other types into its own error enum.
Key terms: thiserror::Error, #[derive(Error)], #[error], std::error::Error, Display, Debug, #[from], From, error enum
17.6 Blanket trait implementations
The section explains how blanket trait implementations give methods or conversions to groups of types, then shows how to recognize and use examples in custom code and the standard library.
- Blanket implementations and bounds: Explain how implementing a trait for a generic type T can cover other people’s types, and how a trait bound limits which types receive the implementation.
Why it matters: This explains why a method can become available on many types without implementing the trait separately for each one. - External error conversion example: Describe how the section uses a blanket implementation for Error types to put errors from external crates into its own SystemError enum.
Why it matters: It shows a practical use for a bounded blanket implementation when combining external errors with your own error type. - Standard library trait relationships: Explain the standard-library blanket implementations connecting Display to ToString and From to Into and TryFrom.
Why it matters: These examples help explain why implementing one trait can make other trait behavior available without additional implementations. - BTreeMap array conversion bounds: Describe what the shown From implementation requires to construct a BTreeMap from an array of key-value tuples.
Why it matters: Reading the bounds and array signature lets you recognize when this conversion is available.
Key terms: blanket trait implementation, generic type T, trait bound, std::error::Error, SystemError, ExternalCrateError, .to_system_error()?, Display, ToString, .to_string()
17.7 lazy_static and once_cell
This section explains why lazy_static and once_cell remain useful for static variables that cannot be initialized with const functions.
- Run-time static initialization: Explain that lazy_static and once_cell allow static variables that cannot be initialized with a const fn to be initialized at run time instead of compile time. Mention that Rust 1.63 made const-fn initialization possible for some cases that previously needed one of these crates.
Why it matters: It explains when these crates are useful despite Rust’s support for const functions.
Key terms: lazy_static, once_cell, const fn, lazily initiated statics
17.7.1 Lazy static: Lazily evaluated statics
This section shows how lazy_static lets a program define a globally accessible static whose initialization requires runtime code.
- Runtime Initialization of Statics: Explain why the proposed global Logger cannot be initialized as an ordinary static and how lazy_static addresses that limitation.
Why it matters: It explains when an ordinary static is insufficient and why lazy_static is useful. - Lazy Static Declaration: Describe how the section declares a lazy static and what it says about the term static ref.
Why it matters: It gives the reader the concrete change needed to make the example’s global value available throughout the program.
Key terms: static, const, lazy_static, runtime, lazy_static!, static ref
17.7.2 OnceCell: A cell to only write to once
This section shows how OnceCell holds a value initialized once, including after the program has started, and identifies its thread-safe and non-thread-safe forms.
- OnceCell initialization: Describe how to create an empty OnceCell for a type and initialize its value with .set().
Why it matters: This is the basic pattern for using a OnceCell. - Delayed value flexibility: Explain why OnceCell is useful when a value becomes available later, and describe where it can be placed.
Why it matters: It lets you create a place for a value before the program has that value. - Thread safety variants: Distinguish the thread-safe and non-thread-safe choices described for OnceCell, including their standard-library names.
Why it matters: The choice depends on whether the value needs to be shared between threads.
Key terms: OnceCell, OnceCell::new(), .set(), Logger, lazy_static, sync, unsync, std::cell::OnceCell, std::sync::OnceLock
17.8 Summary
Where this is used later
- Developing the code (chapter 23) · The earlier introduction to serde and deserialization.
- Developing the code (chapter 23) · Prior use of Instant for measuring time
- Further development and cleanup (chapter 23) · Using anyhow
- cargo doc (chapter 18) · The earlier explanation of blanket trait implementations.
- thread_local! (chapter 21) · The earlier lazy_static! example provides the shared-global comparison.
Concepts explained in this chapter
- chronochrono is a Rust date-and-time library providing temporal types, arithmetic, formatting, and time-zone support.
- InstantInstant is a Rust standard-library type representing a monotonic point on a system's elapsed-time clock, typically measured from system boot.
- blanket implementationA blanket implementation is an implementation of a trait for every type satisfying specified bounds or implementing another trait.
- thiserrorthiserror is a Rust library crate that provides derive macros for defining custom error types with standard error-trait implementations.
- SystemTimeSystemTime is a Rust standard-library type representing measurements from the system clock for interaction with external entities such as files and processes.
- Unix epochThe Unix epoch is the reference instant at 00:00:00 UTC on January 1, 1970, from which Unix time is measured.
- durationA duration is an amount or span of time, expressed as an interval or in temporal units.
- std::timestd::time is the Rust standard-library module containing basic types and functions for measuring elapsed time and system-clock time.
- OnceCellOnceCell is a type that permits a value to be initialized once and subsequently accessed without repeated initialization.
- rayonRayon is a Rust library for data-parallel programming that automatically distributes iterator-based work across threads.
- lazy_staticlazy_static is a Rust crate that provides statics initialized on first runtime access when compile-time constant initialization is insufficient.
- once_cellonce_cell is a Rust crate that provides lazily initialized single-assignment and mutable cell abstractions for values that cannot be initialized as compile-time constants.
- anyhowanyhow is a Rust library for simplifying error handling by representing diverse failure causes through a single dynamically composed error type.
- From traitThe From trait defines conversion of one Rust type into another specified type.
- TryFromTryFrom is a Rust trait for fallible conversion between types that may fail for some input values.
- genericsA generic function is a Rust function parameterized by one or more type parameters and usable with multiple concrete types.
- reqwestreqwest is a Rust library for making HTTP requests, including through asynchronous clients.
- reqwest::ClientA reqwest Client is an HTTP client handle that executes requests and manages client-side request configuration and resources.
- crateAn external crate is a Rust compilation unit published outside the current project for other programs to depend on.
- static fieldA Rust program item representing a value stored at a fixed memory location for the entire program lifetime.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
