// 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} }