Skip to content
Merged

42 #43

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
22 changes: 22 additions & 0 deletions openapi/normalize_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<FixedOffset>`` 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)
Expand All @@ -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


Expand Down
8 changes: 4 additions & 4 deletions src/models/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ pub struct Deploy {
pub id: uuid::Uuid,
/// Время запуска деплоя.
#[serde(rename = "started_at")]
pub started_at: chrono::DateTime<chrono::FixedOffset>,
pub started_at: String,
/// Время окончания деплоя. Определено для завершенных деплоев
#[serde(rename = "ended_at", deserialize_with = "Option::deserialize")]
pub ended_at: Option<chrono::DateTime<chrono::FixedOffset>>,
pub ended_at: Option<String>,
#[serde(rename = "status")]
pub status: models::DeployStatus,
/// Сообщение коммита.
Expand All @@ -41,8 +41,8 @@ impl Deploy {
app_id: String,
commit_sha: String,
id: uuid::Uuid,
started_at: chrono::DateTime<chrono::FixedOffset>,
ended_at: Option<chrono::DateTime<chrono::FixedOffset>>,
started_at: String,
ended_at: Option<String>,
status: models::DeployStatus,
commit_msg: String
) -> Deploy {
Expand Down
45 changes: 45 additions & 0 deletions tests/deploy_naive_datetimes.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading