Skip to content

Commit

Permalink
Patch-based workflow for tutorials in Pavex's documentation (#112)
Browse files Browse the repository at this point in the history
Snippets and error messages in tutorials tend to go stale: the codebase
changes and there is no automated mechanism to remind you which snippets
need to be updated.
This PR migrates Pavex's tutorial to a patch-based workflow: no snippet
is "hard-coded" in the docs. Everything is generated from a starter
project and a series of `git` patches, applied one after the other.
Each code example from the tutorial is "just" an include from one of the
materialized stages of the tutorial project. The same applies to error
messages: we auto-generate them by running commands against a specific
stage of the tutorial project.

To get this working properly, I had to make a few more changes:
- `pavex_cli` will now consider the `color` setting (and the shell
properties) when emitting the `ERROR` header. It used to be always
coloured, no matter what.
- We no longer pull the template for `pavex new` down from GitHub: it's
now embedded into the binary. This solves some annoying problems with
GitHub Actions (i.e. the commit SHA that an action checks out is a
detached state which doesn't exist on the corresponding branch,
resulting in errors). It also allows `pavex new` to work offline and
removes the need for a patched `cargo-generate`.
  • Loading branch information
LukeMathWalker authored Dec 10, 2023
1 parent 250a65c commit 6b1d88c
Show file tree
Hide file tree
Showing 24 changed files with 1,011 additions and 262 deletions.
74 changes: 73 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
- main

jobs:
buildDocs:
build_pavex_cli:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand All @@ -21,6 +21,57 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: "./libs -> ./libs/target"
key: "build-pavex-cli"
- name: Build CLI
run: |
cd libs
cargo build --package pavex_cli --bin pavex --release
- name: Store CLI artifact
uses: actions/upload-artifact@v3
with:
name: pavex_cli
path: libs/target/release/pavex

build_tutorial_generator:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/[email protected]
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: "./doc_examples/tutorial_generator -> ./doc_examples/tutorial_generator/target"
- name: Build CLI
run: |
cd doc_examples/tutorial_generator
cargo build
- name: Store CLI artifact
uses: actions/upload-artifact@v3
with:
name: tutorial_generator
path: doc_examples/tutorial_generator/target/debug/tutorial_generator

build_docs:
runs-on: ubuntu-latest
needs:
- build_pavex_cli
- build_tutorial_generator
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set git identity
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
- name: Install Rust
uses: actions-rust-lang/[email protected]
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: "./libs -> ./libs/target"
key: "build-pavex-docs"
- name: Build API reference
run: |
cd libs
Expand All @@ -34,6 +85,27 @@ jobs:
with:
fail: true
args: --base . --exclude-loopback --exclude-path="docs/api_reference" --require-https --verbose --no-progress docs
- name: Download pavex CLI artifact
uses: actions/download-artifact@v3
with:
name: pavex_cli
path: ~/.cargo/bin
- name: Mark as executable
run: chmod +x ~/.cargo/bin/pavex
- name: Download tutorial_generator CLI artifact
uses: actions/download-artifact@v3
with:
name: tutorial_generator
path: ~/.cargo/bin
- name: Mark as executable
run: chmod +x ~/.cargo/bin/tutorial_generator
- uses: cargo-bins/cargo-binstall@main
- name: Install cargo-px
run: cargo binstall -y --github-token=${{ secrets.GITHUB_TOKEN }} [email protected]
- name: Generate quickstart tutorial
run: |
cd doc_examples/quickstart
tutorial_generator
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/libs/target
/libs/vendor
/libs/.cargo
/doc_examples/**/target
/examples/**/vendor
/examples/**/target
/examples/**/.cargo
Expand Down
3 changes: 3 additions & 0 deletions doc_examples/quickstart/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!*.patch
!tutorial.yml
33 changes: 33 additions & 0 deletions doc_examples/quickstart/01.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs
index 4a196d8..a4224a1 100644
--- a/demo/src/blueprint.rs
+++ b/demo/src/blueprint.rs
@@ -4,6 +4,7 @@ use pavex::f;

/// The main blueprint, containing all the routes, constructors and error handlers
/// required by our API.
+// --8<-- [start:blueprint_definition]
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
register_common_constructors(&mut bp);
@@ -13,6 +14,7 @@ pub fn blueprint() -> Blueprint {
bp.route(GET, "/api/ping", f!(crate::routes::status::ping));
bp
}
+// --8<-- [end:blueprint_definition]

