diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 130559162..c1df80a4f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -53,7 +53,7 @@ jobs: name: tutorial_generator path: doc_examples/tutorial_generator/target/debug/tutorial_generator - build_docs: + is_up_to_date: runs-on: ubuntu-latest needs: - build_pavex_cli @@ -72,24 +72,6 @@ jobs: components: rust-docs-json - name: Install Rust stable toolchain uses: actions-rust-lang/setup-rust-toolchain@v1.6.0 - - name: Cache dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: "./libs -> ./libs/target" - key: "build-pavex-docs" - - name: Build API reference - run: | - cd libs - cargo doc --package pavex --no-deps - - name: Copy API reference files - run: | - mkdir -p docs/api_reference - cp -r libs/target/doc/* docs/api_reference - - name: Link Checker - uses: lycheeverse/lychee-action@v1 - 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: @@ -111,6 +93,32 @@ jobs: run: | cd doc_examples/quickstart tutorial_generator --verify + + build_docs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust stable toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1.6.0 + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "./libs -> ./libs/target" + key: "build-pavex-docs" + - name: Build API reference + run: | + cd libs + cargo doc --package pavex --no-deps + - name: Copy API reference files + run: | + mkdir -p docs/api_reference + cp -r libs/target/doc/* docs/api_reference + - name: Link Checker + uses: lycheeverse/lychee-action@v1 + with: + fail: true + args: --base . --exclude-loopback --exclude-path="docs/api_reference" --require-https --verbose --no-progress docs - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx diff --git a/doc_examples/quickstart/01.patch b/doc_examples/quickstart/01.patch deleted file mode 100644 index abda6dca4..000000000 --- a/doc_examples/quickstart/01.patch +++ /dev/null @@ -1,33 +0,0 @@ -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 - )); diff --git a/doc_examples/quickstart/02-new_submodule.snap b/doc_examples/quickstart/02-new_submodule.snap new file mode 100644 index 000000000..497cef2e1 --- /dev/null +++ b/doc_examples/quickstart/02-new_submodule.snap @@ -0,0 +1,4 @@ +```rust title="demo/src/routes/mod.rs" hl_lines="1" +pub mod greet; +pub mod status; +``` \ No newline at end of file diff --git a/doc_examples/quickstart/02-register_new_route.snap b/doc_examples/quickstart/02-register_new_route.snap new file mode 100644 index 000000000..22cce8734 --- /dev/null +++ b/doc_examples/quickstart/02-register_new_route.snap @@ -0,0 +1,16 @@ +```rust title="demo/src/blueprint.rs" hl_lines="9 10 11 12 13" +\\ [...] +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + register_common_constructors(&mut bp); + + 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 +``` \ No newline at end of file diff --git a/doc_examples/quickstart/02-route_def.snap b/doc_examples/quickstart/02-route_def.snap new file mode 100644 index 000000000..13ad28721 --- /dev/null +++ b/doc_examples/quickstart/02-route_def.snap @@ -0,0 +1,7 @@ +```rust title="demo/src/routes/greet.rs" +use pavex::response::Response; + +pub fn greet() -> Response { + todo!() +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/02.patch b/doc_examples/quickstart/02.patch index ecd14fb8e..a4b88c724 100644 --- a/doc_examples/quickstart/02.patch +++ b/doc_examples/quickstart/02.patch @@ -1,9 +1,10 @@ diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs +index 4a196d8..0a6857d 100644 --- a/demo/src/blueprint.rs +++ b/demo/src/blueprint.rs -@@ -12,6 +12,11 @@ pub fn blueprint() -> Blueprint { +@@ -11,6 +11,11 @@ pub fn blueprint() -> Blueprint { add_telemetry_middleware(&mut bp); - + bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); + bp.route( + GET, @@ -12,9 +13,10 @@ diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs + ); bp } - // <--8-- [start:blueprint_definition] + diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs new file mode 100644 +index 0000000..38ec1e3 --- /dev/null +++ b/demo/src/routes/greet.rs @@ -0,0 +1,5 @@ @@ -24,10 +26,9 @@ new file mode 100644 + todo!() +} diff --git a/demo/src/routes/mod.rs b/demo/src/routes/mod.rs +index 822c729..d709a21 100644 --- 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; + pub mod status; diff --git a/doc_examples/quickstart/03-route_def.snap b/doc_examples/quickstart/03-route_def.snap new file mode 100644 index 000000000..77da6225d --- /dev/null +++ b/doc_examples/quickstart/03-route_def.snap @@ -0,0 +1,13 @@ +```rust title="demo/src/routes/greet.rs" +use pavex::request::RouteParams; +use pavex::response::Response; + +#[RouteParams] +pub struct GreetParams { + pub name: String, /* (1)! */ +} + +pub fn greet(params: RouteParams /* (2)! */) -> Response { + todo!() +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/03.patch b/doc_examples/quickstart/03.patch index 531c0fe57..20e042d6c 100644 --- a/doc_examples/quickstart/03.patch +++ b/doc_examples/quickstart/03.patch @@ -9,7 +9,7 @@ index 38ec1e3..adfbbd5 100644 -pub fn greet() -> Response { +#[RouteParams] +pub struct GreetParams { -+ pub name: String, ++ pub name: String /* (1)! */ +} + +pub fn greet(params: RouteParams /* (2)! */) -> Response { diff --git a/doc_examples/quickstart/04-register_common_invocation.snap b/doc_examples/quickstart/04-register_common_invocation.snap new file mode 100644 index 000000000..a7b74fad0 --- /dev/null +++ b/doc_examples/quickstart/04-register_common_invocation.snap @@ -0,0 +1,9 @@ +```rust title="demo/src/blueprint.rs" hl_lines="4" +\\ [...] +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + register_common_constructors(&mut bp); + \\ [...] + bp +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/04-route_def.snap b/doc_examples/quickstart/04-route_def.snap new file mode 100644 index 000000000..af2a27cf9 --- /dev/null +++ b/doc_examples/quickstart/04-route_def.snap @@ -0,0 +1,16 @@ +```rust title="demo/src/routes/greet.rs" +use pavex::request::route::RouteParams; +use pavex::response::Response; + +#[RouteParams] +pub struct GreetParams { + pub name: String, +} + +pub fn greet(params: RouteParams) -> Response { + let GreetParams { name }/* (1)! */ = params.0; + Response::ok() // (2)! + .set_typed_body(format!("Hello, {name}!")) // (3)! + .box_body() +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/04-route_params_constructor.snap b/doc_examples/quickstart/04-route_params_constructor.snap new file mode 100644 index 000000000..cb87bb0d7 --- /dev/null +++ b/doc_examples/quickstart/04-route_params_constructor.snap @@ -0,0 +1,11 @@ +```rust title="demo/src/blueprint.rs" hl_lines="4 5 6 7" +\\ [...] +fn register_common_constructors(bp: &mut Blueprint) { + \\ [...] + bp.constructor( + f!(pavex::request::route::RouteParams::extract), + Lifecycle::RequestScoped, + ) + \\ [...] +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/04.patch b/doc_examples/quickstart/04.patch index 3da3cd436..576522213 100644 --- a/doc_examples/quickstart/04.patch +++ b/doc_examples/quickstart/04.patch @@ -1,15 +1,16 @@ diff --git a/demo/src/routes/greet.rs b/demo/src/routes/greet.rs -index adfbbd5..fbcb3fc 100644 +index 0d94360..fbcb3fc 100644 --- a/demo/src/routes/greet.rs +++ b/demo/src/routes/greet.rs -@@ -1,4 +1,4 @@ +@@ -1,11 +1,14 @@ -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 struct GreetParams { +- pub name: String, /* (1)! */ ++ pub name: String, } -pub fn greet(params: RouteParams /* (2)! */) -> Response { diff --git a/doc_examples/quickstart/05-bis.patch b/doc_examples/quickstart/05-bis.patch index a5377899c..afd018f4b 100644 --- a/doc_examples/quickstart/05-bis.patch +++ b/doc_examples/quickstart/05-bis.patch @@ -1,8 +1,7 @@ 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 { +@@ -11,11 +11,7 @@ pub fn blueprint() -> Blueprint { add_telemetry_middleware(&mut bp); bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); @@ -14,24 +13,16 @@ index f1434bb..88fd441 100644 + 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 { +@@ -8,7 +8,7 @@ pub struct GreetParams { pub name: String, } --// --8<-- [start:user_agent] -pub fn greet(params: RouteParams, user_agent: UserAgent /* (1)! */) -> Response { +pub fn greet(params: RouteParams, 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}!")) diff --git a/doc_examples/quickstart/05-error.snap b/doc_examples/quickstart/05-error.snap index 4d9a1d605..ddc63dcc5 100644 --- a/doc_examples/quickstart/05-error.snap +++ b/doc_examples/quickstart/05-error.snap @@ -3,19 +3,19 @@ ERROR: │ instance of `demo::user_agent::UserAgent` as input, but I can't find a constructor for that │ type. │ - │ ╭─[demo/src/blueprint.rs:14:1] - │ 14 │ bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); - │ 15 │ bp.route(GET, "/api/greet/:name", f!(crate::routes::greet::greet)); + │ ╭─[demo/src/blueprint.rs:13:1] + │ 13 │ bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); + │ 14 │ bp.route(GET, "/api/greet/:name", f!(crate::routes::greet::greet)); │ · ───────────────┬─────────────── │ · ╰── The request handler was registered here - │ 16 │ bp + │ 15 │ bp │ ╰──── - │ ╭─[demo/src/routes/greet.rs:13:1] - │ 13 │ - │ 14 │ pub fn greet(params: RouteParams, user_agent: UserAgent) -> Response { + │ ╭─[demo/src/routes/greet.rs:10:1] + │ 10 │ + │ 11 │ pub fn greet(params: RouteParams, user_agent: UserAgent) -> Response { │ · ────┬──── │ · ╰── I don't know how to construct an instance of this input parameter - │ 15 │ if let UserAgent::Unknown = user_agent { + │ 12 │ if let UserAgent::Unknown = user_agent { │ ╰──── │ help: Register a constructor for `demo::user_agent::UserAgent` diff --git a/doc_examples/quickstart/05-inject.snap b/doc_examples/quickstart/05-inject.snap new file mode 100644 index 000000000..146808843 --- /dev/null +++ b/doc_examples/quickstart/05-inject.snap @@ -0,0 +1,13 @@ +```rust title="demo/src/routes/greet.rs" hl_lines="4" +\\ [...] +use crate::user_agent::UserAgent; +\\ [...] +pub fn greet(params: RouteParams, 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(); + } + \\ [...] +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/05-new_submodule.snap b/doc_examples/quickstart/05-new_submodule.snap new file mode 100644 index 000000000..763cdb622 --- /dev/null +++ b/doc_examples/quickstart/05-new_submodule.snap @@ -0,0 +1,9 @@ +```rust title="demo/src/lib.rs" hl_lines="7" +pub use blueprint::blueprint; + +mod blueprint; +pub mod configuration; +pub mod routes; +pub mod telemetry; +pub mod user_agent; +``` \ No newline at end of file diff --git a/doc_examples/quickstart/05-user_agent.snap b/doc_examples/quickstart/05-user_agent.snap new file mode 100644 index 000000000..0d6a6ab78 --- /dev/null +++ b/doc_examples/quickstart/05-user_agent.snap @@ -0,0 +1,8 @@ +```rust title="demo/src/user_agent.rs" +pub enum UserAgent { + /// No `User-Agent` header was provided. + Unknown, + /// The value of the `User-Agent` header for the incoming request. + Known(String), +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/05.patch b/doc_examples/quickstart/05.patch index 1566fe6e4..26fec8b53 100644 --- a/doc_examples/quickstart/05.patch +++ b/doc_examples/quickstart/05.patch @@ -1,5 +1,4 @@ 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 @@ @@ -13,17 +12,13 @@ index f8a6fe0..6fe333a 100644 -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 @@ +@@ -1,14 +1,21 @@ 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 { @@ -34,14 +29,12 @@ index fbcb3fc..662c93c 100644 - 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, 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}!")) @@ -49,7 +42,6 @@ index fbcb3fc..662c93c 100644 } 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 @@ diff --git a/doc_examples/quickstart/06-extract.snap b/doc_examples/quickstart/06-extract.snap new file mode 100644 index 000000000..0047fd813 --- /dev/null +++ b/doc_examples/quickstart/06-extract.snap @@ -0,0 +1,16 @@ +```rust title="demo/src/user_agent.rs" hl_lines="4 5 6 7 8 9 10 11 12 13" +use pavex::http::header::USER_AGENT; +\\ [...] +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!(), + } + } +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/06-register.snap b/doc_examples/quickstart/06-register.snap new file mode 100644 index 000000000..32383d60b --- /dev/null +++ b/doc_examples/quickstart/06-register.snap @@ -0,0 +1,11 @@ +```rust title="demo/src/blueprint.rs" hl_lines="10 11 12 13" +\\ [...] +pub fn blueprint() -> Blueprint { + \\ [...] + bp.constructor( + f!(crate::user_agent::UserAgent::extract), + Lifecycle::RequestScoped, + ); + \\ [...] +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/06.patch b/doc_examples/quickstart/06.patch index 46b8a3519..314a4f1bc 100644 --- a/doc_examples/quickstart/06.patch +++ b/doc_examples/quickstart/06.patch @@ -1,14 +1,15 @@ diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs -index f1434bb..e74936a 100644 +index 44c8a85..e3623cd 100644 --- a/demo/src/blueprint.rs +++ b/demo/src/blueprint.rs -@@ -4,11 +4,17 @@ use pavex::f; +@@ -1,5 +1,5 @@ ++use pavex::blueprint::{Blueprint, constructor::Lifecycle, router::GET}; + use pavex::blueprint::constructor::CloningStrategy; +-use pavex::blueprint::{constructor::Lifecycle, router::GET, Blueprint}; + 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 { +@@ -8,6 +8,11 @@ pub fn blueprint() -> Blueprint { let mut bp = Blueprint::new(); register_common_constructors(&mut bp); @@ -16,31 +17,66 @@ index f1434bb..e74936a 100644 + 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] +@@ -22,7 +27,7 @@ fn register_common_constructors(bp: &mut Blueprint) { + f!(pavex::request::query::QueryParams::extract), + Lifecycle::RequestScoped, + ) +- .error_handler(f!( ++ .error_handler(f!( + pavex::request::query::errors::ExtractQueryParamsError::into_response + )); + +@@ -31,7 +36,7 @@ fn register_common_constructors(bp: &mut Blueprint) { + f!(pavex::request::route::RouteParams::extract), + Lifecycle::RequestScoped, + ) +- .error_handler(f!( ++ .error_handler(f!( + pavex::request::route::errors::ExtractRouteParamsError::into_response + )); - /// Common constructors used by all routes. - fn register_common_constructors(bp: &mut Blueprint) { +@@ -40,14 +45,14 @@ fn register_common_constructors(bp: &mut Blueprint) { + f!(pavex::request::body::JsonBody::extract), + Lifecycle::RequestScoped, + ) +- .error_handler(f!( ++ .error_handler(f!( + pavex::request::body::errors::ExtractJsonBodyError::into_response + )); + bp.constructor( + f!(pavex::request::body::BufferedBody::extract), + Lifecycle::RequestScoped, + ) +- .error_handler(f!( ++ .error_handler(f!( + pavex::request::body::errors::ExtractBufferedBodyError::into_response + )); + bp.constructor( +@@ -62,7 +67,7 @@ fn add_telemetry_middleware(bp: &mut Blueprint) { + f!(crate::telemetry::RootSpan::new), + Lifecycle::RequestScoped, + ) +- .cloning(CloningStrategy::CloneIfNecessary); ++ .cloning(CloningStrategy::CloneIfNecessary); + + bp.wrap(f!(crate::telemetry::logger)); + } diff --git a/demo/src/user_agent.rs b/demo/src/user_agent.rs -index f16d4c1..fb72632 100644 +index f16d4c1..c9d0771 100644 --- a/demo/src/user_agent.rs +++ b/demo/src/user_agent.rs -@@ -1,6 +1,20 @@ +@@ -1,6 +1,22 @@ +use pavex::http::header::USER_AGENT; +use pavex::request::RequestHead; + pub enum UserAgent { -- /// No `User-Agent` header was provided. + /// No `User-Agent` header was provided. Unknown, -- /// The value of the `User-Agent` header for the incoming request. + /// The value of the `User-Agent` header for the incoming request. Known(String), } + diff --git a/doc_examples/quickstart/07-error.snap b/doc_examples/quickstart/07-error.snap index 42eed3d69..43876656e 100644 --- a/doc_examples/quickstart/07-error.snap +++ b/doc_examples/quickstart/07-error.snap @@ -3,12 +3,12 @@ ERROR: │ handler for it. If I don't have an error handler, I don't know what to do with the error when │ the constructor fails! │ - │ ╭─[demo/src/blueprint.rs:12:1] - │ 12 │ bp.constructor( - │ 13 │ f!(crate::user_agent::UserAgent::extract), + │ ╭─[demo/src/blueprint.rs:11:1] + │ 11 │ bp.constructor( + │ 12 │ f!(crate::user_agent::UserAgent::extract), │ · ────────────────────┬──────────────────── │ · ╰── The fallible constructor was registered here - │ 14 │ Lifecycle::RequestScoped, + │ 13 │ Lifecycle::RequestScoped, │ ╰──── │ help: Add an error handler via `.error_handler` diff --git a/doc_examples/quickstart/07-extract.snap b/doc_examples/quickstart/07-extract.snap new file mode 100644 index 000000000..bc4a26131 --- /dev/null +++ b/doc_examples/quickstart/07-extract.snap @@ -0,0 +1,14 @@ +```rust title="demo/src/user_agent.rs" +use pavex::http::header::{ToStrError, USER_AGENT}; +use pavex::request::RequestHead; +\\ [...] +impl UserAgent { + pub fn extract(request_head: &RequestHead) -> Result { + let Some(user_agent) = request_head.headers.get(USER_AGENT) else { + return Ok(UserAgent::Unknown); + }; + + user_agent.to_str().map(|s| UserAgent::Known(s.into())) + } +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/07.patch b/doc_examples/quickstart/07.patch index a71ab9f73..8d31b506d 100644 --- a/doc_examples/quickstart/07.patch +++ b/doc_examples/quickstart/07.patch @@ -1,20 +1,16 @@ diff --git a/demo/src/user_agent.rs b/demo/src/user_agent.rs -index fb72632..bb0f278 100644 +index c9d0771..bb1f65b 100644 --- a/demo/src/user_agent.rs +++ b/demo/src/user_agent.rs -@@ -1,4 +1,6 @@ +@@ -1,4 +1,4 @@ -use pavex::http::header::USER_AGENT; -+// --8<-- [start:new_import] +use pavex::http::header::{ToStrError, USER_AGENT}; -+// --8<-- [end:new_import] use pavex::request::RequestHead; pub enum UserAgent { -@@ -6,15 +8,14 @@ pub enum UserAgent { - Known(String), +@@ -9,14 +9,11 @@ pub enum UserAgent { } -+// --8<-- [start:new_extract] impl UserAgent { - pub fn extract(request_head: &RequestHead) -> Self { + pub fn extract(request_head: &RequestHead) -> Result { @@ -30,4 +26,3 @@ index fb72632..bb0f278 100644 + user_agent.to_str().map(|s| UserAgent::Known(s.into())) } } -+// --8<-- [end:new_extract] diff --git a/doc_examples/quickstart/08-error_handler.snap b/doc_examples/quickstart/08-error_handler.snap new file mode 100644 index 000000000..05ae15396 --- /dev/null +++ b/doc_examples/quickstart/08-error_handler.snap @@ -0,0 +1,8 @@ +```rust title="demo/src/user_agent.rs" +\\ [...] +pub fn invalid_user_agent(_e: &ToStrError) -> Response { + Response::bad_request() + .set_typed_body("The `User-Agent` header value must be a valid UTF-8 string") + .box_body() +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/08-register.snap b/doc_examples/quickstart/08-register.snap new file mode 100644 index 000000000..b8b7358ee --- /dev/null +++ b/doc_examples/quickstart/08-register.snap @@ -0,0 +1,12 @@ +```rust title="demo/src/blueprint.rs" hl_lines="8" +\\ [...] +pub fn blueprint() -> Blueprint { + \\ [...] + bp.constructor( + f!(crate::user_agent::UserAgent::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!(crate::user_agent::invalid_user_agent)); + \\ [...] +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/08.patch b/doc_examples/quickstart/08.patch index 47fd7b73d..eabff8d6c 100644 --- a/doc_examples/quickstart/08.patch +++ b/doc_examples/quickstart/08.patch @@ -1,30 +1,28 @@ diff --git a/demo/src/blueprint.rs b/demo/src/blueprint.rs -index e74936a..d9f5203 100644 +index e3623cd..c431f69 100644 --- a/demo/src/blueprint.rs +++ b/demo/src/blueprint.rs -@@ -12,7 +12,8 @@ pub fn blueprint() -> Blueprint { +@@ -11,7 +11,8 @@ pub fn blueprint() -> Blueprint { bp.constructor( f!(crate::user_agent::UserAgent::extract), Lifecycle::RequestScoped, - ); + ) -+ .error_handler(f!(crate::user_agent::invalid_user_agent)); - // --8<-- [end:new_constructor_registration] ++ .error_handler(f!(crate::user_agent::invalid_user_agent)); add_telemetry_middleware(&mut bp); + diff --git a/demo/src/user_agent.rs b/demo/src/user_agent.rs -index bb0f278..7cad189 100644 +index bb1f65b..b1c935c 100644 --- a/demo/src/user_agent.rs +++ b/demo/src/user_agent.rs -@@ -19,3 +19,11 @@ impl UserAgent { +@@ -17,3 +17,9 @@ impl UserAgent { + user_agent.to_str().map(|s| UserAgent::Known(s.into())) } } - // --8<-- [end:new_extract] + -+// --8<-- [start:new_error_handler] +pub fn invalid_user_agent(_e: &ToStrError) -> Response { + Response::bad_request() + .set_typed_body("The `User-Agent` header value must be a valid UTF-8 string") + .box_body() +} -+// --8<-- [end:new_error_handler] diff --git a/doc_examples/quickstart/09-greet_test.snap b/doc_examples/quickstart/09-greet_test.snap new file mode 100644 index 000000000..c3ab63850 --- /dev/null +++ b/doc_examples/quickstart/09-greet_test.snap @@ -0,0 +1,22 @@ +```rust title="demo_server/tests/integration/greet.rs" +use pavex::http::StatusCode; + +use crate::helpers::TestApi; + +#[tokio::test] +async fn greet_happy_path() { + let api = TestApi::spawn().await; + let name = "Ursula"; + + let response = api + .api_client + .get(&format!("{}/api/greet/{name}", &api.api_address)) + .header("User-Agent", "Test runner") + .send() + .await + .expect("Failed to execute request."); + + assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); + assert_eq!(response.text().await.unwrap(), "Hello, Ursula!"); +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/09-new_test_module.snap b/doc_examples/quickstart/09-new_test_module.snap new file mode 100644 index 000000000..8eb91e597 --- /dev/null +++ b/doc_examples/quickstart/09-new_test_module.snap @@ -0,0 +1,5 @@ +```rust title="demo_server/tests/integration/main.rs" hl_lines="1" +mod greet; +mod helpers; +mod ping; +``` \ No newline at end of file diff --git a/doc_examples/quickstart/09-ping_test.snap b/doc_examples/quickstart/09-ping_test.snap new file mode 100644 index 000000000..6681285a1 --- /dev/null +++ b/doc_examples/quickstart/09-ping_test.snap @@ -0,0 +1,15 @@ +```rust title="demo_server/tests/integration/ping.rs" +use pavex::http::StatusCode; + +//(1)! +use crate::helpers::TestApi; + +#[tokio::test] +async fn ping_works() { + let api = TestApi::spawn().await; //(2)! + + let response = api.get_ping().await; //(3)! + + assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/09.patch b/doc_examples/quickstart/09.patch index 337fc442d..090c74740 100644 --- a/doc_examples/quickstart/09.patch +++ b/doc_examples/quickstart/09.patch @@ -25,15 +25,13 @@ index 0000000..fb02807 + assert_eq!(response.text().await.unwrap(), "Hello, Ursula!"); +} diff --git a/demo_server/tests/integration/main.rs b/demo_server/tests/integration/main.rs -index d131971..9f0a3cc 100644 +index 7a28419..e8440f3 100644 --- a/demo_server/tests/integration/main.rs +++ b/demo_server/tests/integration/main.rs @@ -1,2 +1,3 @@ + mod helpers; +mod greet; -+mod helpers; mod ping; --mod helpers; -\ No newline at end of file diff --git a/demo_server/tests/integration/ping.rs b/demo_server/tests/integration/ping.rs index c79eb0e..bfd2726 100644 --- a/demo_server/tests/integration/ping.rs diff --git a/doc_examples/quickstart/10-greet_test.snap b/doc_examples/quickstart/10-greet_test.snap new file mode 100644 index 000000000..d3024d75b --- /dev/null +++ b/doc_examples/quickstart/10-greet_test.snap @@ -0,0 +1,22 @@ +```rust title="demo_server/tests/integration/greet.rs" +\\ [...] +#[tokio::test] +async fn non_utf8_user_agent_is_rejected() { + let api = TestApi::spawn().await; + let name = "Ursula"; + + let response = api + .api_client + .get(&format!("{}/api/greet/{name}", &api.api_address)) + .header("User-Agent", b"hello\xfa".as_slice()) + .send() + .await + .expect("Failed to execute request."); + + assert_eq!(response.status().as_u16(), StatusCode::BAD_REQUEST.as_u16()); + assert_eq!( + response.text().await.unwrap(), + "The `User-Agent` header value must be a valid UTF-8 string" + ); +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/demo-blueprint_definition.snap b/doc_examples/quickstart/demo-blueprint_definition.snap new file mode 100644 index 000000000..9438a3745 --- /dev/null +++ b/doc_examples/quickstart/demo-blueprint_definition.snap @@ -0,0 +1,12 @@ +```rust title="demo/src/blueprint.rs" +\\ [...] +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + register_common_constructors(&mut bp); + + add_telemetry_middleware(&mut bp); + + bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); + bp +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/demo-ping_handler.snap b/doc_examples/quickstart/demo-ping_handler.snap new file mode 100644 index 000000000..261312401 --- /dev/null +++ b/doc_examples/quickstart/demo-ping_handler.snap @@ -0,0 +1,9 @@ +```rust title="demo/src/routes/status.rs" +use pavex::http::StatusCode; + +/// Respond with a `200 OK` status code to indicate that the server is alive +/// and ready to accept new requests. +pub fn ping() -> StatusCode { + StatusCode::OK +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/demo-route_registration.snap b/doc_examples/quickstart/demo-route_registration.snap new file mode 100644 index 000000000..f3dd28be4 --- /dev/null +++ b/doc_examples/quickstart/demo-route_registration.snap @@ -0,0 +1,12 @@ +```rust title="demo/src/blueprint.rs" hl_lines="8" +\\ [...] +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + register_common_constructors(&mut bp); + + add_telemetry_middleware(&mut bp); + + bp.route(GET, "/api/ping", f!(crate::routes::status::ping)); + bp +} +``` \ No newline at end of file diff --git a/doc_examples/quickstart/tutorial.yml b/doc_examples/quickstart/tutorial.yml index a0e2b14f5..b9bba4f05 100644 --- a/doc_examples/quickstart/tutorial.yml +++ b/doc_examples/quickstart/tutorial.yml @@ -1,23 +1,109 @@ bootstrap: | - pavex new demo && cd demo && git add . && git commit -m "Initial commit" + pavex new demo && cd demo && cargo fmt && git add . && git commit -m "Initial commit" starter_project_folder: demo +snippets: + - name: "blueprint_definition" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..15" ] + - name: "route_registration" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..15" ] + hl_lines: [ 8 ] + - name: "ping_handler" + source_path: "demo/src/routes/status.rs" + ranges: [ ".." ] steps: - - patch: "01.patch" - patch: "02.patch" + snippets: + - name: "new_submodule" + source_path: "demo/src/routes/mod.rs" + ranges: [ ".." ] + hl_lines: [ 1 ] + - name: "route_def" + source_path: "demo/src/routes/greet.rs" + ranges: [ ".." ] + - name: "register_new_route" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..19" ] + hl_lines: [ 9, 10, 11, 12, 13 ] - patch: "03.patch" + snippets: + - name: "route_def" + source_path: "demo/src/routes/greet.rs" + ranges: [ ".." ] - patch: "04.patch" + snippets: + - name: "route_def" + source_path: "demo/src/routes/greet.rs" + ranges: [ ".." ] + - name: "register_common_invocation" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..9", "18..20" ] + hl_lines: [ 4 ] + - name: "route_params_constructor" + source_path: "demo/src/blueprint.rs" + ranges: [ "22..23", "33..37", "60..61" ] + hl_lines: [ 4, 5, 6, 7 ] - patch: "05.patch" + snippets: + - name: "new_submodule" + source_path: "demo/src/lib.rs" + ranges: [ ".." ] + hl_lines: [ 7 ] + - name: "user_agent" + source_path: "demo/src/user_agent.rs" + ranges: [ ".." ] + - name: "inject" + source_path: "demo/src/routes/greet.rs" + ranges: [ "3..4", "10..16", "20..21" ] + hl_lines: [ 4 ] - patch: "05-bis.patch" commands: - command: "cargo px c -q" expected_outcome: "failure" expected_output_at: "05-error.snap" - patch: "06.patch" + snippets: + - name: "extract" + source_path: "demo/src/user_agent.rs" + ranges: [ "0..1", "10..23" ] + hl_lines: [ 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] + - name: "register" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..7", "10..14", "20..21" ] + hl_lines: [ 10, 11, 12, 13 ] - patch: "07.patch" + snippets: + - name: "extract" + source_path: "demo/src/user_agent.rs" + ranges: [ "0..2", "10..19" ] commands: - command: "cargo px c -q" expected_outcome: "failure" expected_output_at: "07-error.snap" - patch: "08.patch" + snippets: + - name: "error_handler" + source_path: "demo/src/user_agent.rs" + ranges: [ "20..25" ] + - name: "register" + source_path: "demo/src/blueprint.rs" + ranges: [ "6..7", "10..15", "21..22" ] + hl_lines: [ 8 ] - patch: "09.patch" + snippets: + - name: "ping_test" + source_path: "demo_server/tests/integration/ping.rs" + ranges: [ ".." ] + - name: "new_test_module" + source_path: "demo_server/tests/integration/main.rs" + ranges: [ ".." ] + hl_lines: [ 1 ] + - name: "greet_test" + source_path: "demo_server/tests/integration/greet.rs" + ranges: [ ".." ] - patch: "10.patch" + snippets: + - name: "greet_test" + source_path: "demo_server/tests/integration/greet.rs" + ranges: [ "21.." ] diff --git a/doc_examples/tutorial_generator/Cargo.lock b/doc_examples/tutorial_generator/Cargo.lock index 45d72bf90..cc67347e3 100644 --- a/doc_examples/tutorial_generator/Cargo.lock +++ b/doc_examples/tutorial_generator/Cargo.lock @@ -14,6 +14,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -249,6 +258,7 @@ name = "tutorial_generator" version = "0.1.0" dependencies = [ "anyhow", + "camino", "console", "fs-err", "run_script", diff --git a/doc_examples/tutorial_generator/Cargo.toml b/doc_examples/tutorial_generator/Cargo.toml index 4d7d52e4c..299190485 100644 --- a/doc_examples/tutorial_generator/Cargo.toml +++ b/doc_examples/tutorial_generator/Cargo.toml @@ -12,3 +12,4 @@ serde = { version = "1", features = ["derive"] } run_script = "0.10" similar = { version = "2.3", features = ["inline"] } console = "0.15.1" +camino = { version = "1", features = ["serde1"] } diff --git a/doc_examples/tutorial_generator/src/main.rs b/doc_examples/tutorial_generator/src/main.rs index e00ed79d5..2659d58ef 100644 --- a/doc_examples/tutorial_generator/src/main.rs +++ b/doc_examples/tutorial_generator/src/main.rs @@ -1,8 +1,10 @@ use std::collections::HashMap; use std::io::Write; +use std::str::FromStr; use std::time::Duration; use anyhow::Context; +use camino::Utf8PathBuf; use console::style; use run_script::types::ScriptOptions; use similar::{Algorithm, ChangeTag, TextDiff}; @@ -11,6 +13,7 @@ use similar::{Algorithm, ChangeTag, TextDiff}; struct TutorialManifest { bootstrap: String, starter_project_folder: String, + snippets: Vec, steps: Vec, } @@ -18,9 +21,25 @@ struct TutorialManifest { struct Step { patch: String, #[serde(default)] + snippets: Vec, + #[serde(default)] commands: Vec, } +#[derive(Debug, serde::Deserialize)] +struct StepSnippet { + name: String, + /// The path to the source file, relative to the root of the project + /// after the corresponding patch has been applied. + source_path: Utf8PathBuf, + ranges: Vec, + #[serde(default)] + /// Which lines should be highlighted in the snippet. + /// The line numbers are relative to the start of the snippet, **not** to the + /// line numbers in the original source file. + hl_lines: Vec, +} + #[derive(Debug, serde::Deserialize)] struct StepCommand { command: String, @@ -59,13 +78,13 @@ fn main() -> Result<(), anyhow::Error> { script_outcome.exit_on_failure("Failed to run the boostrap script"); // Apply the patches - let mut previous_dir = tutorial_manifest.starter_project_folder; + let mut previous_dir = tutorial_manifest.starter_project_folder.clone(); for step in &tutorial_manifest.steps { println!("Applying patch: {}", step.patch); let next_dir = patch_directory_name(&step.patch); let script_outcome = run_script(&format!( r#"cp -r {previous_dir} {next_dir} - cd {next_dir} && patch -p1 < ../{} && git add . && git commit -am "{}""#, + cd {next_dir} && patch -p1 < ../{} && cargo fmt && git add . && git commit -am "{}""#, step.patch, step.patch )) .context("Failed to apply patch")?; @@ -74,6 +93,138 @@ fn main() -> Result<(), anyhow::Error> { } let mut errors = vec![]; + + // Extract the snippets + let (repo_dir, snippets) = ( + tutorial_manifest.starter_project_folder.as_str(), + tutorial_manifest.snippets.as_slice(), + ); + let iterator = std::iter::once((repo_dir, snippets)).chain( + tutorial_manifest + .steps + .iter() + .map(|step| (patch_directory_name(&step.patch), step.snippets.as_slice())), + ); + + for (repo_dir, snippets) in iterator { + for snippet in snippets { + println!("Extracting snippet: {}", snippet.name); + let ranges = snippet + .ranges + .iter() + .map(|range| range.parse::()) + .collect::, _>>()?; + + let repo_dir = Utf8PathBuf::from_str(repo_dir).unwrap(); + let source_filepath = repo_dir.join(&snippet.source_path); + let source_file = fs_err::read_to_string(&source_filepath)?; + + let is_rust = source_filepath.extension() == Some("rs"); + + let mut extracted_snippet = String::new(); + + { + use std::fmt::Write; + if is_rust { + write!( + &mut extracted_snippet, + "```rust title=\"{}\"", + snippet.source_path + ) + .unwrap(); + + if !snippet.hl_lines.is_empty() { + write!(&mut extracted_snippet, " hl_lines=\"").unwrap(); + for (idx, line) in snippet.hl_lines.iter().enumerate() { + if idx > 0 { + write!(&mut extracted_snippet, " ").unwrap(); + } + write!(&mut extracted_snippet, "{}", line).unwrap(); + } + write!(&mut extracted_snippet, "\"").unwrap(); + } + + extracted_snippet.push('\n'); + } + + let extracted_block = ranges + .iter() + .map(|range| range.extract_lines(&source_file)) + .collect::>(); + + let mut previous_leading_whitespaces = 0; + for (i, block) in extracted_block.iter().enumerate() { + let current_leading_whitespaces = block + .lines() + .next() + .map(|l| l.chars().take_while(|c| c.is_whitespace()).count()) + .unwrap_or(0); + + let add_ellipsis = if i > 0 { + true + } else { + let not_from_the_start = match &ranges[i] { + SourceRange::Range(r) => r.start > 0, + SourceRange::RangeInclusive(r) => *r.start() > 0, + SourceRange::RangeFrom(r) => r.start > 0, + SourceRange::RangeFull => false, + }; + not_from_the_start + }; + + if add_ellipsis { + let comment_leading_whitespaces = + if current_leading_whitespaces > previous_leading_whitespaces { + current_leading_whitespaces + } else { + previous_leading_whitespaces + }; + let indent = " ".repeat(comment_leading_whitespaces); + if i != 0 { + extracted_snippet.push('\n'); + } + writeln!(&mut extracted_snippet, "{indent}\\\\ [...]").unwrap(); + } + extracted_snippet.push_str(&block); + previous_leading_whitespaces = block + .lines() + .last() + .map(|l| l.chars().take_while(|c| c.is_whitespace()).count()) + .unwrap_or(0); + } + + if is_rust { + write!(&mut extracted_snippet, "\n```").unwrap(); + } + } + + let snippet_filename = format!("{}-{}.snap", repo_dir, snippet.name); + + let mut options = fs_err::OpenOptions::new(); + options.write(true).create(true).truncate(true); + if verify { + let expected_snippet = + fs_err::read_to_string(&snippet_filename).context("Failed to read file")?; + if expected_snippet != extracted_snippet { + let mut err_msg = format!( + "Expected snippet did not match actual snippet for {} (snippet: `{}`).\n", + repo_dir, snippet.name, + ); + print_changeset(&expected_snippet, &extracted_snippet, &mut err_msg)?; + errors.push(err_msg); + } + } else { + let mut file = options + .open(&snippet_filename) + .context("Failed to open/create expectation file")?; + file.write_all(extracted_snippet.as_bytes()) + .expect("Failed to write to expectation file"); + } + } + } + + // Execute all commands and either verify the output or write it to a file + for step in &tutorial_manifest.steps { for command in &step.commands { println!( @@ -84,7 +235,8 @@ fn main() -> Result<(), anyhow::Error> { assert!( command.expected_output_at.ends_with(".snap"), - "All expected output file must use the `.snap` file extension" + "All expected output file must use the `.snap` file extension. Found: {}", + command.expected_output_at ); let script_outcome = run_script(&format!(r#"cd {patch_dir} && {}"#, command.command))?; @@ -122,7 +274,7 @@ fn main() -> Result<(), anyhow::Error> { } } } - + if !errors.is_empty() { eprintln!("One or more snapshots didn't match the expected value."); for error in errors { @@ -206,7 +358,93 @@ impl ScriptOutcome { } } -pub fn print_changeset( +enum SourceRange { + Range(std::ops::Range), + RangeInclusive(std::ops::RangeInclusive), + RangeFrom(std::ops::RangeFrom), + RangeFull, +} + +impl SourceRange { + fn extract_lines(&self, source: &str) -> String { + let mut lines = source.lines(); + let iterator: Box> = match self { + SourceRange::Range(range) => Box::new( + lines + .by_ref() + .skip(range.start) + .take(range.end - range.start), + ), + SourceRange::RangeInclusive(range) => Box::new( + lines + .by_ref() + .skip(*range.start()) + .take(*range.end() - *range.start() + 1), + ), + SourceRange::RangeFrom(range) => Box::new(lines.by_ref().skip(range.start)), + SourceRange::RangeFull => Box::new(lines.by_ref()), + }; + let mut buffer = String::new(); + for (idx, line) in iterator.enumerate() { + if idx > 0 { + buffer.push('\n'); + } + buffer.push_str(line); + } + buffer + } +} + +impl FromStr for SourceRange { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s == ".." { + return Ok(SourceRange::RangeFull); + } else if s.starts_with("..") { + anyhow::bail!( + "Ranges must always specify a starting line. Invalid range: `{}`", + s + ); + } + if s.contains("..=") { + let mut parts = s.split("..="); + let start: usize = parts + .next() + .unwrap() + .parse() + .context("Range start line must be a valid number")?; + match parts.next() { + Some(end) => { + let end: usize = end + .parse() + .context("Range end line must be a valid number")?; + Ok(SourceRange::RangeInclusive(start..=end)) + } + None => Ok(SourceRange::RangeFrom(start..)), + } + } else { + let mut parts = s.split(".."); + let start: usize = parts + .next() + .unwrap() + .parse() + .context("Range start line must be a valid number")?; + match parts.next() { + Some(s) if s.is_empty() => Ok(SourceRange::RangeFrom(start..)), + None => Ok(SourceRange::RangeFrom(start..)), + Some(end) => { + let end: usize = end + .parse() + .context("Range end line must be a valid number")?; + Ok(SourceRange::Range(start..end)) + } + } + } + } +} + +fn print_changeset( old: &str, new: &str, buffer: &mut impl std::fmt::Write, diff --git a/docs/getting_started/quickstart.md b/docs/getting_started/quickstart.md index 960bd7b09..db24caa3f 100644 --- a/docs/getting_started/quickstart.md +++ b/docs/getting_started/quickstart.md @@ -101,9 +101,7 @@ It's the type you'll use to define your API: routes, middlewares, error handlers You can find the [`Blueprint`][Blueprint] for the `demo` project in the `demo/src/blueprint.rs` file: -```rust title="demo/src/blueprint.rs" ---8<-- "doc_examples/quickstart/01/demo/src/blueprint.rs:blueprint_definition" -``` +--8<-- "doc_examples/quickstart/demo-blueprint_definition.snap" ## Routing @@ -113,9 +111,7 @@ All the routes exposed by your API must be registered with its [`Blueprint`][Blu In the snippet below you can see the registration of the `GET /api/ping` route, the one you targeted with your `curl` request. -```rust title="demo/src/blueprint.rs" hl_lines="7" ---8<-- "doc_examples/quickstart/01/demo/src/blueprint.rs:blueprint_definition" -``` +--8<-- "doc_examples/quickstart/demo-route_registration.snap" It specifies: @@ -127,9 +123,7 @@ It specifies: The `ping` function is the handler for the `GET /api/ping` route: -```rust title="demo/src/routes/status.rs" ---8<-- "doc_examples/quickstart/01/demo/src/routes/status.rs" -``` +--8<-- "doc_examples/quickstart/demo-ping_handler.snap" It's a public function that returns a [`StatusCode`][StatusCode]. [`StatusCode`][StatusCode] is a valid response type for a Pavex handler since it implements @@ -146,20 +140,14 @@ body. Create a new module, `greet.rs`, in the `demo/src/routes` folder: -```rust title="demo/src/routes/lib.rs" hl_lines="1" ---8<-- "doc_examples/quickstart/02/demo/src/routes/mod.rs" -``` +--8<-- "doc_examples/quickstart/02-new_submodule.snap" -```rust title="demo/src/routes/greet.rs" ---8<-- "doc_examples/quickstart/02/demo/src/routes/greet.rs" -``` +--8<-- "doc_examples/quickstart/02-route_def.snap" The body of the `greet` handler is stubbed out with `todo!()` for now, but we'll fix that soon enough. Let's register the new route with the [`Blueprint`][Blueprint] in the meantime: -```rust title="demo/src/blueprint.rs" hl_lines="8 9 10 11 12" ---8<-- "doc_examples/quickstart/02/demo/src/blueprint.rs:blueprint_definition" -``` +--8<-- "doc_examples/quickstart/02-register_new_route.snap" 1. Dynamic route parameters are prefixed with a colon (`:`). @@ -167,9 +155,8 @@ Let's register the new route with the [`Blueprint`][Blueprint] in the meantime: To access the `name` route parameter from your new handler you must use the [`RouteParams`][RouteParams] extractor: -```rust title="demo/src/routes/greet.rs" ---8<-- "doc_examples/quickstart/03/demo/src/routes/greet.rs" -``` + +--8<-- "doc_examples/quickstart/03-route_def.snap" 1. The name of the field must match the name of the route parameter as it appears in the path we registered with the [`Blueprint`][Blueprint]. @@ -178,9 +165,7 @@ To access the `name` route parameter from your new handler you must use the [`Ro You can now return the expected response from the `greet` handler: -```rust title="demo/src/routes/greet.rs" hl_lines="10 11 12 13" ---8<-- "doc_examples/quickstart/04/demo/src/routes/greet.rs" -``` +--8<-- "doc_examples/quickstart/04-route_def.snap" 1. This is an example of Rust's [destructuring syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values). @@ -218,21 +203,13 @@ it knows how to construct them. Let's zoom in on [`RouteParams`][RouteParams]: how does the framework know how to construct it? You need to go back to the [`Blueprint`][Blueprint] to find out: -```rust title="demo/src/blueprint.rs" hl_lines="3" ---8<-- "doc_examples/quickstart/04/demo/src/blueprint.rs:blueprint_definition" -``` +--8<-- "doc_examples/quickstart/04-register_common_invocation.snap" The `register_common_constructors` function takes care of registering constructors for a set of types that are defined in the `pavex` crate itself and commonly used in Pavex applications. If you check out its definition, you'll see that it registers a constructor for [`RouteParams`][RouteParams]: -```rust title="pavex/src/blueprint.rs" hl_lines="3 4 5 6" -fn register_common_constructors(bp: &mut Blueprint) { - // [...] ---8<-- "doc_examples/quickstart/04/demo/src/blueprint.rs:route_params_constructor" - // [...] -} -``` +--8<-- "doc_examples/quickstart/04-route_params_constructor.snap" It specifies: @@ -252,26 +229,16 @@ We only want to greet people who include a `User-Agent` header in their request( Let's start by defining a new `UserAgent` type: -```rust title="demo/src/lib.rs" hl_lines="7" ---8<-- "doc_examples/quickstart/05/demo/src/lib.rs" -``` +--8<-- "doc_examples/quickstart/05-new_submodule.snap" -```rust title="demo/src/user_agent.rs" ---8<-- "doc_examples/quickstart/05/demo/src/user_agent.rs" -``` +--8<-- "doc_examples/quickstart/05-user_agent.snap" ### Missing constructor What if you tried to inject `UserAgent` into your `greet` handler straight away? Would it work? Let's find out! -```rust title="demo/src/routes/greet.rs" hl_lines="4" -//! [...] ---8<-- "doc_examples/quickstart/05/demo/src/routes/greet.rs:user_agent_import" ---8<-- "doc_examples/quickstart/05/demo/src/routes/greet.rs:user_agent" - // [...] -} -``` +--8<-- "doc_examples/quickstart/05-inject.snap" 1. New input parameter! @@ -294,17 +261,11 @@ that will be injected by the framework at runtime. Since you need to look at headers, ask for [`RequestHead`][RequestHead] as input parameter: the incoming request data, minus the body. -```rust title="demo/src/user_agent.rs" hl_lines="10 11 12 13 14 15 16 17 18 19" ---8<-- "doc_examples/quickstart/06/demo/src/user_agent.rs" -``` +--8<-- "doc_examples/quickstart/06-extract.snap" Now register the new constructor with the [`Blueprint`][Blueprint]: -```rust title="demo/src/blueprint.rs" hl_lines="5 6 7 8" ---8<-- "doc_examples/quickstart/06/demo/src/blueprint.rs:new_constructor_registration" - // [...] -} -``` +--8<-- "doc_examples/quickstart/06-register.snap" [`Lifecycle::RequestScoped`][Lifecycle::RequestScoped] is the right choice for this type: the data in `UserAgent` is request-specific. @@ -322,12 +283,7 @@ Panicking for bad user input is poor behavior: you should handle the issue grace Let's change the signature of `UserAgent::extract` to return a `Result` instead: -```rust title="demo/src/user_agent.rs" ---8<-- "doc_examples/quickstart/07/demo/src/user_agent.rs:new_import" -// [...] - ---8<-- "doc_examples/quickstart/07/demo/src/user_agent.rs:new_extract" -``` +--8<-- "doc_examples/quickstart/07-extract.snap" 1. `ToStrError` is the error type returned by `to_str` when the header value is not valid UTF-8. @@ -356,18 +312,11 @@ error handler. Define a new `invalid_user_agent` function in `demo/src/user_agent.rs`: -```rust title="demo/src/user_agent.rs" -// [...] ---8<-- "doc_examples/quickstart/08/demo/src/user_agent.rs:new_error_handler" -``` +--8<-- "doc_examples/quickstart/08-error_handler.snap" Then register the error handler with the [`Blueprint`][Blueprint]: -```rust title="demo/src/blueprint.rs" hl_lines="9" ---8<-- "doc_examples/quickstart/08/demo/src/blueprint.rs:new_constructor_registration" - // [...] -} -``` +--8<-- "doc_examples/quickstart/08-register.snap" The application should compile successfully now. @@ -384,9 +333,7 @@ interact with it, after all. The template project includes a reference example for the `/api/ping` endpoint: -```rust title="demo_server/tests/integration/ping.rs" ---8<-- "doc_examples/quickstart/09/demo_server/tests/integration/ping.rs" -``` +--8<-- "doc_examples/quickstart/09-ping_test.snap" 1. `TestApi` is a helper struct that provides a convenient interface to interact with the application. It's defined in `demo_server/tests/helpers.rs`. @@ -397,23 +344,16 @@ The template project includes a reference example for the `/api/ping` endpoint: Let's write a new integration test to verify the behaviour on the happy path for `GET /api/greet/:name`: -```rust title="demo_server/tests/integration/main.rs hl_lines="1" ---8<-- "doc_examples/quickstart/09/demo_server/tests/integration/main.rs" -``` +--8<-- "doc_examples/quickstart/09-new_test_module.snap" -```rust title="demo_server/tests/integration/greet.rs" ---8<-- "doc_examples/quickstart/09/demo_server/tests/integration/greet.rs" -``` +--8<-- "doc_examples/quickstart/09-greet_test.snap" It follows the same pattern as the `ping` test: it spawns a new instance of the application, issues a request to it and verifies that the response is correct. Let's complement it with a test for the unhappy path as well: requests with a malformed `User-Agent` header should be rejected. -```rust title="demo_server/tests/integration/greet.rs" -// [...] ---8<-- "doc_examples/quickstart/10/demo_server/tests/integration/greet.rs" -``` +--8<-- "doc_examples/quickstart/10-greet_test.snap" `cargo px test` should report three passing tests now. As a bonus exercise, try to add a test for the case where the `User-Agent` header is missing.