Skip to content

Commit 89869b1

Browse files
feat: generate OpenAPI specification from methods and actions
1 parent e9b48c7 commit 89869b1

25 files changed

Lines changed: 317 additions & 60 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/axum/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async fn main() {
5454
let api_router = OpenApiRouter::new()
5555
.route("/protected", get(async || "Protected"))
5656
.route_layer(from_fn(auth_required::<User>))
57-
.nest("/auth", AuthRoutes::openapi_router::<User, ()>());
57+
.nest("/auth", AuthRoutes::new(shield).openapi_router());
5858

5959
// Initialize router
6060
let (router, openapi) = OpenApiRouter::new()

examples/leptos-axum/src/main.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ async fn main() {
44
use std::sync::Arc;
55

66
use axum::{Router, middleware::from_fn, routing::get};
7-
use leptos::{
8-
config::{LeptosOptions, get_configuration},
9-
context::provide_context,
10-
};
7+
use leptos::{config::get_configuration, context::provide_context};
118
use leptos_axum::{LeptosRoutes, generate_route_list};
129
use shield::{Shield, ShieldOptions};
1310
use shield_bootstrap::BootstrapLeptosStyle;
@@ -62,7 +59,7 @@ async fn main() {
6259
let router = Router::new()
6360
.route("/api/protected", get(async || "Protected"))
6461
.route_layer(from_fn(auth_required::<User>))
65-
.nest("/api/auth", AuthRoutes::router::<User, LeptosOptions>())
62+
.nest("/api/auth", AuthRoutes::new(shield).router())
6663
.leptos_routes_with_context(
6764
&leptos_options,
6865
routes,

packages/core/shield/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ utoipa = ["dep:utoipa"]
1616
async-trait.workspace = true
1717
bon.workspace = true
1818
chrono = { workspace = true, features = ["serde"] }
19+
convert_case = "0.8.0"
1920
futures.workspace = true
2021
serde = { workspace = true, features = ["derive"] }
2122
serde_json.workspace = true

packages/core/shield/src/action.rs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
use std::any::Any;
22

3-
use async_trait::async_trait;
4-
use serde::{Deserialize, Serialize};
5-
63
use crate::{
74
error::ShieldError,
85
form::Form,
@@ -11,6 +8,38 @@ use crate::{
118
response::Response,
129
session::{BaseSession, MethodSession},
1310
};
11+
use async_trait::async_trait;
12+
use serde::{Deserialize, Serialize};
13+
#[cfg(feature = "utoipa")]
14+
use utoipa::openapi::HttpMethod;
15+
16+
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17+
pub enum ActionMethod {
18+
Get,
19+
Post,
20+
Put,
21+
Delete,
22+
Options,
23+
Head,
24+
Patch,
25+
Trace,
26+
}
27+
28+
#[cfg(feature = "utoipa")]
29+
impl From<ActionMethod> for HttpMethod {
30+
fn from(value: ActionMethod) -> Self {
31+
match value {
32+
ActionMethod::Get => Self::Get,
33+
ActionMethod::Post => Self::Post,
34+
ActionMethod::Put => Self::Put,
35+
ActionMethod::Delete => Self::Delete,
36+
ActionMethod::Options => Self::Options,
37+
ActionMethod::Head => Self::Head,
38+
ActionMethod::Patch => Self::Patch,
39+
ActionMethod::Trace => Self::Trace,
40+
}
41+
}
42+
}
1443

1544
// TODO: Think of a better name.
1645
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -46,6 +75,12 @@ pub trait Action<P: Provider, S>: ErasedAction + Send + Sync {
4675

4776
fn name(&self) -> String;
4877

78+
fn openapi_summary(&self) -> &'static str;
79+
80+
fn openapi_description(&self) -> &'static str;
81+
82+
fn method(&self) -> ActionMethod;
83+
4984
fn condition(&self, _provider: &P, _session: &MethodSession<S>) -> Result<bool, ShieldError> {
5085
Ok(true)
5186
}
@@ -66,6 +101,12 @@ pub trait ErasedAction: Send + Sync {
66101

67102
fn erased_name(&self) -> String;
68103

104+
fn erased_openapi_summary(&self) -> &'static str;
105+
106+
fn erased_openapi_description(&self) -> &'static str;
107+
108+
fn erased_method(&self) -> ActionMethod;
109+
69110
fn erased_condition(
70111
&self,
71112
provider: &(dyn Any + Send + Sync),
@@ -100,6 +141,18 @@ macro_rules! erased_action {
100141
self.name()
101142
}
102143

144+
fn erased_openapi_summary(&self) -> &'static str {
145+
self.openapi_summary()
146+
}
147+
148+
fn erased_openapi_description(&self) -> &'static str {
149+
self.openapi_description()
150+
}
151+
152+
fn erased_method(&self) -> $crate::ActionMethod {
153+
self.method()
154+
}
155+
103156
fn erased_condition(
104157
&self,
105158
provider: &(dyn std::any::Any + Send + Sync),

packages/core/shield/src/actions/sign_out.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use crate::{Form, Input, InputType, InputTypeSubmit, MethodSession, Provider, Sh
33
const ACTION_ID: &str = "sign-out";
44
const ACTION_NAME: &str = "Sign out";
55

6+
// TODO: Sign out should be a global action that is independent of the method.
7+
// TODO: Add hooks, so the method can still perform custom sign out.
8+
69
pub struct SignOutAction;
710

811
impl SignOutAction {

packages/core/shield/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod error;
44
mod form;
55
mod method;
66
mod options;
7+
mod path;
78
mod provider;
89
mod request;
910
mod response;
@@ -19,6 +20,7 @@ pub use error::*;
1920
pub use form::*;
2021
pub use method::*;
2122
pub use options::*;
23+
pub use path::*;
2224
pub use provider::*;
2325
pub use request::*;
2426
pub use response::*;
File renamed without changes.

packages/core/shield/src/shield.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
use std::{any::Any, collections::HashMap, sync::Arc};
22

3+
#[cfg(feature = "utoipa")]
4+
use convert_case::{Case, Casing};
35
use futures::future::try_join_all;
4-
use tracing::{debug, warn};
6+
use tracing::warn;
7+
#[cfg(feature = "utoipa")]
8+
use utoipa::{
9+
IntoParams,
10+
openapi::{
11+
OpenApi, PathItem, Paths,
12+
path::{Operation, ParameterIn},
13+
},
14+
};
515

616
use crate::{
717
action::{ActionForms, ActionMethodForm, ActionProviderForm},
818
error::{ActionError, MethodError, ProviderError, SessionError, ShieldError},
919
method::ErasedMethod,
1020
options::ShieldOptions,
21+
path::ActionPathParams,
1122
request::Request,
1223
response::ResponseType,
1324
session::Session,
@@ -184,16 +195,12 @@ impl<U: User> Shield<U> {
184195
.erased_call(provider, &base_session, &*method_session, request)
185196
.await?;
186197

187-
debug!("response {:#?}", response);
188-
189198
for session_action in &response.session_actions {
190199
session_action
191200
.call(method_id, provider_id, &session)
192201
.await?;
193202
}
194203

195-
debug!("session actions processed");
196-
197204
Ok(response.r#type)
198205
}
199206

@@ -232,6 +239,49 @@ impl<U: User> Shield<U> {
232239
None => Ok(None),
233240
}
234241
}
242+
243+
#[cfg(feature = "utoipa")]
244+
pub fn openapi(&self) -> OpenApi {
245+
let mut paths = Paths::builder();
246+
247+
for method in self.methods.values() {
248+
for action in method.erased_actions() {
249+
use utoipa::openapi::Response;
250+
251+
let method_id = method.erased_id();
252+
let action_id = action.erased_id();
253+
254+
// TODO: Query, request body, responses.
255+
256+
paths = paths.path(
257+
format!("/{}/{}/{{providerId}}", method_id, action_id),
258+
PathItem::builder()
259+
.operation(
260+
action.erased_method().into(),
261+
Operation::builder()
262+
.operation_id(Some(format!(
263+
"{}{}",
264+
action_id.to_case(Case::Camel),
265+
method_id.to_case(Case::UpperCamel)
266+
)))
267+
.summary(Some(action.erased_openapi_summary()))
268+
.description(Some(action.erased_openapi_description()))
269+
.tag("auth")
270+
.parameters(Some(ActionPathParams::into_params(|| {
271+
Some(ParameterIn::Path)
272+
})))
273+
.response(
274+
"500",
275+
Response::builder().description("Internal server error."),
276+
),
277+
)
278+
.build(),
279+
);
280+
}
281+
}
282+
283+
OpenApi::builder().paths(paths.build()).build()
284+
}
235285
}
236286

237287
#[cfg(test)]

packages/integrations/shield-axum/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
mod error;
22
mod extract;
33
mod middleware;
4-
mod path;
54
mod router;
65
mod routes;
76

0 commit comments

Comments
 (0)