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  }