test
Install rustup
and cargo
by following this guide.
Install protoc
required by the prost
package by following this guide
Install these VSCode extensions:
Change these VSCode settings (see .vscode/settings.json
):
Setting | Reason |
---|---|
"rust-analyzer.checkOnSave.command": "clippy" |
Additional linting |
"rust-analyzer.cargo.unsetTest": ["core","tokio","tokio-macros"] |
Fix false-positive tokio error |
"protoc": { "options": ["--proto_path=${workspaceRoot}/infra/proto"] } |
Fix false-positive proto import errors |
Generate your launch.json
(see .vscode/launch.json
).
The LLDB debugger extension we installed will do this 'out of the box'.
Cargo is the official rust package manager (think npm, composer, etc), but it also helps with local development by making it easy to perform common operations in our rust workspace.
cargo build
Compile the packages in this workspace.cargo check
Analyze the packages in this workspace.cargo clippy
Doescargo check
with additional linting.cargo run
Run a binary or example.cargo test
Run the tests for packages in this workspace.
Term | Type | Definition | Notes | Min-Max | Contains |
---|---|---|---|---|---|
Workspace | Folder | Contains packages. | Defaults to the location of the top Cargo.toml file. |
1 | 1-n packages |
Package | Folder | Source from which crates are built. | Defaults to workspace and must have Cargo.toml and src/ in root. |
1-n | 1-n crates |
Crate | Folder | Umbrella term for compiled binary, libraries, exercises, integration tests, and the source that builds them. | Entry points default to src/main.rs , src/lib.rs , src/exercises/ , src/tests/ . |
1-n | 1-n modules |
Module | File / Folder | Compilation unit built from *.rs files (excluding src/main.rs and src/lib.rs ). |
mod.rs files required to define modules that are folders. |
1-n | 0-n items, 0-n submodules |
Submodule | See module | Any module located directly under the folder for the current module. | Only root module cannot be a submodule. | See module | |
Item | Code | Component of a crate, mostly definitions. | Many types see the docs. | 0-n | |
Library | File | Compiled code library. | Defaults to src/lib.rs . |
0-1 | |
Binary | File | Executable compiled from local source code and imported libraries. | Defaults to src/main.rs . |
0-n |
Note: I have intentionally excluded the supported module layout that replaces the mod.rs
file with a {module_name}.rs
file as a sibling to it's matching {module_name}/
folder.
. # Workspace root
├── Cargo.toml # Workspace config (defines sub-packages, etc)
├── Cargo.lock # Workspace lock (think shared package.lock)
├── my_lib/ # Package root
| ├── Cargo.toml # Package config (think package.json)
| ├── src/ # Crates source
| │ ├── lib.rs # Lib crate entry point
| │ └── my_module/ # Module
| │ ├── mod.rs # Module entry point (think index.ts)
| │ ├── my_module.rs # Module definitions (instead of in mod.rs)
| │ └── my_submodule.rs # Submodule (re-exported by mod.rs)
| └── tests/ # Integration tests
| └── my_test.rs # Test module
├── my_bin/ # Package root
| ├── Cargo.toml # Package config (has my_lib dependency)
| ├── src/ # Crates source
| │ ├── main.rs # Bin crate entry point
| | └── my_module.rs # Module
| └── tests/ # Integration tests
| └── my_test.rs # Test module
└── target/ # Shared compilation output
A module can take two basic forms:
{file}.rs
(Excludingmod.rs
,src/main.rs
,src/lib.rs
files)- File name determines module name.
- Expected to export some
pub
definitions. - Cannot contain submodules (obviously).
- Cannot create other modules.
- Avoid re-exporting definitions from other modules.
{dir}/mod.rs
- Directory name determines module name.
- Defined by
mod.rs
but treat as hierarchically positioned at{dir}
. - Avoid creating definitions in the
mod.rs
file. - Expected to create all submodules via
mod
. - Expected to contain a file with name matching
{dir}
to hold this module's definitions (re-exported inmod.rs
). - Optionally contains submodules.
- Optionally re-export definitions from submodules via
use
. - Avoid re-exporting definitions from descendent modules.
DocBlocks are denoted by ///
and use markdown formatting.
/// First line is short summary.
///
/// Next is detailed/verbose documentation.
///
/// Code blocks have implicit `fn main()` and `extern crate <cratename>`,
/// allowing them to run as unit tests or on demand by developers when
/// part of a lib crate.
///
/// The `no_run` flag will prevent code from running as a test, and you
/// can allow panics with `should_panic`.
///
/// ```
/// # // hidden lines start with `#` symbol, but they're still compiled!
/// let result = crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
For a more complete overview see the docs.
Unit tests are located alongside the code being tested, within the same module scope, generally in the same file. They can also be located in the DocBlock for a function.
Integration tests are located in the package/tests/
directory next to package/src/
. They import our compiled libraries and test the public-facing contents (often in combination).
The Rust compilation will perform some default linting (warnings about unused code, etc), but we use cargo clippy
for additional linting on file save. Global modifications to the linting rules are declared inside the crate entry-point file (e.g. lib.rs
).
Files should follow this basic layout:
// lint rules
#![allow(non_snake_case)]
// pub use/mod
pub mod my_public_module;
pub use std::fmt::Display;
// use/mod
mod my_private_module;
use serde_json::Value;
// types
type Foo = Vec<(Value, Value)>;
type Bar = Vec<(String, String)>;
// definitions
enum FooBar { Foo, Bar }
impl Display for FooBar { /*...*/ }
Use explicit return
statements where possible inside functions.
fn foo() -> i32 { return 42; } // <- good
fn foo() -> i32 { 42 } // <- bad
fn foo() {
let mut vec = vec![1, 2, 3, 4];
vec.retain(|&x| x % 2 == 0); // <- acceptable in closures
}
Implicit returns are fine when working with early-exit features like Result
.
fn foo<T>(s: &str) -> Result<T> {
let x = serde_json::from_str::<T>(s)?; // <- implicit return on Err()
return Ok(x); // <- explicit where possible
}
Avoid using .unwrap()
except when panicking is desirable. Instead make use of the anyhow
lib and ?
.
// don't do this
fn bad<T>(s: &str) -> Result<T> {
return Ok(serde_json::from_str::<T>(s).unwrap()); // unwrap() may panic
}
// do this
fn good<T>(s: &str) -> Result<T> {
return Ok(serde_json::from_str::<T>(s)?); // ? may return Err()
}
Enums should make use of the strum
lib, commonly AsRefStr
and EnumString
.
use strum_macros::{EnumString, AsRefStr, Display};
#[derive(EnumString, AsRefStr, Display)]
#[strum(ascii_case_insensitive)]
pub enum Side { Ask, Bid }
fn foo() {
// EnumString allows us to create an enum from a &str
let y: Side = Side::from_str("ask").unwrap();
// AsRefStr allows us to get an enum value as &str
let x: &str = Side::Ask.as_ref();
// Display allows us to get an enum value as String
let z: String = Side::Bid.to_string();
}