| name | yamuse-drift |
| description | Use when a yamuse model no longer matches what the Yandex Music API sends — a field arrived as the wrong type, a response stopped parsing, `on_field_repair` is firing, or you want to find fields the API sends that the crate does not model yet. |
chasing api drift in yamuse
the api is private and changes without notice. the crate is built to survive that
rather than to notice it, so drift is silent by default and you have to go looking.
there are two different problems here and they need different tools. getting
them confused wastes the most time.
| symptom | what it is | the tool |
|---|
a field you want is always None, or you suspect the api sends more | a field the crate never modelled | drift::unmapped_fields |
on_field_repair fires, or a call fails with "gave up after repairing" | a field whose type changed | tolerant::deserialize's report |
finding fields the crate does not model
YM_TOKEN=... cargo run --example drift
that example is the whole workflow: fetch raw with Client::get_raw, parse into
the model, and diff with drift::unmapped_fields. to check an endpoint it does
not cover, add a check::<Model>(&client, label, path, params) line.
an unmapped field is not a bug. it is a field you could be using. the fix is to
add it to the model in src/models/<domain>.rs — Option if scalar, Vec if a
list, Option<serde_json::Value> if the shape is not settled — and add it to that
module's survives_an_empty_object test.
note that a serde_json::Value field swallows everything nested inside it, so
those subtrees never appear in the report. that is the cost of keeping them.
when a field's type changed
tolerant::deserialize repairs one field rather than failing the response, and
reports what it did. wire it up:
Client::builder()
.token(token)
.on_field_repair(|repair| {
if repair.is_lossy() {
eprintln!("{repair}");
}
})
read the Repair kind before doing anything:
Narrowed — a json float with no fractional part became an integer. this is
the js/rust number mismatch, not drift. nothing to fix, and is_lossy() is
false so it can be filtered out entirely.
Dropped — the value did not fit at all and that field is now None. this
is real. either the api changed or the model was always wrong.
fixing a Dropped
-
capture the actual payload: client.get_raw(path, params) and print it.
-
decide what the field really is now.
-
if the api sends one new type, change the model field's type. that is a
breaking change if the field is public — most are.
-
if the api sends either shape depending on the call, model both rather than
picking a side. the crate already has two of these:
models::Id — a number in some responses, a string in others, sometimes
compound ("42:1001").
models::FlagOrList — a flag by the field's name, a list on the wire.
an untagged enum like those is the honest answer when the api is genuinely
inconsistent. guessing wrong costs you the field on half the calls.
"gave up after repairing 16 distinct mistyped fields"
MAX_REPAIRS was hit, and the call failed rather than returning a mostly-empty
model. this almost never means sixteen fields changed at once. check, in order:
- is the payload even the right one? an endpoint that started answering an
error object, or a redirect page, trips this immediately.
- is one model wrong in a way that repeats? the budget counts distinct
fields with array indices collapsed, so a single bad field in a long list
counts once — but a struct used in several places counts once per place.
the error message names the last field it tried; start there.
reproducing without a live account
tests/transport.rs runs against wiremock, so a payload that broke in
production becomes a regression test with no token involved:
Mock::given(method("GET"))
.and(path("/the/endpoint"))
.respond_with(envelope(&serde_json::json!({ "theField": ["not", "a", "bool"] })))
.mount(&server)
.await;
add one of those whenever you fix a drift, or the next release re-breaks it.