Documentation
¶
Overview ¶
Package docdiff is PayCLI's structural diff of two Payload documents.
It answers "what exactly would change?" — before a write (`--dry-run`), between the draft and the published version of a page, between two documents, or between two exported files — without the caller piping both sides through `jq -S` and `diff`, which reports reformatted JSON and shifted array indices instead of the one field that changed.
Three properties separate it from a line diff:
- **Rows are matched by `id`.** Payload gives every array and blocks row a server-generated `id`, so a reordered layout is reported as the moves it is, and an edit inside a moved row is reported at the row's NEW path, not as "every row after the insert changed".
- **Rich text is compared by structure but reported as text.** A Lexical editor state is a deep JSON tree; a one-word edit in it is reported once, at the field, with `from_text`/`to_text`, instead of as a leaf path six levels into `root.children`.
- **PATCH semantics are modelled.** ApplyPatch computes the document Payload would store after `PATCH` with a body — keys absent from the body keep their stored value, named groups merge key by key, and an array row whose `id` matches a stored row keeps that row's unsent keys — so DiffPatch reports what a write would really change, not the difference between a partial body and a whole document.
Nothing here performs I/O, reads the clock or knows about the CLI. Values are the generic shapes encoding/json produces (map[string]any, []any, string, bool, nil, and json.Number or float64 for numbers); inputs are never mutated.
Index ¶
Constants ¶
const ( // KindRichText marks a change to a Lexical editor state. From/To are // replaced by FromText/ToText unless [Options.Full] is set. KindRichText = "richtext" // KindRow marks the addition, removal or move of a whole array row. KindRow = "row" )
Kinds carried in Change.Kind.
const RichTextMarker = "$richtext"
RichTextMarker is the key of the object that stands in for an abbreviated editor state inside a reported value: {"$richtext": "plain text"}.
Variables ¶
var DefaultIgnoreKeys = []string{"createdAt", "updatedAt"}
DefaultIgnoreKeys are ignored at every depth unless Options.IncludeTimestamps is set: Payload rewrites them on every save, so a diff that reports them reports nothing but the fact that a save happened.
Functions ¶
func ApplyPatch ¶
ApplyPatch returns the document Payload would store after a PATCH of current with body. Neither argument is modified. The body's own top-level `id` is ignored: the id in the URL decides which document is written.
func Equal ¶
Equal is JSON equality: numbers compare by value whatever their Go type (json.Number from a UseNumber decoder, float64 from a plain one), objects by key set and values, arrays element-wise.
Types ¶
type Change ¶
type Change struct {
Op Op `json:"op"`
Path string `json:"path"`
// Kind is "" for a plain value, KindRichText or KindRow.
Kind string `json:"kind,omitempty"`
// ID and BlockType identify a row for row-level changes, so the caller can
// build a `pay blocks` selector (`id:<ID>`) without re-reading the doc.
ID any `json:"id,omitempty"`
BlockType string `json:"block_type,omitempty"`
// FromIndex and ToIndex are set on a move.
FromIndex *int `json:"from_index,omitempty"`
ToIndex *int `json:"to_index,omitempty"`
// From and To are the values. Which of them is rendered depends on Op:
// add renders `to`, remove renders `from`, change renders both (either may
// be null), move renders neither.
From any `json:"from"`
To any `json:"to"`
// FromText and ToText are the plain text of a rich-text value.
FromText *string `json:"from_text,omitempty"`
ToText *string `json:"to_text,omitempty"`
// Detail qualifies a change: "formatting" is a rich-text change whose
// plain text is identical (bold, a link, a heading level…).
Detail string `json:"detail,omitempty"`
// contains filtered or unexported fields
}
Change is one difference. Path is written in the §10.2 echo-diff notation (`layout[2].blocks[1].title`). Every index in it addresses the RIGHT document — the one a write produces, and the one the next `pay blocks` edit runs against — except the last index of a row removal, which is the row's position on the LEFT (it has no position on the right).
func (Change) MarshalJSON ¶
MarshalJSON renders only the fields that mean something for the op, in a fixed order. A plain struct tag cannot express "from is present and null" (a change from null to a value) versus "from is absent" (an add).
type Op ¶
type Op string
Op is the kind of one change.
const ( // OpAdd is a key or row present only on the right. OpAdd Op = "add" // OpRemove is a key or row present only on the left. OpRemove Op = "remove" // OpChange is a value that differs between the two sides. OpChange Op = "change" // OpMove is a row (matched by id) whose position changed relative to the // other matched rows. OpMove Op = "move" )
type Options ¶
type Options struct {
// IncludeTimestamps stops ignoring DefaultIgnoreKeys.
IncludeTimestamps bool
// IgnoreKeys are additional object keys ignored at every depth.
IgnoreKeys []string
// IgnorePaths are path patterns whose value — and everything below it —
// is ignored. A pattern is a path in change notation; `[]` or `[*]`
// matches any index: "meta.image", "layout[].blockName", "layout[3]".
IgnorePaths []string
// LeafPaths are path patterns (same grammar) whose value is compared and
// replaced as one unit rather than descended into — a `json` field, whose
// object value Payload stores wholesale. A caller with a schema passes
// them; without one, every non-rich-text object is treated as a group.
LeafPaths []string
// NullDistinct reports null versus an absent key as a change. By default
// the two are equal: Payload returns null for an unset field, and a
// hand-written file or a PATCH body usually omits it.
NullDistinct bool
// Full keeps every value lossless: rich-text changes carry the editor
// states in from/to, and editor states nested inside added or removed
// values are not abbreviated.
Full bool
}
Options tunes a diff. The zero value is the documented default.
type Result ¶
type Result struct {
Identical bool `json:"identical"`
Summary Summary `json:"summary"`
Changes []Change `json:"changes"`
}
Result is one diff.
type Summary ¶
type Summary struct {
Total int `json:"total"`
Added int `json:"added"`
Removed int `json:"removed"`
Changed int `json:"changed"`
Moved int `json:"moved"`
// RichText counts the changes whose Kind is KindRichText (they are also
// counted under their op).
RichText int `json:"rich_text"`
// Fields are the top-level document fields at least one change touches,
// sorted — the keys a minimal PATCH would have to carry.
Fields []string `json:"fields"`
}
Summary counts the changes.