Best Practices
Here's a bunch of best practices and design patterns I've discovered in my time writing Rust code.
A lot of these points come from this awesome article, so I'd recommend checking that out too.
Quick Links
For those too lazy to scroll:
Format Your Code
It's always nice to have your code formatted correctly and when everyone follows a common coding standard, it's even nicer when your computer can enforce all this for you. You should probably use rustfmt for this...
$ cargo install rustfmt
By default, this also installs a fmt
sub-command for use with cargo
, so now
you can reformat your entire library with a simple cargo fmt
call.
If you're using the Atom
editor and have rustfmt
installed then it can run
rustfmt
every time you hit save using atom-beautify.
$ apm install atom-beautify
To configure rustfmt
, just drop a file called rustfmt.toml
(or
.rustfmt.toml
if you want) with a bunch of configuration options. You can get
the list of valid configuration options from rustfmt itself.
rustfmt --config-help
This is what my usual .rustfmt.toml
looks like:
$ cat .rustfmt.toml
format_strings = false
reorder_imports = true
take_source_hints = false
report_todo = "Always"
report_fixme = "Always"
Document All the Things!
It's always nice when you can flick through a crate's documentation and tell straight away how to use it and what everything does. Therefore as a bare minimum you should make sure that all exported functions, structs, and enums have at least a line or two describing what they do.
To enforce this, you can add a lint to your root lib.rs
to prevent any changes
which add undocumented public interfaces.
#![deny(missing_docs)]
Next up, for any major function or struct, make sure you add a couple of examples in the doc comment. These are also compiled and run when you run your test suite (you do have tests, don't you?) so it's easy to keep them up to date.
/// (summary line here)
///
/// some more info about the function...
///
/// # Examples
///
/// ```
/// let src = "(print 1 2)";
/// let ast = parse(src).unwrap();
/// ```
Add Even More Lints
You can get the compiler to do even more checking for you by simply adding a couple extra lints to your crate.
#![deny(missing_copy_implementations,
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
unused_extern_crates,
unused_results,
variant_size_differences)]
Continuous Integration
If you use GitHub for storing your code then adding CI is a fairly trivial thing
to do. Here's a detailed example .travis.yml
which will automatically test
your library on all three channels of the rust compiler, then generate and
upload your crate's documentation to GitHub Pages.
$ cat .travis.yml
sudo: false
language: rust
rust:
- nightly
- beta
- stable
matrix:
allow_failures:
- rust: nightly
before_script:
- |
pip install 'travis-cargo<0.2' --user &&
export PATH=$HOME/.local/bin:$PATH
script:
- |
travis-cargo build &&
travis-cargo test &&
travis-cargo bench &&
travis-cargo --only stable doc
after_success:
- travis-cargo --only stable doc-upload
env:
global:
- TRAVIS_CARGO_NIGHTLY_FEATURE=dev
- secure: # encrypted stuff
To enable the automatic documentation uploads (i.e. pushing rustdoc's output to
the project's gh-pages
branch) supported by travis-cargo
, you need to add an
environment variable called GH_TOKEN
that contains an access token for your
GitHub account (encrypted and with limited rights). You can create one
here.
To encrypt the token, you can use Travis' CLI tool (installed with
gem install travis
) by running this in your project's root directory
(replace "1234" with your token):
$ travis encrypt "GH_TOKEN=1234" --add env.global
When everything is set up correctly, you should be able to view your project's
documentation at https://$USERNAME.github.io/$PROJECT
.
Error Handling
For non-trivial crates where you may have several common error cases, you might
want to define your own error type. This type can then implement
[std::convert::From][from] for all the errors you may be wrapping. That way the
error integrates nicely with the try!()
macro and your entire library will
be simplified by using only one error and Result
type.
use std::io;
use std::num;
pub type CliResult<T> = Result<T, CliError>;
#[derive(Debug)]
enum CliError {
Io(io::Error),
Parse(num::ParseIntError),
}
impl From<io::Error> for CliError {
fn from(err: io::Error) -> CliError {
CliError::Io(err)
}
}
...
You should also make sure your custom error implements the std::error::Error
trait.
impl Error for CliError {
fn description(&self) -> &str {
match *self {
CliError::Io(ref err) => err.description(),
CliError::Csv(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&Error> {
...
}
}
Miscellaneous Tips
- Don't duplicate your binary's version number in your code. Use
env!("CARGO_PKG_VERSION")
to get the version you set inCargo.toml
at compile time. - Write commit messages in a “conventional” style and use Clog to automatically generate change logs.
- Install more Cargo Subcommands!
- Make sure that all you test each feature of your code (note "unit" in "unit test" doesn't necessarily mean each and every little function).
- Integration tests are awesome too.