Repository: juhaku/utoipa (Rust)

Issue:
OpenApi derive `components` section does not support namespacing
For the OpenApi derive to work all the components must be in scope because you cannot use paths.

Taking from the Axum todo example, this is possible:
```Rust
use crate::todo::{Store, Todo, TodoError};

#[derive(OpenApi)]
#[openapi(
    handlers(
        todo::list_todos,
        todo::create_todo,
        todo::mark_done,
        todo::delete_todo,
    ),
    components(Todo, TodoError),
    modifiers(&SecurityAddon),
    tags(
        (name = "todo", description = "Todo items management API")
    )
)]
struct ApiDoc;
```

this is not:
```Rust
#[derive(OpenApi)]
#[openapi(
    handlers(
        todo::list_todos,
        todo::create_todo,
        todo::mark_done,
        todo::delete_todo,
    ),
    components(crate::todo::Todo, crate::todo::TodoError),
    modifiers(&SecurityAddon),
    tags(
        (name = "todo", description = "Todo items management API")
    )
)]
struct ApiDoc;
```
and if you have lots of `components` this gets painful quickly. The `handlers` section already supports this.


Discussion:
I've also run into this, I was wondering does the OpenApi spec support having names like `crate::todo::TodoError` for components?
I'm not sure about that at all. If not, the macro could support some kind
of renaming system maybe? Currently the only solution is polluting the
namespace.

On Tue, May 31, 2022, 2:33 PM Luke Frisken ***@***.***> wrote:

> I've also run into this, I was wondering does the OpenApi spec support
> having names like crate::todo::TodoError for components?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/juhaku/utoipa/issues/152#issuecomment-1142074331>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAPSMDIENI3KOO6KWEUEOMDVMYBH5ANCNFSM5XLQ2KSA>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

I opted for `use x as y` aliasing syntax to work around it so far
And putting the `ApiDoc` in its own module to avoid polluting the surrounding namespace.
I'm doing both, but having the derive do this would be nice. I'm not sure how flexible derives are, but my dream solution would be something similar to this:
```
components(
  crate::todo::Todo as Todo,
  crate::todo::TodoError as TodoError
)
```
This is relatively small change, The way it handles the components can be changed to support this path expression so that following would work as well. I can test this out in near future when I get time for this.
```rust
components(
  crate::todo::Todo,
  crate::todo::TodoError
)
```



Patch:
diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs
index 04ecb562..90132ae1 100644
--- a/utoipa-gen/src/openapi.rs
+++ b/utoipa-gen/src/openapi.rs
@@ -89,7 +89,7 @@ impl Parse for OpenApiAttr {
 
 #[cfg_attr(feature = "debug", derive(Debug))]
 struct Component {
-    ty: Ident,
+    path: ExprPath,
     generics: Generics,
 }
 
@@ -100,12 +100,16 @@ impl Component {
             .iter()
             .any(|generic| matches!(generic, GenericParam::Lifetime(_)))
     }
+
+    fn get_ident(&self) -> Option<&Ident> {
+        self.path.path.segments.last().map(|segment| &segment.ident)
+    }
 }
 
 impl Parse for Component {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         Ok(Component {
-            ty: input.parse()?,
+            path: input.parse()?,
             generics: input.parse()?,
         })
     }
@@ -272,9 +276,11 @@ fn impl_components(
         let mut components_tokens = components.iter().fold(
             quote! { utoipa::openapi::ComponentsBuilder::new() },
             |mut schema, component| {
-                let ident = &component.ty;
+                let path = &component.path;
+                let ident = component.get_ident().unwrap();
                 let span = ident.span();
                 let component_name = &*ident.to_string();
+
                 let (_, ty_generics, _) = component.generics.split_for_impl();
 
                 let assert_ty_generics = if component.has_lifetime_generics() {
@@ -284,7 +290,7 @@ fn impl_components(
                 };
                 let assert_component = format_ident!("_AssertComponent{}", component_name);
                 tokens.extend(quote_spanned! {span=>
-                    struct #assert_component where #ident #assert_ty_generics: utoipa::Component;
+                    struct #assert_component where #path #assert_ty_generics: utoipa::Component;
                 });
 
                 let ty_generics = if component.has_lifetime_generics() {
@@ -292,9 +298,10 @@ fn impl_components(
                 } else {
                     Some(ty_generics)
                 };
+
                 schema.extend(quote! {
-                    .component(#component_name, <#ident #ty_generics>::component())
-                    .components_from_iter(<#ident #ty_generics>::aliases())
+                    .component(#component_name, <#path #ty_generics>::component())
+                    .components_from_iter(<#path #ty_generics>::aliases())
                 });
 
                 schema


Test patch:
diff --git a/tests/openapi_derive.rs b/tests/openapi_derive.rs
index 51f5a4cd..4a75200a 100644
--- a/tests/openapi_derive.rs
+++ b/tests/openapi_derive.rs
@@ -1,5 +1,6 @@
 #![cfg(feature = "json")]
 
+use serde_json::Value;
 use utoipa::OpenApi;
 
 mod common;
@@ -78,3 +79,29 @@ fn derive_openapi_with_external_docs_only_url() {
         "externalDocs.description" = r###"null"###, "External docs description"
     }
 }
+
+#[test]
+fn derive_openapi_with_components_in_different_module() {
+    mod custom {
+        use utoipa::Component;
+
+        #[derive(Component)]
+        #[allow(unused)]
+        pub(super) struct Todo {
+            name: String,
+        }
+    }
+
+    #[derive(OpenApi)]
+    #[openapi(components(custom::Todo))]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
+    let todo = common::get_json_path(&doc, "components.schemas.Todo");
+
+    assert_ne!(
+        todo,
+        &Value::Null,
+        "Expected components.schemas.Todo not to be null"
+    );
+}