github.com/chainguard-dev/yam@v0.0.7/pkg/yam/formatted/path/path.go (about) 1 package path 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strconv" 8 "strings" 9 ) 10 11 // Note: While YAML allows non-scalar mapping keys, we'll constrain this 12 // expression system to only account for scalar mapping keys. 13 14 type Path struct { 15 parts []Part 16 } 17 18 func Root() Path { 19 return Path{ 20 parts: []Part{rootPart{}}, 21 } 22 } 23 24 const ( 25 rootExpression = "." 26 anyKeyExpression = "*" 27 ) 28 29 var ErrExpressionNotSupported = errors.New("expression not supported") 30 31 var ( 32 regexMapKey = regexp.MustCompile(`^(\.([0-9a-zA-Z_-]+|\*))([\[.].*)?`) 33 regexSeqIndex = regexp.MustCompile(`^\[(\d*)]`) 34 regexRootSeq = regexp.MustCompile(`^\.\[(\d*)]`) 35 ) 36 37 func Parse(expression string) (Path, error) { 38 if expression == rootExpression { 39 return Root(), nil 40 } 41 42 result := Root() // i.e., so far 43 44 remaining := expression 45 for { 46 if remaining == "" { 47 return result, nil 48 } 49 50 // check for the expression to specify a sequence index right off the bat, e.g. ".[42]" 51 submatches := regexRootSeq.FindStringSubmatch(expression) 52 if len(submatches) >= 2 { 53 indexString := submatches[1] 54 if indexString == "" { 55 result = result.AppendSeqPart(anyIndex) 56 remaining = strings.TrimPrefix(remaining, submatches[0]) 57 continue 58 } 59 60 index, err := strconv.Atoi(indexString) 61 if err != nil { 62 return Path{}, ErrExpressionNotSupported 63 } 64 result = result.AppendSeqPart(index) 65 remaining = strings.TrimPrefix(remaining, submatches[0]) 66 continue 67 } 68 69 // check for map key, e.g. ".some-key" 70 submatches = regexMapKey.FindStringSubmatch(remaining) 71 if len(submatches) >= 3 { 72 key := submatches[2] 73 if key == anyKeyExpression { 74 result = result.AppendMapPart(anyKey) 75 remaining = strings.TrimPrefix(remaining, submatches[1]) 76 continue 77 } 78 79 result = result.AppendMapPart(key) 80 remaining = strings.TrimPrefix(remaining, submatches[1]) 81 continue 82 } 83 84 // check for seq index, e.g. "[7]" 85 submatches = regexSeqIndex.FindStringSubmatch(remaining) 86 if len(submatches) >= 2 { 87 indexString := submatches[1] 88 if indexString == "" { 89 result = result.AppendSeqPart(anyIndex) 90 remaining = strings.TrimPrefix(remaining, submatches[0]) 91 continue 92 } 93 94 index, err := strconv.Atoi(indexString) 95 if err != nil { 96 return Path{}, ErrExpressionNotSupported 97 } 98 result = result.AppendSeqPart(index) 99 remaining = strings.TrimPrefix(remaining, submatches[0]) 100 continue 101 } 102 103 // nothing else it could be 104 return Path{}, ErrExpressionNotSupported 105 } 106 } 107 108 func (p Path) AppendMapPart(key string) Path { 109 return Path{ 110 parts: append(p.parts, mapPart{ 111 key: key, 112 }), 113 } 114 } 115 116 func (p Path) AppendSeqPart(index int) Path { 117 return Path{ 118 parts: append(p.parts, seqPart{ 119 index: index, 120 }), 121 } 122 } 123 124 func (p Path) Len() int { 125 return len(p.parts) 126 } 127 128 func (p Path) Last() Part { 129 lastIndex := len(p.parts) - 1 130 return p.parts[lastIndex] 131 } 132 133 func (p Path) String() string { 134 var result string 135 136 for _, part := range p.parts { 137 switch tp := part.(type) { 138 case rootPart: 139 result = "" 140 case mapPart: 141 result += fmt.Sprintf(".%s", tp.key) 142 case seqPart: 143 result += fmt.Sprintf("[%d]", tp.index) 144 } 145 } 146 147 if result == "" { 148 return rootExpression 149 } 150 151 return result 152 } 153 154 func (p Path) Matches(testSubject Path) bool { 155 if len(p.parts) != len(testSubject.parts) { 156 return false 157 } 158 159 for i, patternPart := range p.parts { 160 if !partsMatch(patternPart, testSubject.parts[i]) { 161 return false 162 } 163 } 164 165 return true 166 } 167 168 func partsMatch(pattern, testSubject Part) bool { 169 if pattern.Kind() != testSubject.Kind() { 170 return false 171 } 172 173 switch tp := pattern.(type) { 174 case rootPart: 175 return true 176 177 case mapPart: 178 ts := testSubject.(mapPart) 179 return tp.key == ts.key || tp.key == anyKey 180 181 case seqPart: 182 ts := testSubject.(seqPart) 183 return tp.index == ts.index || tp.index == anyIndex 184 185 } 186 187 return false 188 }