TL;DR Rust (#210)
* start mara code * better alt text * more mara tests * cleanups * blog: start tl;dr rust post * more words * feature complete * little oopses * oops lol
This commit is contained in:
parent
1e61d2ad33
commit
a2fba89738
|
@ -66,12 +66,6 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.1"
|
||||
|
@ -263,6 +257,32 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ba9b5e817f1bc1f2219b5a3474b69f838321b2a2ba8860d6a71c3bfe3d0fc1"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a99aa4aa18448eef4c7d3f86d2720d2d8cad5c860fe9ff9b279293efdc8f5be"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comrak"
|
||||
version = "0.8.2"
|
||||
|
@ -433,6 +453,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0f9683839e579a53258d377fcc0073ca0bf2042ac5e6c60a598069e64403a6d"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
|
@ -758,6 +788,12 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0bd112d44d9d870a6819eb505d04dd92b5e4d94bb8c304924a0872ae7016fb5"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.5.0"
|
||||
|
@ -1139,6 +1175,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1250cdd103eef6bd542b5ae82989f931fc00a41a27f60377338241594410f3"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.0"
|
||||
|
@ -1810,6 +1852,15 @@ dependencies = [
|
|||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06d5a3f5166fb5b42a5439f2eee8b9de149e235961e3eb21c5808fc3ea17ff3e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.0.0"
|
||||
|
@ -2033,9 +2084,21 @@ checksum = "dbdf4ccd1652592b01286a5dbe1e2a77d78afaa34beadd9872a5f7396f92aaa9"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.11"
|
||||
|
@ -2045,6 +2108,16 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.4"
|
||||
|
@ -2055,6 +2128,16 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4f5dd7095c2481b7b3cbed71c8de53085fb3542bc3c2b4c73cba43e8f11c7ba"
|
||||
dependencies = [
|
||||
"sharded-slab",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
|
@ -2414,8 +2497,8 @@ checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57"
|
|||
name = "xesite"
|
||||
version = "2.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"comrak",
|
||||
"envy",
|
||||
"glob",
|
||||
|
@ -2437,6 +2520,7 @@ dependencies = [
|
|||
"sitemap",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
"warp",
|
||||
"xml-rs",
|
||||
]
|
||||
|
|
|
@ -9,7 +9,7 @@ repository = "https://github.com/Xe/site"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
color-eyre = "0.5"
|
||||
chrono = "0.4"
|
||||
comrak = "0.8"
|
||||
envy = "0.4"
|
||||
|
@ -30,6 +30,7 @@ thiserror = "1"
|
|||
tokio = { version = "0.2", features = ["macros"] }
|
||||
warp = "0.2"
|
||||
xml-rs = "0.8"
|
||||
url = "2"
|
||||
|
||||
# workspace dependencies
|
||||
go_vanity = { path = "./lib/go_vanity" }
|
||||
|
|
|
@ -0,0 +1,674 @@
|
|||
---
|
||||
title: "TL;DR Rust"
|
||||
date: 2020-09-19
|
||||
tags:
|
||||
- rust
|
||||
- go
|
||||
- golang
|
||||
---
|
||||
|
||||
# TL;DR Rust
|
||||
|
||||
Recently I've been starting to use Rust more and more for larger and larger
|
||||
projects. As things have come up, I realized that I am missing a good reference
|
||||
for common things in Rust as compared to Go. This post contains a quick
|
||||
high-level overview of patterns in Rust and how they compare to patterns
|
||||
in Go. This will focus on code samples. This is no replacement for the [Rust
|
||||
book](https://doc.rust-lang.org/book/), but should help you get spun up on the
|
||||
various patterns used in Rust code.
|
||||
|
||||
Also I'm happy to introduce Mara to the blog!
|
||||
|
||||
[Hey, happy to be here! I'm Mara, I'll interject with side information,
|
||||
challenge assertions and more! Thanks for inviting
|
||||
me!](conversation://Mara/hacker)
|
||||
|
||||
Let's start somewhere simple: functions.
|
||||
|
||||
## Making Functions
|
||||
|
||||
Functions are defined using `fn` instead of `func`:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo() {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
Arguments can be passed by separating the name from the type with a colon:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo(bar int) {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo(bar: i32) {}
|
||||
```
|
||||
|
||||
### Returns
|
||||
|
||||
Values can be returned by adding `-> Type` to the function declaration:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo() int {
|
||||
return 2
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
return 2;
|
||||
}
|
||||
```
|
||||
|
||||
In Rust values can also be returned on the last statement without the `return`
|
||||
keyword or a terminating semicolon:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
2
|
||||
}
|
||||
```
|
||||
|
||||
[Hmm, what if I try to do something like this. Will this
|
||||
work?](conversation://Mara/hmm)
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
if some_cond {
|
||||
2
|
||||
}
|
||||
|
||||
4
|
||||
}
|
||||
```
|
||||
|
||||
Let's find out! The compiler spits back an error:
|
||||
|
||||
```
|
||||
error[E0308]: mismatched types
|
||||
--> src/lib.rs:3:9
|
||||
|
|
||||
2 | / if some_cond {
|
||||
3 | | 2
|
||||
| | ^ expected `()`, found integer
|
||||
4 | | }
|
||||
| | -- help: consider using a semicolon here
|
||||
| |_____|
|
||||
| expected this to be `()`
|
||||
```
|
||||
|
||||
This happens because most basic statements in Rust can return values. The best
|
||||
way to fix this would be to move the `4` return into an `else` block:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
if some_cond {
|
||||
2
|
||||
} else {
|
||||
4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Otherwise, the compiler will think you are trying to use that `if` as a
|
||||
statement, such as like this:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let val = if some_cond { 2 } else { 4 };
|
||||
```
|
||||
|
||||
### Functions that can fail
|
||||
|
||||
The [Result](https://doc.rust-lang.org/std/result/) type represents things that
|
||||
can fail with specific errors. The [eyre Result
|
||||
type](https://docs.rs/eyre) represents things that can fail
|
||||
with any error. For readability, this post will use the eyre Result type.
|
||||
|
||||
[The angle brackets in the `Result` type are arguments to the type, this allows
|
||||
the Result type to work across any type you could imagine.](conversation://Mara/hacker)
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
import "errors"
|
||||
|
||||
func divide(x, y int) (int, err) {
|
||||
if y == 0 {
|
||||
return 0, errors.New("cannot divide by zero")
|
||||
}
|
||||
|
||||
return x / y, nil
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use eyre::{eyre, Result};
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32> {
|
||||
match y {
|
||||
0 => Err(eyre!("cannot divide by zero")),
|
||||
_ => Ok(x / y),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Huh? I thought Rust had the <a
|
||||
href="https://doc.rust-lang.org/std/error/trait.Error.html">Error trait</a>,
|
||||
shouldn't you be able to use that instead of a third party package like
|
||||
eyre?](conversation://Mara/wat)
|
||||
|
||||
Let's try that, however we will need to make our own error type because the
|
||||
[`eyre!`](https://docs.rs/eyre/0.6.0/eyre/macro.eyre.html) macro creates its own
|
||||
transient error type on the fly.
|
||||
|
||||
First we need to make our own simple error type for a DivideByZero error:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DivideByZero;
|
||||
|
||||
impl fmt::Display for DivideByZero {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "cannot divide by zero")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DivideByZero {}
|
||||
```
|
||||
|
||||
So now let's use it:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32, DivideByZero> {
|
||||
match y {
|
||||
0 => Err(DivideByZero{}),
|
||||
_ => Ok(x / y),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However there is still one thing left: the function returns a DivideByZero
|
||||
error, not _any_ error like the [error interface in
|
||||
Go](https://godoc.org/builtin#error). In order to represent that we need to
|
||||
return something that implements the Error trait:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32, impl Error> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
And for the simple case, this will work. However as things get more complicated
|
||||
this simple facade will not work due to reality and its complexities. This is
|
||||
why I am shipping as much as I can out to other packages like eyre or
|
||||
[anyhow](https://docs.rs/anyhow). Check out this code in the [Rust
|
||||
Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=946057d8eb02f388cb3f03bae226d10d)
|
||||
to mess with this code interactively.
|
||||
|
||||
[Pro tip: eyre (via <a href="https://docs.rs/color-eyre">color-eyre</a>) also
|
||||
has support for adding <a href="https://docs.rs/color-eyre/0.5.4/color_eyre/#custom-sections-for-error-reports-via-help-trait">custom
|
||||
sections and context</a> to errors similar to Go's <a href="https://godoc.org/fmt#Errorf">`fmt.Errorf` `%w`
|
||||
format argument</a>, which will help in real world
|
||||
applications. When you do need to actually make your own errors, you may want to look into
|
||||
crates like <a href="https://docs.rs/thiserror">thiserror</a> to help with
|
||||
automatically generating your error implementation.](conversation://Mara/hacker)
|
||||
|
||||
### The `?` Operator
|
||||
|
||||
In Rust, the `?` operator checks for an error in a function call and if there is
|
||||
one, it automatically returns the error and gives you the result of the function
|
||||
if there was no error. This only works in functions that return either an Option
|
||||
or a Result.
|
||||
|
||||
[The <a href="https://doc.rust-lang.org/std/option/index.html">Option</a> type
|
||||
isn't shown in very much detail here, but it acts like a "this thing might not exist and it's your
|
||||
responsibility to check" container for any value. The closest analogue in Go is
|
||||
making a pointer to a value or possibly putting a value in an `interface{}`
|
||||
(which can be annoying to deal with in practice).](conversation://Mara/hacker)
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func doThing() (int, error) {
|
||||
result, err := divide(3, 4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use eyre::Result;
|
||||
|
||||
fn do_thing() -> Result<i32> {
|
||||
let result = divide(3, 4)?;
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
If the second argument of divide is changed to `0`, then `do_thing` will return
|
||||
an error.
|
||||
|
||||
[And how does that work with eyre?](conversation://Mara/hmm)
|
||||
|
||||
It works with eyre because eyre has its own error wrapper type called
|
||||
[`Report`](https://docs.rs/eyre/0.6.0/eyre/struct.Report.html), which can
|
||||
represent anything that implements the Error trait.
|
||||
|
||||
## Macros
|
||||
|
||||
Rust macros are function calls with `!` after their name:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
println!("hello, world");
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
Variables are created using `let`:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
var foo int
|
||||
var foo = 3
|
||||
foo := 3
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let foo: i32;
|
||||
let foo = 3;
|
||||
```
|
||||
|
||||
### Mutability
|
||||
|
||||
In Rust, every variable is immutable (unchangeable) by default. If we try to
|
||||
change those variables above we get a compiler error:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn main() {
|
||||
let foo: i32;
|
||||
let foo = 3;
|
||||
foo = 4;
|
||||
}
|
||||
```
|
||||
|
||||
This makes the compiler return this error:
|
||||
|
||||
```
|
||||
error[E0384]: cannot assign twice to immutable variable `foo`
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
3 | let foo = 3;
|
||||
| ---
|
||||
| |
|
||||
| first assignment to `foo`
|
||||
| help: make this binding mutable: `mut foo`
|
||||
4 | foo = 4;
|
||||
| ^^^^^^^ cannot assign twice to immutable variable
|
||||
```
|
||||
|
||||
As the compiler suggests, you can create a mutable variable by adding the `mut`
|
||||
keyword after the `let` keyword. There is no analog to this in Go.
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let mut foo: i32 = 0;
|
||||
foo = 4;
|
||||
```
|
||||
|
||||
[This is slightly a lie. There's more advanced cases involving interior
|
||||
mutability and other fun stuff like that, however this is a more advanced topic
|
||||
that isn't covered here.](conversation://Mara/hacker)
|
||||
|
||||
### Lifetimes
|
||||
|
||||
Rust does garbage collection at compile time. It also passes ownership of memory
|
||||
to functions as soon as possible. For example:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let quo = divide(4, 8)?;
|
||||
let other_quo = divide(quo, 5)?;
|
||||
|
||||
// Fails compile because ownership of quo was given to divide to create other_quo
|
||||
let yet_another_quo = divide(quo, 4)?;
|
||||
```
|
||||
|
||||
To work around this you can pass a reference to the divide function:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let other_quo = divide(&quo, 5);
|
||||
let yet_another_quo = divide(&quo, 4)?;
|
||||
```
|
||||
|
||||
Or even create a clone of it:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let other_quo = divide(quo.clone(), 5);
|
||||
let yet_another_quo = divide(quo, 4)?;
|
||||
```
|
||||
|
||||
[You can also get more fancy with <a
|
||||
href="https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html">explicit
|
||||
lifetime annotations</a>, however as of Rust's 2018 edition they aren't usually
|
||||
required unless you are doing something weird. This is something that is also
|
||||
covered in more detail in <a
|
||||
href="https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html">The
|
||||
Rust Book</a>.](conversation://Mara/hacker)
|
||||
|
||||
### Passing Mutability
|
||||
|
||||
Sometimes functions need mutable variables. To pass a mutable reference, add
|
||||
`&mut` before the name of the variable:
|
||||
|
||||
```rust
|
||||
let something = do_something_to_quo(&mut quo)?;
|
||||
```
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Imports
|
||||
|
||||
External dependencies are declared using the [Cargo.toml
|
||||
file](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html):
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
eyre = "0.6"
|
||||
```
|
||||
|
||||
This depends on the crate [anyhow](https://crates.io/anyhow) at version 1.0.x.
|
||||
|
||||
Dependencies can also have optional features:
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.10", features = ["json"] }
|
||||
```
|
||||
|
||||
This depends on the crate [reqwest](https://crates.io/reqwest) at version 0.10.x
|
||||
with the `json` feature enabled (in this case it enables reqwest being able to
|
||||
automagically convert things to/from json using Serde).
|
||||
|
||||
External dependencies can be used with the `use` statement:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
import "github.com/foo/bar"
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use foo; // -> foo now has the members of crate foo behind the :: operator
|
||||
use foo::Bar; // -> Bar is now exposed as a type in this file
|
||||
|
||||
use eyre::{eyre, Result}; // exposes the eyre! and Result members of eyre
|
||||
```
|
||||
|
||||
[This doesn't cover how the <a
|
||||
href="http://www.sheshbabu.com/posts/rust-module-system/">module system</a>
|
||||
works, however the post I linked there covers this better than I
|
||||
can.](conversation://Mara/hacker)
|
||||
|
||||
## Async/Await
|
||||
|
||||
Async functions may be interrupted to let other things execute as needed. This
|
||||
program uses [tokio](https://tokio.rs/) to handle async tasks. To run an async
|
||||
task and wait for its result, do this:
|
||||
|
||||
```
|
||||
// rust
|
||||
|
||||
let printer_fact = reqwest::get("https://printerfacts.cetacean.club/fact")
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
println!("your printer fact is: {}", printer_fact);
|
||||
```
|
||||
|
||||
This will populate `response` with an amusing fact about everyone's favorite
|
||||
household pet, the [printer](https://printerfacts.cetacean.club).
|
||||
|
||||
To make an async function, add the `async` keyword before the `fn` keyword:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
async fn get_text(url: String) -> Result<String> {
|
||||
reqwest::get(&url)
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
}
|
||||
```
|
||||
|
||||
This can then be called like this:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
let printer_fact = get_text("https://printerfacts.cetacean.club/fact").await?;
|
||||
```
|
||||
|
||||
## Public/Private Types and Functions
|
||||
|
||||
Rust has three privacy levels for functions:
|
||||
|
||||
- Only visible to the current file (no keyword, lowercase in Go)
|
||||
- Visible to anything in the current crate (`pub(crate)`, internal packages in
|
||||
go)
|
||||
- Visible to everyone (`pub`, upper case in Go)
|
||||
|
||||
[You can't get a perfect analog to `pub(crate)` in Go, but <a
|
||||
href="https://docs.google.com/document/d/1e8kOo3r51b2BWtTs_1uADIA5djfXhPT36s6eHVRIvaU/edit">internal
|
||||
packages can get close to this behavior.](conversation://Mara/hacker)
|
||||
|
||||
## Structures
|
||||
|
||||
Rust structures are created using the `struct` keyword:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
type Client struct {
|
||||
Token string
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
pub struct Client {
|
||||
pub token: String,
|
||||
}
|
||||
```
|
||||
|
||||
If the `pub` keyword is not specified before a member name, it will not be
|
||||
usable outside the Rust source code file it is defined in:
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
token string
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
pub(crate) struct Client {
|
||||
token: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Encoding structs to JSON
|
||||
|
||||
[serde](https://serde.rs) is used to convert structures to json. The Rust
|
||||
compiler's
|
||||
[derive](https://doc.rust-lang.org/stable/rust-by-example/trait/derive.html)
|
||||
feature is used to automatically implement the conversion logic.
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct Response {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
Rust has a few string types that do different things. You can read more about
|
||||
this [here](https://fasterthanli.me/blog/2020/working-with-strings-in-rust/),
|
||||
but at a high level most projects only uses a few of them:
|
||||
|
||||
- `&str`, a slice reference to a String owned by someone else
|
||||
- String, an owned UTF-8 string
|
||||
- PathBuf, a filepath string (encoded in whatever encoding the OS running this
|
||||
code uses for filesystems)
|
||||
|
||||
The strings are different types for safety reasons. See the linked blogpost for
|
||||
more detail about this.
|
||||
|
||||
## Enumerations / Tagged Unions
|
||||
|
||||
Enumerations, also known as tagged unions, are a way to specify a superposition
|
||||
of one of a few different kinds of values in one type. The main place they are
|
||||
used in this project is for command line parsing with
|
||||
[structopt](https://docs.rs/structopt/0.3.14/structopt/). There is no easy
|
||||
analog for this in Go.
|
||||
|
||||
```rust
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(about = "A simple release management tool")]
|
||||
pub(crate) enum Cmd {
|
||||
/// Creates a new release for a git repo
|
||||
Cut {
|
||||
#[structopt(flatten)]
|
||||
common: Common,
|
||||
/// Changelog location
|
||||
#[structopt(long, short, default_value="./CHANGELOG.md")]
|
||||
changelog: PathBuf,
|
||||
},
|
||||
|
||||
/// Runs releases as triggered by GitHub Actions
|
||||
GitHubAction {
|
||||
#[structopt(flatten)]
|
||||
gha: GitHubAction,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Enum variants can be matched using the `match` keyword:
|
||||
|
||||
```rust
|
||||
match cmd {
|
||||
Cmd::Cut { common, changelog } => {
|
||||
cmd::cut::run(common, changelog).await
|
||||
}
|
||||
Cmd::GitHubAction { gha } => {
|
||||
cmd::github_action::run(gha).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All variants of an enum must be matched in order for the code to compile.
|
||||
|
||||
## Testing
|
||||
|
||||
Test functions need to be marked with the `#[test]` annotation, then they will
|
||||
be run alongside `cargo test`:
|
||||
|
||||
```rust
|
||||
mod tests { // not required but it is good practice
|
||||
#[test]
|
||||
fn math_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
|
||||
#[tokio::test] // needs tokio as a dependency
|
||||
async fn http_works() {
|
||||
let _ = get_html("https://within.website").await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Avoid the use of `unwrap()` outside of tests. In the wrong cases, using
|
||||
`unwrap()` in production code can cause the server to crash and can incur data
|
||||
loss.
|
||||
|
||||
[Alternatively, you can also use the <a href="https://learning-rust.github.io/docs/e4.unwrap_and_expect.html#expect">`.expect()`</a> method instead
|
||||
of `.unwrap()`. This lets you attach a message that will be shown when the
|
||||
result isn't Ok.](conversation://Mara/hacker)
|
||||
|
||||
---
|
||||
|
||||
This is by no means comprehensive, see the rust book or [Learn X in Y Minutes
|
||||
Where X = Rust](https://learnxinyminutes.com/docs/rust/) for more information.
|
||||
This code is written to be as boring and obvious as possible. If things don't
|
||||
make sense, please reach out and don't be afraid to ask questions.
|
|
@ -33,6 +33,7 @@ mkShell {
|
|||
|
||||
SITE_PREFIX = "devel.";
|
||||
CLACK_SET = "Ashlynn,Terry Davis,Dennis Ritchie";
|
||||
RUST_LOG = "info";
|
||||
RUST_LOG = "debug";
|
||||
RUST_BACKTRACE = "1";
|
||||
GITHUB_SHA = "devel";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
use color_eyre::eyre::{Result, WrapErr};
|
||||
use comrak::nodes::{Ast, AstNode, NodeValue};
|
||||
use comrak::{format_html, parse_document, markdown_to_html, Arena, ComrakOptions};
|
||||
use std::cell::RefCell;
|
||||
use crate::templates::Html;
|
||||
use url::Url;
|
||||
|
||||
pub fn render(inp: &str) -> Result<String> {
|
||||
let mut options = ComrakOptions::default();
|
||||
|
||||
options.extension.autolink = true;
|
||||
options.extension.table = true;
|
||||
options.extension.description_lists = true;
|
||||
options.extension.superscript = true;
|
||||
options.extension.strikethrough = true;
|
||||
options.extension.footnotes = true;
|
||||
|
||||
options.render.unsafe_ = true;
|
||||
|
||||
let arena = Arena::new();
|
||||
let root = parse_document(&arena, inp, &options);
|
||||
|
||||
iter_nodes(root, &|node| {
|
||||
let mut data = node.data.borrow_mut();
|
||||
match &mut data.value {
|
||||
&mut NodeValue::Link(ref mut link) => {
|
||||
let base = Url::parse("https://christine.website/")?;
|
||||
let u = base.join(std::str::from_utf8(&link.url.clone())?)?;
|
||||
if u.scheme() != "conversation" {
|
||||
return Ok(());
|
||||
}
|
||||
let parent = node.parent().unwrap();
|
||||
node.detach();
|
||||
let mut message = vec![];
|
||||
for child in node.children() {
|
||||
format_html(child, &options, &mut message)?;
|
||||
}
|
||||
let message = std::str::from_utf8(&message)?;
|
||||
let message = markdown_to_html(message, &options);
|
||||
let mood = without_first(u.path());
|
||||
let name = u.host_str().unwrap_or("Mara");
|
||||
|
||||
let mut html = vec![];
|
||||
crate::templates::mara(&mut html, mood, name, Html(message))?;
|
||||
|
||||
let new_node =
|
||||
arena.alloc(AstNode::new(RefCell::new(Ast::new(NodeValue::HtmlInline(html)))));
|
||||
parent.append(new_node);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut html = vec![];
|
||||
format_html(root, &options, &mut html).unwrap();
|
||||
|
||||
String::from_utf8(html).wrap_err("post is somehow invalid UTF-8")
|
||||
}
|
||||
|
||||
fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F) -> Result<()>
|
||||
where
|
||||
F: Fn(&'a AstNode<'a>) -> Result<()>,
|
||||
{
|
||||
f(node)?;
|
||||
for c in node.children() {
|
||||
iter_nodes(c, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn without_first(string: &str) -> &str {
|
||||
string
|
||||
.char_indices()
|
||||
.nth(1)
|
||||
.and_then(|(i, _)| string.get(i..))
|
||||
.unwrap_or("")
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{post::Post, signalboost::Person};
|
||||
use anyhow::Result;
|
||||
use comrak::{markdown_to_html, ComrakOptions};
|
||||
use color_eyre::eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
pub mod markdown;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "clackSet")]
|
||||
|
@ -14,21 +15,6 @@ pub struct Config {
|
|||
resume_fname: PathBuf,
|
||||
}
|
||||
|
||||
pub fn markdown(inp: &str) -> String {
|
||||
let mut options = ComrakOptions::default();
|
||||
|
||||
options.extension.autolink = true;
|
||||
options.extension.table = true;
|
||||
options.extension.description_lists = true;
|
||||
options.extension.superscript = true;
|
||||
options.extension.strikethrough = true;
|
||||
options.extension.footnotes = true;
|
||||
|
||||
options.render.unsafe_ = true;
|
||||
|
||||
markdown_to_html(inp, &options)
|
||||
}
|
||||
|
||||
async fn patrons() -> Result<Option<patreon::Users>> {
|
||||
use patreon::*;
|
||||
let creds: Credentials = envy::prefixed("PATREON_").from_env().unwrap();
|
||||
|
@ -72,7 +58,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> {
|
|||
let cfg: Config = serde_dhall::from_file(cfg).parse()?;
|
||||
let sb = cfg.signalboost.clone();
|
||||
let resume = fs::read_to_string(cfg.resume_fname.clone())?;
|
||||
let resume: String = markdown(&resume);
|
||||
let resume: String = markdown::render(&resume)?;
|
||||
let blog = crate::post::load("blog")?;
|
||||
let gallery = crate::post::load("gallery")?;
|
||||
let talks = crate::post::load("talks")?;
|
||||
|
@ -145,7 +131,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use color_eyre::eyre::Result;
|
||||
#[tokio::test]
|
||||
async fn init() -> Result<()> {
|
||||
let _ = pretty_env_logger::try_init();
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::Result;
|
||||
use color_eyre::eyre::Result;
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response};
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
use std::sync::Arc;
|
||||
|
@ -21,6 +21,7 @@ fn with_state(
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let _ = kankyo::init();
|
||||
pretty_env_logger::init();
|
||||
log::info!("starting up commit {}", env!("GITHUB_SHA"));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/// This code was borrowed from @fasterthanlime.
|
||||
|
||||
use anyhow::{Result};
|
||||
use color_eyre::eyre::{Result};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use chrono::prelude::*;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
use glob::glob;
|
||||
use std::{cmp::Ordering, fs};
|
||||
|
||||
|
@ -75,8 +75,10 @@ pub fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
|
||||
for path in glob(&format!("{}/*.markdown", dir))?.filter_map(Result::ok) {
|
||||
log::debug!("loading {:?}", path);
|
||||
let body = fs::read_to_string(path.clone()).expect("things to work");
|
||||
let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str()).expect("stuff to work");
|
||||
let body =
|
||||
fs::read_to_string(path.clone()).wrap_err_with(|| format!("can't read {:?}", path))?;
|
||||
let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str())
|
||||
.wrap_err_with(|| format!("can't parse frontmatter of {:?}", path))?;
|
||||
let markup = &body[content_offset..];
|
||||
let date = NaiveDate::parse_from_str(&fm.clone().date, "%Y-%m-%d")?;
|
||||
|
||||
|
@ -84,7 +86,8 @@ pub fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
front_matter: fm,
|
||||
link: format!("{}/{}", dir, path.file_stem().unwrap().to_str().unwrap()),
|
||||
body: markup.to_string(),
|
||||
body_html: crate::app::markdown(&markup),
|
||||
body_html: crate::app::markdown::render(&markup)
|
||||
.wrap_err_with(|| format!("can't parse markdown for {:?}", path))?,
|
||||
date: {
|
||||
DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)),
|
||||
|
@ -97,7 +100,7 @@ pub fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
}
|
||||
|
||||
if result.len() == 0 {
|
||||
Err(anyhow!("no posts loaded"))
|
||||
Err(eyre!("no posts loaded"))
|
||||
} else {
|
||||
result.sort();
|
||||
result.reverse();
|
||||
|
@ -108,7 +111,7 @@ pub fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use color_eyre::eyre::Result;
|
||||
|
||||
#[test]
|
||||
fn blog() {
|
||||
|
|
|
@ -13,7 +13,7 @@ pub struct Person {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use color_eyre::eyre::Result;
|
||||
#[test]
|
||||
fn load() -> Result<()> {
|
||||
let _people: Vec<super::Person> = serde_dhall::from_file("./signalboost.dhall").parse()?;
|
||||
|
|
|
@ -62,61 +62,62 @@
|
|||
<p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p>
|
||||
}
|
||||
|
||||
<script>
|
||||
<p>The art for Mara was drawn by <a href="https://selic.re/">Selicre</a>.</p>
|
||||
|
||||
// The actual function. Set this as an onclick function for your "Share on Mastodon" button
|
||||
function share_on_mastodon() @{
|
||||
<script>
|
||||
// The actual function. Set this as an onclick function for your "Share on Mastodon" button
|
||||
function share_on_mastodon() @{
|
||||
// Prefill the form with the user's previously-specified Mastodon instance, if applicable
|
||||
var default_url = localStorage['mastodon_instance'];
|
||||
|
||||
// If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt.
|
||||
if (!default_url)
|
||||
default_url = "https://";
|
||||
default_url = "https://";
|
||||
|
||||
var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url);
|
||||
if (instance) @{
|
||||
// Handle URL formats
|
||||
if ( !instance.startsWith("https://") && !instance.startsWith("http://") )
|
||||
instance = "https://" + instance;
|
||||
// Handle URL formats
|
||||
if ( !instance.startsWith("https://") && !instance.startsWith("http://") )
|
||||
instance = "https://" + instance;
|
||||
|
||||
// get the current page's url
|
||||
var url = window.location.href;
|
||||
|
||||
// get the page title from the og:title meta tag, if it exists.
|
||||
var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content");
|
||||
|
||||
// Otherwise, use the <title> tag as the title
|
||||
if (!title) var title = document.getElementsByTagName("title")[0].innerHTML;
|
||||
|
||||
// Handle slash
|
||||
if ( !instance.endsWith("/") )
|
||||
instance = instance + "/";
|
||||
// get the current page's url
|
||||
var url = window.location.href;
|
||||
|
||||
// Cache the instance/domain for future requests
|
||||
localStorage['mastodon_instance'] = instance;
|
||||
// get the page title from the og:title meta tag, if it exists.
|
||||
var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content");
|
||||
|
||||
// Hashtags
|
||||
var hashtags = "#blogpost";
|
||||
// Otherwise, use the <title> tag as the title
|
||||
if (!title) var title = document.getElementsByTagName("title")[0].innerHTML;
|
||||
|
||||
@if post.front_matter.series.is_some() {
|
||||
hashtags += "#@post.front_matter.series.as_ref().unwrap()";
|
||||
}
|
||||
// Handle slash
|
||||
if ( !instance.endsWith("/") )
|
||||
instance = instance + "/";
|
||||
|
||||
@if post.front_matter.tags.is_some() {
|
||||
hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag }";
|
||||
}
|
||||
// Cache the instance/domain for future requests
|
||||
localStorage['mastodon_instance'] = instance;
|
||||
|
||||
// Tagging users, such as offical accounts or the author of the post
|
||||
var author = "@@cadey@@mst3k.interlinked.me";
|
||||
// Hashtags
|
||||
var hashtags = "#blogpost";
|
||||
|
||||
// Create the Share URL
|
||||
// https://someinstance.tld/share?text=URL%20encoded%20text
|
||||
mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author);
|
||||
@if post.front_matter.series.is_some() {
|
||||
hashtags += "#@post.front_matter.series.as_ref().unwrap()";
|
||||
}
|
||||
|
||||
// Open a new window at the share location
|
||||
window.open(mastodon_url, '_blank');
|
||||
@if post.front_matter.tags.is_some() {
|
||||
hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag }";
|
||||
}
|
||||
|
||||
// Tagging users, such as offical accounts or the author of the post
|
||||
var author = "@@cadey@@mst3k.interlinked.me";
|
||||
|
||||
// Create the Share URL
|
||||
// https://someinstance.tld/share?text=URL%20encoded%20text
|
||||
mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author);
|
||||
|
||||
// Open a new window at the share location
|
||||
window.open(mastodon_url, '_blank');
|
||||
@}
|
||||
@}
|
||||
@}
|
||||
</script>
|
||||
|
||||
@:footer_html()
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
@(mood: &str, character: &str, message: Html<String>)
|
||||
<div class="grid conversation">
|
||||
<div class="cell -2of12">
|
||||
<div class="content">
|
||||
<picture>
|
||||
<source srcset="https://cdn.christine.website/file/christine-static/stickers/@character.to_lowercase()/@(mood).webp" height="75%" width="75%" type="image/webp">
|
||||
<img src="https://cdn.christine.website/file/christine-static/stickers/@character.to_lowercase()/@(mood).png" height="75%" width="75%" alt="@character is @mood">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell -10of12">
|
||||
<div class="content">
|
||||
<p>
|
||||
<b>@character</b>
|
||||
</p>
|
||||
<blockquote>
|
||||
@message
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue