DEV Community

Sean Larkin
Sean Larkin

Posted on

One Crate a Day: has-flag

Pixel art image of a Crab reading a book

Welcome to "One Crate a Day", my daily journal as I dive into the world of Rust programming. As a JavaScript developer with a background in open-source, I've decided to take on the challenge of re-writing popular JavaScript packages for the Rust community.

Here are a few tl;dr goals I have for this project:

  • 👨‍🎓Learn Rust & Popular JavaScript modules
  • 📦Contribute to the Rust community by publishing new Crates
  • 👨‍🏫Share my learnings, tips, and best practices with you

If you've come here to learn how to install, start Rust from scratch, or setup the toolchain for your IDE/environment, see the Rust Handbook! Otherwise, lets dive in!

Crate #1: has-env-flag

Original Package: sindresorhus/has-flag

Sindre's JavaScript packages exemplify the concepts of single purpose and reusability. Often times only implemented as a single exported function. has-flag is no different in this regard.

Review

This single function module allows developers to quickly detect the presence of a specific flag in the argv (arguments passed to the script/binary that was run). The code for the module is as follows:

import process from 'process'; // eslint-disable-line node/prefer-global/process

export default function hasFlag(flag, argv = process.argv) {
  const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
  const position = argv.indexOf(prefix + flag);
  const terminatorPosition = argv.indexOf('--');
  return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
}

Enter fullscreen mode Exit fullscreen mode

With this module, we can easily check for the presence of a specific argument ("flag") based on what was passed into our script. Here's an example from the README:

// foo.js
import hasFlag from 'has-flag';

hasFlag('unicorn');
//=> true

hasFlag('--unicorn');
//=> true

hasFlag('f');
//=> true

hasFlag('-f');
//=> true

hasFlag('foo=bar');
//=> true

hasFlag('foo');
//=> false

hasFlag('rainbow');
//=> false
Enter fullscreen mode Exit fullscreen mode
$ node foo.js -f --unicorn --foo=bar -- --rainbow
Enter fullscreen mode Exit fullscreen mode

Approach

To start my journey, I decided to craft my unit tests first. This way, I could work backwards until I had my solution.

Learning #1: Cargo uses Conventions

Cargo, which is the out-of-the-box tool for running tests, installing packages/dependencies, and more, has conventions for building libraries versus binaries.

By default, Cargo looks for one of two conventions in the workspace:

  • Libraries: If you are building a Rust library ("Crate"), Cargo will enforce the presence of src/lib.rs in your workspace.

  • Binaries: If you are building a Rust binary, Cargo will enforce the presence of src/main.rs.

I love this! Every time I crack open a Rust project, I know exactly where to start/begin reading code.

Learning #2: How to write tests

Next, I learned how to write tests in Rust. From all of the packages I've looked at, and according to the Rust Handbook, unit tests are written in the same file! Additionally, you'll see the usage of Macros heavily in Rust tests. Macros are a powerful tool for creating code expansion at compile time, saving you from writing lines of boilerplate code.

Take the following test I wrote in my module as an example:

// Macro for setting up a test module
#[cfg(test)]
mod tests {
    // Gives access to outer scope (not just the `pub fn`'s)
    // great to test functions used in Dependency Injection
    use super::*;

    // Macro that turns a function into a unit test
    #[test]
    fn args_with_value_not_matching_double_dash() {
        let args = vec!["--foo", "--unicorn=rainbow", "--bar"];
        let expected_value = "unicorn=rainbow";

        assert!(_has_flag(
            args.into_iter().map(ToString::to_string),
            expected_value
        ))
    }
}
Enter fullscreen mode Exit fullscreen mode
  • #[cfg(test)] macro: This is our macro for setting up our test module. It instructs Rust to compile and run the test code only when you run cargo test

  • #[test] macro: This macro converts the function into a unit test! You will use macros like assert!() or assert_eq!() to validate the results of the code you want to test.

  • use super::*: Exposes the outer scope to your inner test module. The glob allows anything defined in the outer scope to be used in your test functions (anything not defined with pub fn)!

Learning 3: Working with std::env

After implementing the tests I decided to take a crack at writing this module. Much thanks to Steve Klabnik, who helped me with getting the types for the function and setting up dependency injection, this was the first iteration:

pub fn has_flag(flag: &str) -> bool {
    _has_flag(std::env::args(), flag)
}

fn _has_flag<I: Iterator<Item = String>>(mut args: I, flag: &str) -> bool {
    let prefix = if flag.starts_with('-') {
        ""
    } else {
        if flag.len() == 1 {
            "-"
        } else {
            "--"
        }
    };

    let position = args.position(|arg| arg == format!("{}{}", prefix, flag));
    let terminator_position = args.position(|arg| arg == "--");
    position.is_some() && (!terminator_position.is_some() || position < terminator_position)
}
Enter fullscreen mode Exit fullscreen mode
  • std::env::args(): This is how you can access argv or arguments that the program was started with. std is the Rust standard library and is available to all Rust crates by default.

  • .position(): Searches for an element in an iterator, and returns its index. I saw this as comparable to .indexOf in JavaScript. However I made some mistakes in my code using this and I'll explain why.

  • .is_some() & is_none(): There is no concept of null in Rust. Rather, Option is a type which is meant to handle the no value being returned. In a few parts of our code, we don't really care what the index is, rather if the index exists. .is_some() is the perfect usage for that here.

Learning 4: Mutability and Bugs!

This code that I initially committed and published actually had a few bugs! However, my initial tests were passing. I had created a GitHub Issue to come back when I had time and implement all of the tests from has-flag's test suite.

My PR introducing the full tests was failing so I knew there must be a bug in the code itself. After getting some feedback from the "Beginners" channel on The Rust Programming Language Discord, I started to realize some of the mistakes I had made. Here is the updated code which passed tests:

pub fn has_flag(flag: &str) -> bool {
    _has_flag(std::env::args(), flag)
}

fn _has_flag<I: Iterator<Item = String>>(args: I, flag: &str) -> bool {
    let prefix = if flag.starts_with('-') {
        ""
    } else if flag.len() == 1 {
        "-"
    } else {
        "--"
    };

    let formatted_flag = format!("{}{}", prefix, flag);
    args.take_while(|arg| arg != "--")
        .any(|arg| arg == formatted_flag)
}
Enter fullscreen mode Exit fullscreen mode
  • mut args => args: The first code smell I should have noticed when I wrote my first iteration, was that the Rust compiler was forcing me to add mut next to my args parameter for the function. This didn't make sense to me because mut is only needed if I am mutating args.

  • .position() mutates: This led me to noticing that .position() was not the function I wanted to use. .position() consumes items in the iterator until the predicate is matched. So the code determining terminator_position was actually accessing a mutated args. All my tests involving the -- args terminator were failing as a result!

  • .take_while(): Takes an iterator, and runs through it until the predicate is true, and then returns an iterator of those items up until the predicate. I think of this similar to an iterator filter. This was perfect for us to use here because we don't want to match args passed after the -- terminator. It also massively simplified our code!

Ship it🎉

This iteration passed all of our tests and I was able to happily merge and publish a new release of has-env-flag. As I continue to work on this journal, I will encounter many new challenges and learnings about Rust, and I can't wait to share them with you.

Resources

Oldest comments (0)