Skip to content

Comments

feat: add 'Query' derive to manage custom Utoipa Query descriptions#1890

Merged
mrizzi merged 4 commits intoguacsec:mainfrom
mrizzi:feat-macro-generate-openapi
Aug 1, 2025
Merged

feat: add 'Query' derive to manage custom Utoipa Query descriptions#1890
mrizzi merged 4 commits intoguacsec:mainfrom
mrizzi:feat-macro-generate-openapi

Conversation

@mrizzi
Copy link
Contributor

@mrizzi mrizzi commented Jul 22, 2025

Implementing the QueryDoc custom Derive macro in order to use the referenced struct as the source for the fields allowed to:

  • have filters for in the q query parameter
  • sort by in the sort query parameter

AdvisoryQuery and VulnerabilityQuery are two examples of how to use such a macro from now on.
The actual descriptions format might need further changes based on the LLM/MCP effectiveness.

@ctron in the first review I would like to understand if the overall structure for the macro is fine and eventually apply refactoring to move it somewhere else in the project or rename them. Thank you.

Summary by Sourcery

Introduce a custom derive macro and supporting crates to automate generation of Utoipa query and sort parameter descriptions based on struct fields

New Features:

  • Add trustify-query crate with Query trait and TrustifyQuery IntoParams implementation
  • Add trustify-query-derive crate with derive(Query) proc macro to generate query and sort descriptions
  • Derive Query for AdvisoryQuery and VulnerabilityQuery and use TrustifyQuery in Utoipa annotations for endpoints
  • Include new query and query-derive crates in workspace and relevant Cargo.toml files

Enhancements:

  • Replace hardcoded EBNF grammar blocks in openapi.yaml with dynamically generated descriptions

@mrizzi mrizzi requested a review from ctron July 22, 2025 15:27
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jul 22, 2025

Reviewer's Guide

This PR introduces a custom derive macro and supporting crate to generate OpenAPI descriptions for query and sort parameters from struct definitions, replaces hardcoded parameter docs in the OpenAPI spec with those dynamic descriptions, refactors the advisory/vulnerability endpoints to use the new TrustifyQuery wrapper and custom query structs, and integrates the new crates into the workspace configuration.

File-Level Changes

Change Details Files
Introduce Query derive macro and wrapper crate for dynamic query parameter descriptions
  • Created proc-macro derive Query in query/query-derive crate generating EBNF-based descriptions from struct fields
  • Defined Query trait and TrustifyQuery wrapper in query crate implementing IntoParams for q and sort parameters
query/query-derive/src/lib.rs
query/query-derive/Cargo.toml
query/src/lib.rs
query/Cargo.toml
Replace static parameter docs in OpenAPI spec with dynamic descriptions
  • Removed hardcoded EBNF grammar for q and sort in openapi.yaml
  • Updated parameter descriptions to use dynamic output from TrustifyQuery
openapi.yaml
Refactor endpoints to use TrustifyQuery with custom query structs
  • Added AdvisoryQuery and VulnerabilityQuery structs annotated with #[derive(Query)]
  • Replaced inline Query params with TrustifyQuery and TrustifyQuery in utoipa path definitions
modules/fundamental/src/advisory/endpoints/mod.rs
modules/fundamental/src/vulnerability/endpoints/mod.rs
Integrate new query crates into Cargo workspace
  • Added trustify-query and trustify-query-derive to root workspace members
  • Updated dependencies in root and modules/fundamental Cargo.toml to include new crates
Cargo.toml
modules/fundamental/Cargo.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov
Copy link

codecov bot commented Jul 22, 2025

Codecov Report

❌ Patch coverage is 96.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.26%. Comparing base (da07c38) to head (48ba9a8).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
query/query-derive/src/lib.rs 95.71% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1890      +/-   ##
==========================================
+ Coverage   68.06%   68.26%   +0.19%     
==========================================
  Files         365      367       +2     
  Lines       23063    23216     +153     
  Branches    23063    23216     +153     
==========================================
+ Hits        15698    15848     +150     
- Misses       6486     6488       +2     
- Partials      879      880       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@jcrossley3 jcrossley3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal is to add the valid field names to openapi.yaml, can't this entire PR be replaced by the following suggested changes?

I'm having a hard time justifying the complexity of the extra modules/macros.

Comment on lines 58 to 66
#[allow(dead_code)]
#[derive(QueryDoc)]
struct AdvisoryQuery {
average_score: i32,
average_severity: String,
modified: Date,
title: String,
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(dead_code)]
#[derive(QueryDoc)]
struct AdvisoryQuery {
average_score: i32,
average_severity: String,
modified: Date,
title: String,
}
/// List advisories
///
/// Valid field names to use in sort/filter queries:
/// - average_score
/// - average_severity
/// - modified
/// - title
///

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to have a customized OpenAPI description for each endpoint similar to the detailed (but hardcoded) EBNF grammar definition (without having to copy & paste the same grammar in each endpoint) currently available in https://github.com/trustification/trustify/blob/ecc14e850b4cb470cada306a29fe82ce63e38601/common/src/db/query.rs#L141-L210

