Kv regex scan (#1195)
* Extract typed values from strings via regex * more specific ctor name
This commit is contained in:
144
utils/kv_regex.go
Normal file
144
utils/kv_regex.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Scanner holds the source string to be searched.
|
||||
type Scanner struct {
|
||||
src string
|
||||
}
|
||||
|
||||
func NewKVRegexScanner(s string) Scanner { return Scanner{src: s} }
|
||||
|
||||
// Raw returns the first capture group by default, or a named group if provided.
|
||||
// Example:
|
||||
//
|
||||
// re := regexp.MustCompile(`key=(\d+)`) // group 1
|
||||
// kv.Raw(re) -> "123", true
|
||||
// re2 := regexp.MustCompile(`key=(?P<val>\d+)`) // named group "val"
|
||||
// kv.Raw(re2, "val") -> "123", true
|
||||
func (kv Scanner) Raw(re *regexp.Regexp, group ...string) (string, bool) {
|
||||
idx, ok := resolveGroupIndex(re, group...)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
m := re.FindStringSubmatch(kv.src)
|
||||
if len(m) == 0 || idx >= len(m) {
|
||||
return "", false
|
||||
}
|
||||
return m[idx], true
|
||||
}
|
||||
|
||||
func (kv Scanner) Uint64(re *regexp.Regexp, group ...string) (uint64, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
u, err := strconv.ParseUint(raw, 10, 64)
|
||||
return u, err == nil
|
||||
}
|
||||
|
||||
func (kv Scanner) Int64(re *regexp.Regexp, group ...string) (int64, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
i, err := strconv.ParseInt(raw, 10, 64)
|
||||
return i, err == nil
|
||||
}
|
||||
|
||||
func (kv Scanner) Uint(re *regexp.Regexp, group ...string) (uint, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
u, err := strconv.ParseUint(raw, 10, 0)
|
||||
return uint(u), err == nil
|
||||
}
|
||||
|
||||
func (kv Scanner) Int(re *regexp.Regexp, group ...string) (int, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
i, err := strconv.ParseInt(raw, 10, 0)
|
||||
return int(i), err == nil
|
||||
}
|
||||
|
||||
func (kv Scanner) Float64(re *regexp.Regexp, group ...string) (float64, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
f, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (kv Scanner) Bool(re *regexp.Regexp, group ...string) (bool, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
switch strings.ToLower(raw) {
|
||||
case "true", "1", "yes":
|
||||
return true, true
|
||||
case "false", "0", "no":
|
||||
return false, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
// DurationNs treats the captured value as nanoseconds and returns time.Duration.
|
||||
func (kv Scanner) DurationNs(re *regexp.Regexp, group ...string) (time.Duration, bool) {
|
||||
u, ok := kv.Uint64(re, group...)
|
||||
if !ok || u > math.MaxInt64 {
|
||||
return 0, false
|
||||
}
|
||||
return time.Duration(u), true
|
||||
}
|
||||
|
||||
// String returns the captured string, unquoting if it looks like "..." (handles \" escapes).
|
||||
func (kv Scanner) String(re *regexp.Regexp, group ...string) (string, bool) {
|
||||
raw, ok := kv.Raw(re, group...)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if len(raw) >= 2 && raw[0] == '"' && raw[len(raw)-1] == '"' {
|
||||
if unq, err := strconv.Unquote(raw); err == nil {
|
||||
return unq, true
|
||||
}
|
||||
}
|
||||
return raw, true
|
||||
}
|
||||
|
||||
func indexOfSubexpName(re *regexp.Regexp, name string) int {
|
||||
for i, n := range re.SubexpNames() {
|
||||
if n == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func resolveGroupIndex(re *regexp.Regexp, group ...string) (int, bool) {
|
||||
if len(group) == 0 || group[0] == "" {
|
||||
if re.NumSubexp() < 1 {
|
||||
return -1, false // no capturing groups
|
||||
}
|
||||
return 1, true // default to first group
|
||||
}
|
||||
idx := indexOfSubexpName(re, group[0])
|
||||
if idx <= 0 {
|
||||
return -1, false // named group missing
|
||||
}
|
||||
return idx, true
|
||||
}
|
||||
153
utils/kv_regex_test.go
Normal file
153
utils/kv_regex_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRaw_DefaultFirstGroup(t *testing.T) {
|
||||
src := `num-pushed=(guint64)123 num-gap=(uint64)456`
|
||||
kv := NewKVRegexScanner(src)
|
||||
|
||||
rePushed := regexp.MustCompile(`\bnum-pushed=\(g?uint64\)(\d+)`)
|
||||
reGap := regexp.MustCompile(`\bnum-gap=\(g?uint64\)(\d+)`)
|
||||
|
||||
rawPushed, ok := kv.Raw(rePushed)
|
||||
require.True(t, ok, "expected match for num-pushed")
|
||||
require.Equal(t, "123", rawPushed)
|
||||
|
||||
rawGap, ok := kv.Raw(reGap)
|
||||
require.True(t, ok, "expected match for num-gap")
|
||||
require.Equal(t, "456", rawGap)
|
||||
}
|
||||
|
||||
func TestRaw_NamedGroup(t *testing.T) {
|
||||
src := `plc-duration=(guint64)20000000`
|
||||
kv := NewKVRegexScanner(src)
|
||||
|
||||
reDur := regexp.MustCompile(`\bplc-duration=\(g?uint64\)(?P<ns>\d+)`)
|
||||
|
||||
raw, ok := kv.Raw(reDur, "ns")
|
||||
require.True(t, ok, "expected match for plc-duration ns group")
|
||||
require.Equal(t, "20000000", raw)
|
||||
}
|
||||
|
||||
func TestUint64_Int64_Uint_Int(t *testing.T) {
|
||||
src := `u64=(guint64)184 r64=(int64)-42 u=(uint)48000 i=(int)7`
|
||||
kv := NewKVRegexScanner(src)
|
||||
|
||||
reU64 := regexp.MustCompile(`\bu64=\(g?uint64\)(\d+)`)
|
||||
reI64 := regexp.MustCompile(`\br64=\(g?int64\)(-?\d+)`)
|
||||
reU := regexp.MustCompile(`\bu=\(g?uint\)(\d+)`)
|
||||
reI := regexp.MustCompile(`\bi=\(g?int\)(-?\d+)`)
|
||||
|
||||
vU64, ok := kv.Uint64(reU64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint64(184), vU64)
|
||||
|
||||
vI64, ok := kv.Int64(reI64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int64(-42), vI64)
|
||||
|
||||
vU, ok := kv.Uint(reU)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint(48000), vU)
|
||||
|
||||
vI, ok := kv.Int(reI)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 7, vI)
|
||||
}
|
||||
|
||||
func TestFloat64_AndRejectNaNInf(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`rms=(double)-12.25 bad1=(double)nan bad2=(double)inf`)
|
||||
reOk := regexp.MustCompile(`\brms=\(g?double\)(-?[0-9.]+)`)
|
||||
reNaN := regexp.MustCompile(`\bbad1=\(g?double\)([^,}\s]+)`)
|
||||
reInf := regexp.MustCompile(`\bbad2=\(g?double\)([^,}\s]+)`)
|
||||
|
||||
v, ok := kv.Float64(reOk)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, -12.25, v)
|
||||
|
||||
_, ok = kv.Float64(reNaN)
|
||||
require.False(t, ok, "Float64 should reject NaN")
|
||||
|
||||
_, ok = kv.Float64(reInf)
|
||||
require.False(t, ok, "Float64 should reject Inf")
|
||||
}
|
||||
|
||||
func TestBool_CaseAndAliases(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`b1=(gboolean)TRUE b2=(boolean)false b3=(boolean)Yes b4=(boolean)0 other=x`)
|
||||
reB1 := regexp.MustCompile(`\bb1=\(g?boolean\)([^,}\s]+)`)
|
||||
reB2 := regexp.MustCompile(`\bb2=\(g?boolean\)([^,}\s]+)`)
|
||||
reB3 := regexp.MustCompile(`\bb3=\(g?boolean\)([^,}\s]+)`)
|
||||
reB4 := regexp.MustCompile(`\bb4=\(g?boolean\)([^,}\s]+)`)
|
||||
|
||||
v1, ok := kv.Bool(reB1)
|
||||
require.True(t, ok)
|
||||
require.True(t, v1)
|
||||
|
||||
v2, ok := kv.Bool(reB2)
|
||||
require.True(t, ok)
|
||||
require.False(t, v2)
|
||||
|
||||
v3, ok := kv.Bool(reB3)
|
||||
require.True(t, ok)
|
||||
require.True(t, v3)
|
||||
|
||||
v4, ok := kv.Bool(reB4)
|
||||
require.True(t, ok)
|
||||
require.False(t, v4)
|
||||
}
|
||||
|
||||
func TestString_Unquote(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`msg=(string)"hello \"world\"" raw=(string)plain`)
|
||||
reQ := regexp.MustCompile(`\bmsg=\(string\)("(?:[^"\\]|\\.)*")`)
|
||||
reRaw := regexp.MustCompile(`\braw=\(string\)([^,}\s]+)`)
|
||||
|
||||
s, ok := kv.String(reQ)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, `hello "world"`, s)
|
||||
|
||||
s, ok = kv.String(reRaw)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "plain", s)
|
||||
}
|
||||
|
||||
func TestDurationNs(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`d=(guint64)20000000`) // 20ms
|
||||
re := regexp.MustCompile(`\bd=\(g?uint64\)(\d+)`)
|
||||
|
||||
d, ok := kv.DurationNs(re)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 20*time.Millisecond, d)
|
||||
}
|
||||
|
||||
func TestDurationNs_OverflowGuard(t *testing.T) {
|
||||
overflow := strconv.FormatUint(math.MaxUint64, 10)
|
||||
kv := NewKVRegexScanner(`d=(guint64)` + overflow)
|
||||
re := regexp.MustCompile(`\bd=\(g?uint64\)(\d+)`)
|
||||
|
||||
_, ok := kv.DurationNs(re)
|
||||
require.False(t, ok, "DurationNs should fail on overflow")
|
||||
}
|
||||
|
||||
func TestNoCapturingGroup_ReturnsFalse(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`nogrp=(uint)7`)
|
||||
re := regexp.MustCompile(`\bnogrp=\(g?uint\)\d+`) // no capturing group
|
||||
|
||||
_, ok := kv.Uint(re)
|
||||
require.False(t, ok, "expected false when regex has no capturing group")
|
||||
}
|
||||
|
||||
func TestMissingNamedGroup_ReturnsFalse(t *testing.T) {
|
||||
kv := NewKVRegexScanner(`v=(int)42`)
|
||||
re := regexp.MustCompile(`\bv=\(g?int\)(?P<val>\d+)`)
|
||||
|
||||
_, ok := kv.Int(re, "nope") // named group doesn't exist
|
||||
require.False(t, ok, "expected false when named group is missing")
|
||||
}
|
||||
Reference in New Issue
Block a user