Skip to content

Commit

Permalink
feat(selectors): add multi-selector object (#137)
Browse files Browse the repository at this point in the history
often selectors can be specified multiple times. This object
allows to deal with them easier by executing, collecting and
deduplicating all of them.
  • Loading branch information
Tieske committed Jan 18, 2024
1 parent 83c3ea4 commit f38f736
Showing 1 changed file with 176 additions and 0 deletions.
176 changes: 176 additions & 0 deletions yamlbasics/selectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package yamlbasics

import (
"fmt"

"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
"gopkg.in/yaml.v3"
)

//
//
// NodeSet implementation, just a list of yaml nodes
//
//

// represents a set of yaml nodes
type NodeSet []*yaml.Node

// IsIntersection returns true if all nodes in the subset also appear in the main set.
// nil entries will be ignored. Returns true if subset is empty.
func IsIntersection(mainSet NodeSet, subset NodeSet) bool {
if len(subset) == 0 {
return true
}
if len(mainSet) == 0 {
return false
}

// deduplicate
seen := make(map[*yaml.Node]bool)
for _, node := range mainSet {
if node != nil {
seen[node] = true
}
}

for _, node := range subset {
if node != nil && !seen[node] {
return false
}
}
return true
}

// Intersection returns the intersection of the two given sets of nodes.
// nil entries will be ignored. The result will have no duplicates.
func Intersection(set1, set2 NodeSet) NodeSet {
if len(set1) == 0 || len(set2) == 0 {
return make(NodeSet, 0)
}

// deduplicate
seen1 := make(map[*yaml.Node]bool)
for _, node := range set1 {
if node != nil {
seen1[node] = true
}
}

intersection := make(NodeSet, 0)
seen2 := make(map[*yaml.Node]bool)
for _, node := range set2 {
if node != nil && seen1[node] && !seen2[node] {
seen2[node] = true
intersection = append(intersection, node)
}
}
return intersection
}

// SubtractSet returns the set of nodes that are in mainSet but not in setToSubtract.
// nil entries will be ignored. The result will have no duplicates.
func SubtractSet(mainSet NodeSet, setToSubtract NodeSet) NodeSet {
if len(mainSet) == 0 || len(setToSubtract) == 0 {
return make(NodeSet, 0)
}

// deduplicate
seen1 := make(map[*yaml.Node]bool)
for _, node := range setToSubtract {
if node != nil {
seen1[node] = true
}
}

subtracted := make(NodeSet, 0)
seen2 := make(map[*yaml.Node]bool)
for _, node := range mainSet {
if node != nil && !seen1[node] && !seen2[node] {
seen2[node] = true
subtracted = append(subtracted, node)
}
}
return subtracted
}

//
//
// SelectorSet implementation, handles mutiple instead of 1 JSONpath selector
//
//

// Represents a set of JSONpath selectors. Call NewSelectorSet to create one.
// The SelectorSet can be empty, in which case it will return only empty results.
type SelectorSet struct {
selectors []*yamlpath.Path // the compiled selectors
source []string // matching source strings of the selectors
initialized bool // indicator whether is was initialized or not
}

// NewSelectorSet compiles the given selectors into a list of yaml nodes.
// If any of the selectors is invalid, an error will be returned.
// If the selectors are omitted/empty then an empty set is returned.
func NewSelectorSet(selectors []string) (SelectorSet, error) {
var (
set SelectorSet
err error
)

set.selectors = make([]*yamlpath.Path, len(selectors))
set.source = make([]string, len(selectors))
for i, selector := range selectors {
set.source[i] = selector
set.selectors[i], err = yamlpath.NewPath(selector)
if err != nil {
return SelectorSet{}, fmt.Errorf("selector '%s' is not a valid JSONpath expression; %w", selector, err)
}
}
set.initialized = true
return set, nil
}

// IsEmpty returns true if the selector set is empty.
func (set *SelectorSet) IsEmpty() bool {
return set.selectors == nil || len(set.selectors) == 0
}

// GetSources returns a copy of the selector sources
func (set *SelectorSet) GetSources() []string {
sources := make([]string, 0)
copy(sources, set.source)
return sources
}

// Find executes the given selectors on the given yaml node.
// The result will never be nil, will not have duplicates, but can be an empty array.
// An error is only returned if any of the selectors errors when searching.
// nodeToSearch cannot be nil, in which case it will panic.
func (set *SelectorSet) Find(nodeToSearch *yaml.Node) (NodeSet, error) {
if !set.initialized {
panic("selector set uninitialized, call NewSelectorSet to create and initialize one")
}
if nodeToSearch == nil {
panic("expected nodeToSearch to be non-nil")
}
if set.selectors == nil || len(set.selectors) == 0 {
return make(NodeSet, 0), nil
}

results := make(NodeSet, 0)
seen := make(map[*yaml.Node]bool)
for i, selector := range set.selectors {
matches, err := selector.Find(nodeToSearch)
if err != nil {
return nil, fmt.Errorf("failed to execute selector '%s'; %w", set.source[i], err)
}
for _, match := range matches {
if match != nil && !seen[match] {
results = append(results, match)
seen[match] = true
}
}
}

return results, nil
}

0 comments on commit f38f736

Please sign in to comment.