expand patterns

This commit is contained in:
Annika Hannig 2023-11-22 16:33:17 +01:00
parent d0bed6e6ce
commit be5568d530
No known key found for this signature in database
GPG Key ID: 62E226E47DDCE58D
2 changed files with 304 additions and 0 deletions

187
pkg/config/expand.go Normal file
View File

@ -0,0 +1,187 @@
package config
import (
"fmt"
"regexp"
"strings"
)
// Compile input pattern regex
var (
expandMatchPlaceholder = regexp.MustCompile(`(?U:{.*}+?)`)
expandMatchWildcardShorthard = regexp.MustCompile(`(?U:{{.*}}+?)`)
)
// Extract all matches from the input string.
// The pattern to find is {INPUT}. The input string
// itself can contain new matches.
func expandFindPlaceholders(s string) []string {
// Find all matches
results := expandMatchPlaceholder.FindAllString(s, -1)
if len(results) == 0 {
return []string{}
}
matches := []string{}
for _, result := range results {
key := expandGetKey(result)
subP := expandFindPlaceholders(key)
matches = append(matches, result)
matches = append(matches, subP...)
}
return matches
}
// Extract the key from the placeholder
func expandGetKey(s string) string {
// Strip the enclosing curly braces
s = strings.TrimPrefix(s, "{")
s = strings.TrimSuffix(s, "}")
return s
}
// ExpandMap holds the current state of variables
type ExpandMap map[string]string
// Retrieve a set of matching variables, by iterating variables.
// Whenever a key matches the wildcard, the prefix is removed.
// Example:
// pattern = "AS*", key = "AS2342", value = "2342"
func (e ExpandMap) matchWildcard(pattern string) []string {
matches := []string{}
// Strip the wildcard from the pattern.
pattern = strings.TrimSuffix(pattern, "*")
// Iterate variables and add match to result set
for k, _ := range e {
if strings.HasPrefix(k, pattern) {
key := strings.TrimPrefix(k, pattern)
matches = append(matches, key)
}
}
return matches
}
// Get all substitutions for a given key.
// This method will return an error, if a placeholder
// does not match.
func (e ExpandMap) getSubstitutions(key string) []string {
// Check if the placeholder is a wildcard
if strings.HasSuffix(key, "*") {
return e.matchWildcard(key)
}
// Check if the placeholder is direct match
if val, ok := e[key]; ok {
return []string{val}
}
return []string{}
}
// Get placeholder level. This is the number of opening
// curly braces in the placeholder.
func expandGetLevel(s string) int {
level := 0
for _, c := range s {
if c == '{' {
level++
}
}
return level
}
// Preprocess input string and resolve syntactic sugar.
// Replace {{VAR}} with {VAR{VAR}} to make it easier
// to access the wildcard value.
func expandPreprocess(s string) string {
// Find all access shorthands and replace them
// with the full syntax
results := expandMatchWildcardShorthard.FindAllString(s, -1)
for _, match := range results {
// Wildcard {{KEY*}} -> KEY
key := match[2 : len(match)-3]
expr := fmt.Sprintf("{%s{%s*}}", key, key)
s = strings.Replace(s, match, expr, -1)
}
return s
}
// Expand variables by recursive substitution and expansion
func (e ExpandMap) Expand(s string) ([]string, error) {
// Preprocess syntactic sugar: replace {{VAR}}
// with {VAR{VAR}}
s = expandPreprocess(s)
// Find all placeholders and substitute them
placeholders := expandFindPlaceholders(s)
if len(placeholders) == 0 {
return []string{s}, nil
}
// Find substitutions for each placeholder
substitutions := map[string][]string{}
for _, p := range placeholders {
key := expandGetKey(p)
subs := e.getSubstitutions(key)
if len(subs) == 0 {
level := expandGetLevel(p)
if level == 1 {
err := fmt.Errorf("No substitution for %s in '%s'", p, s)
return []string{}, err
}
continue
}
substitutions[p] = subs
}
// Apply substitutions
subsRes := []string{s}
for p, subs := range substitutions {
subsExp := []string{}
for _, s := range subsRes {
for _, sub := range subs {
res := strings.Replace(s, p, sub, -1)
subsExp = append(subsExp, res)
}
}
subsRes = subsExp
}
// Expand recursively
results := []string{}
for _, s := range subsRes {
res, err := e.Expand(s)
if err != nil {
return []string{}, err
}
results = append(results, res...)
}
return results, nil
}
// Add a new variable to the map. Key and value are
// expanded.
func (e ExpandMap) AddExpr(expr string) error {
// Expand expression
res, err := e.Expand(expr)
if err != nil {
return err
}
for _, exp := range res {
// Split key and value
parts := strings.SplitN(exp, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("Invalid expression '%s'", expr)
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
e[key] = val
}
return nil
}

117
pkg/config/expand_test.go Normal file
View File

@ -0,0 +1,117 @@
package config
import (
"testing"
)
// Text variable pattern matching
func TestExpandMatch(t *testing.T) {
exp := ExpandMap{
"AS2342": "",
"AS1111": "",
"FOOBAR": "foo",
}
matches := exp.matchWildcard("AS*")
if len(matches) != 2 {
t.Errorf("Expected 2 matches, got %d", len(matches))
}
for _, m := range matches {
t.Log("Match wildcard:", m)
}
}
// Test variable expansion / substitution
func TestFindPlaceholders(t *testing.T) {
s := "{FOO} BAR {AS{AS*}}"
placeholders := expandFindPlaceholders(s)
if len(placeholders) != 3 {
t.Errorf("Expected 3 placeholders, got %d", len(placeholders))
}
t.Log(placeholders)
}
// Test variable expansion / substitution
func TestExpand(t *testing.T) {
s := "{FOO} BAR {AS{AS*}} AS {AS*}"
exp := ExpandMap{
"AS2342": "AS2342",
"AS1111": "AS1111",
"FOO": "foo",
}
results, err := exp.Expand(s)
if err != nil {
t.Error(err)
}
t.Log(results)
}
func TestExpandErr(t *testing.T) {
s := "{FOO} BAR {AS{AS*}} AS {AS*} {UNKNOWN}"
exp := ExpandMap{
"AS2342": "AS2342",
"AS1111": "AS1111",
"FOO": "foo",
"FN": "fn",
"FA": "fa",
}
_, err := exp.Expand(s)
t.Log(err)
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestExpandPreprocess(t *testing.T) {
s := "FOO {FOO} {{AS*}} {F*} {{F*}} {X{X*}}"
expect := "FOO {FOO} {AS{AS*}} {F*} {F{F*}} {X{X*}}"
s = expandPreprocess(s)
if s != expect {
t.Errorf("Expected '%s', got '%s'", expect, s)
}
t.Log(s)
s = "TEST {{FN}}"
s = expandPreprocess(s)
t.Log(s)
}
func TestExpandAddExpr(t *testing.T) {
e := ExpandMap{
"FOO": "foo23",
"BAR": "bar42",
"bar42": "BAM",
}
if err := e.AddExpr("FOOBAR = {FOO}{BAR}{{BAR}}"); err != nil {
t.Fatal(err)
}
t.Log(e)
if e["FOOBAR"] != "foo23bar42BAM" {
t.Error("Expected 'foo23bar42BAM', got", e["FOOBAR"])
}
}
func TestExpandBgpCommunities(t *testing.T) {
e := ExpandMap{
"ASRS01": "6695",
"ASRS02": "4617",
"SW1001": "edge01.fra2",
"SW1002": "edge01.fra6",
"SW2038": "edge01.nyc1",
}
// Some large communities:
expr := "{{AS*}}:911:{SW*} = Redistribute to {{SW*}}"
exp, err := e.Expand(expr)
if err != nil {
t.Fatal(err)
}
t.Log(exp)
}