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
draft
needs-review
publish
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::Post
Layout::Gallery
Layout::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 theValueEnum
derive to work properly (We’d get a compiler stoppage with an explanation if we forgot it).Default
allows us to specify what the value should be if we don’t specify which variant to use. So we can instantiate a newPostStatus
and the value we get is specified by theDefault
implementation. We also use the#[default]
attribute to specify which variant will be that default value.Debug
allows us to usedbg!
on values of this typeSerialize
allows 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