Considering only this PR will have all of the derive procedural macro code, the changes required for using it will be just a matter of defining a struct with the list of fields, the same list you have in the proposed comment but with the benefit of being able to further improve/manage it in the future as we need.

Comment on lines 33 to 41
#[allow(dead_code)]
#[derive(QueryDoc)]
struct VulnerabilityQuery {
base_score: i32,
base_severity: String,
modified: Date,
title: String,
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(dead_code)]
#[derive(QueryDoc)]
struct VulnerabilityQuery {
base_score: i32,
base_severity: String,
modified: Date,
title: String,
}
/// List vulnerabilities
///
/// Valid field names to use in sort/filter queries:
/// - base_score
/// - base_severity
/// - modified
/// - title
///

Copy link
Contributor

@ctron ctron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the PR. Learning about this and leveraging it for a seamless integration.

Maybe it makes sense to change the name away from "doc", indicating that this could be more than just doc in the future.

@mrizzi
Copy link
Contributor Author

mrizzi commented Jul 23, 2025

I like the PR. Learning about this and leveraging it for a seamless integration.

Maybe it makes sense to change the name away from "doc", indicating that this could be more than just doc in the future.

Cool, I've renamed it 👍

@bobmcwhirter
Copy link
Contributor

If this is purely to add in common docs, can you use something like on each operation to amend additional docs?

#[doc = include_str!("../../EBNF_LANGUAGE_DEETS.md")]

ref: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html

@mrizzi
Copy link
Contributor Author

mrizzi commented Jul 23, 2025

If this is purely to add in common docs, can you use something like on each operation to amend additional docs?

#[doc = include_str!("../../EBNF_LANGUAGE_DEETS.md")]

ref: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html

This PR is more about having a documentation template (i.e. the EBNF grammar) populated with custom fields each endpoint manages providing a solution that allows us to further improve it.
It looks to me that the referenced md file would be anyway the same for all of the endpoints without letting us customize it for each endpoint.

@mrizzi
Copy link
Contributor Author

mrizzi commented Jul 23, 2025

@sourcery-ai summary

@mrizzi mrizzi force-pushed the feat-macro-generate-openapi branch from f7302a4 to ed3e90b Compare July 23, 2025 17:00
@mrizzi mrizzi changed the title feat: add 'QueryDoc' derive to manage custom Utoipa Query descriptions feat: add 'Query' derive to manage custom Utoipa Query descriptions Jul 23, 2025
mrizzi added 2 commits July 24, 2025 10:32
Signed-off-by: mrizzi <mrizzi@redhat.com>
@mrizzi mrizzi force-pushed the feat-macro-generate-openapi branch from ed3e90b to 5d6172d Compare July 24, 2025 08:38
Signed-off-by: mrizzi <mrizzi@redhat.com>
@mrizzi mrizzi force-pushed the feat-macro-generate-openapi branch from d7d2603 to 78bde90 Compare July 24, 2025 13:28
@mrizzi
Copy link
Contributor Author

mrizzi commented Jul 24, 2025

@sourcery-ai summary

@mrizzi
Copy link
Contributor Author

mrizzi commented Jul 24, 2025

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mrizzi - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `query/query-derive/src/lib.rs:16` </location>
<code_context>
+
+fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
+    let name = &ast.ident;
+    let fields = match &ast.data {
+        Data::Struct(data_struct) => data_struct
+            .fields
+            .clone()
+            .into_iter()
+            .map(|field| match field.ident {
+                Some(ref ident) => ident.to_string(),
+                None => String::default(),
+            })
+            .collect::<Vec<String>>(),
+        Data::Enum(_) => vec![],
+        Data::Union(_) => vec![],
+    };
</code_context>

<issue_to_address>
The macro currently ignores tuple and unnamed struct fields.

If tuple structs aren't supported, add a compile error or explicit panic to prevent silent failures.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let fields = match &ast.data {
        Data::Struct(data_struct) => data_struct
            .fields
            .clone()
            .into_iter()
            .map(|field| match field.ident {
                Some(ref ident) => ident.to_string(),
                None => String::default(),
            })
            .collect::<Vec<String>>(),
        Data::Enum(_) => vec![],
        Data::Union(_) => vec![],
    };
