-
-
Notifications
You must be signed in to change notification settings - Fork 304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: OpenAPI integration #984
base: master
Are you sure you want to change the base?
Conversation
Looks like a good start. I think the yaml also should contain a few other things:
|
9055d99
to
3e106d0
Compare
cfg for tests
3e106d0
to
ea48919
Compare
Hey @NexVeridian Can I help you finish this pr? |
@DenuxPlays yeah of course |
How can I help? Also just one Note to your pr description. |
thanks! this would be nice if possible: impl Routes {
/// .add_openapi(routes!(get_action, post_action))
pub fn add_openapi(mut self, method: UtoipaMethodRouter<AppContext>) -> Self {}
} any of the unchecked ones in the pr description would be great: |
static JWT_LOCATION: OnceLock<Option<JWTLocation>> = OnceLock::new(); | ||
|
||
#[must_use] | ||
pub fn get_jwt_location_from_ctx(ctx: &AppContext) -> JWTLocation { | ||
ctx.config | ||
.auth | ||
.as_ref() | ||
.and_then(|auth| auth.jwt.as_ref()) | ||
.and_then(|jwt| jwt.location.as_ref()) | ||
.unwrap_or(&JWTLocation::Bearer) | ||
.clone() | ||
} | ||
|
||
pub fn set_jwt_location_ctx(ctx: &AppContext) { | ||
set_jwt_location(get_jwt_location_from_ctx(ctx)); | ||
} | ||
|
||
pub fn set_jwt_location(jwt_location: JWTLocation) -> &'static Option<JWTLocation> { | ||
JWT_LOCATION.get_or_init(|| Some(jwt_location)) | ||
} | ||
|
||
fn get_jwt_location() -> Option<&'static JWTLocation> { | ||
JWT_LOCATION.get().unwrap_or(&None).as_ref() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like this might be flaky in cargo test
because the global state isn't reset between test runs
tests are good in cargo nextest
and should be fine for users
if it's used in the utoipa::path then yeah, I'm not sure if they would be, I haven't used those before |
They would to provide Basic paginagtion metadata |
I had to do something similar recently and If I understand correctly you would need to generate something like this. I had a problem where the generated openapi schema didn't see that X struct was a query parameter.
|
5f4c63f still has some errors that I don't know how to fix, one of you two should take a look maybe |
I think the Solution would be something like cfg_of and we have to define the struct two times. |
I think this would work, I tested it on your branch. The cfg_attr for the trait bounds is pretty ugly so I think @DenuxPlays has a point with using cfg_of and having two structs. Pragnation.rs
pragnate/mod.rs
|
I had something like the following in mind. pagination.rs use cfg_if::cfg_if;
cfg_if! {
if #[cfg(any(
feature = "openapi_swagger",
feature = "openapi_redoc",
feature = "openapi_scalar"
))] {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Pager<T: utoipa::ToSchema> {
#[serde(rename(serialize = "results"))]
pub results: T,
#[serde(rename(serialize = "pagination"))]
pub info: PagerMeta,
}
} else {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Pager<T> {
#[serde(rename(serialize = "results"))]
pub results: T,
#[serde(rename(serialize = "pagination"))]
pub info: PagerMeta,
}
}
}
cfg_if! {
if #[cfg(any(
feature = "openapi_swagger",
feature = "openapi_redoc",
feature = "openapi_scalar"
))] {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct PagerMeta {
#[serde(rename(serialize = "page"))]
pub page: u64,
#[serde(rename(serialize = "page_size"))]
pub page_size: u64,
#[serde(rename(serialize = "total_pages"))]
pub total_pages: u64,
#[serde(rename(serialize = "total_items"))]
pub total_items: u64,
}
} else {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct PagerMeta {
#[serde(rename(serialize = "page"))]
pub page: u64,
#[serde(rename(serialize = "page_size"))]
pub page_size: u64,
#[serde(rename(serialize = "total_pages"))]
pub total_pages: u64,
#[serde(rename(serialize = "total_items"))]
pub total_items: u64,
}
}
}
cfg_if! {
if #[cfg(any(
feature = "openapi_swagger",
feature = "openapi_redoc",
feature = "openapi_scalar"
))] {
impl<T: utoipa::ToSchema> Pager<T> {
#[must_use]
pub const fn new(results: T, meta: PagerMeta) -> Self {
Self {
results,
info: meta,
}
}
}
} else {
impl<T> Pager<T> {
#[must_use]
pub const fn new(results: T, meta: PagerMeta) -> Self {
Self {
results,
info: meta,
}
}
}
}
} I think that @SorenEdwards implementation for the query is good but we should also add ToSchema to it. #[cfg_attr(
any(
feature = "openapi_swagger",
feature = "openapi_redoc",
feature = "openapi_scalar"
),
derive(utoipa::IntoParams, utoipa::ToSchema)
)]
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct PaginationQuery {
#[serde(
default = "default_page_size",
rename = "page_size",
deserialize_with = "deserialize_pagination_filter"
)]
pub page_size: u64,
#[serde(
default = "default_page",
rename = "page",
deserialize_with = "deserialize_pagination_filter"
)]
pub page: u64,
} |
After reviewing this implementation again, I feel we should consider moving it into an initializer maintained in a separate repository. For example: Could you outline the changes needed in Loco to allow managing OpenAPI as an initializer? |
Note: I don't think thats possible. |
Everything is possible. When initializing is public create, you need to load |
@kaplanelad @DenuxPlays @NexVeridian I am actually using it in the initializer right now on my project/work. If you are interested I can add a post about it. I think the real upside of adding a feature in loco would be the code generation aspect as I do a lot of this by hand currently and it can get pretty tedious. If you have any suggestions I would love to help out making the initializer approach work. That being said seaorms --extra-model-derive could fix ToSchema not being added to the generated db entities. |
I am not sure what you mean. So unless you copy the structs it isnt possible right? |
Yeah I also use it as an initializer. It is just a Smoother experience. Just a small note you should Never Serve entities over your api. |
Yes, it can be great. can you also share how you implement it with an example repo? |
|
There are some issues with some of the options, like creating code generation in the future might be harder, here are some options for converting this pr to a initializer: option 1 - probably not possibleGet the original function from a option 2have the user manually add the data to the initializer, similar to #855 # controllers::auth
pub fn api_routes() -> OpenApiRouter<AppContext> {
OpenApiRouter::new().routes(routes!(register, verify, login, forgot, reset))
} then pass let (_, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
.merge(controllers::auth::api_routes())
.merge(controllers::responses::api_routes())
.split_for_parts(); option 3Merge these two into loco:
option 4This comment is also a good option but has these drawbacks, as stated in the comment:
|
All the options look legitimate to me when we move them to the initializer. WDYT? |
What do you mean? We cannot convert this feature to an initializer at least not without dropping 50% of the features. |
Could you please clarify which features are going to be supported and which ones won’t,and why? I’d prefer to support this feature as an initializer (that is supported by Loco teams) and keep them separate from the base Loco codebase. |
There are probably some other things I forgot but the integration wouldn't feel like an integration |
Why not make the internal Loco route responses public so they can be accessed externally? This way, the initializer can retrieve the app context, which provides access to everything required by OpenAPI.
Same for here, we can expose it |
I am not sure what you mean. How can you "expose" something that needs to replaced when you want to use it with utoipa? Also the struct is already exposed but the derive(Schema) is missing. |
option 2:this option removes automatic schema collection option 3:this option has automatic schema collection the only things that can be extracted is: the config, the tests, and this section app_routes.rs#L239-L290 option 1:
this is talking about option 1, which is probably not possible or I can't figure it out, maybe if you do something that looks bad like this: |
related #855
TODO:
enable: true
since that's handled by the feature.merge(Redoc::with_url("/redoc", api.clone()))
openapi
into featureall_openapi
swagger-ui
redoc
scalar
SecurityAddon
impl Modify for SecurityAddon
somewhere, maybe with configsrc/tests_cfg/db.rs:86:1
src/tests_cfg/config.rs
test_from_folder_openapi()
utoipa::path
if possibleget
inget(get_action_openapi)
is still grabbed withroutes!(get_action_openapi)
AppContext
- check thatapi_router.routes(method.with_state::<AppContext>(()))
doesn't break the ctx with.layer
cargo test
is broken withJWT_LOCATION.get_or_init
,nextest
works correctlycargo loco generate controller --openapi
utoipa::path
routes!
macrocc @DenuxPlays