Skip to content

Commit

Permalink
almost there
Browse files Browse the repository at this point in the history
  • Loading branch information
kellerza committed May 25, 2021
1 parent 1072d21 commit 2464a53
Show file tree
Hide file tree
Showing 10 changed files with 610 additions and 424 deletions.
230 changes: 230 additions & 0 deletions clab/config/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package config

import (
"fmt"
"regexp"
"strconv"
"strings"

"inet.af/netaddr"
)

func typeof(val interface{}) string {
switch val.(type) {
case string:
return "string"
case int, int16, int32:
return "int"
}
return ""
}

func hasInt(val interface{}) (int, bool) {
if i, err := strconv.Atoi(fmt.Sprintf("%v", val)); err == nil {
return i, true
}
return 0, false
}

func expectFunc(val interface{}, format string) (interface{}, error) {
t := typeof(val)
vals := fmt.Sprintf("%s", val)

// known formats
switch format {
case "str", "string":
if t == "string" {
return "", nil
}
return "", fmt.Errorf("string expected, got %s (%v)", t, val)
case "int":
if _, ok := hasInt(val); ok {
return "", nil
}
return "", fmt.Errorf("int expected, got %s (%v)", t, val)
case "ip":
if _, err := netaddr.ParseIPPrefix(vals); err == nil {
return "", nil
}
return "", fmt.Errorf("IP/mask expected, got %v", val)
}

// try range
if matched, _ := regexp.MatchString(`\d+-\d+`, format); matched {
iv, ok := hasInt(val)
if !ok {
return "", fmt.Errorf("int expected, got %s (%v)", t, val)
}
r := strings.Split(format, "-")
i0, _ := hasInt(r[0])
i1, _ := hasInt(r[1])
if i1 < i0 {
i0, i1 = i1, i0
}
if i0 <= iv && iv <= i1 {
return "", nil
}
return "", fmt.Errorf("value (%d) expected to be in range %d-%d", iv, i0, i1)
}

// Try regex
matched, err := regexp.MatchString(format, vals)
if err != nil || !matched {
return "", fmt.Errorf("value %s does not match regex %s %v", vals, format, err)
}

return "", nil
}

var funcMap = map[string]interface{}{
"optional": func(val interface{}, format string) (interface{}, error) {
if val == nil {
return "", nil
}
return expectFunc(val, format)
},
"expect": expectFunc,
// "require": func(val interface{}) (interface{}, error) {
// if val == nil {
// return nil, errors.New("required value not set")
// }
// return val, nil
// },
"ip": func(val interface{}) (interface{}, error) {
s := fmt.Sprintf("%v", val)
a := strings.Split(s, "/")
return a[0], nil
},
"ipmask": func(val interface{}) (interface{}, error) {
s := fmt.Sprintf("%v", val)
a := strings.Split(s, "/")
return a[1], nil
},
"default": func(in ...interface{}) (interface{}, error) {
if len(in) < 2 {
return nil, fmt.Errorf("default value expected")
}
if len(in) > 2 {
return nil, fmt.Errorf("too many arguments")
}

val := in[len(in)-1]
def := in[0]

switch v := val.(type) {
case nil:
return def, nil
case string:
if v == "" {
return def, nil
}
case bool:
if !v {
return def, nil
}
}
// if val == nil {
// return def, nil
// }

// If we have a input value, do some type checking
tval, tdef := typeof(val), typeof(def)
if tval == "string" && tdef == "int" {
if _, err := strconv.Atoi(val.(string)); err == nil {
tval = "int"
}
if tdef == "str" {
if _, err := strconv.Atoi(def.(string)); err == nil {
tdef = "int"
}
}
}
if tdef != tval {
return val, fmt.Errorf("expected type %v, got %v (value=%v)", tdef, tval, val)
}

// Return the value
return val, nil
},
"contains": func(substr string, str string) (interface{}, error) {
return strings.Contains(fmt.Sprintf("%v", str), fmt.Sprintf("%v", substr)), nil
},
"split": func(sep string, val interface{}) (interface{}, error) {
// Start and end values
if val == nil {
return []interface{}{}, nil
}
if sep == "" {
sep = " "
}

v := fmt.Sprintf("%v", val)

res := strings.Split(v, sep)
r := make([]interface{}, len(res))
for i, p := range res {
r[i] = p
}
return r, nil
},
"join": func(sep string, val interface{}) (interface{}, error) {
if sep == "" {
sep = " "
}
// Start and end values
switch v := val.(type) {
case []interface{}:
if val == nil {
return "", nil
}
res := make([]string, len(v))
for i, v := range v {
res[i] = fmt.Sprintf("%v", v)
}
return strings.Join(res, sep), nil
case []string:
return strings.Join(v, sep), nil
case []int, []int16, []int32:
return strings.Trim(strings.Replace(fmt.Sprint(v), " ", sep, -1), "[]"), nil
}
return nil, fmt.Errorf("expected array [], got %v", val)
},
"slice": func(start, end int, val interface{}) (interface{}, error) {
// string or array
switch v := val.(type) {
case string:
if start < 0 {
start += len(v)
}
if end < 0 {
end += len(v)
}
return v[start:end], nil
case []interface{}:
if start < 0 {
start += len(v)
}
if end < 0 {
end += len(v)
}
return v[start:end], nil
}
return nil, fmt.Errorf("not an array")
},
"index": func(idx int, val interface{}) (interface{}, error) {
// string or array
switch v := val.(type) {
case string:
if idx < 0 {
idx += len(v)
}
return v[idx], nil
case []interface{}:
if idx < 0 {
idx += len(v)
}
return v[idx], nil
}
return nil, fmt.Errorf("not an array")
},
}
123 changes: 123 additions & 0 deletions clab/config/functions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package config

