aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/json.go
blob: 04b96ac926d1b7f32515c893b88daebc1ad0be74 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// SPDX-License-Identifier: GPL-3.0-or-later

package plan

import (
	"sort"
	"time"

	"krino/internal/scan"
)

// 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"`
	// Unmatched holds the files no rule matched that still raised a
	// warning (a content test that could not read them), as the text
	// plan's warnings list does; other unmatched files are left out.
	Unmatched []JSONFile `json:"unmatched,omitempty"`
	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"`
	Excluded string     `json:"excluded,omitempty"` // the exclude form that set the file aside
	Steps    []JSONStep `json:"steps"`
	Warnings []string   `json:"warnings,omitempty"` // raised while matching, then while planning
}

// FileNotes is what matching found about one file besides its chain.
type FileNotes struct {
	File     scan.File
	Excluded string
	Warnings []string
}

// 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. notes are matching's findings
// by Rel, for files with a chain and for unmatched files alike (triage 28h).
func NewJSONDir(name, root string, chains []Chain, notes map[string]FileNotes, warnings []string) JSONDir {
	files := make([]JSONFile, 0, len(chains))
	inPlan := make(map[string]bool, len(chains))
	for _, ch := range chains {
		inPlan[ch.File.Rel] = true
		note := notes[ch.File.Rel]
		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,
			Excluded: note.Excluded,
			Steps:    steps,
			Warnings: append(append([]string(nil), note.Warnings...), ch.Warnings...),
		})
	}
	var unmatched []JSONFile
	for rel, note := range notes {
		if !inPlan[rel] && len(note.Warnings) > 0 {
			unmatched = append(unmatched, JSONFile{Rel: rel, Size: note.File.Size, ModTime: note.File.ModTime, Steps: []JSONStep{}, Warnings: note.Warnings})
		}
	}
	sort.Slice(unmatched, func(i, j int) bool { return unmatched[i].Rel < unmatched[j].Rel })
	return JSONDir{Name: name, Root: root, Files: files, Unmatched: unmatched, Warnings: warnings}
}