docdiff

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 8 Imported by: 0

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:

  1. **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".
  2. **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`.
  3. **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

View Source
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.

View Source
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

View Source
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

func ApplyPatch(current, body map[string]any, opts Options) map[string]any

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

func Equal(a, b any) bool

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.

func JoinIndex

func JoinIndex(path string, i int) string

JoinIndex appends an array index to a path.

func JoinKey

func JoinKey(path, key string) string

JoinKey appends an object key to a path.

func RootField

func RootField(path string) string

RootField is the top-level key a path starts with ("layout" for "layout[2].title"), or "" for the empty path.

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

func (c Change) MarshalJSON() ([]byte, error)

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.

func Diff

func Diff(left, right any, opts Options) Result

Diff compares two documents (or any two JSON values) and returns every difference in a deterministic order: object keys sorted, and within an array the removed rows first (left order), then the right rows in order.

func DiffPatch

func DiffPatch(current, body map[string]any, opts Options) Result

DiffPatch is Diff(current, ApplyPatch(current, body)): exactly what a PATCH with body would change on current.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL