Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion crates/codex-api/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,13 @@ The following paths are exempt from rate limiting:

// Series endpoints
v1::handlers::list_series,
v1::handlers::list_series_full,
v1::handlers::search_series,
v1::handlers::list_series_filtered,
v1::handlers::list_series_external_index,
v1::handlers::list_series_alphabetical_groups,
v1::handlers::get_series,
v1::handlers::get_series_full,
v1::handlers::patch_series,
v1::handlers::get_series_books,
v1::handlers::purge_series_deleted_books,
Expand Down Expand Up @@ -303,6 +305,7 @@ The following paths are exempt from rate limiting:
v1::handlers::list_books,
v1::handlers::list_books_filtered,
v1::handlers::get_book,
v1::handlers::get_book_full,
v1::handlers::patch_book,
v1::handlers::get_adjacent_books,
v1::handlers::get_book_file,
Expand Down Expand Up @@ -487,6 +490,17 @@ The following paths are exempt from rate limiting:
v1::handlers::task_queue::reprocess_series_titles,
v1::handlers::task_queue::reprocess_library_series_titles,

// Library jobs: per-library scheduled work, plus the field-group catalog
// its editor is built from.
v1::handlers::library_jobs::list_jobs,
v1::handlers::library_jobs::get_job,
v1::handlers::library_jobs::create_job,
v1::handlers::library_jobs::patch_job,
v1::handlers::library_jobs::delete_job,
v1::handlers::library_jobs::run_job_now,
v1::handlers::library_jobs::dry_run_job,
v1::handlers::library_jobs::list_field_groups,

// Filesystem endpoints
v1::handlers::browse_filesystem,
v1::handlers::list_drives,
Expand Down Expand Up @@ -952,8 +966,10 @@ The following paths are exempt from rate limiting:
v1::dto::CreateApiKeyResponse,
v1::dto::UpdateApiKeyRequest,
v1::dto::PaginatedResponse<v1::dto::SeriesDto>,
v1::dto::PaginatedResponse<v1::dto::FullSeriesResponse>,
v1::dto::PaginatedResponse<v1::dto::BookDto>,
v1::dto::PaginatedResponse<v1::dto::UserDto>,
v1::dto::PaginatedResponse<v1::dto::SeriesExternalIndexDto>,

// Metrics DTOs
v1::dto::MetricsDto,
Expand Down Expand Up @@ -1301,7 +1317,13 @@ The following paths are exempt from rate limiting:
// Third-Party Compatibility
(name = "Komga", description = "Komga-compatible API for third-party apps (Komic, etc.)"),
),
modifiers(&SecurityAddon, &OperationIdPrefixer, &TagGroupsModifier, &NullableRefFlattener),
modifiers(
&SecurityAddon,
&OperationIdPrefixer,
&TagGroupsModifier,
&NullableRefFlattener,
&GenericArgumentReferencer,
),
)]
pub struct ApiDoc;

Expand Down Expand Up @@ -1355,6 +1377,120 @@ impl utoipa::Modify for SecurityAddon {
/// is left in place: that is an API design problem (the endpoint should answer
/// `204 No Content`) and hiding it here would make the document claim a body
/// that the server may not send.
/// Make a generic instantiation reference its type argument instead of
/// expanding it inline.
///
/// utoipa renders `PaginatedResponse<SeriesDto>` by inlining `SeriesDto`'s
/// schema into `data.items`, even though `SeriesDto` is registered as its own
/// component. The document is correct either way, but every generator that has
/// to name an inline schema invents a fresh type for it:
/// `swift-openapi-generator` produces
/// `PaginatedResponseSeriesDto.DataPayloadPayload` where `getSeries` produces
/// `SeriesDto`. The two are structurally identical and not interchangeable, so
/// a client needs a hand-written conversion for every paginated endpoint.
///
/// This walks each `Base_Argument` component and replaces any subschema that is
/// byte-identical to `Argument`'s own component with a `$ref` to it. Requiring
/// an exact match is what makes the rewrite safe: it can only ever collapse a
/// copy of a schema the document already contains under that name, so nothing
/// about the described wire format changes.
///
/// It is not specific to pagination — `KomgaPage<T>` and any future generic get
/// the same treatment.
struct GenericArgumentReferencer;

impl utoipa::Modify for GenericArgumentReferencer {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let Some(components) = openapi.components.as_mut() else {
return;
};

let known: std::collections::BTreeMap<String, serde_json::Value> = components
.schemas
.iter()
.filter_map(|(name, schema)| {
serde_json::to_value(schema).ok().map(|v| (name.clone(), v))
})
.collect();

for (name, schema) in components.schemas.iter_mut() {
// `Base_Argument` is the shape utoipa emits for a concrete
// instantiation of a generic.
let Some((_, argument)) = name.split_once('_') else {
continue;
};
if argument == name {
continue;
}
let Some(target) = known.get(argument) else {
continue;
};
reference_argument(schema, argument, target);
}
}
}

fn reference_argument(
schema: &mut utoipa::openapi::RefOr<utoipa::openapi::Schema>,
argument: &str,
target: &serde_json::Value,
) {
use utoipa::openapi::{Ref, RefOr};

if matches!(schema, RefOr::T(_)) && serde_json::to_value(&*schema).ok().as_ref() == Some(target)
{
*schema = RefOr::Ref(Ref::from_schema_name(argument));
return;
}
reference_argument_children(schema, argument, target);
}

fn reference_argument_children(
schema: &mut utoipa::openapi::RefOr<utoipa::openapi::Schema>,
argument: &str,
target: &serde_json::Value,
) {
use utoipa::openapi::{RefOr, Schema, schema::AdditionalProperties};

let RefOr::T(schema) = schema else {
return;
};

match schema {
Schema::Object(object) => {
for (_, property) in object.properties.iter_mut() {
reference_argument(property, argument, target);
}
if let Some(additional) = object.additional_properties.as_deref_mut()
&& let AdditionalProperties::RefOr(value) = additional
{
reference_argument(value, argument, target);
}
}
Schema::Array(array) => {
if let utoipa::openapi::schema::ArrayItems::RefOrSchema(items) = &mut array.items {
reference_argument(items, argument, target);
}
}
Schema::OneOf(one_of) => {
for item in one_of.items.iter_mut() {
reference_argument(item, argument, target);
}
}
Schema::AllOf(all_of) => {
for item in all_of.items.iter_mut() {
reference_argument(item, argument, target);
}
}
Schema::AnyOf(any_of) => {
for item in any_of.items.iter_mut() {
reference_argument(item, argument, target);
}
}
_ => {}
}
}

struct NullableRefFlattener;

impl utoipa::Modify for NullableRefFlattener {
Expand Down
1 change: 1 addition & 0 deletions crates/codex-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod image_limit;
pub mod middleware;
pub mod observability;
pub mod permissions;
pub mod ranged_file;
pub mod routes;
pub mod web;

Expand Down
Loading
Loading