utoipa_actix_web/
scope.rs

1//! Implement `utoipa` extended [`Scope`] for [`actix_web::Scope`].
2//!
3//! See usage from [`scope`][fn@scope].
4
5use core::fmt;
6use std::cell::{Cell, RefCell};
7
8use actix_service::{IntoServiceFactory, ServiceFactory, Transform};
9use actix_web::body::MessageBody;
10use actix_web::dev::{AppService, HttpServiceFactory, ServiceRequest, ServiceResponse};
11use actix_web::guard::Guard;
12use actix_web::{Error, Route};
13
14use crate::service_config::ServiceConfig;
15use crate::OpenApiFactory;
16
17/// Wrapper type for [`actix_web::Scope`] and [`utoipa::openapi::OpenApi`] with additional path
18/// prefix created with `scope::scope("path-prefix")` call.
19///
20/// See usage from [`scope`][fn@scope].
21pub struct Scope<T>(
22    actix_web::Scope<T>,
23    RefCell<utoipa::openapi::OpenApi>,
24    Cell<String>,
25);
26
27impl<T> From<actix_web::Scope<T>> for Scope<T>
28where
29    T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
30{
31    fn from(value: actix_web::Scope<T>) -> Self {
32        Self(
33            value,
34            RefCell::new(utoipa::openapi::OpenApiBuilder::new().build()),
35            Cell::new(String::new()),
36        )
37    }
38}
39
40impl<'s, T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>>
41    From<&'s str> for Scope<T>
42where
43    Scope<T>: std::convert::From<actix_web::Scope>,
44{
45    fn from(value: &'s str) -> Self {
46        let scope = actix_web::Scope::new(value);
47        let s: Scope<T> = scope.into();
48        Scope(s.0, s.1, Cell::new(String::from(value)))
49    }
50}
51
52/// Create a new [`Scope`] with given _`scope`_ e.g. `scope("/api/v1")`.
53///
54/// This behaves exactly same way as [`actix_web::Scope`] but allows automatic _`schema`_ and
55/// _`path`_ collection from `service(...)` calls directly or via [`ServiceConfig::service`].
56///
57/// # Examples
58///
59/// _**Create new scoped service.**_
60///
61/// ```rust
62/// # use actix_web::{get, App};
63/// # use utoipa_actix_web::{AppExt, scope};
64/// #
65///  #[utoipa::path()]
66///  #[get("/handler")]
67///  pub async fn handler() -> &'static str {
68///      "OK"
69///  }
70/// let _ = App::new()
71///     .into_utoipa_app()
72///     .service(scope::scope("/api/v1/inner").configure(|cfg| {
73///         cfg.service(handler);
74///     }));
75/// ```
76pub fn scope<
77    I: Into<Scope<T>>,
78    T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
79>(
80    scope: I,
81) -> Scope<T> {
82    scope.into()
83}
84
85impl<T> Scope<T>
86where
87    T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
88{
89    /// Passthrough implementation for [`actix_web::Scope::guard`].
90    pub fn guard<G: Guard + 'static>(self, guard: G) -> Self {
91        let scope = self.0.guard(guard);
92        Self(scope, self.1, self.2)
93    }
94
95    /// Passthrough implementation for [`actix_web::Scope::app_data`].
96    pub fn app_data<U: 'static>(self, data: U) -> Self {
97        Self(self.0.app_data(data), self.1, self.2)
98    }
99
100    /// Passthrough implementation for [`actix_web::Scope::wrap`].
101    pub fn wrap<M, B>(
102        self,
103        middleware: M,
104    ) -> Scope<
105        impl ServiceFactory<
106            ServiceRequest,
107            Config = (),
108            Response = ServiceResponse<B>,
109            Error = Error,
110            InitError = (),
111        >,
112    >
113    where
114        M: Transform<
115                T::Service,
116                ServiceRequest,
117                Response = ServiceResponse<B>,
118                Error = Error,
119                InitError = (),
120            > + 'static,
121        B: MessageBody,
122    {
123        let scope = self.0.wrap(middleware);
124        Scope(scope, self.1, self.2)
125    }
126
127    /// Synonymous for [`UtoipaApp::configure`][utoipa_app_configure]
128    ///
129    /// [utoipa_app_configure]: ../struct.UtoipaApp.html#method.configure
130    pub fn configure<F>(self, cfg_fn: F) -> Self
131    where
132        F: FnOnce(&mut ServiceConfig),
133    {
134        let mut openapi = self.1.borrow_mut();
135
136        let scope = self.0.configure(|config| {
137            let mut service_config = ServiceConfig::new(config);
138
139            cfg_fn(&mut service_config);
140
141            let other_paths = service_config.1.take();
142            openapi.paths.merge(other_paths);
143            let schemas = service_config.2.take();
144            let components = openapi
145                .components
146                .get_or_insert(utoipa::openapi::Components::new());
147            components.schemas.extend(schemas);
148        });
149        drop(openapi);
150
151        Self(scope, self.1, self.2)
152    }
153
154    /// Synonymous for [`UtoipaApp::service`][utoipa_app_service]
155    ///
156    /// [utoipa_app_service]: ../struct.UtoipaApp.html#method.service
157    pub fn service<F>(self, factory: F) -> Self
158    where
159        F: HttpServiceFactory + OpenApiFactory + 'static,
160    {
161        let mut schemas = Vec::<(
162            String,
163            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
164        )>::new();
165        {
166            let mut openapi = self.1.borrow_mut();
167            let other_paths = factory.paths();
168            factory.schemas(&mut schemas);
169            openapi.paths.merge(other_paths);
170            let components = openapi
171                .components
172                .get_or_insert(utoipa::openapi::Components::new());
173            components.schemas.extend(schemas);
174        }
175
176        let app = self.0.service(factory);
177
178        Self(app, self.1, self.2)
179    }
180
181    /// Passthrough implementation for [`actix_web::Scope::route`].
182    pub fn route(self, path: &str, route: Route) -> Self {
183        Self(self.0.route(path, route), self.1, self.2)
184    }
185
186    /// Passthrough implementation for [`actix_web::Scope::default_service`].
187    pub fn default_service<F, U>(self, f: F) -> Self
188    where
189        F: IntoServiceFactory<U, ServiceRequest>,
190        U: ServiceFactory<
191                ServiceRequest,
192                Config = (),
193                Response = ServiceResponse,
194                Error = actix_web::Error,
195            > + 'static,
196        U::InitError: fmt::Debug,
197    {
198        Self(self.0.default_service(f), self.1, self.2)
199    }
200
201    /// Synonymous for [`UtoipaApp::map`][utoipa_app_map]
202    ///
203    /// [utoipa_app_map]: ../struct.UtoipaApp.html#method.map
204    pub fn map<
205        F: FnOnce(actix_web::Scope<T>) -> actix_web::Scope<NF>,
206        NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
207    >(
208        self,
209        op: F,
210    ) -> Scope<NF> {
211        let scope = op(self.0);
212        Scope(scope, self.1, self.2)
213    }
214}
215
216impl<T, B> HttpServiceFactory for Scope<T>
217where
218    T: ServiceFactory<
219            ServiceRequest,
220            Config = (),
221            Response = ServiceResponse<B>,
222            Error = Error,
223            InitError = (),
224        > + 'static,
225    B: MessageBody + 'static,
226{
227    fn register(self, config: &mut AppService) {
228        let Scope(scope, ..) = self;
229        scope.register(config);
230    }
231}
232
233impl<T> OpenApiFactory for Scope<T> {
234    fn paths(&self) -> utoipa::openapi::path::Paths {
235        let prefix = self.2.take();
236        let mut openapi = self.1.borrow_mut();
237        let mut paths = std::mem::take(&mut openapi.paths);
238
239        let prefixed_paths = paths
240            .paths
241            .into_iter()
242            .map(|(path, item)| {
243                let path = format!("{prefix}{path}");
244
245                (path, item)
246            })
247            .collect::<utoipa::openapi::path::PathsMap<_, _>>();
248        paths.paths = prefixed_paths;
249
250        paths
251    }
252
253    fn schemas(
254        &self,
255        schemas: &mut Vec<(
256            String,
257            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
258        )>,
259    ) {
260        let mut api = self.1.borrow_mut();
261        if let Some(components) = &mut api.components {
262            schemas.extend(std::mem::take(&mut components.schemas));
263        }
264    }
265}