go-stdlib-rust post (#215)

This commit is contained in:
Cadey Ratio 2020-09-27 12:35:24 -04:00 committed by GitHub
parent e460ebdcbe
commit f106c2c9d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 760 additions and 9 deletions

16
Cargo.lock generated
View File

@ -1301,6 +1301,18 @@ dependencies = [
"sha-1 0.8.2",
]
[[package]]
name = "pfacts"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83abebdb324c30f176d449513f0134bafbf976d5279c6554742599e3996d1629"
dependencies = [
"rand 0.7.3",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "pin-project"
version = "0.4.22"
@ -2507,6 +2519,7 @@ dependencies = [
"color-eyre",
"comrak",
"envy",
"eyre",
"glob",
"go_vanity",
"hyper",
@ -2516,12 +2529,15 @@ dependencies = [
"log",
"mime",
"patreon",
"pfacts",
"pretty_env_logger",
"prometheus",
"rand 0.7.3",
"reqwest",
"ructe",
"serde",
"serde_dhall",
"serde_json",
"serde_yaml",
"sitemap",
"thiserror",

View File

@ -40,6 +40,12 @@ patreon = { path = "./lib/patreon" }
[build-dependencies]
ructe = { version = "0.12", features = ["warp02"] }
[dev-dependencies]
pfacts = "0"
serde_json = "1"
eyre = "0.6"
reqwest = { version = "0.10", features = ["json"] }
[workspace]
members = [
"./lib/go_vanity",

View File

@ -0,0 +1,617 @@
---
title: Rust Crates that do What the Go Standard library Does
date: 2020-09-27
tags:
- rust
---
# Rust Crates that do What the Go Standard library Does
One of Go's greatest strengths is how batteries-included the standard library
is. You can do most of what you need to do with only the standard library. On
the other hand, Rust's standard library is severely lacking by comparison.
However, the community has capitalized on this and been working on a bunch of
batteries that you can include in your rust projects. I'm going to cover a bunch
of them in this post in a few sections.
[A lot of these are actually used to help make this blog site
work!](conversation://Mara/hacker)
## Logging
Go has logging out of the box with package [`log`](https://godoc.org/log).
Package `log` is a very uncontroversial logger. It does what it says it does and
with little fuss. However it does not include a lot of niceties like logging
levels and context-aware values.
In Rust, we have the [`log`](https://docs.rs/log/) crate which is a very simple
interface. It uses the `error!`, `warn!`, `info!`, `debug!` and `trace!` macros
which correlate to the highest and lowest levers. If you want to use `log` in a
Rust crate, you can add it to your `Cargo.toml` file like this:
```toml
[dependencies]
log = "0.4"
```
Then you can use it in your Rust code like this:
```rust
use log::{error, warn, info, debug, trace};
fn main() {
trace!("starting main");
debug!("debug message");
info!("this is some information");
warn!("oh no something bad is about to happen");
error!("oh no it's an error");
}
```
[Wait, where does that log to? I ran that example locally but I didn't see any
of the messages anywhere](conversation://Mara/wat)
This is because the `log` crate doesn't directly log anything anywhere, it is a
facade that other packages build off of.
[`pretty_env_logger`](https://docs.rs/pretty_env_logger) is a commonly used
crate with the `log` facade. Let's add it to the program and work from there:
```toml
[dependencies]
log = "0.4"
pretty_env_logger = "0.4"
```
Then let's enable it in our code:
```rust
use log::{error, warn, info, debug, trace};
fn main() {
pretty_env_logger::init();
trace!("starting main");
debug!("debug message");
info!("this is some information");
warn!("oh no something bad is about to happen");
error!("oh no it's an error");
}
```
And now let's run it with `RUST_LOG=trace`:
```console
$ env RUST_LOG=trace cargo run --example logger_test
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `/home/cadey/code/christine.website/target/debug/logger_test`
TRACE logger_test > starting main
DEBUG logger_test > debug message
INFO logger_test > this is some information
WARN logger_test > oh no something bad is about to happen
ERROR logger_test > oh no it's an error
```
There are [many
other](https://docs.rs/log/0.4.11/log/#available-logging-implementations)
consumers of the log crate and implementing a consumer is easy should you want
to do more than `pretty_env_logger` can do on its own. However, I have found
that `pretty_env_logger` does just enough on its own. See its documentation for
more information.
## Flags
Go's standard library has the [`flag`](https://godoc.org/flag) package out of
the box. This package is incredibly basic, but is surprisingly capable in terms
of what you can actually do with it. A common thing to do is use flags for
configuration or other options, such as
[here](https://github.com/Xe/hlang/blob/44bb74efa6f124ca05483a527c0e735ce0fca143/main.go#L15-L22):
```go
package main
import "flag"
var (
program = flag.String("p", "", "h program to compile/run")
outFname = flag.String("o", "", "if specified, write the webassembly binary created by -p here")
watFname = flag.String("o-wat", "", "if specified, write the uncompiled webassembly created by -p here")
port = flag.String("port", "", "HTTP port to listen on")
writeTao = flag.Bool("koan", false, "if true, print the h koan and then exit")
writeVersion = flag.Bool("v", false, "if true, print the version of h and then exit")
)
```
This will make a few package-global variables that will contain the values of
the command-line arguments.
In Rust, a commonly used command line parsing package is
[`structopt`](https://docs.rs/structopt). It works in a bit of a different way
than Go's `flag` package does though. `structopt` focuses on loading options into
a structure rather than into globally mutable variables.
[Something you may notice in Rust-land is that globally mutable state is talked
about as if it is something to be avoided. It's not inherently bad, but it does
make things more likely to crash at runtime. In most cases, these global
variables with package `flag` are fine, but only if they are ever written to
before the program really starts to do what it needs to do. If they are ever
written to and read from dynamically at runtime, then you can get into a lot of
problems such as <a href="https://en.wikipedia.org/wiki/Race_condition">race
conditions</a>.](conversation://Mara/hacker)
Here's a quick example copied from [pa'i](https://github.com/Xe/pahi):
```rust
#[derive(Debug, StructOpt)]
#[structopt(
name = "pa'i",
about = "A WebAssembly runtime in Rust meeting the Olin ABI."
)]
struct Opt {
/// Backend
#[structopt(short, long, default_value = "cranelift")]
backend: String,
/// Print syscalls on exit
#[structopt(short, long)]
function_log: bool,
/// Do not cache compiled code?
#[structopt(short, long)]
no_cache: bool,
/// Binary to run
#[structopt()]
fname: String,
/// Main function
#[structopt(short, long, default_value = "_start")]
entrypoint: String,
/// Arguments of the wasm child
#[structopt()]
args: Vec<String>,
}
```
This has the Rust compiler generate the needed argument parsing code for you, so
you can just use the values as normal:
```rust
fn main() {
let opt = Opt::from_args();
debug!("args: {:?}", opt.args);
}
```
You can even handle subcommands with this, such as in
[palisade](https://github.com/lightspeed/palisade/blob/master/src/main.rs). This
package should handle just about everything you'd do with the `flag` package,
but will also work for cases where `flag` falls apart.
## Errors
Go's standard library has the [`error`
interface](https://godoc.org/builtin#error) which lets you create a type that
describes why functions fail to do what they intend. Rust has the [`Error`
trait](https://doc.rust-lang.org/std/error/trait.Error.html) which lets you also
create a type that describes why functions fail to do what they intend.
In [my last post](https://christine.website/blog/TLDR-rust-2020-09-19) I
described [`eyre`](https://docs.rs/eyre) and the Result type. However, this time
we're going to dive into [`thiserror`](https://docs.rs/thiserror) for making our
own error type. Let's add `thiserror` to our crate:
```toml
[dependencies]
thiserror = "1"
```
And then let's re-implement our `DivideByZero` error from the last post:
```rust
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
struct DivideByZero;
impl fmt::Display for DivideByZero {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot divide by zero")
}
}
```
The compiler made our error instance for us! It can even do that for more
complicated error types like this one that wraps a lot of other error cases and
error types in [maj](https://tulpa.dev/cadey/maj):
```rust
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("TLS error: {0:?}")]
TLS(#[from] TLSError),
#[error("URL error: {0:?}")]
URL(#[from] url::ParseError),
#[error("Invalid DNS name: {0:?}")]
InvalidDNSName(#[from] webpki::InvalidDNSNameError),
#[error("IO error: {0:?}")]
IO(#[from] std::io::Error),
#[error("Response parsing error: {0:?}")]
ResponseParse(#[from] crate::ResponseError),
#[error("Invalid URL scheme {0:?}")]
InvalidScheme(String),
}
```
[These `#[error("whatever")]` annotations will show up when the error message is
printed. See <a
href="https://docs.rs/thiserror/1.0.20/thiserror/#details">here</a> for more
information on what details you can include here.](conversation://Mara/hacker)
## Serialization / Deserialization
Go has JSON encoding/decoding in its standard library via package
[`encoding/json`](https://godoc.org/encoding/json). This allows you to define
types that can be read from and write to JSON easily. Let's take this simple
JSON object representing a comment from some imaginary API as an example:
```json
{
"id": 31337,
"author": {
"id": 420,
"name": "Cadey"
},
"body": "hahaha its is an laughter image",
"in_reply_to": 31335
}
```
In Go you could write this as:
```go
type Author struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Comment struct {
ID int `json:"id"`
Author Author `json:"author"`
Body string `json:"body"`
InReplyTo int `json:"in_reply_to"`
}
```
Rust does not have this capability out of the box, however there is a fantastic
framework available known as [serde](https://serde.rs/) which works across JSON
and every other serialization method that you can think of. Let's add serde and
its JSON support to our crate:
```toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```
[You might notice that the dependency line for serde is different here. Go's
JSON package works by using <a
href="https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go">struct
tags</a> as metadata, but Rust doesn't have these. We need to use Rust's derive
feature instead.](conversation://Mara/hacker)
So, to use serde for our comment type, we would write Rust that looks like this:
```rust
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub id: i32,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Comment {
pub id: i32,
pub author: Author,
pub body: String,
pub in_reply_to: i32,
}
```
And then we can load that from JSON using code like this:
```rust
fn main() {
let data = r#"
{
"id": 31337,
"author": {
"id": 420,
"name": "Cadey"
},
"body": "hahaha its is an laughter image",
"in_reply_to": 31335
}
"#;
let c: Comment = serde_json::from_str(data).expect("json to parse");
println!("comment: {:#?}", c);
}
```
And you can use it like this:
```console
$ cargo run --example json
Compiling xesite v2.0.1 (/home/cadey/code/christine.website)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/examples/json`
comment: Comment {
id: 31337,
author: Author {
id: 420,
name: "Cadey",
},
body: "hahaha its is an laughter image",
in_reply_to: 31335,
}
```
## HTTP
Many APIs expose their data over HTTP. Go has the
[`net/http`](https://godoc.org/net/http) package that acts as a production-grade
(Google uses this in production) HTTP client and server. This allows you to get
going with new projects very easily. The Rust standard library doesn't have this
out of the box, but there are some very convenient crates that can fill in the
blanks.
### Client
For an HTTP client, we can use [`reqwest`](https://docs.rs/reqwest). It can also
seamlessly integrate with serde to allow you to parse JSON from HTTP without any
issues. Let's add reqwest to our crate as well as [`tokio`](https://tokio.rs) to
act as an asynchronous runtime:
```toml
[dependencies]
reqwest = { version = "0.10", features = ["json"] }
tokio = { version = "0.2", features = ["full"] }
```
[We need `tokio` because Rust doesn't ship with an asynchronous runtime by
default. Go does as a core part of the standard library (and arguably the
language), but `tokio` is about equivalent to most of the important things that
the Go runtime handles for you. This omission may seem annoying, but it makes it
easy for you to create a custom asynchronous runtime should you need
to.](conversation://Mara/hacker)
And then let's integrate with that imaginary comment api at
[https://xena.greedo.xeserv.us/files/comment.json](https://xena.greedo.xeserv.us/files/comment.json):
```rust
use eyre::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub id: i32,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Comment {
pub id: i32,
pub author: Author,
pub body: String,
pub in_reply_to: i32,
}
#[tokio::main]
async fn main() -> Result<()> {
let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment.json")
.await?
.json()
.await?;
println!("comment: {:#?}", c);
Ok(())
}
```
And then let's run this:
```console
$ cargo run --example http
Compiling xesite v2.0.1 (/home/cadey/code/christine.website)
Finished dev [unoptimized + debuginfo] target(s) in 2.20s
Running `target/debug/examples/http`
comment: Comment {
id: 31337,
author: Author {
id: 420,
name: "Cadey",
},
body: "hahaha its is an laughter image",
in_reply_to: 31335,
}
```
[But what if the response status is not 200?](conversation://Mara/hmm)
We can change the code to something like this:
```rust
let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment2.json")
.await?
.error_for_status()?
.json()
.await?;
```
And then when we run it we get an error back:
```console
$ cargo run --example http_fail
Compiling xesite v2.0.1 (/home/cadey/code/christine.website)
Finished dev [unoptimized + debuginfo] target(s) in 1.84s
Running `/home/cadey/code/christine.website/target/debug/examples/http_fail`
Error: HTTP status client error (404 Not Found) for url (https://xena.greedo.xeserv.us/files/comment2.json)
```
This combined with the other features in `reqwest` give you an very capable HTTP
client that does even more than Go's HTTP client does out of the box.
### Server
As for HTTP servers though, let's take a look at [`warp`](https://docs.rs/warp).
`warp` is a HTTP server framework that builds on top of Rust's type system.
You can add warp to your dependencies like this:
```toml
[dependencies]
warp = "0.2"
```
Let's take a look at its ["Hello, World" example](https://github.com/seanmonstar/warp/blob/master/examples/hello.rs):
```rust
use warp::Filter;
#[tokio::main]
async fn main() {
// GET /hello/warp => 200 OK with body "Hello, warp!"
let hello = warp::path!("hello" / String)
.map(|name| format!("Hello, {}!", name));
warp::serve(hello)
.run(([127, 0, 0, 1], 3030))
.await;
}
```
We can then build up multiple routes with its `or` pattern:
```
let hello = warp::path!("hello" / String)
.map(|name| format!("Hello, {}!", name));
let health = warp::path!(".within" / "health")
.map(|| "OK");
let routes = hello.or(health);
```
And even inject other datatypes into your handlers with filters such as in the
[printer facts API server](https://tulpa.dev/cadey/printerfacts/src/branch/main/src/main.rs):
```
let fact = {
let facts = pfacts::make();
warp::any().map(move || facts.clone())
};
let fact_handler = warp::get()
.and(warp::path("fact"))
.and(fact.clone())
.and_then(give_fact);
```
`warp` is an extremely capable HTTP server and can work across everything you
need for production-grade web apps.
[The blog you are looking at right now is powered by
warp!](conversation://Mara/hacker)
## Templating
Go's standard library also includes HTML and plain text templating with its
packages [`html/template`](https://godoc.org/html/template) and
[`text/template`](https://godoc.org/text/template). There are many solutions for
templating HTML in Rust, but the one I like the most is
[`ructe`](https://docs.rs/ructe). `ructe` uses Cargo's
[build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html) feature
to generate Rust code for its templates at compile time. This allows your HTML
templates to be compiled into the resulting application binary, allowing them to
render at ludicrous speeds. To use it, you need to add it to your
`build-dependencies` section of your `Cargo.toml`:
```toml
[build-dependencies]
ructe = { version = "0.12", features = ["warp02"] }
```
You will also need to add the [`mime`](https://docs.rs/mime) crate to your
dependencies because the generated template code will require it at runtime.
```toml
[dependencies]
mime = "0.3.0"
```
Once you've done this, create a new folder named `templates` in your current
working directory. Create a file called `hello.rs.html` and put the following in
it:
```html
@(title: String, message: String)
<html>
<head>
<title>@title</title>
</head>
<body>
<h1>@title</h1>
<p>@message</p>
</body>
</html>
```
Now add the following to the bottom of your `main.rs` file:
```rust
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
```
And then use the template like this:
```rust
use warp::{http::Response, Filter, Rejection, Reply};
async fn hello_html(message: String) -> Result<impl Reply, Rejection> {
Response::builder()
.html(|o| templates::index_html(o, "Hello".to_string(), message).unwrap().clone()))
}
```
And hook it up in your main function:
```rust
let hello_html_rt = warp::path!("hello" / "html" / String)
.and_then(hello_html);
let routes = hello_html_rt.or(health).or(hello);
```
For a more comprehensive example, check out the [printerfacts
server](https://tulpa.dev/cadey/printerfacts). It also shows how to handle 404
responses and other things like that.
---
Wow, this covered a lot. I've included most of the example code in the
[`examples`](https://github.com/Xe/site/tree/master/examples) folder of [this
site's GitHub repo](https://github.com/Xe/site). I hope it will help you on your
journey in Rust. This is documentation that I wish I had when I was learning
Rust.

27
examples/http.rs Normal file
View File

@ -0,0 +1,27 @@
use eyre::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub id: i32,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Comment {
pub id: i32,
pub author: Author,
pub body: String,
pub in_reply_to: i32,
}
#[tokio::main]
async fn main() -> Result<()> {
let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment.json")
.await?
.json()
.await?;
println!("comment: {:#?}", c);
Ok(())
}

28
examples/http_fail.rs Normal file
View File

@ -0,0 +1,28 @@
use eyre::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub id: i32,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Comment {
pub id: i32,
pub author: Author,
pub body: String,
pub in_reply_to: i32,
}
#[tokio::main]
async fn main() -> Result<()> {
let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment2.json")
.await?
.error_for_status()?
.json()
.await?;
println!("comment: {:#?}", c);
Ok(())
}

32
examples/json.rs Normal file
View File

@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub id: i32,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Comment {
pub id: i32,
pub author: Author,
pub body: String,
pub in_reply_to: i32,
}
fn main() {
let data = r#"
{
"id": 31337,
"author": {
"id": 420,
"name": "Cadey"
},
"body": "hahaha its is an laughter image",
"in_reply_to": 31335
}
"#;
let c: Comment = serde_json::from_str(data).expect("json to parse");
println!("comment: {:#?}", c);
}

11
examples/logger_test.rs Normal file
View File

@ -0,0 +1,11 @@
use log::{debug, error, info, trace, warn};
fn main() {
pretty_env_logger::init();
trace!("starting main");
debug!("debug message");
info!("this is some information");
warn!("oh no something bad is about to happen");
error!("oh no it's an error");
}

14
examples/warp.rs Normal file
View File

@ -0,0 +1,14 @@
use warp::Filter;
#[tokio::main]
async fn main() {
let hello = warp::path!("hello" / String)
.map(|name| format!("Hello, {}!", name));
let health = warp::path!(".within" / "health")
.map(|| "OK");
let routes = hello.or(health);
warp::serve(routes)
.run(([0, 0, 0, 0], 3030))
.await;
}

View File

@ -5,10 +5,10 @@
"homepage": "",
"owner": "justinwoo",
"repo": "easy-dhall-nix",
"rev": "90957969850a44481c6e150350c56e8b53b29e1e",
"sha256": "1hsmp3cb0k554kh0jlfzpdzx2b8ndyh2gdykmw9hw41haaw16mmi",
"rev": "3e9101c5dfd69a9fc28fe4998aff378f91bfcb64",
"sha256": "1nsn1n4sx4za6jipcid1293rdw8lqgj9097s0khiij3fz0bzhrg9",
"type": "tarball",
"url": "https://github.com/justinwoo/easy-dhall-nix/archive/90957969850a44481c6e150350c56e8b53b29e1e.tar.gz",
"url": "https://github.com/justinwoo/easy-dhall-nix/archive/3e9101c5dfd69a9fc28fe4998aff378f91bfcb64.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"naersk": {
@ -29,10 +29,10 @@
"homepage": "https://github.com/nmattia/niv",
"owner": "nmattia",
"repo": "niv",
"rev": "fad2a6cbfb2e7cdebb7cb0ad2f5cc91e2c9bc06b",
"sha256": "0mghc1j0rd15spdjx81bayjqr0khc062cs25y5dcfzlxk4ynyc6m",
"rev": "29ddaaf4e099c3ac0647f5b652469dfc79cd3b53",
"sha256": "1va6myp07gkspgxfch8z3rs9nyvys6jmgzkys6a2c4j09qxp1bs0",
"type": "tarball",
"url": "https://github.com/nmattia/niv/archive/fad2a6cbfb2e7cdebb7cb0ad2f5cc91e2c9bc06b.tar.gz",
"url": "https://github.com/nmattia/niv/archive/29ddaaf4e099c3ac0647f5b652469dfc79cd3b53.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs": {
@ -41,10 +41,10 @@
"homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS",
"repo": "nixpkgs-channels",
"rev": "4aa5466cbc741097218a1c494a7b832a17d1967d",
"sha256": "0w6gg1hxcx83l8s83frxyjm5dwri06a3cy1i4358ny2lcrxq4qap",
"rev": "72b9660dc18ba347f7cd41a9504fc181a6d87dc3",
"sha256": "1cqgpw263bz261bgz34j6hiawi4hi6smwp6981yz375fx0g6kmss",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs-channels/archive/4aa5466cbc741097218a1c494a7b832a17d1967d.tar.gz",
"url": "https://github.com/NixOS/nixpkgs-channels/archive/72b9660dc18ba347f7cd41a9504fc181a6d87dc3.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"xepkgs": {