/// Common constructors used by all routes.
fn register_common_constructors(bp: &mut Blueprint) {
@@ -26,10 +28,12 @@ fn register_common_constructors(bp: &mut Blueprint) {
));

// Route parameters
+ // --8<-- [start:route_params_constructor]
bp.constructor(
f!(pavex::request::route::RouteParams::extract),
Lifecycle::RequestScoped,
)
+ // --8<-- [end:route_params_constructor]
.error_handler(f!(
pavex::request::route::errors::ExtractRouteParamsError::into_response
));
33 changes: 33 additions & 0 deletions doc_examples/quickstart/02.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs
--- a/demo/src/blueprint.rs
+++ b/demo/src/blueprint.rs
@@ -12,6 +12,11 @@ pub fn blueprint() -> Blueprint {
add_telemetry_middleware(&mut bp);

bp.route(GET, "/api/ping", f!(crate::routes::status::ping));
+ bp.route(
+ GET,
+ "/api/greet/:name", /* (1)! */
+ f!(crate::routes::greet::greet),
+ );
bp
}
// <--8-- [start:blueprint_definition]
diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs
new file mode 100644
--- /dev/null
+++ b/demo/src/routes/greet.rs
@@ -0,0 +1,5 @@
+use pavex::response::Response;
+
+pub fn greet() -> Response {
+ todo!()
+}
diff --git a/demo/src/routes/mod.rs b/demo/src/routes/mod.rs
--- a/demo/src/routes/mod.rs
+++ b/demo/src/routes/mod.rs
@@ -1 +1,2 @@
-pub mod status;
\ No newline at end of file
+pub mod greet;
+pub mod status;
17 changes: 17 additions & 0 deletions doc_examples/quickstart/03.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs
index 38ec1e3..adfbbd5 100644
--- a/demo/src/routes/greet.rs
+++ b/demo/src/routes/greet.rs
@@ -1,5 +1,11 @@
+use pavex::request::RouteParams;
use pavex::response::Response;

