Presumably we have a pre-set amount of post layouts and statuses for publishing.
For layouts lets say we have three types of posts:
- blog post
- image gallery
- code example
and for publishing status lets use
draftneeds-reviewpublish
Ideally, the user of this CLI wouldn’t be able to mistype and enter “publesh” or “neesd-review”.
These are great candidates for Rust’s enums.
Rust enums
We’ve already seen structs, which allow us to type a set of fields and enter values for each field when we instantiate it.
Enums, by contrast, allow you to pick one of the possible variants.
In this Layout example, we can have values that are
Layout::PostLayout::GalleryLayout::Code
enum Layout {
Post,
Gallery,
Code,
}
We’ve made it so that there are no other options in this enum, so if we use this enum with Clap’s argument parsing, we’ll be able to notify a user of our CLI any time they mis-type an argument… instead of them finding out later on when trying to publish.
The Layout and PostStatus enums are organized in exactly the same ways. The same derives, the same serde attribute, and comments.
#[derive(
clap::ValueEnum, Clone, Default, Debug, Serialize,
)]
#[serde(rename_all = "kebab-case")]
enum Layout {
/// blog post
#[default]
Post,
/// image gallery
Gallery,
/// code example
Code,
}
#[derive(
clap::ValueEnum, Clone, Default, Debug, Serialize,
)]
#[serde(rename_all = "kebab-case")]
enum PostStatus {
/// Draft, don't publish
#[default]
Draft,
/// Needs Review
NeedsReview,
/// Publishable
Publish,
}
Each enum is decorated with a series of derive macros.
Aside from clap’s ValueEnum, These are:
Clone, which allows us to use.clone(), and is also required for theValueEnumderive to work properly (We’d get a compiler stoppage with an explanation if we forgot it).Defaultallows us to specify what the value should be if we don’t specify which variant to use. So we can instantiate a newPostStatusand the value we get is specified by theDefaultimplementation. We also use the#[default]attribute to specify which variant will be that default value.Debugallows us to usedbg!on values of this typeSerializeallows us to use these variants when serializing our structs to yaml with serde, just like how it works for ourFrontmatter.
Because capital letters are a bit awkward to type on the command line, and generally not used by convention, we use an additional serde attribute to define how the variant names are serialized.
One really nice piece of design in the serde crate is how this rename_all is implemented. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE", which all look exactly like what they transform into/from.
We’re using kebab-case, which means our PostStatus variants are: draft, needs-review, and publish.
Finally, the three-slash comments are going to be parsed by Clap and included automatically in our —help text for the CLI.
Using enums in Clap arguments
We’re going to use our enums in two places: Frontmatter and Args.
The relevant types change in the Frontmatter definition.
#[derive(Debug, Serialize)]
struct Frontmatter {
layout: Layout,
tags: Vec<String>,
status: PostStatus,
title: String,
slug: String,
}
and the same types change in Args. Also note that we’ve changed the default_value to default_value_t and added value_enum, which tells Clap that these are enums that can be serialized with a default value that we’ve already defined.
/// Scaffold a new post for your blog
#[derive(Parser, Debug)]
#[clap(version)]
struct Args {
/// The layout the post should use
#[clap(short, long, default_value_t, value_enum)]
layout: Layout,
/// Tags to include
#[clap(short, long = "tag")]
tags: Vec<String>,
/// The title of the post.
///
/// If not provided, the filename will be generated
#[clap(short = 'T', long, default_value = "A Post")]
title: String,
/// Should this post be published?
#[clap(short, long, default_value_t, value_enum)]
status: PostStatus,
/// Where to put the file
#[clap(short, long, default_value = "content")]
output_dir: Utf8PathBuf,
}
Running our application results in the Args values being their respective variants by default
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/scaffold`
[src/main.rs:34] &args = Args {
layout: Post,
tags: [],
title: "A Post",
status: Draft,
output_dir: "content",
}
and each of the variants now shows up in the --help text.
❯ cargo run -- --help
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/scaffold --help`
Scaffold a new post for your blog
Usage: scaffold [OPTIONS]
Options:
-l, --layout <LAYOUT>
The layout the post should use
[default: post]
Possible values:
- post: blog post
- gallery: image gallery
- code: code example
-t, --tag <TAGS>
Tags to include
-T, --title <TITLE>
The title of the post.
If not provided, the filename will be generated
[default: "A Post"]
-s, --status <STATUS>
Should this post be published?
[default: draft]
Possible values:
- draft: Draft, don't publish
- needs-review: Needs Review
- publish: Publishable
-o, --output-dir <OUTPUT_DIR>
Where to put the file
[default: content]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
When given a value that doesn’t exist in our safelist, such as backlog, Clap gives back a nice error message detailing what went wrong and what the possible values are.
❯ cargo run -- -s backlog
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/scaffold -s backlog`
error: invalid value 'backlog' for '--status <STATUS>'
[possible values: draft, needs-review, publish]
For more information, try '--help'.
and our file will now reflect whatever options we give the CLI.
❯ cat content/a-post.md
layout: post
tags: []
status: needs-review
title: A Post
slug: a-post
---
# A Post