-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Patch-based workflow for tutorials in Pavex's documentation (#112)
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
1 parent
250a65c
commit 6b1d88c
Showing
24 changed files
with
1,011 additions
and
262 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ on: | |
- main | ||
|
||
jobs: | ||
buildDocs: | ||
build_pavex_cli: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout repository | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
* | ||
!*.patch | ||
!tutorial.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}!")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
+} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!(), | ||
+ } | ||
+ } | ||
+} |
Oops, something went wrong.