Hello, World!
Hi, I'm Nate Wilcox.
I spend much of my time working to improve zcash at the Electric Coin Company and chillin' with my fam.
← The Goodies.
Site Mechanics
This site source is an mdbook instance, which is automatically deployed to GitHub Pages when Pull-Requests are merged.
This page describes the implementation of this deployment. Let me know if you find this helpful, or if you have issues / questions / improvements, please use this site's issue tracker.
Design Goals
- Rendered content (anything generated by a program) should not be in revision control, because it's not source code. Typically humans shouldn't care about changes to rendered content unless they are diagnosing automation problems. By contrast, humans should definitely care about source, especially for a book.
- The book is automatically rendered to the public site whenever a PR is merged.
- The "rendering branch"
gh-pages
tracks all rendered revisions in its history, while also linking to each render source revision (as a merge).
Components
To achieve these goals, we have several components:
- We use a special purpose
gh-pages
branch that does contain rendered output. This should be the only exception. Other branches can have human-meaningful source. mdbook
must be configured to render into./docs
as a requirement of GitHub Pages: book.toml- We
.gitignore
thedocs/
dir, since this is rendered output, not human-meaningful source: .gitignore - We add a
deploy.toml
GitHub Action which will deploy whenever we merge a PR tomain
branch: .github/workflows/deploy.yaml - We configure the GitHub repository to enable GitHub Page hosting.
Setting It Up
Create and mdbook
See the mdbook guide to get started.
Configure mdbook
and .gitignore
First we need to configure to render to ./docs
by adding this to book.toml:
[build]
# Follow gh pages convention:
build-dir = "./docs"
-and let's also ignore the render dir, because of our first design goal by adding this to .gitignore:
docs/
Add the deploy.yaml
GitHub Action
The .github/workflows/deploy.yaml does all the good stuff. It:
- triggers on any push to
main
(the default repo branch), - installs
mdbook
, - checks out the source,
- creates a new merge commit on
gh-pages
that overwrites the previous content with the new revision onmain
, - renders the source with
mdbook
, - removes
./docs
from.gitignore
since this is the one exception to committing rendered content, - disables
jekyll
(the standard GitHub Pages rendering system), - and finally, commits and pushes the rendered content to the
gh-pages
branch.
This should result in two commits onto gh-pages
per render cycle: one is a merge that overwrites the previous contents of gh-pages
with the new main
content, and the second includes the rendering output. This design enables seeing the deployment history on the gh-pages
which is tracked interleaved merge/render commits. Each merge additionally points to the revision of main
where the content comes from, overwriting any previous changes on the gh-pages
branch.
Configuring GitHub Pages
The final step is to tell GitHub Pages where to find the rendered site content:
- repo -> "Settings" -> "Pages"
- Set source to
gh-pages
branch. You may need to create this branch first; one way is by merging a PR tomain
to trigger the deploy action to produce your first render. - Set the path to
docs/
.
Writing Cycle
Because this rendering only occurs when you merge to main
, it should only be used for "finished" updates you want to publish to the world. Make sure to render and review your edits as you go prior to merging a PR.
On linux I use this command to quickly view the rendered version:
$ mdbook build && xdg-open ./docs/index.html
This works better for me than the mdbook
built-in reloading webserver; do whatever works best for you.
Fin.
Now you can edit the source of your site in a clean manner without the clutter of renderings. Enjoy!
Removing Rust Boilerplate
Remove all the boilerplate!
Warning: I haven't carefully vetted these dependencies for security /
safety (beyond using cargo-audit
):
- Why?
- Use
std
derives derive_more
derive_new
for structnew
functionsderive_builder
for builder patternsanyhow
for concise contextual errorsanyhow-std
provides error context for manystd
APIstestcase
for testing many vectors as independent testsindoc
for indenting string literalsclap
derive API to remove cli option parsing boilerplatecargo-checkmate
to simplify running many checks and producing basic Github CI
Why?
Here are recipes I use to remove boilerplate from rust code. I'm a big fan of doing so for a few reasons:
-
This style makes it apparent to a reader at a glance what behavior is provided.
-
More concise code is often better for readability/comprehensability.
-
Using "boilerplate removal" tools also helps ensure consistency via the tool.
-
Boilerplate-removal crates and tools can be developed and improved which can potentially feed improvements into consuming code, whereas hand-written boilerplate must be updated on a case-by-case basis.
-
If boilerplate removal is consistently used, then any boilerplate-like code is more obviously unique/custom for a purpose.
For example, if a codebase always uses
#[derive(PartialEq)]
when possible, then any customderive PartialEq for MyType …
can immediately cue the reader that there is some unique behavior of this impl. By contrast, if boilerplate is always written out, then readers have to examine each case carefully to determine if it's "standard" or unique behavior.
There are potential downsides to boiler-plate removal:
- Sometimes what you need has an impedence mismatch with some boilerplate-removal tool, so you may try to work around that limitation, and at some point it becomes more complex or cumbersome than hand-written code. (Maybe submit a patch to the tool! 😉)
- Often boiler-plate removal relies on procedural macros, which can slow down build times.
- Changes to boiler-plate removal code may change your code unexpectedly.
- Compiler errors stemming either from the boiler-plate removal or from your code's interaction with it may have more confusing error messages.
Do you have favorite boiler-plate removal not mentioned here? Suggestions welcome.
Use std
derives
Many std
traits can be derived, see this
reference
for more detail:
Clone
Debug
Default
PartialEq
,Eq
,PartialOrd
,Ord
Hash
derive_more
https://docs.rs/derive_more
This crate is full of goodies to remove common trait derivations.
derive_more::From
for enums, especially error enums
Especially useful making enum construction concise. When used with error
types, this allows concisely propagating inner error types with the
?
operator:
#![allow(unused)] fn main() { #[derive(derive_more::From)] pub enum Error { Stdio(std::io::Error), MyApp(crate::AppError), } fn do_stuff() -> Result<(), Error> { // Propagate a `std::io::Error`: let data = std::fs::read("/input/file")?; // Propagate a `crate::AppError`: process_data(data)?; Ok(()) } fn process_data(data: Vec<u8>) -> Result<(), crate::AppError> { todo!(); } }
derive_new
for struct new
functions
Create a new
method automatically.
#![allow(unused)] fn main() { #[derive(derive_new::new)] pub struct MyThing { foo: String, bar: bool, } fn do_stuff() { let thing = MyThing::new("alice".to_string(), true); todo!("do stuff with thing"); } }
derive_builder
for builder patterns
If you need a builder pattern interface, this crate can save a lot of tedious boilerplate.
anyhow
for concise contextual errors
I've become a big fan of anyhow
because it removes the need for
spelling out custom error types. This can be a trade-off, though, since
"transparent" error types are more ergonomic if the consuming code needs
to frequently behave differently based on different error types.
Keep in mind consumers can still distinguish specific errors and
behave accordingly with slightly more verbose code, so assuming that's
relatively uncommon, I believe anyhow
is a good option.
Typically I try to avoid introducing anyhow
into reusable library-style
crates, and only use it in application-level code.
anyhow-std
provides error context for many std
APIs
This is a crate I created after a few too many error messages about file
IO without understanding which path was the culprit. See above about
whether or not to introduce anyhow
in your API, but I definitely reach
for this crate any time I'm writing application-level code.
testcase
for testing many vectors as independent tests
Sometimes you want to verify a function against a bunch of test vector inputs/outputs. If you do this in a loop in a single unit test, you cannot quickly determine which case failed, slowing down debugging.
Use test case to generate separate tests for each input/output case so that failures help you more quickly find the source of the bug.
#![allow(unused)] fn main() { #[test_case("3" => 3)] #[test_case("7" => 7)] #[test_case("+42" => 42)] #[test_case("-5" => -5)] #[test_case("-0xa0" => -160)] fn parse_int(s: &str) -> i32 { s.parse().unwrap() } }
Running cargo test
quickly shows which cases pass/fail:
running 5 tests
test parse_int::_0xa0_expects_160 ... FAILED
test parse_int::_42_expects_42 ... ok
test parse_int::_7_expects_7 ... ok
test parse_int::_5_expects_5 ... ok
test parse_int::_3_expects_3 ... ok
-so now we immediately see that our .parse()
call doesn't work for
-0xa0
as an input, whereas the other cases are fine.
indoc
for indenting string literals
If you find yourself littering \n
in string literals a lot, you probably
would benefit in code readability with indoc
. I find it especially
useful to use in tests with test_case
for parsers or formatters.
clap
derive API to remove cli option parsing boilerplate
Use clap
with the derive
API
to cut down on cli parsing boilerplate.
cargo-checkmate
to simplify running many checks and producing basic Github CI
This is a crate I created specifically because I got tired of repeatedly running different checks on each commit. By using the git hook, this helps me ensure a crate or workspace builds, passes clippy tests, generates docs without warnings, etc…