full-stack-skills/rust-skills

rust-workspace

Design Rust project topology — single-crate packages, project-sized multi-crate workspaces (small root-flat, hybrid/domain-grouped, contained crates/, nested, or root-package layouts), workspace-level configuration, dependency DAGs, and crate-boundary decis…

Ver código fuente
Documento original del Skill

Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.

Rust Workspace and Project Topology

Authority: Cargo Book — Workspaces, The Book ch7 and ch14-03, Rust Reference ch7, RFC 1525, matklad — Large Rust Workspaces.

This skill decides how many crates a project should have and how they relate. Its companion rust-module-layout decides what lives inside one crate's `src/`.

Capability Boundaries

✅ Strengths

  1. Single-crate vs multi-crate workspace decision (decision tree with five concrete triggers)
  2. Project-driven workspace layouts: small root-flat, hybrid/domain-grouped, contained crates/, nested, and root-package
  3. Virtual manifest vs root package — trade-offs and migration
  4. Workspace-level shared configuration: [workspace.package], [workspace.dependencies], [workspace.lints]
  5. Dependency direction DAGs — types → core → sdk → server → binary
  6. Diagnosing and refactoring the "mixed root package" anti-pattern
  7. Crate naming, publishing, and version coordination across members

⚠️ Prerequisites

  1. Rust ownership and basic module syntax — see rust-stable
  2. In-crate module layout (lib.rs facade, mod declarations, visibility) — see rust-module-layout

❌ Out of Scope

  1. Cargo.toml [dependencies] syntax and feature resolution → use rust-cargo-build
  2. In-crate src/ directory layout → use rust-module-layout
  3. Rust syntax fundamentals → use rust-stable
  4. Testing organization → use rust-testing

When to Use

  • "Should I split this into a workspace or keep it one crate?"
  • "How do I configure a Cargo workspace?"
  • "Refactor my rbatis-style mixed root package"
  • "Why does cargo only build one crate at the root?"
  • "How do I share dependencies across workspace members?"
  • "Is my dependency direction correct?"

Data Privacy

This skill does not collect, store, or transmit any user data.


Foundations — Packages, Crates, Modules

In-crate module layout depth (mod files, visibility, re-exports) is covered by the companion rust-module-layout skill. This section covers only the parts that affect project-level decisions.

Package vs crate vs workspace

TermMeaning
PackageOne Cargo.toml and the source it points at; what you publish to crates.io
CrateA compilation unit — either a library (src/lib.rs) or a binary (src/main.rs, src/bin/*.rs). A package can contain multiple crates (1 lib + N binaries).
WorkspaceA collection of packages sharing one Cargo.lock and target/.
ModuleA nameable scope inside a crate — declared with mod foo;, resolved to src/foo.rs or src/foo/mod.rs.

Single-package layouts

text
# Library only                      # Binary only                  # Library + binary
my-lib/                             my-app/                        my-crate/
├── Cargo.toml                      ├── Cargo.toml                 ├── Cargo.toml
└── src/                            └── src/                       └── src/
    └── lib.rs                          └── main.rs                 ├── lib.rs
                                                                      └── main.rs

# Multi-binary (one package, multiple binaries)
my-app/
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── main.rs                      # binary named after the package
    └── bin/
        ├── tool_a.rs                # binary `tool_a`
        └── tool_b.rs                # binary `tool_b`

Module declaration (the rule that surprises Java/Python devs)

rust
// src/lib.rs — declare modules explicitly; directories are NOT auto-discovered
pub mod front_of_house;      // loads src/front_of_house.rs OR src/front_of_house/mod.rs
mod back_of_house;           // private
pub(crate) mod utils;        // crate-visible

Everything is private by default; pub exposes. See rust-module-layout for the full visibility model, the parent-bound reachability rule, and the modern foo.rs + foo/ layout.

use paths (quick reference)

rust
use crate::front_of_house::hosting;       // absolute (current crate)
use std::collections::HashMap;            // absolute (external)
use self::back_of_house::Cook;            // relative (current module)
use super::parent_module::helper;         // relative (parent)
use std::{cmp::Ordering, io};             // nested
use std::fmt::Result as FmtResult;        // alias
pub use crate::front_of_house::hosting;   // re-export (facade pattern)

Conditional compilation

rust
#[cfg(target_os = "linux")]
fn only_linux() {}

#[cfg(feature = "serde")]
fn with_serde() {}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct Config;

if cfg!(target_os = "linux") { /* runtime check */ }

Project Scaffolding

bash
cargo new my-app              # Binary project
cargo new my-lib --lib        # Library project
cargo init                    # Initialize current directory

