rows

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package rows is PayCLI's local editor for an array of objects: the rows of a Payload `blocks` field, or of a plain `array` field.

It exists because the one edit a content agent makes most often — "move the CTA above the media block", "drop the third block" — has no API of its own. Payload's REST surface can only replace the whole array, so the edit is a read-modify-write round trip that every caller re-invents with jq and a temp file, and gets wrong in the same two ways: the anchor index shifts once the moved row is lifted out, and a row is addressed by a position the caller read from a stale listing.

Nothing here performs I/O, reads the clock or knows what a Payload document is. A Selector is parsed from a string and matched against a []any; every mutation returns a NEW slice and never aliases the input. That makes each rule below a table test, and it keeps the index arithmetic — the part that is actually hard — in one place instead of in six command files.

Index

Constants

View Source
const (
	KeyID        = "id"
	KeyBlockType = "blockType"
	KeyBlockName = "blockName"
)

Payload's own keys on a blocks row. They are protocol, not content: `blockType` decides which block the row IS, `id` is server-generated, and `blockName` is the admin-UI label. They are named here because the selector grammar addresses rows BY them.

View Source
const SelectorGrammar = `N | -N | first | last | id:VALUE | name:VALUE | type:SLUG | type:SLUG[N]`

SelectorGrammar is the one-paragraph description of the grammar, shared by every command's help so the five forms are documented identically in all of them.

Variables

View Source
var SelectorForms = []string{
	"3           the row at index 3 (0-based)",
	"-1          the last row; -2 the second to last",
	"first,last  sugar for 0 and -1",
	"id:67f3a1   the row whose `id` is 67f3a1 — the only stable handle across edits",
	"name:Hero   the row whose `blockName` is exactly Hero (never a substring)",
	"type:cta    EVERY row whose `blockType` is cta",
	"type:cta[1] the second cta row (0-based), which is always exactly one row",
}

SelectorForms is the per-form explanation for help output.

Functions

func Copy

func Copy(list []any, src int, a Anchor) ([]any, int, error)

Copy duplicates the row at src into the destination a names, stripping every server-generated `id` on the way — at the top level and at every depth.

The strip is not optional and not a flag. Payload treats a row whose `id` matches an existing row as THAT row: a duplicate that kept its ids does not add a block, it silently rewrites the original and drops one of the two.

func Insert

func Insert(list []any, row any, a Anchor) ([]any, int, error)

Insert puts row at the destination a names and returns the new array plus the index it landed at.

The destination space has len+1 slots (0 .. len), not len: appending to a 3-row array is index 3. A negative `--at` counts back through those slots, so `--at -1` is the last position — consistent with a negative selector meaning the last row.

func LooksLikeBlocks

func LooksLikeBlocks(list []any) bool

LooksLikeBlocks reports whether every object row carries a `blockType`, which is what distinguishes a blocks field from a plain `array` field in a document PayCLI has no schema for. An empty array is not blocks: there is nothing to read it from, and guessing would name a field the caller never asked for.

func Move

func Move(list []any, src int, a Anchor) ([]any, int, error)

Move relocates the row at src to the destination a names and returns the new array plus the index the row ended at.

The anchor is resolved against the array WITH the moved row still in it, and the destination is then recomputed against the array WITHOUT it. That is the whole reason this function exists: "move row 0 after row 3" naively becomes insert-at-4 on a 3-row remainder and lands the row past its anchor. Every hand-written version of this loop gets it wrong once.

func Remove

func Remove(list []any, idx []int) []any

Remove deletes the rows at idx and returns the new array. Indices may arrive in any order and may repeat.

func ResolveMany

func ResolveMany(list []any, sel Selector, field string) ([]int, error)

ResolveMany matches sel and insists on at least one row. Several is fine.

func ResolveOne

func ResolveOne(list []any, sel Selector, field string) (int, error)

ResolveOne matches sel and insists on exactly one row.

func Set

func Set(list []any, idx int, patch map[string]any, unset []string) ([]any, error)

Set applies a field patch to the row at idx and returns the new array. Keys are dotted paths into the row; a nil value in patch deletes the key.

func StripIDs

func StripIDs(v any) any

StripIDs removes every `id` key from a value, at every depth. Exported because `pay blocks add` needs it for a row pasted out of another document.

func Types

func Types(list []any) []string

Types returns the distinct blockTypes present, sorted. Used to tell a caller what a failed `type:` selector could have matched.

Types

type AmbiguousError

type AmbiguousError struct {
	Sel     Selector
	Field   string
	Matched []int
	Have    []Summary
}

AmbiguousError is returned when a selector addressed several rows but the operation acts on exactly one.

