Crates

💭 Crates are a bit similar to the packages in some other languages. Crates compile individually. If the crate has child file modules, those files will get merged with the crate file and compile as a single unit.

💭 A crate can produce an executable/ a binary or a library. src/main.rs is the crate root/ entry point for a binary crate and src/lib.rs is the entry point for a library crate.

01. lib.rs on executable crate

💡 When writing binary crates, we can move the main functionalities to src/lib.rs and use it as a library from src/main.rs. This pattern is quite common on executable crates.

// # Think we run,
cargo new greetings
touch greetings/src/lib.rs

// # It generates,
greetings
 ├── Cargo.toml
 └── src
    ├── lib.rs
    └── main.rs

// # Think we modify following files,

// 01. greetings/src/lib.rs
pub fn hello() {
    println!("Hello, world!");
}

// 02. greetings/src/main.rs
extern crate greetings;

fn main() {
    greetings::hello();
}

💯 As I mentioned earlier, in here we use simplest examples to reduce the complexity of learning materials. But this is how we need to write greetings/src/lib.rs to make the code more testable.

// greetings/src/lib.rs
pub fn hello() -> String {
  //! This returns `Hello, world!` String
  ("Hello, world!").to_string()
}

// 01. Tests for `hello()`
#[test] // Indicates that this is a test function
fn test_hello() {
  assert_eq!(hello(), "Hello, world!");
}

// 02. Tests for `hello()`, Idiomatic way
#[cfg(test)] // Only compiles when running tests
mod tests { // Separates tests from code
  use super::hello; // Import root `hello()` function
  
    #[test]
    fn test_hello() {
        assert_eq!(hello(), "Hello, world!");
    }
}

📖 When importing a crate that has dashes in its name “like-this”, which is not a valid Rust identifier, it will be converted by changing the dashes to underscores, so you would write extern crate like_this;

lib.rs can link with multiple files.

// # Think we run,
cargo new phrases
touch phrases/src/lib.rs
touch phrases/src/greetings.rs

// # It generates,
phrases
 ├── Cargo.toml
 └── src
    ├── greetings.rs
    ├── lib.rs
    └── main.rs
   
// # Think we modify following files,

// 01. phrases/src/greetings.rs
pub fn hello() {
    println!("Hello, world!");
}

// 02. phrases/src/main.rs
extern crate phrases;

fn main() {
    phrases::greetings::hello();
}

// 03. phrases/src/lib.rs
pub mod greetings; // ⭐️ Import `greetings` module as a public module

02. Dependency crate on Cargo.toml

When the code in the lib.rs file is getting larger, we can move those into a separate library crate and use it as a dependency of the main crate. As we mentioned earlier, a dependency can be specified from a folder path, git repository or by crates.io.

a. Using folder path

Let’s see how to create a nested crate and use it as a dependency using folder path,

// # Think we run,
cargo new phrases
cargo new phrases/greetings --lib

// # It generates,
phrases
 ├── Cargo.toml
 ├── greetings
   ├── Cargo.toml
   └── src
      └── lib.rs
 └── src
    └── main.rs

// # Think we modify following files,

// 01. phrases/Cargo.toml
[package]
name = "phrases"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]

[dependencies]
greetings = { path = "greetings" }

// 02. phrases/greetings/src/lib.rs
pub fn hello() {
    println!("Hello, world!");
}

// 03. phrases/src/main.rs
extern crate greetings;

fn main() {
    greetings::hello();
}

b. Using git repository

If you want to use a library crate on multiple projects, one way is moving crate code to a git repository and use it as a dependency when needed.

// -- Cargo.toml --
[dependencies]

// 01. Get the latest commit on the master branch
rocket = { git = "https://github.com/SergioBenitez/Rocket" }

// 02. Get the latest commit of a specific branch
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "v0.3" }

// 03. Get a specific tag
rocket = { git = "https://github.com/SergioBenitez/Rocket", tag = "v0.3.2" }

// 04. Get a specific revision (on master or any branch, according to rev)
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "8183f636305cef4adaa9525506c33cbea72d1745" }

c. Using crates.io

The other way is uploading it to crates.io and use it as a dependency when needed.

🚧 First, let’s create a simple “Hello world” crate and upload it to crates.io.

// # Think we run,
cargo new test_crate_hello_world --lib

// # It generates,
test_crate_hello_world
 ├── Cargo.toml
 └── src
    └── lib.rs
   
// # Think we modify following files,

// 01. test_crate_hello_world/Cargo.toml
[package]
name = "test_crate_hello_world"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]

description = "A Simple Hello World Crate"
repository = "https://github.com/dumindu/test_crate_hello_world"
keywords = ["hello", "world"]
license = "Apache-2.0"

[dependencies]

// 02. test_crate_hello_world/src/lib.rs
//! A Simple Hello World Crate

/// This function returns the greeting; `Hello, world!`
pub fn hello() -> String {
    ("Hello, world!").to_string()
}

#[cfg(test)]
mod tests {

    use super::hello;
    
    #[test]
    fn test_hello() {
        assert_eq!(hello(), "Hello, world!");
    }
}

💭 //! doc comments are used to write crate and module-level documentation. On other places, we have to use /// outside of the block. And when uploading a crate to crates.io, cargo generates the documentation from these doc comments and host it on docs.rs.

💡 We have to add the description and license fields to Cargo.toml. Otherwise, we will get error: api errors: missing or empty metadata fields: description, license. Please see http://doc.crates.io/manifest.html

To upload this to crates.io,

  1. We have to create an account on crates.io to acquire an API token
  2. Then run cargo login <token> with that API token and cargo publish

📖 This is how it describes on Cargo Docs with more details.

  • You’ll need an account on crates.io to acquire an API token. To do so, visit the home page and log in via a GitHub account (required for now). After this, visit your Account Settings page and run the cargo login command specified. Ex. cargo login abcdefghijklmnopqrstuvwxyz012345
  • The next step is to package up your crate into a format that can be uploaded to crates.io. For this we’ll use the cargo package sub-command.
  • Now, it can be uploaded to crates.io with the cargo publish command.
  • If you’d like to skip the cargo package step, the cargo publish sub-command will automatically package up the local crate if a copy isn’t found already.

The name of our crate is test_crate_hello_world. So it can be found on, 📦 https://crates.io/crates/test_crate_hello_world 📑 https://docs.rs/test_crate_hello_world

💯 crates.io supports readme files as well. To enable it, we have to add the readme field to Cargo.toml. Ex: readme="README.md"

🏗️ Okay then, Let’s see how we can use this from another crate.

// # Think we run,
cargo new greetings

// # It generates,
greetings
 ├── Cargo.toml
 └── src
    └── main.rs

// # Think we modify following files,

// 01. greetings/Cargo.toml
[package]
name = "greetings"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]

[dependencies]
test_crate_hello_world = "0.1.0"

// 02. greetings/src/main.rs
extern crate test_crate_hello_world;

fn main() {
    println!("{}", test_crate_hello_world::hello());
}

By default, Cargo looks dependencies on crates.io. So we have to add only the crate name and a version string to Cargo.toml and then run cargo build to fetch the dependencies and compile them.