For multi-crate workspaces, create the root virtual manifest by hand (there is no cargo workspace new), choose the member topology from the decision model below, then run cargo new --lib <selected-member-path>. For a small root-flat workspace that may be cargo new --lib my-core; for a contained large workspace it may be cargo new --lib crates/my-core. For templates, use `cargo-generate`.


Workspace Patterns (single-crate vs multi-crate)

Authority: Cargo Book — Workspaces, The Book ch14-03, matklad — Large Rust Workspaces, RFC 1525.

A workspace is a collection of one or more packages that share a single Cargo.lock and target/ directory. Workspaces exist for four reasons: faster builds (shared dependency compilation), coordinated versions (one lockfile, one cargo publish flow), cleaner boundaries (each crate is an independent compilation unit with its own public API), and independent reuse (users can depend on one crate without pulling in the others).

Decision tree — how many crates?

Is this project one cohesive library or one binary?
├── Yes → Single-crate layout (Section 8.1)
└── No, it has multiple subdomains
    ├── Do the subdomains share types and call each other heavily?
    │   └── Single-crate with module directories (use rust-module-layout skill)
    └── At least one subdomain is independently useful / independently versioned / has different deps?
        └── Multi-crate workspace (Section 8.2)

Rule of thumb: split into crates only when at least one of these is true (from references/production-workspace-boundaries.md):

  1. The subdomain requires independent publishing or third-party reuse
  2. The subdomain needs distinct feature / target / no_std / WASM boundaries
  3. You need to prohibit a reverse dependency at compile time (e.g., core must not depend on tokio)
  4. The subdomain has an independently versioned public API
  5. Test/build lifecycles are significantly different

If none of these apply, prefer modules over crates. Modules are cheaper (no publish, no version coordination, no separate Cargo.toml). See the rust-module-layout skill for in-crate organization.

Single-crate layout

The default for small-to-medium projects. One Cargo.toml, one src/. Four variants: library only, binary only, library + binary (same package), or multi-binary (src/bin/<name>.rs). See examples/single-crate.md for all four skeletons.

When to graduate to a workspace: when a binary or library needs an independent dependency/feature/target boundary, release trajectory, or build/test lifecycle. A second binary alone can remain under src/bin/ in the same package.

Multi-crate workspace — two flavors of root

Cargo supports two kinds of root Cargo.toml:

FlavorWhat's in rootWhen to use
Virtual manifest[workspace] only — no `[package]`Default for new workspaces. Clean root, no top-level src/; members may be root-flat, hybrid, or contained.
Root package[workspace] + [package] + src/ at rootSmall (2-3 package) workspaces where one package is unambiguously primary; you accept the trade-offs below.

The Cargo team and community (notably matklad's Large Rust Workspaces) recommend virtual manifests for any non-trivial workspace:

  1. Root pollution — a [package] at the root forces src/, tests/, benches/ into the top level alongside every other crate's directory.
  2. Command ergonomics — with a root package, cargo build at the root builds only the root package; --workspace is needed for everything. Virtual manifests build all members by default.
  3. Publishing friction — root package + members leads to confusing cargo publish ordering.

Project-driven layout patterns

PatternLayoutWhenUsed by
A. Root-flat virtual<member>/ beside root Cargo.tomlDefault for small cohesive workspaces, usually 2-8 packages/membersTokio, Serde, Clap
B. Hybrid/domain-groupedcore members at root plus support/*, examples/*, or another real familyGrowing workspaces with stable families or noisy adapters/examplesframework and migration workspaces
C. Contained/groupedcrates/<member>/ or crates/<category>/*Large workspaces, multi-language repositories, or roots that need a Rust containerBevy-style large repositories
D. Nested sub-workspacesvendor/<sub>/ with own [workspace]Git submodule or vendored-workspace isolation (rare)vendored upstream workspaces
E. Root package[package] at root plus sibling/contained members2-3 packages with one genuinely primary cratelibrary plus companion CLI

Workspace member count is a signal, not a law. Decide package/crate boundaries first, then consider repository root noise, independent publishing, adapter/plugin families, targets, examples/tests, other languages, and established paths. As a review trigger: 2-8 cohesive packages normally stay root-flat; around 8-20 compare root-flat with a hybrid; 20+ commonly benefits from grouping or crates/. Never create one Cargo package per Maven/Gradle module without a Rust boundary reason.

Full skeletons and the decision matrix: references/workspace-layouts.md.

Pattern A skeleton — small workspace default

text
my-project/
├── Cargo.toml                      # virtual: [workspace]
├── my-core/                        # library
│   ├── Cargo.toml
│   └── src/lib.rs
├── my-net/                         # library
│   └── ...
└── my-cli/                         # binary
    └── src/main.rs
toml
# root Cargo.toml — virtual manifest
[workspace]
resolver = "3"
members = ["my-core", "my-net", "my-cli"]

[workspace.package]
edition = "2024"
version = "0.1.0"
license = "Apache-2.0"

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }   # pin once
my-core = { path = "my-core" }                      # internal deps here
toml
# my-net/Cargo.toml
[package]
name = "my-net"
version.workspace = true               # inherits from [workspace.package]
edition.workspace = true

[dependencies]
serde.workspace = true                 # inherits from [workspace.dependencies]
my-core.workspace = true               # internal workspace dep

The "mixed root package" anti-pattern

A common mistake from developers coming from Java/Maven or Python: putting a real [package] with substantial code at the workspace root while also having member crates.

toml
# Cargo.toml — DON'T (anonymized from a real public crate)
[workspace]
members = ["codegen", "macro-driver", "example"]

[package]                            # ❌ root package + workspace together
name = "my_framework"
version = "4.9.6"

[dependencies]
my-codegen = { path = "codegen" }
my-macro-driver = { path = "macro-driver" }
text
my-framework/
├── Cargo.toml                       # 163 lines mixing workspace + package config
├── src/                             # ❌ the root package's source pollutes the root
│   ├── lib.rs                       # 30 lines of `pub use *::*`
│   ├── crud.rs                      # 591 lines (monster file — see rust-module-layout)
│   ├── executor.rs                  # 642 lines
│   └── plugin/
├── codegen/                         # member crate
├── macro-driver/                    # member crate
└── tests/

Five problems: root pollution, asymmetric commands (cargo build builds only the root), coupled versioning, no clear facade (lib.rs becomes a glob re-export hub), and migration is a breaking change once published.

Refactor target — virtual manifest with all crates under crates/:

text
my-framework/
├── Cargo.toml                       # virtual manifest — workspace config only
└── crates/
    ├── my-framework/                # was: src/ at root
    │   ├── Cargo.toml
    │   └── src/
    ├── codegen/
    ├── macro-driver/
    └── example/

Migration is mechanical and preserves the published crate name — full six-step procedure in references/mixed-root-package-antipattern.md and a worked example in examples/migration-mixed-to-virtual.md.

Workspace-level shared configuration

Three workspace-level tables eliminate drift across members. See references/workspace-dependencies.md for the full syntax.

toml
# root Cargo.toml
[workspace.package]                  # shared package metadata
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"

[workspace.dependencies]             # shared external + internal deps
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
my-core = { path = "crates/core" }

[workspace.lints.rust]               # shared lint config
unsafe_code = "forbid"
missing_docs = "warn"

Members opt in with .workspace = true:

toml
# crates/net/Cargo.toml
[package]
name = "my-net"
version.workspace = true             # inherits 0.1.0
edition.workspace = true

[dependencies]
serde.workspace = true               # inherits pinned version + features
my-core.workspace = true

[lints]
workspace = true                     # inherits shared lints

Dependency direction (DAG)

Crate dependencies form a directed acyclic graph. Cycles are forbidden (cargo will error). Plan direction deliberately.

text
low-level types ──► domain logic ──► application ──► binary
   (my-types)         (my-core)         (my-server)     (my-cli)

Anti-pattern — leaky direction:

toml
# crates/core/Cargo.toml — ❌ core depending on CLI concerns
[dependencies]
clap = "4"                           # CLI lib has no business in core
my-cli = { path = "../cli" }         # core cannot depend on the binary

Verification:

bash
cargo tree                                    # visualize the dep graph
cargo tree --invert --package my-core         # what depends on my-core?

If my-types shows up as depending on anything non-std, the direction is wrong. Layering rules in references/dependency-direction.md.

Reference example — sa-token-rs (11-crate virtual workspace)

`sa-token-rs` is a real-world Rust port of the Java Sa-Token permission framework — an exemplar of every pattern this skill teaches. Virtual manifest, 11 crates, clean 5-layer DAG, crates/<category>/ grouping, full [workspace.*] inheritance.

Layout (contained + domain-grouped hybrid)

text
sa-token-rs/
├── Cargo.toml                          # virtual manifest
└── crates/
    ├── sa-token/                       # Layer 3: facade (public API)
    ├── sa-token-core/                  # Layer 1: core types & traits (no internal deps)
    ├── sa-token-derive/                # Layer 2: proc-macro (core only)
    ├── sa-token-context-mock/          # Layer 2: mock context (core only)
    ├── sa-token-dao-memory/            # Layer 2: memory DAO (core only)
    ├── sa-token-dao-redis/             # Layer 4: redis DAO (facade + core)
    ├── sa-token-axum/                  # Layer 4: axum adapter (facade + core)
    ├── sa-token-plugin/                # domain grouping inside container
    │   ├── sa-token-jwt/               # Layer 4: JWT plugin
    │   └── sa-token-sign/              # Layer 4: signature plugin
    ├── sa-token-demo/                  # domain grouping inside container
    │   └── sa-token-demo-axum/         # Layer 5: binary example
    └── sa-token-test/                  # Layer 5: integration tests

DAG — 5 layers, every arrow points up

text
L1  sa-token-core
      ▲
L2  sa-token-derive   sa-token-context-mock   sa-token-dao-memory
      ▲                      ▲                       ▲
L3  sa-token  (facade — aggregates L2 leaves into one public API)
      ▲
L4  sa-token-dao-redis   sa-token-axum   sa-token-jwt   sa-token-sign
      ▲
L5  sa-token-demo-axum   sa-token-test   (binaries + tests)

Why it's exemplary

  1. Virtual manifest — no root [package]; cargo build builds all 11 by default.
  2. Contained + domain-grouped hybridcrates/sa-token-* for leaves, with plugin and demo families grouped beneath crates/.
  3. Full `[workspace.] inheritance** — version, edition = "2024", rust-version = "1.85", license, repository, [workspace.lints] (unsafecode = "forbid"`, `missingdocs = "warn", clippy::pedantic = "warn"), and [workspace.dependencies]` for internal + external deps.
  4. Feature-gated optional depsa-token exposes redis = ["dep:sa-token-dao-redis"].
  5. Clean DAG — core has zero internal deps; facade aggregates; adapters/plugins/DAOs sit on top; demos/tests consume everything.

See references/sa-token-rs-case-study.md for the full member-by-member Cargo.toml breakdown, DAG verification commands, and how the layout supports independent publishing.

Workspace commands cheat sheet

bash
cargo build --workspace               # build every member (default in virtual manifest)
cargo build -p my-core                # build one member
cargo test --workspace                # test every member
cargo check --workspace               # fast type-check everything
cargo doc --workspace --no-deps       # docs for every member
cargo publish -p my-core              # publish one member
cargo run -p my-cli                   # run a specific binary

cargo workspaces version minor       # bump all members in lockstep (needs cargo-workspaces)
cargo workspaces publish              # publish all members in topological order

For batch version bumps and publishes across all members, install `cargo-workspaces`.


Workflow

  1. Decide project type and crate boundaries — start single-crate; split only for independent reuse/versioning, dependency or target isolation, or a distinct lifecycle. Otherwise use modules.
  2. Inventory topology signals — resulting package count, publish units, adapter/plugin families, examples/tests, repository root noise, languages, and compatibility-sensitive existing paths.
  3. Choose a layout — small cohesive workspace: root-flat; growing workspace with real families: hybrid/domain-grouped; large or multi-language workspace: contained/grouped. Treat counts as review triggers, not hard thresholds.
  4. Pin shared metadata and deps at workspace level[workspace.package], [workspace.dependencies], [workspace.lints]. Members opt in with .workspace = true.
  5. Verify dependency directioncargo tree --invert --package my-core must show only higher-level crates depending on lower-level ones. No reverse edges, no cycles.
  6. Select naming conventions — snake_case crate names, kebab-case in Cargo.toml name. Use full words; avoid 2-letter abbreviations (see rust-module-layout's naming reference).
  7. Validatecargo check --workspace, cargo tree, cargo doc --workspace --no-deps. Inspect the generated docs sidebar — it should reflect your intended public API surface.

Gotchas

  1. Distinguish between crate:: (references the current crate root) and ::other_crate_name:: (absolute path reference to an external crate).
  2. Restricted visibility such as pub(crate) is not an Edition boundary; verify the project's Rust version rather than assuming Edition 2015 forbids it.
  3. Both module.rs and module/mod.rs are supported; new code typically prefers the former, but do not misinterpret a deprecated warning as an error for using mod.rs.
  4. Workspace resolver settings apply globally; Edition 2021 defaults to resolver 2, while Edition 2024 defaults to resolver 3.
  5. The path argument in pub(in path) must point to ancestor modules of the current item and cannot be used to expose visibility across arbitrary sibling modules.
  6. Use of the #[path] attribute bypasses filesystem conventions — module paths no longer follow default file tree structures after application.
  7. A virtual manifest cannot contain `[dependencies]` or `[package]`. If you see failed to parse manifest at ... missing field package, you've mixed virtual and root-package syntax. Either remove [package] (virtual) or add it (root package) — don't half-do both.
  8. `cargo build` at a root-package workspace only builds the root. Use --workspace to build everything. With a virtual manifest, cargo build already builds all members — fewer surprises.
  9. Workspace globs match one level only. members = ["crates/*"] does not match crates/libs/core/; use crates/libs/* or explicit paths. Root-flat members can use explicit names or a carefully scoped glob, but never members = ["*"] because it captures non-package directories.
  10. Workspace-internal `path` deps still need versions for publish. my-core = { path = "../core", version = "0.1.0" } — without version, cargo publish rejects it. Use [workspace.dependencies] to keep the version in one place.
  11. Bumping a workspace-shared dep requires editing only the root `Cargo.toml`. Don't re-pin it in member crates — that defeats the purpose and creates drift.
  12. Renaming a published crate is a breaking change. Add a deprecated alias crate (pub use my_new_name::*;) under the old name for one release cycle before removing it.

On-Demand Resources

Official References

del mismo repositorio

Más Skills

Todos los Skills
full-stack-skills
Comunidad

rust-api-design

Design Rust library APIs that follow the Rust API Guidelines — naming (C-CASE, C-CONV, C-GETTER), interop traits (C-COMMON-TRAITS, C-CONVERT, C-ITER, C-SERDE), predictability (C-INTUITIVE, C-CONST), flexibility (C-GENERIC, C-NEWTYPE, C-EXT), type safety (C-BOOL, C-NONZERO, C-WRAPPER, C-STR), dependability (C-PANIC, C-UNWRAP), debuggability (C-DEBUG), and future-proofing (C-SEALED, C-STRUCT-FIELD, C-NON-EXHAUSTIVE). Use when users design a public crate API, choose between generics/concrete/newtype, decide trait bounds, hide implementation, avoid breakage, or ask "what is idiomatic Rust API design"; hand semver and publish workflow to rust-semver, lint config to rust-style-clippy, and in-crate layout to rust-module-layout.

instalaciones
1
GitHub Stars
5
Actualizado
19 sept
full-stack-skills
Comunidad

rust-by-example

Show Rust patterns through short compilable examples — type conversions (From/Into/TryFrom/as/Deref), flow control (if let/while let/match/loop), functions and closures (Fn/FnMut/FnOnce, captures), modules (mod/use/pub/super/self), generics and traits (bounds/associated types/trait objects), error handling (?/Result/thiserror/anyhow), attributes (derive/cfg/inline/allow), unsafe (raw pointers/unions/ABI), procedural macros (derive/attribute/function-like), and inline asm. Use when users ask "how do I write X in Rust", need a concrete pattern with copy-pasteable code, or are migrating from Java/Python/Go/C++ and want the Rust equivalent; hand architecture decisions to rust-api-design/rust-workspace, std API selection to rust-stdlib, and async runtime to rust-concurrency.

instalaciones
1
GitHub Stars
5
Actualizado
19 sept
full-stack-skills
Comunidad

rust-cargo-build

Configure, operate, diagnose, and automate Cargo for Rust packages and workspaces. Cover manifests and targets, commands, dependency resolution and features, profiles, build scripts, configuration and environment variables, caches and build diagnostics, cross-compilation, registries, packaging, publishing, metadata, CI reproducibility, and stable-versus-nightly feature gates. Use for Cargo.toml, Cargo.lock, .cargo/config.toml, cargo build/check/run/tree/metadata/package/publish, resolver or feature problems, build output and performance, private registries, and beginner Cargo workflows. Route crate selection and supply-chain audits to rust-dependencies, workspace topology to rust-workspace, test design to rust-testing, documentation design to rust-documentation, lint policy to rust-style-clippy, and API compatibility decisions to rust-semver.

instalaciones
1
GitHub Stars
5
Actualizado
19 sept
full-stack-skills
Comunidad

rust-cli

Design, implement, test, and release production Rust command-line applications, including command contracts, subcommands, configuration precedence, stdin/stdout/stderr, exit codes, file safety, daemon IPC, terminal handling, packaging, and process-level tests. Use when users ask for a Rust CLI, command parser, clap integration, Unix-style pipelines, daemon clients, PTY/TUI behavior, shell completion, or CLI release engineering.

instalaciones
1
GitHub Stars
5
Actualizado
19 sept