Skip to content

Commit

Permalink
docs: Add build_path doc and http service book chapter (#604)
Browse files Browse the repository at this point in the history
  • Loading branch information
spencewenski authored Feb 2, 2025
1 parent 8ff85d6 commit 7e23a34
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 2 deletions.
5 changes: 5 additions & 0 deletions book/examples/service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ publish = false

[dependencies]
roadster = { path = "../../.." }
aide = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
axum = { workspace = true }
25 changes: 25 additions & 0 deletions book/examples/service/src/http/example_b.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use aide::transform::TransformOperation;
use axum::extract::State;
use axum::Json;
use roadster::app::context::AppContext;
use roadster::error::RoadsterResult;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::instrument;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExampleBResponse {}

#[instrument(skip_all)]
pub async fn example_b_get(
State(_state): State<AppContext>,
) -> RoadsterResult<Json<ExampleBResponse>> {
Ok(Json(ExampleBResponse {}))
}

pub fn example_b_get_docs(op: TransformOperation) -> TransformOperation {
op.description("Example B API.")
.tag("Example B")
.response_with::<200, Json<ExampleBResponse>, _>(|res| res.example(ExampleBResponse {}))
}
25 changes: 25 additions & 0 deletions book/examples/service/src/http/example_c.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use aide::transform::TransformOperation;
use axum::extract::State;
use axum::Json;
use roadster::app::context::AppContext;
use roadster::error::RoadsterResult;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::instrument;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExampleCResponse {}

#[instrument(skip_all)]
pub async fn example_c_get(
State(_state): State<AppContext>,
) -> RoadsterResult<Json<ExampleCResponse>> {
Ok(Json(ExampleCResponse {}))
}

pub fn example_c_get_docs(op: TransformOperation) -> TransformOperation {
op.description("Example C API.")
.tag("Example C")
.response_with::<200, Json<ExampleCResponse>, _>(|res| res.example(ExampleCResponse {}))
}
34 changes: 34 additions & 0 deletions book/examples/service/src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
mod example_b;
mod example_c;
mod open_api;

use aide::axum::routing::get_with;
use aide::axum::ApiRouter;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use roadster::api::http::build_path;
use roadster::app::context::AppContext;
use roadster::service::http::builder::HttpServiceBuilder;

const BASE: &str = "/api";

pub async fn http_service(state: &AppContext) -> HttpServiceBuilder<AppContext> {
HttpServiceBuilder::new(Some(BASE), state)
// Multiple routers can be registered and they will all be merged together using the
// `axum::Router::merge` method.
.router(Router::new().route(&build_path(BASE, "/example_a"), get(example_a)))
// Create your routes as an `ApiRouter` in order to include it in the OpenAPI schema.
.api_router(ApiRouter::new().api_route(
&build_path(BASE, "/example_b"),
get_with(example_b::example_b_get, example_b::example_b_get_docs),
))
.api_router(ApiRouter::new().api_route(
&build_path(BASE, "/example_c"),
get_with(example_c::example_c_get, example_c::example_c_get_docs),
))
}

async fn example_a() -> impl IntoResponse {
()
}
35 changes: 35 additions & 0 deletions book/examples/service/src/http/open_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use roadster::app::context::AppContext;
use roadster::app::{prepare, PrepareOptions, RoadsterApp, RoadsterAppBuilder};
use roadster::error::RoadsterResult;
use roadster::service::http::service::{HttpService, OpenApiArgs};
use roadster::util::empty::Empty;

type App = RoadsterApp<AppContext, Empty, Empty>;

async fn open_api() -> RoadsterResult<()> {
// Build the app
let app: App = RoadsterApp::builder()
.state_provider(|context| Ok(context))
.add_service_provider(move |registry, state| {
Box::pin(async move {
registry
.register_builder(crate::http::http_service(state).await)
.await?;
Ok(())
})
})
.build();

// Prepare the app
let prepared = prepare(app, PrepareOptions::builder().build()).await?;

// Get the `HttpService`
let http_service = prepared.service_registry.get::<HttpService>()?;

// Get the OpenAPI schema
let schema = http_service.open_api_schema(&OpenApiArgs::builder().build())?;

println!("{schema}");

Ok(())
}
2 changes: 2 additions & 0 deletions book/examples/service/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod http;

use roadster::app::context::AppContext;
use roadster::app::RoadsterApp;
use roadster::service::http::service::HttpService;
Expand Down
62 changes: 60 additions & 2 deletions book/src/features/services/http/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
# HTTP Service with [Axum](https://crates.io/crates/axum)

See:
The [HttpService](https://docs.rs/roadster/latest/roadster/service/http/service/struct.HttpService.html) provides
support for serving an HTTP API using [axum](https://docs.rs/axum/latest/axum/).

- <https://docs.rs/roadster/latest/roadster/service/http/index.html>
```rust,ignore
{{#include ../../../../examples/service/src/http/mod.rs:14:}}
```

<details>
<summary>example_b module</summary>

```rust,ignore
{{#include ../../../../examples/service/src/http/example_b.rs:10:}}
```

</details>

<details>
<summary>example_c module</summary>

```rust,ignore
{{#include ../../../../examples/service/src/http/example_c.rs:10:}}
```

</details>

## OpenAPI Schema

If the `open-api` feature is enabled, the service also supports generating an OpenAPI schema. The OpenAPI schema can be
accessed in various ways.

### Via HTTP API

It's served by default at `/<base>/_docs/api.json`

```shell
# First, run your app
cargo run

# In a separate shell or browser, navigate to the API, e.g.
curl localhost:3000/api/_docs/api.json
```

### Via CLI command

It can be generated via a CLI command

```shell
cargo run -- roadster open-api -o $HOME/open-api.json
```

### Via the `HttpService` directly

It can also be generated programmatically using the `HttpService` directly.

```rust,ignore
{{#include ../../../../examples/service/src/http/open_api.rs:7:}}
```

## Docs.rs links

- [http service](https://docs.rs/roadster/latest/roadster/service/http/index.html)
55 changes: 55 additions & 0 deletions src/api/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,61 @@ pub mod docs;
pub mod health;
pub mod ping;

/// This method is provided to help build API paths given a parent and child route. This is useful
/// because we recommend building your [`Router`]s and combining them using [`Router::merge`]
/// instead of [`Router::nest`] in order to allow the default-enabled
/// [`tower_http::normalize_path::NormalizePathLayer`] to work for all routes of your app --
/// otherwise, it doesn't work for nested routes.
///
/// # Examples
/// ```rust
/// # use axum::Router;
/// # use roadster::api::http::build_path;
/// # use roadster::app::context::AppContext;
/// # use roadster::service::http::builder::HttpServiceBuilder;
/// #
/// const BASE: &str = "/api";
///
/// async fn http_service(state: &AppContext) -> HttpServiceBuilder<AppContext> {
/// HttpServiceBuilder::new(Some(BASE), state)
/// .router(example_a::routes(BASE))
/// .router(example_b::routes(BASE))
/// }
///
/// mod example_a {
/// # use axum::Router;
/// # use axum::routing::get;
/// # use axum_core::response::IntoResponse;
/// # use roadster::api::http::build_path;
/// # use roadster::app::context::AppContext;
/// #
/// pub fn routes(parent: &str) -> Router<AppContext> {
/// // Use `build_path` to build a path relative to the parent path.
/// Router::new().route(&build_path(parent, "/example_a"), get(example_a))
/// }
///
/// async fn example_a() -> impl IntoResponse {
/// ()
/// }
/// }
///
/// mod example_b {
/// # use axum::Router;
/// # use axum::routing::get;
/// # use axum_core::response::IntoResponse;
/// # use roadster::api::http::build_path;
/// # use roadster::app::context::AppContext;
/// #
/// pub fn routes(parent: &str) -> Router<AppContext> {
/// // Use `build_path` to build a path relative to the parent path.
/// Router::new().route(&build_path(parent, "/example_b"), get(example_a))
/// }
///
/// async fn example_a() -> impl IntoResponse {
/// ()
/// }
/// }
/// ```
pub fn build_path(parent: &str, child: &str) -> String {
// Clean the path to make sure it is valid:
// 1. Remove any occurrences of double `/`, e.g. `/foo//bar`
Expand Down

0 comments on commit 7e23a34

Please sign in to comment.