Functional Weave
Code in Rust

collections.diff@1.0.0

README.md

2,642 bytes · view raw

# collections.diff

Compares two lists of records matched by `key` and says what happened between
them: records that were added, records that were removed, and records present
in both whose contents changed, with the names of the fields that differ. It
is the heart of a sync job, an audit log or a "review changes before import"
screen.

ORDER: `added` and `changed` follow the order of the `after` list, `removed`
follows the order of the `before` list. `unchanged` is only a count, because a
caller showing a diff never wants the untouched rows back.

CHANGED FIELDS are listed by name, sorted by Unicode code point. Sorting,
rather than keeping the records' own field order, is deliberate: JavaScript
reorders integer-like object keys ("2" before "a") and the three languages
would otherwise disagree about the same records.

EQUALITY is deep JSON equality, spelled out the same way in all three
languages rather than inherited from each one's `==`:

- numbers compare by value, so `1` and `1.0` are equal (JSON does not
  distinguish them, and a round trip through a float column must not show up
  as a change);
- `true` is not `1`, and `"1"` is not `1`: a value that changed type did change;
- lists are equal when they have the same length and equal items in the same
  order;
- maps are equal when every field is equal, where a missing field and a null
  field count as the same. That is the house rule across the collections
  capabilities, and it stops a serialiser that drops nulls from making every
  record look changed. It applies to the top-level fields too: a field that
  goes from null to absent is not reported.

KEYS are rendered the way `collections.group-by-key` renders them: strings are
themselves, whole numbers their digits, booleans `true`/`false`. So a record
keyed `1` in one list and `"1"` in the other is the same record (and the key
field itself is then reported as changed, because its type did change). A
fractional number, a list or a map at the key is an error.

ERRORS, not guesses: a record with no value (or null) at the key, and a key
that appears twice in the same list. A diff that silently picked one of two
duplicates would report changes that never happened. Also an empty key name
and a non-list argument. Inputs are never modified; the records in the result
are the records passed in.

The result types are generic (`RecordDiff<T>`, `RecordChange<T>`) and this
function returns them over `record`. That keeps the door open for a typed
variant, and it is also how the manifest gets a record-valued field past the
Python type generator, which does not yet import `Any` for a bare `record`
field.