func (*AmbiguousError) Error

func (e *AmbiguousError) Error() string

type Anchor

type Anchor struct {
	Mode  Mode
	Index int
	Sel   Selector
	// Label is how the caller SPELLED the destination, when that is clearer
	// than the mode it compiled to. `--last` compiles to index -1, and an op
	// log reading "(index -1)" makes a reader work out what the caller wrote;
	// "(last)" does not. Empty means "describe the mode".
	Label string
}

Anchor is a destination. Before/After are expressed relative to another ROW rather than to an index on purpose: an index read out of a listing is stale the moment anything else in the pipeline edits the array, while "after the media block" stays true.

func (Anchor) Describe

func (a Anchor) Describe() string

Describe renders the anchor for an error message or an op log. It returns "" when the destination is already stated by the sentence around it — an index the caller can read off the op's own text.

type Kind

type Kind string

Kind is the form a Selector took. The grammar is closed — five forms, no escapes, no wildcards — for the same reason `--path` is not jq: an open grammar guarantees a caller sends something plausible that this package does not implement, and gets an unspecified failure instead of a named one.

const (
	// KindIndex is a bare integer: `0`, `3`, `-1`. Negative counts from the
	// end, so `-1` is the last row.
	KindIndex Kind = "index"
	// KindID is `id:<value>` — the row's server-generated `id`.
	KindID Kind = "id"
	// KindName is `name:<value>` — an exact `blockName` match.
	KindName Kind = "name"
	// KindType is `type:<slug>` — every row with that `blockType`, or the
	// n-th one with `type:<slug>[n]`.
	KindType Kind = "type"
)

type Mode

type Mode string

Mode is how an Anchor names a destination.

const (
	// ModeAppend puts the row after every existing row. It is the default for
	// an insert, and the only mode that needs no argument.
	ModeAppend Mode = "append"
	// ModeIndex is `--at N` / `--to N`, a literal destination index.
	ModeIndex Mode = "index"
	// ModeBefore is `--before SEL`: end up immediately above that row.
	ModeBefore Mode = "before"
	// ModeAfter is `--after SEL`: end up immediately below that row.
	ModeAfter Mode = "after"
)

type NoMatchError

type NoMatchError struct {
	Sel   Selector
	Field string
	Have  []Summary
}

NoMatchError is returned when a selector that had to address a row addressed none. It carries the rows' summary so the caller can print what DOES exist instead of only what does not.

func (*NoMatchError) Error

func (e *NoMatchError) Error() string

type Selector

type Selector struct {
	// Raw is the string the caller wrote, kept verbatim for error messages.
	Raw string
	// Kind is which of the five forms was used.
	Kind Kind
	// Index is set for KindIndex and may be negative.
	Index int
	// Value is the id, blockName or blockType being matched.
	Value string
	// Nth is the `[n]` subscript of `type:cta[1]`, nil when absent. It is
	// separate from Index so that "the second cta" and "row 2" cannot be
	// confused by a reader of this struct.
	Nth *int
}

Selector addresses one or more rows. The zero value is unusable; build one with ParseSelector.

func ParseSelector

func ParseSelector(s string) (Selector, error)

ParseSelector parses one selector. It never consults the rows, so a selector can be validated before a document has been read.

func (Selector) Match

func (s Selector) Match(list []any) []int

Match returns the indices sel addresses, ascending. An empty result is not an error here: the caller decides whether "no match" is fatal, because `rm` on an already-absent row and `mv` of a row that must exist want opposite answers.

type Summary

type Summary struct {
	Index     int    `json:"index"`
	ID        string `json:"id,omitempty"`
	BlockType string `json:"block_type,omitempty"`
	BlockName string `json:"block_name,omitempty"`
	// Selector is the shortest selector that addresses THIS row and no other.
	// It is emitted rather than left to the caller because the shortest one is
	// not the obvious one: an index is stale after the next edit in the pipe,
	// so an `id:` is preferred whenever the row has an id.
	Selector string `json:"selector"`
	// Fields is the row's own keys, minus Payload's plumbing, so a caller can
	// see what `pay blocks set` could address without printing the whole row.
	Fields []string `json:"fields,omitempty"`
	// Row is the complete row. It is only filled in for `--long`.
	Row any `json:"row,omitempty"`
}

Summary is one row as `pay blocks ls` reports it: enough to choose a row and to write a selector for it, and nothing else.

func Summarize

func Summarize(list []any) []Summary

Summarize renders every row. It never fails: a row that is not an object still gets an entry, because a listing that silently skips rows would make the printed indices disagree with the real ones.

Jump to

Keyboard shortcuts

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