Guide contents

Lesson 47 of 94

Crates, Packages & Workspaces

The previous lesson organized code inside one crate. This one zooms out to the build: how Cargo turns a directory into crates, how crates depend on each other, and how larger projects are split into workspaces.

Concept Rust (Cargo)
manifest file Cargo.toml
compiled artifact crate (a library .rlib or an executable)
unit of distribution package (one Cargo.toml)
multi-package project workspace
registry crates.io
dependency declaration name = "version" (names are globally unique, no group)
dependency kinds [dev-dependencies], [build-dependencies]
optional dependencies and conditional code features
documentation rustdoc (/// comments), with runnable doc tests
build, test, lint cargo build, cargo test, cargo clippy

Packages and crates

Two words that are easy to mix up:

  • A crate is one compilation unit: the compiler builds one crate at a time. A crate is either a library (other crates can depend on it) or a binary (it has main and produces an executable). The crate root is its top file: src/lib.rs or src/main.rs.
  • A package is a directory with a Cargo.toml. It holds at most one library crate and any number of binary crates.

1. The anatomy of Cargo.toml

TOMLCargo.toml
[package]
name = "invoicer"          # crate name; `-` becomes `_` in code (`invoicer::...`)
version = "0.3.1"          # semver: MAJOR.MINOR.PATCH
edition = "2024"           # language edition
description = "Generate and send invoices"
license = "MIT OR Apache-2.0"
rust-version = "1.85"      # minimum supported Rust version

[dependencies]
regex = "1"
serde = { version = "1", features = ["derive"] }

[dev-dependencies]         # only for tests, examples and benchmarks
pretty_assertions = "1"

The edition (2015, 2018, 2021, 2024) selects a set of language rules. Crates of different editions link together freely, so upgrading your edition never forces your dependencies to upgrade.

2. The standard layout

Cargo finds targets by convention, so a small Cargo.toml is enough:

Text
invoicer/
├── Cargo.toml
├── Cargo.lock            exact versions used (commit it for applications)
├── src/
│   ├── lib.rs            library crate `invoicer`
│   ├── main.rs           binary crate `invoicer` (can `use invoicer::...`)
│   └── bin/
│       └── import.rs     extra binary: `cargo run --bin import`
├── tests/                integration tests (each file is its own crate)
├── examples/             `cargo run --example demo`
├── benches/              benchmarks
└── target/               build output

A package with both lib.rs and main.rs is the usual shape for an application: the logic lives in the library, where it can be tested and reused, and main.rs is a thin shell that parses arguments and calls it.

Rust
// src/main.rs: the binary depends on the library of the same package
use invoicer::{Invoice, render};

fn main() {
    let inv = Invoice::sample();
    println!("{}", render(&inv));
}

3. The everyday Cargo commands

Terminal
cargo new invoicer            # new binary package (--lib for a library)
cargo add regex               # add a dependency to Cargo.toml
cargo add serde --features derive
cargo add --dev pretty_assertions
cargo build                   # debug build into target/debug
cargo build --release         # optimized build into target/release
cargo run -- --input a.csv    # build and run; args after -- go to your program
cargo test                    # unit, integration and doc tests
cargo check                   # type-check only, much faster than build
cargo clippy                  # lints
cargo fmt                     # format
cargo doc --open              # build HTML docs for you and all dependencies
cargo tree                    # dependency tree
cargo update                  # move Cargo.lock to the newest compatible versions

4. Using a dependency: regex

After cargo add regex, the crate is available by name. Nothing needs to be declared in code: dependencies are in scope as regex::..., and use shortens paths as usual.

Rust
use regex::Regex;

fn main() {
    let re = Regex::new(r"(?P<user>[\w.]+)@(?P<domain>[\w.]+\.\w+)").unwrap();
    let text = "Contact ana@example.com or support@shop.io for help.";
    for caps in re.captures_iter(text) {
        println!("user={} domain={}", &caps["user"], &caps["domain"]);
    }
}
Output
user=ana domain=example.com
user=support domain=shop.io

5. serde and serde_json: dependency features

serde provides the Serialize/Deserialize traits; its derive feature adds the #[derive(...)] macros. Features let a crate ship optional code that you only compile when you ask for it (serde = { version = "1", features = ["derive"] }).

Rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Invoice {
    id: u32,
    customer: String,
    lines: Vec<(String, u32)>,
}

fn main() {
    let inv = Invoice { id: 7, customer: String::from("ACME"), lines: vec![(String::from("widget"), 1250)] };
    let json = serde_json::to_string(&inv).unwrap();
    println!("{json}");

    let back: Invoice = serde_json::from_str(r#"{"id":8,"customer":"Globex","lines":[]}"#).unwrap();
    println!("{back:?}");
}
Output
{"id":7,"customer":"ACME","lines":[["widget",1250]]}
Invoice { id: 8, customer: "Globex", lines: [] }

serde generates the conversion code at compile time, so there's no reflection, and a type that can't be serialized is a compile error rather than a runtime exception.

6. anyhow in a binary

Error-handling crates are typical dependencies of applications. Here anyhow wraps any error with context. The Error Handling section, coming up next, covers it in depth in thiserror & anyhow; for now it's enough to know that anyhow::Result<T> accepts any error type and .with_context(...) adds a message to it.

Rustexits with an error
use anyhow::{Context, Result};

fn parse_port(raw: &str) -> Result<u16> {
    raw.trim().parse::<u16>().with_context(|| format!("invalid port {raw:?}"))
}

fn main() -> Result<()> {
    println!("port {}", parse_port("8080")?);
    println!("port {}", parse_port("80800")?);
    Ok(())
}
Output
port 8080
Error: invalid port "80800"

Caused by:
    number too large to fit in target type

7. rayon: parallelism from a crate

Some capabilities, like parallel iterators, come from crates in Rust. rayon adds par_iter(), which splits the work over a thread pool and keeps results in order.

Rust
use rayon::prelude::*;

fn is_prime(n: u64) -> bool {
    n >= 2 && (2..).take_while(|d| d * d <= n).all(|d| n % d != 0)
}

fn main() {
    let count = (1..200_000u64).into_par_iter().filter(|&n| is_prime(n)).count();
    let squares: Vec<u64> = vec![1, 2, 3, 4].par_iter().map(|x| x * x).collect();
    println!("{count} primes below 200000");
    println!("{squares:?}");
}
Output
17984 primes below 200000
[1, 4, 9, 16]

Note the use rayon::prelude::*;: par_iter is a trait method, and as the previous lesson showed, trait methods only work when the trait is in scope. Crate preludes exist for exactly this.

8. Semver and version requirements

A version requirement in Cargo.toml is a range. The default ("1.4") is a caret requirement: any version that semver says is compatible.

TOMLCargo.toml
[dependencies]
regex = "1.10"            # same as ^1.10: >=1.10.0, <2.0.0
time = "0.3.30"           # below 1.0 the minor is the breaking part: >=0.3.30, <0.4.0
log = "~0.4.20"           # tilde: >=0.4.20, <0.5.0 (patch updates only for 1.x+)
uuid = "=1.8.0"           # exactly this version (rarely a good idea)
rand = ">=0.8, <0.10"     # explicit range

Cargo picks the newest version that satisfies every requirement and records it in Cargo.lock. cargo build then uses the locked versions until you run cargo update. Cargo can also include two major versions of the same crate side by side when two dependencies need incompatible ones.

9. Other kinds of dependencies

TOMLCargo.toml
[dependencies]
# From crates.io (the default)
serde = "1"
# A local crate in the same project
billing-core = { path = "../billing-core" }
# Straight from git (pin a tag or rev for reproducible builds)
fancy-log = { git = "https://github.com/example/fancy-log", tag = "v0.2.0" }
# Renamed in code: `use json::...` instead of `use serde_json::...`
json = { package = "serde_json", version = "1" }
# Optional: only compiled when the "cli" feature is on (see below)
clap = { version = "4", optional = true }

[dev-dependencies]
proptest = "1"

[build-dependencies]      # used by build.rs only
cc = "1"

[target.'cfg(windows)'.dependencies]   # platform-specific
winapi = "0.3"

10. Conditional compilation with #[cfg]

#[cfg(...)] removes an item from compilation unless the condition holds. It's resolved at compile time, like a preprocessor, but on whole items rather than text. cfg!(...) is the expression form and evaluates to a bool.

Rust
#[cfg(target_os = "linux")]
fn config_dir() -> &'static str {
    "~/.config/myapp"
}

#[cfg(target_os = "windows")]
fn config_dir() -> &'static str {
    r"%APPDATA%\myapp"
}

#[cfg(not(any(target_os = "linux", target_os = "windows")))]
fn config_dir() -> &'static str {
    "~/Library/Application Support/myapp"
}

fn main() {
    println!("config dir: {}", config_dir());
    println!("64-bit: {}", cfg!(target_pointer_width = "64"));
    println!("unix family: {}", cfg!(unix));
}
Output
config dir: ~/.config/myapp
64-bit: true
unix family: true

The code for other platforms isn't even compiled into your binary.

11. Debug vs release: debug_assertions

cargo build compiles with debug_assertions on; cargo build --release turns it off. debug_assert! and integer overflow checks follow it, and you can test it yourself.

Rust
fn average(values: &[f64]) -> f64 {
    debug_assert!(!values.is_empty(), "average of empty slice");
    values.iter().sum::<f64>() / values.len() as f64
}

fn main() {
    if cfg!(debug_assertions) {
        println!("debug build: extra checks enabled");
    } else {
        println!("release build");
    }
    println!("{}", average(&[2.0, 4.0, 9.0]));
}
Output
debug build: extra checks enabled
5

12. Defining features

Features are named flags declared in Cargo.toml. They can enable optional dependencies and other features, and code checks them with #[cfg(feature = "...")].

TOMLCargo.toml
[features]
default = ["json"]            # enabled unless the user opts out
json = ["dep:serde_json"]     # enabling "json" pulls in the optional dependency
cli = ["dep:clap"]
full = ["json", "cli"]        # a feature can enable other features

[dependencies]
serde_json = { version = "1", optional = true }
clap = { version = "4", optional = true }
Terminal
cargo build                               # default features: json
cargo build --no-default-features         # nothing optional
cargo build --features cli                # json (default) + cli
cargo build --all-features

For users of your crate, features are chosen in their Cargo.toml: invoicer = { version = "0.3", default-features = false, features = ["cli"] }.

13. Feature-gated code

This example is compiled without any features, so the #[cfg(feature = "json")] version of export is removed and the fallback is used. With --features json, the other function would be compiled instead.

Rust
#[derive(Debug)]
struct Report {
    title: String,
    total: u32,
}

#[cfg(feature = "json")]
fn export(r: &Report) -> String {
    format!("{{\"title\":\"{}\",\"total\":{}}}", r.title, r.total)
}

#[cfg(not(feature = "json"))]
fn export(r: &Report) -> String {
    format!("{}: {}", r.title, r.total)
}

fn main() {
    let r = Report { title: String::from("Q3 sales"), total: 1234 };
    println!("{}", export(&r));
    println!("json enabled: {}", cfg!(feature = "json"));
}
Output
Q3 sales: 1234
json enabled: false

Features are meant to be additive: enabling a feature should only add things. Cargo unifies features across the dependency graph, so if two crates depend on invoicer with different features, it's built once with both sets enabled.

14. cfg_attr: attributes that depend on a condition

#[cfg_attr(condition, attribute)] applies an attribute only when the condition holds. A common use: derive extra traits only in tests or only when a feature is on.

Rust
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))] // PartialEq only when compiling tests
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p.clone());
}
Output
Point { x: 1, y: 2 }

15. Doc comments

/// documents the item that follows; //! documents the enclosing item (usually the crate or module, at the top of the file). Both are Markdown. cargo doc turns them into HTML, and docs.rs publishes them for every crate on crates.io.

Rust
//! Text utilities for building URL slugs.

/// Converts a title into a URL slug.
///
/// Letters and digits are kept (lowercased); runs of anything else become one `-`.
pub fn slugify(title: &str) -> String {
    let mut slug = String::new();
    for c in title.chars() {
        if c.is_alphanumeric() {
            slug.extend(c.to_lowercase());
        } else if !slug.is_empty() && !slug.ends_with('-') {
            slug.push('-');
        }
    }
    slug.trim_end_matches('-').to_string()
}

fn main() {
    println!("{}", slugify("Hello, World! Rust 2024"));
    println!("{}", slugify("  --Already-a-slug--  "));
}
Output
hello-world-rust-2024
already-a-slug

The rustdoc convention is prose plus a few standard headings: # Examples, # Errors, # Panics, and # Safety.

16. Doc tests: examples that are run

Code blocks inside doc comments are compiled and run by cargo test. Examples in your documentation can't silently go stale. A doc test is compiled as a separate crate that depends on yours, so it uses your crate by name (slugkit here).

Rustcargo test (doc tests)
/// Converts a title into a URL slug.
///
/// # Examples
///
/// ```
/// use slugkit::slugify;
///
/// assert_eq!(slugify("Hello, World!"), "hello-world");
/// assert_eq!(slugify("Rust 2024 edition"), "rust-2024-edition");
/// ```
pub fn slugify(title: &str) -> String {
    let mut slug = String::new();
    for c in title.chars() {
        if c.is_alphanumeric() {
            slug.extend(c.to_lowercase());
        } else if !slug.is_empty() && !slug.ends_with('-') {
            slug.push('-');
        }
    }
    slug.trim_end_matches('-').to_string()
}

/// Truncates a slug to at most `max` characters without ending on `-`.
///
/// ```
/// assert_eq!(slugkit::truncate("hello-big-world", 9), "hello-big");
/// assert_eq!(slugkit::truncate("hello-big-world", 6), "hello");
/// ```
pub fn truncate(slug: &str, max: usize) -> &str {
    let end = slug.char_indices().nth(max).map_or(slug.len(), |(i, _)| i);
    slug[..end].trim_end_matches('-')
}
Output
running 2 tests
test src/lib.rs - slugify (line 5) ... ok
test src/lib.rs - truncate (line 25) ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

17. Doc test options: ?, should_panic, no_run, hidden lines

A doc example can return Result to use ? (end it with Ok::<(), E>(())), be marked should_panic, no_run (compile only), or ignore. Lines starting with # are compiled but hidden from the rendered docs, which keeps setup code out of the reader's way.

Rustcargo test (doc tests)
/// Parses `key=value` lines.
///
/// # Errors
///
/// Returns an error naming the first line that has no `=`.
///
/// ```
/// # fn main() -> Result<(), String> {
/// let pairs = config::parse("host=localhost\nport=8080")?;
/// assert_eq!(pairs[1], ("port".to_string(), "8080".to_string()));
/// # Ok(())
/// # }
/// ```
///
/// # Panics
///
/// Panics if the input is larger than 1 MiB.
///
/// ```should_panic
/// let huge = "a=b\n".repeat(300_000);
/// config::parse(&huge).unwrap();
/// ```
pub fn parse(text: &str) -> Result<Vec<(String, String)>, String> {
    assert!(text.len() <= 1 << 20, "config too large");
    text.lines()
        .filter(|l| !l.trim().is_empty())
        .map(|l| match l.split_once('=') {
            Some((k, v)) => Ok((k.trim().to_string(), v.trim().to_string())),
            None => Err(format!("missing '=' in line {l:?}")),
        })
        .collect()
}

