Skip to content

Commit

Permalink
Project structure docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeMathWalker committed Dec 19, 2023
1 parent 4aaa79b commit 8cbcc15
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 14 deletions.
19 changes: 19 additions & 0 deletions doc_examples/quickstart/demo-bp_binary.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
```rust title="demo/src/bin/bp.rs"
use cargo_px_env::generated_pkg_manifest_path;
use demo::blueprint;
use pavex_cli_client::Client;
use std::error::Error;
/// Generate the `demo_server_sdk` crate using Pavex's CLI.
///
/// Pavex will automatically wire all our routes, constructors and error handlers
/// into the a "server SDK" that can be used by the final API server binary to launch
/// the application.
fn main() -> Result<(), Box<dyn Error>> {
let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into();
Client::new()
.generate(blueprint(), generated_dir)
.execute()?;
Ok(())
}
```
50 changes: 50 additions & 0 deletions doc_examples/quickstart/demo-bp_server_binary.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
```rust title="demo_server/src/bin/api.rs"
use anyhow::Context;
use demo_server::{
configuration::load_configuration,
telemetry::{get_subscriber, init_telemetry},
};
use demo_server_sdk::{build_application_state, run};
use pavex::server::Server;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let subscriber = get_subscriber("demo".into(), "info".into(), std::io::stdout);
init_telemetry(subscriber)?;
// We isolate all the server setup and launch logic in a separate function
// in order to have a single choke point where we make sure to log fatal errors
// that will cause the application to exit.
if let Err(e) = _main().await {
tracing::error!(
error.msg = %e,
error.error_chain = ?e,
"The application is exiting due to an error"
)
}
Ok(())
}
async fn _main() -> anyhow::Result<()> {
// Load environment variables from a .env file, if it exists.
let _ = dotenvy::dotenv();
let config = load_configuration(None)?;
let application_state = build_application_state().await;
let tcp_listener = config
.server
.listener()
.await
.context("Failed to bind the server TCP listener")?;
let address = tcp_listener
.local_addr()
.context("The server TCP listener doesn't have a local socket address")?;
let server_builder = Server::new().listen(tcp_listener);
tracing::info!("Starting to listen for incoming requests at {}", address);
run(server_builder, application_state).await;
Ok(())
}
```
4 changes: 4 additions & 0 deletions doc_examples/quickstart/demo-cargo_px_in_manifest.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
\\ [...]
[package.metadata.px.generate]
generator_type = "cargo_workspace_binary"
generator_name = "bp"
13 changes: 13 additions & 0 deletions doc_examples/quickstart/demo-ping_test.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
```rust title="demo_server/tests/integration/ping.rs"
use crate::helpers::TestApi;
use pavex::http::StatusCode;
#[tokio::test]
async fn ping_works() {
let api = TestApi::spawn().await;
let response = api.get_ping().await;
assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16());
}
```
5 changes: 5 additions & 0 deletions doc_examples/quickstart/demo-project_structure.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
demo/
demo_server/
demo_server_sdk/
Cargo.toml
README.md
16 changes: 16 additions & 0 deletions doc_examples/quickstart/tutorial.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ snippets:
- name: "ping_handler"
source_path: "demo/src/routes/status.rs"
ranges: [ ".." ]
- name: "cargo_px_in_manifest"
source_path: "demo_server_sdk/Cargo.toml"
ranges: [ "5.." ]
- name: "bp_binary"
source_path: "demo/src/bin/bp.rs"
ranges: [ ".." ]
- name: "bp_server_binary"
source_path: "demo_server/src/bin/api.rs"
ranges: [ ".." ]
- name: "ping_test"
source_path: "demo_server/tests/integration/ping.rs"
ranges: [ ".." ]
commands:
- command: "exa --oneline -F --group-directories-first"
expected_outcome: "success"
expected_output_at: "demo-project_structure.snap"
steps:
- patch: "02.patch"
snippets:
Expand Down
27 changes: 16 additions & 11 deletions doc_examples/tutorial_generator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ use similar::{Algorithm, ChangeTag, TextDiff};
struct TutorialManifest {
bootstrap: String,
starter_project_folder: String,
#[serde(default)]
snippets: Vec<StepSnippet>,
#[serde(default)]
steps: Vec<Step>,
#[serde(default)]
commands: Vec<StepCommand>,
}