-pub fn greet() -> Response {
+#[RouteParams]
+pub struct GreetParams {
+ pub name: String,
+}
+
+pub fn greet(params: RouteParams<GreetParams> /* (2)! */) -> Response {
todo!()
}
22 changes: 22 additions & 0 deletions doc_examples/quickstart/04.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs
index adfbbd5..fbcb3fc 100644
--- a/demo/src/routes/greet.rs
+++ b/demo/src/routes/greet.rs
@@ -1,4 +1,4 @@
-use pavex::request::RouteParams;
+use pavex::request::route::RouteParams;
use pavex::response::Response;

#[RouteParams]
@@ -6,6 +6,9 @@ pub struct GreetParams {
pub name: String,
}

-pub fn greet(params: RouteParams<GreetParams> /* (2)! */) -> Response {
- todo!()
+pub fn greet(params: RouteParams<GreetParams>) -> Response {
+ let GreetParams { name }/* (1)! */ = params.0;
+ Response::ok() // (2)!
+ .set_typed_body(format!("Hello, {name}!")) // (3)!
+ .box_body()
}
37 changes: 37 additions & 0 deletions doc_examples/quickstart/05-bis.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs
index f1434bb..88fd441 100644
--- a/demo/src/blueprint.rs
+++ b/demo/src/blueprint.rs
@@ -12,11 +12,7 @@ pub fn blueprint() -> Blueprint {
add_telemetry_middleware(&mut bp);

bp.route(GET, "/api/ping", f!(crate::routes::status::ping));
- bp.route(
- GET,
- "/api/greet/:name", /* (1)! */
- f!(crate::routes::greet::greet),
- );
+ bp.route(GET, "/api/greet/:name", f!(crate::routes::greet::greet));
bp
}
// --8<-- [end:blueprint_definition]
diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs
index 220a89e..526c16f 100644
--- a/demo/src/routes/greet.rs
+++ b/demo/src/routes/greet.rs
@@ -11,14 +11,12 @@ pub struct GreetParams {
pub name: String,
}

-// --8<-- [start:user_agent]
-pub fn greet(params: RouteParams<GreetParams>, user_agent: UserAgent /* (1)! */) -> Response {
+pub fn greet(params: RouteParams<GreetParams>, user_agent: UserAgent) -> Response {
if let UserAgent::Unknown = user_agent {
return Response::unauthorized()
.set_typed_body("You must provide a `User-Agent` header")
.box_body();
}
- // --8<-- [end:user_agent]
let GreetParams { name } = params.0;
Response::ok()
.set_typed_body(format!("Hello, {name}!"))
61 changes: 61 additions & 0 deletions doc_examples/quickstart/05.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
diff --git a/demo/src/lib.rs b/demo/src/lib.rs
index f8a6fe0..6fe333a 100644
--- a/demo/src/lib.rs
+++ b/demo/src/lib.rs
@@ -1,6 +1,7 @@
+pub use blueprint::blueprint;
+
mod blueprint;
pub mod configuration;
pub mod routes;
pub mod telemetry;
-
-pub use blueprint::blueprint;
+pub mod user_agent;
diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs
index fbcb3fc..662c93c 100644
--- a/demo/src/routes/greet.rs
+++ b/demo/src/routes/greet.rs
@@ -1,14 +1,26 @@
use pavex::request::route::RouteParams;
use pavex::response::Response;

+// --8<-- [start:user_agent_import]
+use crate::user_agent::UserAgent;
+
+// --8<-- [end:user_agent_import]
+
#[RouteParams]
pub struct GreetParams {
pub name: String,
}

-pub fn greet(params: RouteParams<GreetParams>) -> Response {
- let GreetParams { name }/* (1)! */ = params.0;
- Response::ok() // (2)!
- .set_typed_body(format!("Hello, {name}!")) // (3)!
+// --8<-- [start:user_agent]
+pub fn greet(params: RouteParams<GreetParams>, user_agent: UserAgent /* (1)! */) -> Response {
+ if let UserAgent::Unknown = user_agent {
+ return Response::unauthorized()
+ .set_typed_body("You must provide a `User-Agent` header")
+ .box_body();
+ }
+ // --8<-- [end:user_agent]
+ let GreetParams { name } = params.0;
+ Response::ok()
+ .set_typed_body(format!("Hello, {name}!"))
.box_body()
}
diff --git a/demo/src/user_agent.rs b/demo/src/user_agent.rs
new file mode 100644
index 0000000..f16d4c1
--- /dev/null
+++ b/demo/src/user_agent.rs
@@ -0,0 +1,6 @@
+pub enum UserAgent {
+ /// No `User-Agent` header was provided.
+ Unknown,
+ /// The value of the `User-Agent` header for the incoming request.
+ Known(String),
+}
58 changes: 58 additions & 0 deletions doc_examples/quickstart/06.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs
index f1434bb..e74936a 100644
--- a/demo/src/blueprint.rs
+++ b/demo/src/blueprint.rs
@@ -4,11 +4,17 @@ use pavex::f;

/// The main blueprint, containing all the routes, constructors and error handlers
/// required by our API.
-// --8<-- [start:blueprint_definition]
+// --8<-- [start:new_constructor_registration]
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
register_common_constructors(&mut bp);

+ bp.constructor(
+ f!(crate::user_agent::UserAgent::extract),
+ Lifecycle::RequestScoped,
+ );
+ // --8<-- [end:new_constructor_registration]
+
add_telemetry_middleware(&mut bp);

bp.route(GET, "/api/ping", f!(crate::routes::status::ping));
@@ -19,7 +25,6 @@ pub fn blueprint() -> Blueprint {
);
bp
}
-// --8<-- [end:blueprint_definition]

/// Common constructors used by all routes.
fn register_common_constructors(bp: &mut Blueprint) {
diff --git a/demo/src/user_agent.rs b/demo/src/user_agent.rs
index f16d4c1..fb72632 100644
--- a/demo/src/user_agent.rs
+++ b/demo/src/user_agent.rs
@@ -1,6 +1,20 @@
+use pavex::http::header::USER_AGENT;
+use pavex::request::RequestHead;
+
pub enum UserAgent {
- /// No `User-Agent` header was provided.
Unknown,
- /// The value of the `User-Agent` header for the incoming request.
Known(String),
}
+
+impl UserAgent {
+ pub fn extract(request_head: &RequestHead) -> Self {
+ let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
+ return Self::Unknown;
+ };
+
+ match user_agent.to_str() {
+ Ok(s) => Self::Known(s.into()),
+ Err(_e) => todo!(),
+ }
+ }
+}
Loading

0 comments on commit 6b1d88c

Please sign in to comment.