/// Loads a config file from disk.
///
/// ```no_run
/// let pairs = config::load("/etc/myapp.conf").unwrap();
/// println!("{} settings", pairs.len());
/// ```
pub fn load(path: &str) -> Result<Vec<(String, String)>, String> {
    let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
    parse(&text)
}
Output
running 3 tests
test src/lib.rs - load (line 36) - compile ... ok
test src/lib.rs - parse (line 19) - should panic ... ok
test src/lib.rs - parse (line 7) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The no_run example is compiled (so it can't rot) but not executed, which suits code that touches files or the network.

18. #[deprecated]

Marking an item deprecated makes every use of it a compiler warning. Projects that deny warnings in CI turn it into an error, which this example simulates with #![deny(deprecated)].

Rust✗ does not compile
#![deny(deprecated)]

#[deprecated(since = "0.4.0", note = "use `total_cents` to avoid float rounding")]
pub fn total(prices: &[f64]) -> f64 {
    prices.iter().sum()
}

pub fn total_cents(prices: &[u64]) -> u64 {
    prices.iter().sum()
}

fn main() {
    println!("{}", total(&[0.1, 0.2]));
}
Output
error: use of deprecated function `total`: use `total_cents` to avoid float rounding
  --> src/main.rs:13:20
   |
13 |     println!("{}", total(&[0.1, 0.2]));
   |                    ^^^^^
   |
note: the lint level is defined here
  --> src/main.rs:1:9
   |
 1 | #![deny(deprecated)]
   |         ^^^^^^^^^^

19. #[must_use]

#[must_use] warns when a caller ignores a return value. Result is #[must_use] already, which is why an ignored Result gives a warning. Put it on functions whose result is the whole point, like a builder method or a pure function.

Rust✗ does not compile
#![deny(unused_must_use)]

#[must_use = "this returns a new string; the original is unchanged"]
fn with_suffix(name: &str, suffix: &str) -> String {
    format!("{name}{suffix}")
}

fn main() {
    let name = String::from("report");
    with_suffix(&name, ".pdf"); // result dropped: probably a bug
    println!("{name}");
}
Output
error: unused return value of `with_suffix` that must be used
  --> src/main.rs:10:5
   |
10 |     with_suffix(&name, ".pdf"); // result dropped: probably a bug
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this returns a new string; the original is unchanged
note: the lint level is defined here
  --> src/main.rs:1:9
   |
 1 | #![deny(unused_must_use)]
   |         ^^^^^^^^^^^^^^^
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = with_suffix(&name, ".pdf"); // result dropped: probably a bug
   |     +++++++

20. #[non_exhaustive]: leaving room to grow

A library that might add enum variants or struct fields later can mark them #[non_exhaustive]. Other crates must then include a _ arm when matching, and can't build the struct with a literal, so adding a variant or field isn't a breaking change. Inside the defining crate nothing changes, which is why this is shown as a two-crate snippet.

Rust
// In the library crate `payments`:
#[non_exhaustive]
#[derive(Debug)]
pub enum PaymentError {
    Declined,
    Expired,
}

// In an application that depends on `payments`:
fn explain(e: &payments::PaymentError) -> &'static str {
    match e {
        payments::PaymentError::Declined => "card declined",
        payments::PaymentError::Expired => "card expired",
        _ => "payment failed", // required: future versions may add variants
    }
}

21. Build profiles

Profiles configure how the compiler optimizes. dev is used by cargo build/cargo run, release by --release. You can tune both and define your own.

TOMLCargo.toml
[profile.dev]
opt-level = 1              # a bit of optimization so debug builds aren't painfully slow

[profile.dev.package."*"]
opt-level = 3              # fully optimize dependencies even in dev builds

[profile.release]
lto = "thin"               # link-time optimization across crates
codegen-units = 1          # slower build, faster binary
panic = "abort"            # smaller binary; no unwinding
strip = true               # strip symbols

[profile.profiling]        # custom: cargo build --profile profiling
inherits = "release"
debug = true

A Rust binary's speed depends on these build settings, and a debug build can be 10–50× slower than release. Always benchmark with --release.

22. Build scripts: build.rs

A build.rs file at the package root is compiled and run before your crate. It can generate code, compile C libraries, or set configuration.

Rust
// build.rs
use std::process::Command;

fn main() {
    // Embed the git commit in the binary.
    let out = Command::new("git").args(["rev-parse", "--short", "HEAD"]).output().unwrap();
    let hash = String::from_utf8(out.stdout).unwrap();
    println!("cargo::rustc-env=GIT_HASH={}", hash.trim());
    println!("cargo::rerun-if-changed=.git/HEAD");
}
Rust
// src/main.rs
fn main() {
    // env! reads a compile-time environment variable; Cargo sets CARGO_PKG_* for every crate.
    println!("{} {} ({})", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("GIT_HASH"));
}

23. Workspaces: a multi-crate project

A workspace groups several packages that are developed together. They share one Cargo.lock and one target/ directory, and commands run from the root apply to all members.

Text
shop/
├── Cargo.toml            workspace root (no [package] needed)
├── Cargo.lock            one lock file for everything
├── crates/
│   ├── shop-core/        library: domain types and rules
│   ├── shop-db/          library: persistence, depends on shop-core
│   └── shop-api/         binary: HTTP server, depends on both
└── target/               shared build output
TOMLCargo.toml (workspace root)
[workspace]
resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "0.5.0"
edition = "2024"
license = "MIT"

[workspace.dependencies]   # versions chosen once
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
shop-core = { path = "crates/shop-core" }
shop-db = { path = "crates/shop-db" }

24. A workspace member

Each member has its own Cargo.toml that inherits shared settings with .workspace = true, so versions are declared in one place.