#[derive(Debug, serde::Deserialize)]
Expand Down Expand Up @@ -225,21 +229,23 @@ fn main() -> Result<(), anyhow::Error> {

// 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!(
"Running command for patch `{}`: {}",
step.patch, command.command
);
let patch_dir = patch_directory_name(&step.patch);
let iterator = std::iter::once((repo_dir, tutorial_manifest.commands.as_slice())).chain(
tutorial_manifest
.steps
.iter()
.map(|step| (patch_directory_name(&step.patch), step.commands.as_slice())),
);
for (repo_dir, commands) in iterator {
for command in commands {
println!("Running command for `{}`: {}", repo_dir, command.command);

assert!(
command.expected_output_at.ends_with(".snap"),
"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))?;
let script_outcome = run_script(&format!(r#"cd {repo_dir} && {}"#, command.command))?;

if command.expected_outcome == StepCommandOutcome::Success {
script_outcome.exit_on_failure("Failed to run command which should have succeeded");
Expand All @@ -256,9 +262,8 @@ fn main() -> Result<(), anyhow::Error> {
.context("Failed to read file")?;
if expected_output != output {
let mut err_msg = format!(
"Expected output did not match actual output for patch {} (command: `{}`).\n",
step.patch,
command.command,
"Expected output did not match actual output for {} (command: `{}`).\n",
repo_dir, command.command,
);
print_changeset(&expected_output, &output, &mut err_msg)?;
errors.push(err_msg);
Expand Down
2 changes: 1 addition & 1 deletion docs/ansi.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@

.highlight .-Color-Faint {
font-weight: lighter;
}
}
185 changes: 185 additions & 0 deletions docs/guide/project_structure/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
As you have seen in the [Quickstart](../../getting_started/quickstart/) tutorial,
`pavex new` is a quick way to scaffold a new project and start working on it.
If you execute

```bash
pavex new demo
```

the CLI will create a project with the following structure:

```text
--8<-- "doc_examples/quickstart/demo-project_structure.snap"
```

What is the purpose of all those folders? Why is `cargo-px` needed to build a Pavex project?
Are there any conventions to follow?

This guide will answer all these questions and more.

## TL;DR

If you're in a hurry, here's a quick summary of the most important points:

- A Pavex project is a [Cargo workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html)
with at least three crates:
- a core crate (_library_)
- a server SDK crate (_library_)
- a server crate (_binary_)
- The core crate contains the [`Blueprint`][Blueprint] for your API. It's where you'll spend most of your time.
- The server SDK crate is generated from the core crate by `pavex generate`, which is invoked automatically
by [`cargo-px`][cargo-px] when building or running the project.
- The server crate is the entry point for your application. It's also where you'll write your integration tests.

Using the `demo` project as an example, the relationship between the project crates can be visualised as follows:

```mermaid
graph
d[demo] -->|contains| bp[Blueprint];
bp -->|is used to generate| dss[demo_server_sdk];
dss -->|is invoked by| ds[demo_server];
dss -->|is invoked by| dst[API tests in demo_server];
```

If you want to know more, read on!

## Blueprint

Every Pavex project has, at its core, a [`Blueprint`][Blueprint].
It's the type you use to declare the structure of your API: [routes], middlewares, constructors, error handlers, etc.

--8<-- "doc_examples/quickstart/demo-blueprint_definition.snap"

Think of a [`Blueprint`][Blueprint] as the specification for your API, a **plan for how your application should behave at
runtime**.

### Code generation

You can't run or execute a [`Blueprint`][Blueprint] as-is.

#### `pavex generate`

To convert a [`Blueprint`][Blueprint] into an executable toolkit, you need `pavex generate`.
It's a CLI command that takes a [`Blueprint`][Blueprint] as input and outputs a
Rust crate, the **server SDK** for your Pavex project.

```text hl_lines="3"
--8<-- "doc_examples/quickstart/demo-project_structure.snap"
```

!!! note

As a convention, the generated crate is named `{project_name}_server_sdk`.
In the `demo` project, the generated crate is called `demo_server_sdk`.


#### `cargo-px`

If you went through the [Quickstart](../../../getting_started/quickstart/) tutorial, you might be
wondering: I've never run `pavex generate`! How comes my project worked?

That's thanks to [`cargo-px`][cargo-px]!
If you look into the `Cargo.toml` manifest for the `demo_server_sdk` crate in the `demo` project,
you'll find this section:

```toml
--8<-- "doc_examples/quickstart/demo-cargo_px_in_manifest.snap"
```

It's a [`cargo-px`][cargo-px] configuration section.
The `demo_server_sdk` crate is telling [`cargo-px`][cargo-px] to generate the whole crate
by executing a binary called `bp` (short for `blueprint`) from the current Cargo workspace.

That binary is defined in the `demo` crate:

--8<-- "doc_examples/quickstart/demo-bp_binary.snap"

[`Client::generate`][Client::generate] takes care of serializing the [`Blueprint`][Blueprint]
and passing it as input to `pavex generate`.

All this is done automatically for you when you run `cargo px build` or `cargo px run`.
[`cargo-px`][cargo-px] examines all the crates in your workspace, generates the ones
that need it, and then goes on to complete the build process.

## The server SDK

We've talked at length about how the server SDK is generated, but we haven't yet
discussed what it actually _does_.
The **server SDK is the glue that wires everything together**. It is the code
executed at runtime when a request hits your API.

You can think of it as the output of a macro, with the difference that you can explore it.
It's right there in your filesystem: you can open it, you can read it, you can use it as a way
to get a deeper understanding of how Pavex works under the hood.

At the same time, you actually don't need to know how it works to use it.
As a Pavex user, **you only need to care about** the two public types it exports: **the `run` function and the `ApplicationState`
struct**.

### `ApplicationState`

`ApplicationState` holds all the types with a [`Singleton` lifecycle][Lifecycle::Singleton]
that your application needs to access at runtime when processing a request.

To build an instance of `ApplicationState`, the server SDK exposes a function called `build_application_state`.

### `run`

`run` is the entrypoint of your application.
It takes as input:

- an instance of `ApplicationState`
- a [`pavex::server::Server`][Server] instance

[`pavex::server::Server`][Server] holds the configuration for the HTTP server that will be used to serve your API:
the port(s) to listen on, the number of worker threads to be used, etc.
When you call `run`, the HTTP server starts listening for incoming requests.
You're live!

## The server crate

But who calls `run`?

The server SDK crate is a library, it doesn't contain an executable binary.
That's why you need a **server crate**.

```text hl_lines="2"
--8<-- "doc_examples/quickstart/demo-project_structure.snap"
```

!!! note

As a convention, the server crate is named `{project_name}_server`.
In the `demo` project, the generated crate is called `demo_server`.

### The executable binary

The server crate contains the `main` function that you'll be running to start your application.
In that `main` function you'll be building an instance of `ApplicationState` and passing it to `run`.
You'll be doing a few other things too: initializing your `tracing` subscriber, loading
configuration, etc.

??? info "The `main` function in `demo_server`"

--8<-- "doc_examples/quickstart/demo-bp_server_binary.snap"

Most of this ceremony is taken care for you by the `pavex new` command, but it's good to know
that it's happening (and where it's happening) in case you need to customize it.

### Integration tests

The server crate is also where you'll be writing your **API tests**, also known as **black-box tests**.
These are scenarios that exercise your application as a customer would, by sending HTTP requests and asserting on the
responses.

The `demo` project includes an example of such a test which you can use as a reference:

--8<-- "doc_examples/quickstart/demo-ping_test.snap"

[Blueprint]: ../../api_reference/pavex/blueprint/struct.Blueprint.html
[Client::generate]: ../../api_reference/pavex_cli_client/client/struct.Client.html#method.generate
[Lifecycle::Singleton]: ../../api_reference/pavex/lifecycle/enum.Lifecycle.html#variant.Singleton
[Server]: ../../api_reference/pavex/server/struct.Server.html

[routes]: ../routing/index.md
[cargo-px]: https://github.com/LukeMathWalker/cargo-px
3 changes: 3 additions & 0 deletions docs/mermaid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.mermaid {
text-align: center;
}
Loading

0 comments on commit 8cbcc15

Please sign in to comment.