diff --git a/openapi/normalize_spec.py b/openapi/normalize_spec.py index 7601463..48558b3 100644 --- a/openapi/normalize_spec.py +++ b/openapi/normalize_spec.py @@ -450,6 +450,27 @@ def add_image_upload_body(spec): } +def naive_deploy_datetimes(spec): + """Type deploy timestamps as plain strings. + + The spec declares ``deploy.started_at``/``ended_at`` as ``format: + date-time`` with RFC 3339 examples, but the live API returns naive + timestamps without a UTC offset (e.g. ``2026-07-04T05:43:00``). The + generated ``chrono::DateTime`` fields then fail + deserialization with "premature end of input". Dropping the format yields + ``String`` fields that survive both shapes; ISO-ordered strings still sort + chronologically. + """ + + deploy = spec.get("components", {}).get("schemas", {}).get("deploy") + if not isinstance(deploy, dict): + return + for name in ("started_at", "ended_at"): + prop = deploy.get("properties", {}).get(name) + if isinstance(prop, dict): + prop.pop("format", None) + + def normalize(spec): """Apply all normalization passes to ``spec`` in place and return it.""" fix_path_parameters(spec) @@ -464,6 +485,7 @@ def normalize(spec): nullable_vpc_optional_fields(spec) integer_id_fields(spec) add_image_upload_body(spec) + naive_deploy_datetimes(spec) return spec diff --git a/src/models/deploy.rs b/src/models/deploy.rs index 8710a94..6f6ee20 100644 --- a/src/models/deploy.rs +++ b/src/models/deploy.rs @@ -25,10 +25,10 @@ pub struct Deploy { pub id: uuid::Uuid, /// Время запуска деплоя. #[serde(rename = "started_at")] - pub started_at: chrono::DateTime, + pub started_at: String, /// Время окончания деплоя. Определено для завершенных деплоев #[serde(rename = "ended_at", deserialize_with = "Option::deserialize")] - pub ended_at: Option>, + pub ended_at: Option, #[serde(rename = "status")] pub status: models::DeployStatus, /// Сообщение коммита. @@ -41,8 +41,8 @@ impl Deploy { app_id: String, commit_sha: String, id: uuid::Uuid, - started_at: chrono::DateTime, - ended_at: Option>, + started_at: String, + ended_at: Option, status: models::DeployStatus, commit_msg: String ) -> Deploy { diff --git a/tests/deploy_naive_datetimes.rs b/tests/deploy_naive_datetimes.rs new file mode 100644 index 0000000..5d5d2fc --- /dev/null +++ b/tests/deploy_naive_datetimes.rs @@ -0,0 +1,45 @@ +// Regression test for naive deploy timestamps. +// +// Timeweb's live API returns `started_at`/`ended_at` on +// `GET /api/v1/apps/{app_id}/deploys` without a UTC offset (e.g. +// `2026-07-04T05:43:00`), even though the spec declares `format: date-time` +// with RFC 3339 examples. The generated `Deploy` model must type these +// fields as plain strings (see +// `openapi/normalize_spec.py::naive_deploy_datetimes`) so both the naive +// live shape and the documented RFC 3339 shape deserialize. + +use timeweb_rs::models::Deploy; + +#[test] +fn deploy_tolerates_naive_timestamps() { + let body = r#"{ + "app_id": "2e1c50e8-d947-4945-9988-813fa2fd810c", + "commit_sha": "d802ac241e84d5740fae66d5f950a8cd2c96e775", + "id": "45755624-d25e-4472-a59c-2c0b74ba5242", + "started_at": "2026-07-04T05:43:00", + "ended_at": "2026-07-04T05:46:32", + "status": "success", + "commit_msg": "fix: something" + }"#; + + let deploy: Deploy = serde_json::from_str(body).expect("naive timestamps must deserialize"); + assert_eq!(deploy.started_at, "2026-07-04T05:43:00"); + assert_eq!(deploy.ended_at.as_deref(), Some("2026-07-04T05:46:32")); +} + +#[test] +fn deploy_tolerates_rfc3339_timestamps_and_null_ended_at() { + let body = r#"{ + "app_id": "2e1c50e8-d947-4945-9988-813fa2fd810c", + "commit_sha": "d802ac241e84d5740fae66d5f950a8cd2c96e775", + "id": "45755624-d25e-4472-a59c-2c0b74ba5242", + "started_at": "2024-07-24T10:38:44.000Z", + "ended_at": null, + "status": "stopped", + "commit_msg": "feat: something else" + }"#; + + let deploy: Deploy = serde_json::from_str(body).expect("documented shape must deserialize"); + assert_eq!(deploy.started_at, "2024-07-24T10:38:44.000Z"); + assert_eq!(deploy.ended_at, None); +}