|
| 1 | +use core::fmt; |
| 2 | +use std::future::Future; |
| 3 | +use std::rc::Rc; |
| 4 | + |
| 5 | +use actix_service::{IntoServiceFactory, ServiceFactory}; |
| 6 | +use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse}; |
| 7 | +use actix_web::Error; |
| 8 | +use utoipa::openapi::PathItem; |
| 9 | +use utoipa::OpenApi; |
| 10 | + |
| 11 | +use self::service_config::ServiceConfig; |
| 12 | + |
| 13 | +pub mod scope; |
| 14 | +pub mod service_config; |
| 15 | + |
| 16 | +pub trait PathsFactory { |
| 17 | + fn paths(&self) -> utoipa::openapi::path::Paths; |
| 18 | +} |
| 19 | + |
| 20 | +impl<T: utoipa::Path> PathsFactory for T { |
| 21 | + fn paths(&self) -> utoipa::openapi::path::Paths { |
| 22 | + let methods = T::methods(); |
| 23 | + |
| 24 | + methods |
| 25 | + .into_iter() |
| 26 | + .fold( |
| 27 | + utoipa::openapi::path::Paths::builder(), |
| 28 | + |mut builder, method| { |
| 29 | + builder = builder.path(T::path(), PathItem::new(method, T::operation())); |
| 30 | + |
| 31 | + builder |
| 32 | + }, |
| 33 | + ) |
| 34 | + .build() |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +pub trait AppExt<T> { |
| 39 | + fn into_utoipa_app(self) -> UtoipaApp<T>; |
| 40 | +} |
| 41 | + |
| 42 | +impl<T> AppExt<T> for actix_web::App<T> { |
| 43 | + fn into_utoipa_app(self) -> UtoipaApp<T> { |
| 44 | + UtoipaApp::from(self) |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +pub struct UtoipaApp<T>(actix_web::App<T>, Rc<utoipa::openapi::OpenApi>); |
| 49 | + |
| 50 | +impl<T> From<actix_web::App<T>> for UtoipaApp<T> { |
| 51 | + fn from(value: actix_web::App<T>) -> Self { |
| 52 | + #[derive(OpenApi)] |
| 53 | + struct Api; |
| 54 | + UtoipaApp(value, Rc::new(Api::openapi())) |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +impl<T> UtoipaApp<T> |
| 59 | +where |
| 60 | + T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>, |
| 61 | +{ |
| 62 | + /// Passthrough implementation for [`actix_web::App::app_data`]. |
| 63 | + pub fn app_data<U: 'static>(self, data: U) -> Self { |
| 64 | + let app = self.0.app_data(data); |
| 65 | + Self(app, self.1) |
| 66 | + } |
| 67 | + |
| 68 | + /// Passthrough implementation for [`actix_web::App::data_factory`]. |
| 69 | + pub fn data_factory<F, Out, D, E>(self, data: F) -> Self |
| 70 | + where |
| 71 | + F: Fn() -> Out + 'static, |
| 72 | + Out: Future<Output = Result<D, E>> + 'static, |
| 73 | + D: 'static, |
| 74 | + E: std::fmt::Debug, |
| 75 | + { |
| 76 | + let app = self.0.data_factory(data); |
| 77 | + |
| 78 | + Self(app, self.1) |
| 79 | + } |
| 80 | + |
| 81 | + /// Passthrough implementation for [`actix_web::App::configure`]. |
| 82 | + pub fn configure<F>(mut self, f: F) -> Self |
| 83 | + where |
| 84 | + F: FnOnce(&mut ServiceConfig), |
| 85 | + { |
| 86 | + // TODO get OpenAPI paths???? |
| 87 | + let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect( |
| 88 | + "OpenApi should not have more than one reference when building App with `configure`", |
| 89 | + ); |
| 90 | + |
| 91 | + let app = self.0.configure(|config| { |
| 92 | + let mut service_config = ServiceConfig::new(config); |
| 93 | + |
| 94 | + f(&mut service_config); |
| 95 | + |
| 96 | + let ServiceConfig(_, paths) = service_config; |
| 97 | + api.paths.paths.extend(paths.take().paths); |
| 98 | + }); |
| 99 | + |
| 100 | + Self(app, self.1) |
| 101 | + } |
| 102 | + |
| 103 | + /// Passthrough implementation for [`actix_web::App::route`]. |
| 104 | + pub fn route(self, path: &str, route: actix_web::Route) -> Self { |
| 105 | + let app = self.0.route(path, route); |
| 106 | + |
| 107 | + Self(app, self.1) |
| 108 | + } |
| 109 | + |
| 110 | + /// Passthrough implementation for [`actix_web::App::service`]. |
| 111 | + pub fn service<F>(mut self, factory: F) -> Self |
| 112 | + where |
| 113 | + F: HttpServiceFactory + PathsFactory + 'static, |
| 114 | + { |
| 115 | + let paths = factory.paths(); |
| 116 | + |
| 117 | + // TODO should this be `make_mut`? |
| 118 | + let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect( |
| 119 | + "OpenApi should not have more than one reference when building App with `service`", |
| 120 | + ); |
| 121 | + |
| 122 | + api.paths.paths.extend(paths.paths); |
| 123 | + let app = self.0.service(factory); |
| 124 | + |
| 125 | + Self(app, self.1) |
| 126 | + } |
| 127 | + |
| 128 | + pub fn openapi_service<O, F>(self, factory: F) -> Self |
| 129 | + where |
| 130 | + F: FnOnce(Rc<utoipa::openapi::OpenApi>) -> O, |
| 131 | + O: HttpServiceFactory + 'static, |
| 132 | + { |
| 133 | + let service = factory(self.1.clone()); |
| 134 | + let app = self.0.service(service); |
| 135 | + Self(app, self.1) |
| 136 | + } |
| 137 | + |
| 138 | + /// Passthrough implementation for [`actix_web::App::default_service`]. |
| 139 | + pub fn default_service<F, U>(self, svc: F) -> Self |
| 140 | + where |
| 141 | + F: IntoServiceFactory<U, ServiceRequest>, |
| 142 | + U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error> |
| 143 | + + 'static, |
| 144 | + U::InitError: fmt::Debug, |
| 145 | + { |
| 146 | + Self(self.0.default_service(svc), self.1) |
| 147 | + } |
| 148 | + |
| 149 | + /// Passthrough implementation for [`actix_web::App::external_resource`]. |
| 150 | + pub fn external_resource<N, U>(self, name: N, url: U) -> Self |
| 151 | + where |
| 152 | + N: AsRef<str>, |
| 153 | + U: AsRef<str>, |
| 154 | + { |
| 155 | + Self(self.0.external_resource(name, url), self.1) |
| 156 | + } |
| 157 | + |
| 158 | + pub fn map< |
| 159 | + F: FnOnce(actix_web::App<T>) -> actix_web::App<NF>, |
| 160 | + NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>, |
| 161 | + >( |
| 162 | + self, |
| 163 | + op: F, |
| 164 | + ) -> UtoipaApp<NF> { |
| 165 | + let app = op(self.0); |
| 166 | + UtoipaApp(app, self.1) |
| 167 | + } |
| 168 | + |
| 169 | + pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) { |
| 170 | + ( |
| 171 | + self.0, |
| 172 | + Rc::try_unwrap(self.1).unwrap_or_else(|rc| (*rc).clone()), |
| 173 | + ) |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +#[cfg(test)] |
| 178 | +mod tests { |
| 179 | + use actix_service::Service; |
| 180 | + use actix_web::http::header::{HeaderValue, CONTENT_TYPE}; |
| 181 | + use actix_web::{get, App}; |
| 182 | + |
| 183 | + use super::*; |
| 184 | + |
| 185 | + #[utoipa::path(impl_for = handler2)] |
| 186 | + #[get("/handler2")] |
| 187 | + async fn handler2() -> &'static str { |
| 188 | + "this is message 2" |
| 189 | + } |
| 190 | + |
| 191 | + #[utoipa::path(impl_for = handler)] |
| 192 | + #[get("/handler")] |
| 193 | + async fn handler() -> &'static str { |
| 194 | + "this is message" |
| 195 | + } |
| 196 | + |
| 197 | + #[utoipa::path(impl_for = handler3)] |
| 198 | + #[get("/handler3")] |
| 199 | + async fn handler3() -> &'static str { |
| 200 | + "this is message 3" |
| 201 | + } |
| 202 | + |
| 203 | + #[test] |
| 204 | + fn test_app() { |
| 205 | + fn config(cfg: &mut service_config::ServiceConfig) { |
| 206 | + cfg.service(handler3); |
| 207 | + } |
| 208 | + |
| 209 | + let (_, api) = App::new() |
| 210 | + .into_utoipa_app() |
| 211 | + .service(handler) |
| 212 | + .configure(config) |
| 213 | + .service(scope::scope("/path-prefix").service(handler2).map(|scope| { |
| 214 | + let s = scope.wrap_fn(|req, srv| { |
| 215 | + let fut = srv.call(req); |
| 216 | + async { |
| 217 | + let mut res = fut.await?; |
| 218 | + res.headers_mut() |
| 219 | + .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); |
| 220 | + Ok(res) |
| 221 | + } |
| 222 | + }); |
| 223 | + |
| 224 | + s |
| 225 | + })) |
| 226 | + .split_for_parts(); |
| 227 | + |
| 228 | + dbg!(api); |
| 229 | + // let app = app.service(scope::scope("prefix").service(handler)); |
| 230 | + |
| 231 | + // app |
| 232 | + } |
| 233 | +} |
0 commit comments