aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/json.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 12:58:14 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 12:58:14 +0200
commit24a84671ace373ae331fa83a1ff484990f4dff0e (patch)
treea6b6e3949d7dd241f1d13e079dfb982d758c89a2 /internal/plan/json.go
parent3b36a48b7ce5a53a9366f3b31f94311f178e2553 (diff)
downloadkrino-24a84671ace373ae331fa83a1ff484990f4dff0e.tar.gz
krino-24a84671ace373ae331fa83a1ff484990f4dff0e.zip
krino: planning — chains, placeholders, conflicts, JSON
Diffstat (limited to 'internal/plan/json.go')
-rw-r--r--internal/plan/json.go97
1 files changed, 97 insertions, 0 deletions
diff --git a/internal/plan/json.go b/internal/plan/json.go
new file mode 100644
index 0000000..0b4f1bc
--- /dev/null
+++ b/internal/plan/json.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package plan
+
+import "time"
+
+// jsonNote is carried in every JSON document, warning readers that the
+// shape is not yet stable.
+const jsonNote = "the shape of this document is unstable before krino 1.0"
+
+// JSON is the --json document. The shape is unstable before 1.0 and says
+// so in its own "note" field.
+type JSON struct {
+ Version int `json:"version"`
+ Note string `json:"note"`
+ Dirs []JSONDir `json:"dirs"`
+}
+
+// JSONDir is one directory's plan.
+type JSONDir struct {
+ Name string `json:"name"`
+ Root string `json:"root"`
+ Files []JSONFile `json:"files"`
+ Warnings []string `json:"warnings,omitempty"`
+}
+
+// JSONFile is one file's chain.
+type JSONFile struct {
+ Rel string `json:"rel"`
+ Size int64 `json:"size"`
+ ModTime time.Time `json:"mtime"`
+ Steps []JSONStep `json:"steps"`
+ Warnings []string `json:"warnings,omitempty"`
+}
+
+// JSONStep is one step of a chain. D14: Reason is carried because the text
+// plan already shows it and a machine reader should be able to see why a
+// file matched too; Conflict (the rule's on-conflict policy) is deliberately
+// not: it is a config detail, and its outcome is already visible through
+// dst, displaces and skip.
+type JSONStep struct {
+ Action string `json:"action"`
+ Rule string `json:"rule"`
+ Src string `json:"src"`
+ Dst string `json:"dst,omitempty"`
+ Displaces string `json:"displaces,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ Skip string `json:"skip,omitempty"`
+}
+
+// actionNames maps a Kind onto its JSON action name. This is its own
+// mapping, independent of Kind.String(): the display form renders "DELETE
+// permanently", which must never reach a machine reader. These names match
+// the log's action names in spec §9, so a later `krino log` and a --json
+// plan can be grepped together.
+var actionNames = map[Kind]string{
+ Copy: "copy",
+ Move: "move",
+ Rename: "rename",
+ Trash: "trash",
+ DeletePermanent: "delete",
+}
+
+// NewJSON builds the top-level --json document over dirs. Version and Note
+// are set here, in the one place jsonNote's wording already lives: it is
+// unexported, so a struct literal built outside this package would
+// silently ship an empty "note" and break the document's own contract.
+func NewJSON(dirs []JSONDir) JSON {
+ return JSON{Version: 1, Note: jsonNote, Dirs: dirs}
+}
+
+// NewJSONDir converts one directory's chains.
+func NewJSONDir(name, root string, chains []Chain, warnings []string) JSONDir {
+ files := make([]JSONFile, 0, len(chains))
+ for _, ch := range chains {
+ steps := make([]JSONStep, 0, len(ch.Steps))
+ for _, s := range ch.Steps {
+ steps = append(steps, JSONStep{
+ Action: actionNames[s.Kind],
+ Rule: s.Rule,
+ Src: s.Src,
+ Dst: s.Dst,
+ Displaces: s.Displaces,
+ Reason: s.Reason,
+ Skip: s.Skip,
+ })
+ }
+ files = append(files, JSONFile{
+ Rel: ch.File.Rel,
+ Size: ch.File.Size,
+ ModTime: ch.File.ModTime,
+ Steps: steps,
+ Warnings: ch.Warnings,
+ })
+ }
+ return JSONDir{Name: name, Root: root, Files: files, Warnings: warnings}
+}