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  }