=======
fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let fields = match &ast.data {
        Data::Struct(data_struct) => {
            match &data_struct.fields {
                syn::Fields::Named(fields_named) => {
                    fields_named.named.iter()
                        .map(|field| field.ident.as_ref().unwrap().to_string())
                        .collect::<Vec<String>>()
                }
                syn::Fields::Unnamed(_) | syn::Fields::Unit => {
                    return syn::Error::new_spanned(
                        &ast.ident,
                        "Tuple structs and unit structs are not supported by #[derive(QueryDoc)]"
                    )
                    .to_compile_error()
                    .into();
                }
            }
        }
        Data::Enum(_) => vec![],
        Data::Union(_) => vec![],
    };
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `query/query-derive/src/lib.rs:69` </location>
<code_context>
+    );
+
+    let generated = quote! {
+        impl query::Query for #name {
+            fn generate_query_description() -> String {
+                #query_description.to_string()
</code_context>

<issue_to_address>
The macro assumes the `query` crate is always imported as `query`.

If `query` is renamed or missing, this will fail. Use `$crate` or document the required import path.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    let generated = quote! {
        impl query::Query for #name {
            fn generate_query_description() -> String {
                #query_description.to_string()
            }

            fn generate_sort_description() -> String {
                #sort_description.to_string()
            }
        }
    };
=======
    let generated = quote! {
        impl $crate::Query for #name {
            fn generate_query_description() -> String {
                #query_description.to_string()
            }

            fn generate_sort_description() -> String {
                #sort_description.to_string()
            }
        }
    };
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `modules/fundamental/src/advisory/endpoints/mod.rs:60` </location>
<code_context>
 }

+#[allow(dead_code)]
+#[derive(Query)]
+struct AdvisoryQuery {
+    id: Uuid,
+    identifier: String,
+    version: Option<String>,
+    document_id: String,
+    deprecated: bool,
+    issuer_id: Option<Uuid>,
+    published: Option<OffsetDateTime>,
+    modified: Option<OffsetDateTime>,
+    withdrawn: Option<OffsetDateTime>,
+    title: Option<String>,
+    ingested: OffsetDateTime,
+    label: String,
+}
+
</code_context>

<issue_to_address>
The AdvisoryQuery struct uses non-Option types for some fields, which may not match query parameter semantics.

Since these fields are not Option types, they must always be provided, which may not align with how query parameters are typically used. This could lead to deserialization errors if any of these fields are missing in incoming queries.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +14 to +28
fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let fields = match &ast.data {
Data::Struct(data_struct) => data_struct
.fields
.clone()
.into_iter()
.map(|field| match field.ident {
Some(ref ident) => ident.to_string(),
None => String::default(),
})
.collect::<Vec<String>>(),
Data::Enum(_) => vec![],
Data::Union(_) => vec![],
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The macro currently ignores tuple and unnamed struct fields.

If tuple structs aren't supported, add a compile error or explicit panic to prevent silent failures.

Suggested change
fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let fields = match &ast.data {
Data::Struct(data_struct) => data_struct
.fields
.clone()
.into_iter()
.map(|field| match field.ident {
Some(ref ident) => ident.to_string(),
None => String::default(),
})
.collect::<Vec<String>>(),
Data::Enum(_) => vec![],
Data::Union(_) => vec![],
};
fn impl_query(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let fields = match &ast.data {
Data::Struct(data_struct) => {
match &data_struct.fields {
syn::Fields::Named(fields_named) => {
fields_named.named.iter()
.map(|field| field.ident.as_ref().unwrap().to_string())
.collect::<Vec<String>>()
}
syn::Fields::Unnamed(_) | syn::Fields::Unit => {
return syn::Error::new_spanned(
&ast.ident,
"Tuple structs and unit structs are not supported by #[derive(QueryDoc)]"
)
.to_compile_error()
.into();
}
}
}
Data::Enum(_) => vec![],
Data::Union(_) => vec![],
};

Comment on lines +60 to +69
#[derive(Query)]
struct AdvisoryQuery {
id: Uuid,
identifier: String,
version: Option<String>,
document_id: String,
deprecated: bool,
issuer_id: Option<Uuid>,
published: Option<OffsetDateTime>,
modified: Option<OffsetDateTime>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The AdvisoryQuery struct uses non-Option types for some fields, which may not match query parameter semantics.

Since these fields are not Option types, they must always be provided, which may not align with how query parameters are typically used. This could lead to deserialization errors if any of these fields are missing in incoming queries.

@mrizzi mrizzi marked this pull request as ready for review July 29, 2025 08:18
@mrizzi mrizzi requested a review from ctron July 29, 2025 08:18
@mrizzi mrizzi marked this pull request as draft July 29, 2025 08:19
};
use actix_web::{HttpResponse, Responder, delete, get, post, web};
use query::TrustifyQuery;
use query_derive::Query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can add a pub use query_derive::Query; to query in order to just import use query::Query;.

Copy link
Contributor

@ctron ctron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small nits. but not blocking. looks great.

Signed-off-by: mrizzi <mrizzi@redhat.com>
@mrizzi mrizzi marked this pull request as ready for review August 1, 2025 13:50
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mrizzi - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@mrizzi mrizzi added this pull request to the merge queue Aug 1, 2025
Merged via the queue into guacsec:main with commit 8536269 Aug 1, 2025
8 checks passed
@mrizzi mrizzi deleted the feat-macro-generate-openapi branch August 1, 2025 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants