-
Notifications
You must be signed in to change notification settings - Fork 3
/
step.go
321 lines (290 loc) · 9.67 KB
/
step.go
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
package donothing
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
)
// Special error returned by Step.Walk callbacks when they want to recurse no further into a step's
// descendants.
var NoRecurse = errors.New("noRecurse")
// A Step is an individual action to be performed as part of a procedure.
//
// Steps must have a name (specified with Name()) and may have any number of substeps (provided with
// AddStep()).
//
// Steps may also have outputs and inputs, defined with Output* and Input*. An output defined by one
// step can be referenced as an input by any subsequent step in the procedure.
type Step struct {
// The Step's name, as set by Name()
name string
// The Step's short description, as set by Short()
short string
// The Step's long description, as set by Long()
long string
// The Step's inputs and outputs, if any
inputs []InputDef
outputs []OutputDef
// The Step of which this Step is a child. nil if this is the root step.
parent *Step
// The Step's substeps, if any
children []*Step
}
// Name gives the step a name, which must be unique among siblings.
//
// By convention, step names should be in camelCase.
//
// Step names are used by donothing to refer unambiguously to a step. Each step has an "absolute
// name", which is composed of a dot-separated sequence of the names of all its parent steps,
// followed by the step's own name. For example, the name "restoreBackup.loadData" refers to the
// "loadData" step, which is a child of the "restoreBackup" step.
func (step *Step) Name(s string) {
step.name = s
}
// AbsoluteName returns the step's unique name.
func (step *Step) AbsoluteName() string {
if step.parent == nil {
return step.name
}
return strings.Join([]string{
step.parent.AbsoluteName(),
step.name,
}, ".")
}
// Pos returns the step's position in the tree.
//
// The return value is a slice of integers. The first value is the index of step's ancestor in the
// root step's children slice. The next value is the index of step's next ancestor in THAT
// ancestor's children slice, and so on.
//
// If step does not have a parent (i.e. step is the root step), Pos returns an empty slice.
//
// If step cannot be found in its parent's children slice, Pos panics.
//
// For example, given this procedure:
//
// pcd := NewProcedure()
// pcd.AddStep(func(step *Step) {
// //...
// })
// pcd.AddStep(func(step *Step) {
// step.Name("grandparent")
// step.AddStep(...)
// step.AddStep(...)
// step.AddStep(func(step *Step) {
// step.Name("parent")
// step.AddStep(func(step *Step) {
// step.Name("myStep")
// })
// })
// })
//
// Pos would give the following results:
//
// // Returns []int{}
// pcd.GetStepByName("root").Pos()
// // Returns []int{1}
// pcd.GetStepByName("root.grandparent").Pos()
// // Returns []int{1,2}
// pcd.GetStepByName("root.grandparent.parent").Pos()
// // Returns []int{1,2,0}
// pcd.GetStepByName("root.grandparent.parent.myStep").Pos()
func (step *Step) Pos() []int {
if step.parent == nil {
return []int{}
}
var leafIndex int
leafIndex = -1
for i, child := range step.parent.children {
if child.AbsoluteName() == step.AbsoluteName() {
// it me!
leafIndex = i
break
}
}
if leafIndex == -1 {
panic(fmt.Sprintf("step '%s' not found among parent's children", step.AbsoluteName()))
}
parentPos := step.parent.Pos()
return append(parentPos, leafIndex)
}
// Depth returns the step's depth in the tree.
//
// The root node's depth is 0, the root node's children are at depth 1, and so on.
func (step *Step) Depth() int {
if step.parent == nil {
return 0
}
return step.parent.Depth() + 1
}
// Short gives the step a short description.
//
// The short description will be the name of the step's corresponding section in the rendered
// markdown document.
func (step *Step) Short(s string) {
step.short = s
}
// GetShort returns the step's short description, as set by Short().
func (step *Step) GetShort() string {
return step.short
}
// Long gives the step a long description.
//
// The long description will be the body of the step's corresponding section in the rendered
// markdown document.
//
// The argument passed to Long() is massaged in the following way before being saved:
//
// - Any leading or trailing lines that contain only whitespace are removed
// - If all remaining non-entirely-whitespace lines have the same whitespace prefix, it's
// removed.
//
// For example, if you run
//
// step.Long(`
// A long description of my step.
//
// Blah blah blah.
//
// Indented line.
// `)
//
// The step's long description will be set to
//
// "A long description of my step.\n\nBlah blah blah.\n\n Indented line."
//
// Before a step is rendered, any occurrences of the "backtick standin sequence" in the long
// description will be replaced with backtick characters. By default, the backtick standin sequence
// is "@@". This sequence can be reassigned using Procedure.BacktickStandin().
func (step *Step) Long(s string) {
// Trim leading all-whitespace lines
r := regexp.MustCompile(`\A\s*\n`)
s = r.ReplaceAllString(s, "")
// Trim trailing all-whitespace lines and trailing newline
r = regexp.MustCompile(`\n\s*\z`)
s = r.ReplaceAllString(s, "")
// Remove any common indentation of the remaining lines
s = step.trimCommonIndent(s)
step.long = s
}
// trimCommonIndent removes the longest common leading whitespace string from lines in s.
//
// For example, if s is " if (hello) {\n world\n }", then trimCommonIndent(s) will
// return "if (hello) {\n world\n}".
//
// Empty lines are ignored.
func (step *Step) trimCommonIndent(s string) string {
origLines := strings.Split(s, "\n")
lines := make([]string, 0)
for _, line := range strings.Split(s, "\n") {
// Filter out empty lines
if line != "" {
lines = append(lines, line)
}
}
if len(lines) == 0 {
return s
}
// Set commonPrefix to the longest common prefix of the strings in lines. commonPrefix may still
// contain non-whitespace characters after this stanza.
sort.Strings(lines)
first := lines[0]
last := lines[len(lines)-1]
commonPrefix := ""
for i := 0; i < len(first) && i < len(last); i++ {
if first[i] != last[i] {
break
}
commonPrefix = commonPrefix + first[i:i+1]
}
// Set wsPrefix to the longest whitespace string at the beginning of commonPrefix.
r := regexp.MustCompile(`^(\s*)`)
wsPrefix := r.FindString(commonPrefix)
// Strip wsPrefix from all lines in origLines, creating rsltLines
rsltLines := make([]string, 0)
for _, line := range origLines {
remainder := strings.Replace(line, wsPrefix, "", 1)
rsltLines = append(rsltLines, remainder)
}
return strings.Join(rsltLines, "\n")
}
// GetLong returns the step's long description, as set by Long().
func (step *Step) GetLong() string {
return step.long
}
// AddStep adds a child step to the Step.
//
// A new Step will be instantiated and passed to fn, which is responsible for defining the new child
// step.
func (step *Step) AddStep(fn func(*Step)) {
newStep := NewStep()
newStep.parent = step
fn(newStep)
step.children = append(step.children, newStep)
}
// OutputString specifies a string output to be produced by the step.
//
// name is the output's name, which must be unique within the procedure. If any two outputs have the
// same name – even if the outputs are associated with steps with different parents – the procedure
// will fail to execute or render. Procedure.Check() will also return an error.
//
// desc should be a concise description of the output. This will be used to prompt the user for
// an output value if the Step is manual, and it will also be mentioned in the procedure's Markdown
// documentation.
func (step *Step) OutputString(name string, desc string) {
output := NewOutputDef("string", name, desc)
step.outputs = append(step.outputs, output)
}
// GetOutputDefs returns the step's output definitions.
func (step *Step) GetOutputDefs() []OutputDef {
return step.outputs
}
// InputString specifies a string input taken by the step.
//
// name must match the name of a string output from a previous step. If it doesn't, the procedure
// will fail at the Check step.
func (step *Step) InputString(name string, required bool) {
input := NewInputDef("string", name, required)
step.inputs = append(step.inputs, input)
}
// GetInputDefs returns the step's input definitions.
func (step *Step) GetInputDefs() []InputDef {
return step.inputs
}
// GetChildren returns the step's child steps.
func (step *Step) GetChildren() []*Step {
return step.children
}
// Walk visits every step in the tree, calling fn on each.
//
// It's a depth-first walk, starting with step itself, then proceeding in sequence through the
// children of step and their children, recursively. This is the order in which the steps execute
// when Procedure.Execute() is called, as well as the order in which the steps are rendered into
// documentation.
//
// If fn returns the error NoRecurse, the walk will not proceed into the steps's descendants, but
// will otherwise proceed as normal. If fn returns any other error, Walk immediately aborts and
// returns that error to the caller.
func (step *Step) Walk(fn func(*Step) error) error {
if err := fn(step); err != nil {
return err
}
for _, childStep := range step.children {
if err := childStep.Walk(fn); err != nil && err != NoRecurse {
return err
}
}
return nil
}
// NewStep returns a new step.
//
// Generally, donothing scripts shouldn't call NewStep directly. Instead, they should use
// *Procedure.AddStep or *Step.AddStep.
func NewStep() *Step {
step := new(Step)
step.children = make([]*Step, 0)
step.inputs = make([]InputDef, 0)
step.outputs = make([]OutputDef, 0)
return step
}