ids.go RawHistoryFolder
1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "fmt"
7 "strings"
8)
9
10const (
11 idSeparator = "_"
12 idMaxAttempts = 1000
13 idMaxLength = 100
14)
15
16// idBuilder is a port of IdBuilder in the reference: it validates the ids
17// written in a file and makes unique ids for steps that have none.
18type idBuilder struct {
19 name []rune
20 distinct map[string]bool
21}
22
23func newIDBuilder() *idBuilder { return &idBuilder{distinct: map[string]bool{}} }
24
25func isAlpha(c rune) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
26func isNumeric(c rune) bool { return c >= '0' && c <= '9' }
27
28func (b *idBuilder) appendSegment(value string) {
29 if value == "" {
30 return
31 }
32 rs := []rune(value)
33 if len(b.name) == 0 {
34 first := rs[0]
35 switch {
36 case isAlpha(first) || first == '_':
37 case isNumeric(first) || first == '-':
38 b.name = append(b.name, '_')
39 }
40 } else {
41 b.name = append(b.name, '_')
42 }
43 for _, c := range rs {
44 if isAlpha(c) || isNumeric(c) || c == '_' || c == '-' {
45 b.name = append(b.name, c)
46 } else {
47 b.name = append(b.name, '_')
48 }
49 }
50}
51
52// build returns a unique id from the appended segments, "job" when there are
53// none. It returns "" when a thousand ids are taken.
54func (b *idBuilder) build() string {
55 original := "job"
56 if len(b.name) > 0 {
57 original = string(b.name)
58 }
59 for attempt := 1; attempt < idMaxAttempts; attempt++ {
60 suffix := ""
61 if attempt > 1 {
62 suffix = fmt.Sprintf("_%d", attempt)
63 }
64 n := min(len(original), idMaxLength-len(suffix))
65 candidate := original[:n] + suffix
66 if !b.distinct[candidate] {
67 b.distinct[candidate] = true
68 b.name = nil
69 return candidate
70 }
71 }
72 return ""
73}
74
75// tryAddKnownID records an id written in the file. It returns the error
76// message when the id is invalid or already used.
77func (b *idBuilder) tryAddKnownID(value string) string {
78 if value == "" || !validID(value) || len(value) >= idMaxLength {
79 return fmt.Sprintf("The identifier '%s' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and and must be less than %d characters.", value, idMaxLength)
80 }
81 if strings.HasPrefix(value, idSeparator+idSeparator) {
82 return fmt.Sprintf("The identifier '%s' is invalid. IDs starting with '__' are reserved.", value)
83 }
84 if b.distinct[value] {
85 return fmt.Sprintf("The identifier '%s' may not be used more than once within the same scope.", value)
86 }
87 b.distinct[value] = true
88 return ""
89}
90
91func validID(name string) bool {
92 for i, c := range name {
93 if i == 0 {
94 if !isAlpha(c) && c != '_' {
95 return false
96 }
97 continue
98 }
99 if !isAlpha(c) && !isNumeric(c) && c != '_' && c != '-' {
100 return false
101 }
102 }
103 return true
104}