import (
"bytes"
"fmt"
"strings"
"testing"
"text/template"
)

var test_set = map[string][]string{
// empty values
"default 0 .x": {"0"},
".x | default 0": {"0"},
"default 0 \"\"": {"0"},
"default 0 false": {"0"},
"false | default 0": {"0"},
// ints pass through ok
"default 1 0": {"0"},
// errors
"default .x": {"", "default value expected"},
"default .x 1 1": {"", "too many arguments"},
// type check
"default 0 .i5": {"5"},
"default 0 .sA": {"", "expected type int"},
`default "5" .sA`: {"A"},

`contains "." .sAAA`: {"true"},
`.sAAA | contains "."`: {"true"},
`contains "." .sA`: {"false"},
`.sA | contains "."`: {"false"},

`split "." "a.a"`: {"[a a]"},
`split " " "a bb"`: {"[a bb]"},

`ip "1.1.1.1/32"`: {"1.1.1.1"},
`"1.1.1.1" | ip`: {"1.1.1.1"},
`ipmask "1.1.1.1/32"`: {"32"},
`"1.1.1.1/32" | split "/" | slice 0 1 | join ""`: {"1.1.1.1"},
`"1.1.1.1/32" | split "/" | slice 1 2 | join ""`: {"32"},

`split " " "a bb" | join "-"`: {"a-bb"},
`split "" ""`: {"[]"},
`split "abc" ""`: {"[]"},

`"1.1.1.1/32" | split "/" | index 1`: {"32"},
`"1.1.1.1/32" | split "/" | index -1`: {"32"},
`"1.1.1.1/32" | split "/" | index -2`: {"1.1.1.1"},
`"1.1.1.1/32" | split "/" | index -3`: {"", "out of range"},
`"1.1.1.1/32" | split "/" | index 2`: {"", "out of range"},

`expect "1.1.1.1/32" "ip"`: {""},
`expect "1.1.1.1" "ip"`: {"", "IP/mask"},
`expect "1" "0-10"`: {""},
`expect "1" "10-10"`: {"", "range"},
`expect "1.1" "\\d+\\.\\d+"`: {""},
`expect 11 "\\d"`: {""},
`expect 11 "\\d+"`: {""},
`expect "abc" "^[a-z]+$"`: {""},

`expect 1 "int"`: {""},
`expect 1 "str"`: {"", "string expected"},
`expect 1 "string"`: {"", "string expected"},
`expect .i5 "int"`: {""},
`expect "5" "int"`: {""}, // hasInt
`expect "aa" "int"`: {"", "int expected"},

`optional 1 "int"`: {""},
`optional .x "int"`: {""},
`optional .x "str"`: {""},
`optional .i5 "str"`: {""}, // corner case, although it hasInt everything is always a string
}

func render(templateS string, vars map[string]string) (string, error) {
var err error
buf := new(bytes.Buffer)
ts := fmt.Sprintf("{{ %v }}", strings.Trim(templateS, "{} "))
tem, err := template.New("").Funcs(funcMap).Parse(ts)
if err != nil {
return "", fmt.Errorf("invalid template")
}
err = tem.Execute(buf, vars)
return buf.String(), err
}

func TestRender1(t *testing.T) {

l := map[string]string{
"i5": "5",
"sA": "A",
"sAAA": "aa.",
"dot": ".",
"space": " ",
}

for tem, exp := range test_set {
res, err := render(tem, l)

e := []string{fmt.Sprintf(`{{ %v }} = "%v", error=%v`, tem, res, err)}

// Check value
if res != exp[0] {
e = append(e, fmt.Sprintf("- expected value = %v", exp[0]))
}

// Check errors
if len(exp) > 1 {
ee := fmt.Sprintf("- expected error with %s", exp[1])
if err == nil {
e = append(e, ee)
} else if !strings.Contains(err.Error(), exp[1]) {
e = append(e, ee)
}
} else if err != nil {
e = append(e, "- no error expected")
}

if len(e) > 1 {
t.Error(strings.Join(e, "\n"))
}
}

}
Loading

0 comments on commit 2464a53

Please sign in to comment.