TOMLcrates/shop-api/Cargo.toml
[package]
name = "shop-api"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
shop-core.workspace = true
shop-db.workspace = true
tokio.workspace = true
serde = { workspace = true, features = ["rc"] }   # add features on top
Terminal
cargo build                    # build every member
cargo test -p shop-core        # test one package (-p = --package)
cargo run -p shop-api          # run a binary member
cargo build --workspace --release

25. Why split into crates?

Modules are enough to organize code. Split into separate crates when you want:

  • Faster builds. The crate is the unit of compilation and of parallelism. Changing shop-api doesn't recompile shop-core.
  • Enforced boundaries. shop-core can't accidentally depend on shop-db if it's not in its Cargo.toml, and pub(crate) items stay private across crates.
  • Different dependencies. The CLI can depend on clap without the server pulling it in.
  • Reuse and publishing. A library crate can be published on its own.

A common starting point is a single package with lib.rs + main.rs, moving to a workspace when build times or team boundaries call for it.

26. Publishing to crates.io

Terminal
cargo login                   # once, with an API token from crates.io
cargo package --list          # see which files will be uploaded
cargo publish --dry-run       # build the package as crates.io would
cargo publish                 # upload; versions are immutable once published
cargo yank --version 0.3.1    # stop new projects from picking a broken version

After publishing, docs.rs builds and hosts the documentation automatically. Crate names are first-come, first-served with no groupId, which is why names like serde_json and tokio-postgres carry a prefix.

27. Realistic: a log analyzer using three crates

A small tool that parses access-log lines with regex, aggregates them, and prints a JSON summary with serde_json. In a real project it would be a binary crate with its parsing in lib.rs.

Rust
use regex::Regex;
use serde::Serialize;
use std::collections::BTreeMap;

#[derive(Serialize, Default)]
struct Summary {
    requests: usize,
    errors: usize,
    bytes: u64,
    by_status: BTreeMap<u16, usize>,
    slowest: Option<String>,
}

fn analyze(log: &str) -> Summary {
    let line_re = Regex::new(r#"^"(?P<method>\w+) (?P<path>\S+)" (?P<status>\d{3}) (?P<bytes>\d+) (?P<ms>\d+)ms$"#).unwrap();
    let mut s = Summary::default();
    let mut slowest_ms = 0;
    for line in log.lines() {
        let Some(c) = line_re.captures(line.trim()) else { continue };
        let status: u16 = c["status"].parse().unwrap();
        let ms: u32 = c["ms"].parse().unwrap();
        s.requests += 1;
        s.bytes += c["bytes"].parse::<u64>().unwrap();
        *s.by_status.entry(status).or_default() += 1;
        if status >= 500 {
            s.errors += 1;
        }
        if ms > slowest_ms {
            slowest_ms = ms;
            s.slowest = Some(format!("{} {} ({ms}ms)", &c["method"], &c["path"]));
        }
    }
    s
}

fn main() {
    let log = r#"
        "GET /" 200 512 12ms
        "GET /products" 200 20480 87ms
        not a log line
        "POST /cart" 500 128 340ms
        "GET /products/7" 404 64 9ms
        "GET /" 200 512 11ms
    "#;
    let summary = analyze(log);
    println!("{}", serde_json::to_string_pretty(&summary).unwrap());
}
Output
{
  "requests": 5,
  "errors": 1,
  "bytes": 21696,
  "by_status": {
    "200": 3,
    "404": 1,
    "500": 1
  },
  "slowest": "POST /cart (340ms)"
}

Summary

  • A package is a directory with Cargo.toml; it contains up to one library crate and any number of binary crates. The crate is the unit of compilation.
  • Dependencies are declared with semver ranges; Cargo.lock pins exact versions. Cargo can use multiple major versions of a crate at once.
  • Features turn optional code and dependencies on at compile time; #[cfg(...)], cfg! and #[cfg_attr] select code for features, platforms, tests and debug builds.
  • /// and //! doc comments become HTML docs, and their code blocks run as doc tests.
  • #[deprecated], #[must_use] and #[non_exhaustive] help you evolve a public API.
  • Profiles control optimization; always measure performance with --release.
  • Workspaces group related packages with a shared lock file, build directory and dependency versions.

Next: Testing, where cargo test gets the full treatment: unit tests, integration tests, test organization, fixtures, and property-based testing.