go-stdlib-rust post (#215)
This commit is contained in:
parent
e460ebdcbe
commit
f106c2c9d2
|
@ -1301,6 +1301,18 @@ dependencies = [
|
||||||
"sha-1 0.8.2",
|
"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]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
|
@ -2507,6 +2519,7 @@ dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"comrak",
|
"comrak",
|
||||||
"envy",
|
"envy",
|
||||||
|
"eyre",
|
||||||
"glob",
|
"glob",
|
||||||
"go_vanity",
|
"go_vanity",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
@ -2516,12 +2529,15 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"patreon",
|
"patreon",
|
||||||
|
"pfacts",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
|
"reqwest",
|
||||||
"ructe",
|
"ructe",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_dhall",
|
"serde_dhall",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sitemap",
|
"sitemap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
|
@ -40,6 +40,12 @@ patreon = { path = "./lib/patreon" }
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = { version = "0.12", features = ["warp02"] }
|
ructe = { version = "0.12", features = ["warp02"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pfacts = "0"
|
||||||
|
serde_json = "1"
|
||||||
|
eyre = "0.6"
|
||||||
|
reqwest = { version = "0.10", features = ["json"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"./lib/go_vanity",
|
"./lib/go_vanity",
|
||||||
|
|
|
@ -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.
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -5,10 +5,10 @@
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"owner": "justinwoo",
|
"owner": "justinwoo",
|
||||||
"repo": "easy-dhall-nix",
|
"repo": "easy-dhall-nix",
|
||||||
"rev": "90957969850a44481c6e150350c56e8b53b29e1e",
|
"rev": "3e9101c5dfd69a9fc28fe4998aff378f91bfcb64",
|
||||||
"sha256": "1hsmp3cb0k554kh0jlfzpdzx2b8ndyh2gdykmw9hw41haaw16mmi",
|
"sha256": "1nsn1n4sx4za6jipcid1293rdw8lqgj9097s0khiij3fz0bzhrg9",
|
||||||
"type": "tarball",
|
"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"
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
},
|
},
|
||||||
"naersk": {
|
"naersk": {
|
||||||
|
@ -29,10 +29,10 @@
|
||||||
"homepage": "https://github.com/nmattia/niv",
|
"homepage": "https://github.com/nmattia/niv",
|
||||||
"owner": "nmattia",
|
"owner": "nmattia",
|
||||||
"repo": "niv",
|
"repo": "niv",
|
||||||
"rev": "fad2a6cbfb2e7cdebb7cb0ad2f5cc91e2c9bc06b",
|
"rev": "29ddaaf4e099c3ac0647f5b652469dfc79cd3b53",
|
||||||
"sha256": "0mghc1j0rd15spdjx81bayjqr0khc062cs25y5dcfzlxk4ynyc6m",
|
"sha256": "1va6myp07gkspgxfch8z3rs9nyvys6jmgzkys6a2c4j09qxp1bs0",
|
||||||
"type": "tarball",
|
"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"
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
|
@ -41,10 +41,10 @@
|
||||||
"homepage": "https://github.com/NixOS/nixpkgs",
|
"homepage": "https://github.com/NixOS/nixpkgs",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs-channels",
|
"repo": "nixpkgs-channels",
|
||||||
"rev": "4aa5466cbc741097218a1c494a7b832a17d1967d",
|
"rev": "72b9660dc18ba347f7cd41a9504fc181a6d87dc3",
|
||||||
"sha256": "0w6gg1hxcx83l8s83frxyjm5dwri06a3cy1i4358ny2lcrxq4qap",
|
"sha256": "1cqgpw263bz261bgz34j6hiawi4hi6smwp6981yz375fx0g6kmss",
|
||||||
"type": "tarball",
|
"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"
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
},
|
},
|
||||||
"xepkgs": {
|
"xepkgs": {
|
||||||
|
|
Loading…
Reference in New Issue