goyave.dev/goyave/v4@v4.4.11/util/walk/walk.go (about) 1 package walk 2 3 import ( 4 "bufio" 5 "fmt" 6 "reflect" 7 "strings" 8 "unicode/utf8" 9 ) 10 11 // PathType type of the element being explored. 12 type PathType int 13 14 // FoundType adds extra information about not found elements whether 15 // what's not found is their parent or themselves. 16 type FoundType int 17 18 const ( 19 // PathTypeElement the explored element is used as a final element (leaf). 20 PathTypeElement PathType = iota 21 22 // PathTypeArray the explored element is used as an array and not a final element. 23 // All elements in the array will be explored using the next Path. 24 PathTypeArray 25 26 // PathTypeObject the explored element is used as an object (`map[string]interface{}`) 27 // and not a final element. 28 PathTypeObject 29 ) 30 31 const ( 32 // Found indicates the element could be found. 33 Found FoundType = iota 34 // ParentNotFound indicates one of the parents of the element could no be found. 35 ParentNotFound 36 // ElementNotFound indicates all parents of the element were found but the element 37 // itself could not. 38 ElementNotFound 39 ) 40 41 // Path allows for complex untyped data structure exploration. 42 // An instance of this structure represents a step in exploration. 43 // Items NOT having `PathTypeElement` as a `Type` are expected to have a non-nil `Next`. 44 type Path struct { 45 Next *Path 46 Index *int 47 Name string 48 Type PathType 49 } 50 51 // Context information sent to walk function. 52 type Context struct { 53 Value interface{} 54 Parent interface{} // Either map[string]interface{} or a slice 55 Path *Path // Exact Path to the current element 56 Name string // Name of the current element 57 Index int // If parent is a slice, the index of the current element in the slice, else -1 58 Found FoundType // True if the path could not be completely explored 59 } 60 61 // Walk this path and execute the given behavior for each matching element. Elements are final, 62 // meaning they are the deepest explorable element using this path. 63 // Only `map[string]interface{}` and n-dimensional slices parents are supported. 64 // The given "f" function is executed for each final element matched. If the path 65 // cannot be completed because the step's name doesn't exist in the currently explored map, 66 // the function will be executed as well, with a the `Context`'s `NotFound` field set to `true`. 67 func (p *Path) Walk(currentElement interface{}, f func(Context)) { 68 path := &Path{ 69 Name: p.Name, 70 Type: p.Type, 71 } 72 p.walk(currentElement, nil, -1, path, path, f) 73 } 74 75 func (p *Path) walk(currentElement interface{}, parent interface{}, index int, path *Path, lastPathElement *Path, f func(Context)) { 76 element := currentElement 77 if p.Name != "" { 78 ce, ok := currentElement.(map[string]interface{}) 79 found := ParentNotFound 80 if ok { 81 element, ok = ce[p.Name] 82 if !ok && p.Type == PathTypeElement { 83 found = ElementNotFound 84 } 85 index = -1 86 } 87 if !ok { 88 p.completePath(lastPathElement) 89 f(newNotFoundContext(currentElement, path, p.Name, index, found)) 90 return 91 } 92 parent = currentElement 93 } 94 95 switch p.Type { 96 case PathTypeElement: 97 f(Context{ 98 Value: element, 99 Parent: parent, 100 Path: path, 101 Name: p.Name, 102 Index: index, 103 }) 104 case PathTypeArray: 105 list := reflect.ValueOf(element) 106 if list.Kind() != reflect.Slice { 107 lastPathElement.Type = PathTypeElement 108 f(newNotFoundContext(parent, path, p.Name, index, ParentNotFound)) 109 return 110 } 111 length := list.Len() 112 if p.Index != nil { 113 lastPathElement.Index = p.Index 114 lastPathElement.Next = &Path{Name: p.Next.Name, Type: p.Next.Type} 115 if p.outOfBounds(length) { 116 f(newNotFoundContext(element, path, "", *p.Index, ElementNotFound)) 117 return 118 } 119 v := list.Index(*p.Index) 120 value := v.Interface() 121 p.Next.walk(value, element, *p.Index, path, lastPathElement.Next, f) 122 return 123 } 124 if length == 0 { 125 lastPathElement.Next = &Path{Name: p.Next.Name, Type: PathTypeElement} 126 found := ElementNotFound 127 if p.Next.Type != PathTypeElement { 128 found = ParentNotFound 129 } 130 f(newNotFoundContext(element, path, "", -1, found)) 131 return 132 } 133 for i := 0; i < length; i++ { 134 j := i 135 clone := path.Clone() 136 tail := clone.Tail() 137 tail.Index = &j 138 tail.Next = &Path{Name: p.Next.Name, Type: p.Next.Type} 139 v := list.Index(i) 140 value := v.Interface() 141 p.Next.walk(value, element, i, clone, tail.Next, f) 142 } 143 case PathTypeObject: 144 lastPathElement.Next = &Path{Name: p.Next.Name, Type: p.Next.Type} 145 p.Next.walk(element, parent, index, path, lastPathElement.Next, f) 146 } 147 } 148 149 func (p *Path) outOfBounds(length int) bool { 150 return *p.Index >= length || *p.Index < 0 151 } 152 153 func (p *Path) completePath(lastPathElement *Path) { 154 completedPath := lastPathElement 155 if p.Type == PathTypeArray { 156 i := -1 157 completedPath.Index = &i 158 } 159 if p.Type != PathTypeElement { 160 completedPath.Next = p.Next.Clone() 161 completedPath.Next.setAllMissingIndexes() 162 } 163 } 164 165 func newNotFoundContext(parent interface{}, path *Path, name string, index int, found FoundType) Context { 166 return Context{ 167 Value: nil, 168 Parent: parent, 169 Path: path, 170 Name: name, 171 Index: index, 172 Found: found, 173 } 174 } 175 176 // HasArray returns true if a least one step in the path involves an array. 177 func (p *Path) HasArray() bool { 178 step := p 179 for step != nil { 180 if step.Type == PathTypeArray { 181 return true 182 } 183 step = step.Next 184 } 185 return false 186 } 187 188 // LastParent returns the last step in the path that is not a PathTypeElement, excluding 189 // the first step in the path, or nil. 190 func (p *Path) LastParent() *Path { 191 step := p 192 for step != nil { 193 if step.Next != nil && step.Next.Type == PathTypeElement { 194 return step 195 } 196 step = step.Next 197 } 198 return nil 199 } 200 201 // Tail returns the last step in the path. 202 func (p *Path) Tail() *Path { 203 step := p 204 for step.Next != nil { 205 step = step.Next 206 } 207 return step 208 } 209 210 // Clone returns a deep clone of this Path. 211 func (p *Path) Clone() *Path { 212 clone := &Path{ 213 Name: p.Name, 214 Type: p.Type, 215 Index: p.Index, 216 } 217 if p.Next != nil { 218 clone.Next = p.Next.Clone() 219 } 220 221 return clone 222 } 223 224 // setAllMissingIndexes set Index to -1 for all `PathTypeArray` steps in this path. 225 func (p *Path) setAllMissingIndexes() { 226 i := -1 227 for step := p; step != nil; step = step.Next { 228 if step.Type == PathTypeArray { 229 step.Index = &i 230 } 231 } 232 } 233 234 // Parse transform given path string representation into usable Path. 235 // 236 // Example paths: 237 // 238 // name 239 // object.field 240 // object.subobject.field 241 // object.array[] 242 // object.arrayOfObjects[].field 243 func Parse(p string) (*Path, error) { 244 rootPath := &Path{} 245 path := rootPath 246 247 scanner := createPathScanner(p) 248 for scanner.Scan() { 249 t := scanner.Text() 250 switch t { 251 case "[]": 252 if path.Type == PathTypeArray { 253 path.Next = &Path{ 254 Type: PathTypeArray, 255 } 256 path = path.Next 257 } else { 258 path.Type = PathTypeArray 259 } 260 case ".": 261 if path.Type == PathTypeArray { 262 path.Next = &Path{ 263 Type: PathTypeObject, 264 Next: &Path{ 265 Type: PathTypeElement, 266 }, 267 } 268 path = path.Next.Next 269 } else { 270 path.Type = PathTypeObject 271 path.Next = &Path{ 272 Type: PathTypeElement, 273 } 274 path = path.Next 275 } 276 default: 277 path.Name = t 278 } 279 } 280 281 if err := scanner.Err(); err != nil { 282 return nil, err 283 } 284 285 if path.Type != PathTypeElement { 286 path.Next = &Path{ 287 Type: PathTypeElement, 288 } 289 } 290 291 return rootPath, nil 292 } 293 294 func createPathScanner(path string) *bufio.Scanner { 295 scanner := bufio.NewScanner(strings.NewReader(path)) 296 split := func(data []byte, atEOF bool) (int, []byte, error) { 297 if len(path) == 0 || path[0] == '.' { 298 return len(data), data[:], fmt.Errorf("Illegal syntax: %q", path) 299 } 300 for width, i := 0, 0; i < len(data); i += width { 301 var r rune 302 r, width = utf8.DecodeRune(data[i:]) 303 304 if i+width < len(data) { 305 next, _ := utf8.DecodeRune(data[i+width:]) 306 if isValidSyntax(r, next) { 307 return len(data), data[:], fmt.Errorf("Illegal syntax: %q", path) 308 } 309 310 if r == '.' && i == 0 { 311 return i + width, data[:i+width], nil 312 } else if next == '.' || next == '[' { 313 return i + width, data[:i+width], nil 314 } 315 } else if r == '.' || r == '[' { 316 return len(data), data[:], fmt.Errorf("Illegal syntax: %q", path) 317 } 318 } 319 if atEOF && len(data) > 0 { 320 return len(data), data[:], nil 321 } 322 return 0, nil, nil 323 } 324 scanner.Split(split) 325 return scanner 326 } 327 328 func isValidSyntax(r rune, next rune) bool { 329 return (r == '.' && next == '.') || 330 (r == '[' && next != ']') || 331 (r == '.' && (next == ']' || next == '[')) || 332 (r != '.' && r != '[' && next == ']') || 333 (r == ']' && next != '[' && next != '.') 334 }