From 08e87d85efb70c08f0a2145e93304e8bb668f882 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Fri, 3 Jul 2026 09:45:15 +0700 Subject: [PATCH] #39 fix: expose project apps collection and app project_id the API returns --- openapi/normalize_spec.py | 48 +++++++++++++++++++ src/models/app.rs | 12 ++++- .../get_all_project_resources_200_response.rs | 8 ++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/openapi/normalize_spec.py b/openapi/normalize_spec.py index df31489..7601463 100644 --- a/openapi/normalize_spec.py +++ b/openapi/normalize_spec.py @@ -198,6 +198,53 @@ def add_undocumented_account_status_fields(spec): props.setdefault(name, schema) +def add_undocumented_project_resource_fields(spec): + """Expose project resources and app→project linkage the API returns. + + The live ``GET /projects/{project_id}/resources`` returns far more + collections than the spec documents (verified against production): + ``apps``, ``agents``, ``domains``, ``knowledgebases``, ``mailboxes``, + ``registries``, ``cdn``, ``monitorings`` and ``routers`` alongside the six + documented ones. Only ``apps`` is added here — its element shape was + verified to match the documented ``app`` schema exactly; the other + collections stay untouched until their shapes can be checked against live + data. + + Live ``app`` objects also carry a ``project_id`` the spec omits — the only + way to attribute an app to a project from the apps list. + + Both additions are defensive (optional, nullable) so a missing field never + breaks deserialization. Run on every regeneration, so they survive + upstream spec syncs automatically. + """ + app = spec.get("components", {}).get("schemas", {}).get("app") + if isinstance(app, dict): + app.setdefault("properties", {}).setdefault( + "project_id", {"type": "integer", "nullable": True} + ) + + path = spec.get("paths", {}).get("/api/v1/projects/{project_id}/resources", {}) + schema = ( + path.get("get", {}) + .get("responses", {}) + .get("200", {}) + .get("content", {}) + .get("application/json", {}) + .get("schema", {}) + ) + for part in schema.get("allOf", []): + props = part.get("properties") + if isinstance(props, dict) and "servers" in props: + props.setdefault( + "apps", + { + "type": "array", + "items": {"$ref": "#/components/schemas/app"}, + "nullable": True, + }, + ) + + _RENAMED_PROPERTIES = { "ssh-keys": "ssh_keys", "knowledgebases": "knowledge_bases", @@ -409,6 +456,7 @@ def normalize(spec): localize_tags(spec) nullable_response_id(spec) add_undocumented_account_status_fields(spec) + add_undocumented_project_resource_fields(spec) rename_mismatched_properties(spec) relax_overstrict_required(spec) relax_apps_preset_required(spec) diff --git a/src/models/app.rs b/src/models/app.rs index 4339bac..8119f62 100644 --- a/src/models/app.rs +++ b/src/models/app.rs @@ -120,7 +120,14 @@ pub struct App { pub language: Option, /// Время запуска приложения. #[serde(rename = "start_time", skip_serializing_if = "Option::is_none")] - pub start_time: Option> + pub start_time: Option>, + #[serde( + rename = "project_id", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub project_id: Option> } impl App { @@ -152,7 +159,8 @@ impl App { disk_status: None, is_qemu_agent: None, language: None, - start_time: None + start_time: None, + project_id: None } } } diff --git a/src/models/get_all_project_resources_200_response.rs b/src/models/get_all_project_resources_200_response.rs index 51dc4b5..edf9922 100644 --- a/src/models/get_all_project_resources_200_response.rs +++ b/src/models/get_all_project_resources_200_response.rs @@ -28,6 +28,13 @@ pub struct GetAllProjectResources200Response { pub dedicated_servers: Vec, #[serde(rename = "meta")] pub meta: Box, + #[serde( + rename = "apps", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub apps: Option>>, /// ID запроса, который можно указывать при обращении в службу технической /// поддержки, чтобы помочь определить проблему. #[serde(rename = "response_id", deserialize_with = "Option::deserialize")] @@ -53,6 +60,7 @@ impl GetAllProjectResources200Response { databases, dedicated_servers, meta: Box::new(meta), + apps: None, response_id } }