Skip to content

Commit 2e7fe9f

Browse files
committed
Add explainer for regex patterns
1 parent 4c74848 commit 2e7fe9f

File tree

11 files changed

+1651
-80
lines changed

11 files changed

+1651
-80
lines changed

sds-go/go/dd_sds.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ void append_rule_to_list(long rule_ptr, long list_ptr);
2121
void free_rule_list(long list_ptr);
2222

2323
const char* validate_regex(const char* regex, const char** error_out);
24+
const char* explain_regex(const char* regex, const char** error_out);

sds-go/go/regex.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package dd_sds
2+
3+
/*
4+
#include <stdlib.h>
5+
#include <dd_sds.h>
6+
*/
7+
import "C"
8+
import (
9+
"encoding/json"
10+
"fmt"
11+
"unsafe"
12+
)
13+
14+
// ValidateRegex validates a regex pattern and returns any error message.
15+
// Returns (true, nil) if the regex is valid, or (false, error) if invalid.
16+
func ValidateRegex(regex string) (bool, error) {
17+
cRegex := C.CString(regex)
18+
defer C.free(unsafe.Pointer(cRegex))
19+
20+
result := C.validate_regex(cRegex, nil)
21+
if result == nil {
22+
return true, nil
23+
}
24+
25+
errorMsg := C.GoString(result)
26+
C.free_string(result)
27+
return false, fmt.Errorf("invalid regex: %s", errorMsg)
28+
}
29+
30+
// AstNode represents a node in the regex abstract syntax tree.
31+
// Each node provides detailed information about a specific part of the regex pattern.
32+
type AstNode struct {
33+
// NodeType is the type of syntax element (e.g., "Literal", "Alternation", "Capturing Group")
34+
NodeType string `json:"node_type"`
35+
36+
// Description is a human-readable explanation of what this node does
37+
Description string `json:"description"`
38+
39+
// Start is the character position where this node begins in the original pattern (for highlighting)
40+
Start int `json:"start"`
41+
42+
// End is the character position where this node ends in the original pattern (for highlighting)
43+
End int `json:"end"`
44+
45+
// Children contains nested AST nodes for complex patterns
46+
Children []AstNode `json:"children,omitempty"`
47+
48+
// Properties contains additional metadata about the node
49+
Properties map[string]interface{} `json:"properties,omitempty"`
50+
}
51+
52+
// RegexExplanation contains the result of explaining a regex pattern.
53+
// If the regex is invalid, IsValid will be false and Error will contain the error message.
54+
type RegexExplanation struct {
55+
// IsValid indicates whether the regex pattern was successfully parsed
56+
IsValid bool `json:"is_valid"`
57+
58+
// Error contains the error message if the regex is invalid
59+
Error *string `json:"error,omitempty"`
60+
61+
// Tree is the root node of the Abstract Syntax Tree if the regex is valid
62+
Tree *AstNode `json:"tree,omitempty"`
63+
}
64+
65+
// ExplainRegex parses a regex pattern and returns its Abstract Syntax Tree (AST)
66+
// along with human-readable descriptions of each node.
67+
func ExplainRegex(regex string) (RegexExplanation, error) {
68+
cRegex := C.CString(regex)
69+
defer C.free(unsafe.Pointer(cRegex))
70+
71+
result := C.explain_regex(cRegex, nil)
72+
if result == nil {
73+
return RegexExplanation{
74+
IsValid: false,
75+
Error: stringPtr("Failed to explain regex"),
76+
}, nil
77+
}
78+
79+
jsonStr := C.GoString(result)
80+
C.free_string(result)
81+
82+
var explanation RegexExplanation
83+
if err := json.Unmarshal([]byte(jsonStr), &explanation); err != nil {
84+
return RegexExplanation{
85+
IsValid: false,
86+
Error: stringPtr("Failed to parse explanation JSON"),
87+
}, err
88+
}
89+
90+
return explanation, nil
91+
}
92+
93+
func stringPtr(s string) *string {
94+
return &s
95+
}

sds-go/go/regex_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package dd_sds
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestValidateRegex(t *testing.T) {
8+
_, err := ValidateRegex("hello")
9+
if err != nil {
10+
t.Fatal(err)
11+
}
12+
13+
_, err = ValidateRegex("[")
14+
if err == nil {
15+
t.Fatal("Expected error for invalid regex")
16+
}
17+
}
18+
19+
func TestExplainRegex(t *testing.T) {
20+
tests := []struct {
21+
name string
22+
pattern string
23+
valid bool
24+
}{
25+
{
26+
name: "simple literal",
27+
pattern: "hello",
28+
valid: true,
29+
},
30+
{
31+
name: "digit class",
32+
pattern: "\\d+",
33+
valid: true,
34+
},
35+
{
36+
name: "alternation",
37+
pattern: "a|b|c",
38+
valid: true,
39+
},
40+
{
41+
name: "capturing group",
42+
pattern: "(abc)",
43+
valid: true,
44+
},
45+
{
46+
name: "complex pattern",
47+
pattern: "(\\d{3})-\\d{3}-\\d{4}",
48+
valid: true,
49+
},
50+
{
51+
name: "invalid regex",
52+
pattern: "[",
53+
valid: false,
54+
},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
explanation, err := ExplainRegex(tt.pattern)
60+
if err != nil {
61+
t.Fatalf("ExplainRegex() error = %v", err)
62+
}
63+
64+
if explanation.IsValid != tt.valid {
65+
t.Errorf("ExplainRegex() IsValid = %v, want %v", explanation.IsValid, tt.valid)
66+
}
67+
68+
if tt.valid {
69+
if explanation.Tree == nil {
70+
t.Error("ExplainRegex() Tree should not be nil for valid regex")
71+
}
72+
if explanation.Error != nil {
73+
t.Errorf("ExplainRegex() Error should be nil for valid regex, got %v", *explanation.Error)
74+
}
75+
76+
if explanation.Tree.NodeType == "" {
77+
t.Error("ExplainRegex() Tree.NodeType should not be empty")
78+
}
79+
if explanation.Tree.Description == "" {
80+
t.Error("ExplainRegex() Tree.Description should not be empty")
81+
}
82+
} else {
83+
if explanation.Error == nil {
84+
t.Error("ExplainRegex() Error should not be nil for invalid regex")
85+
}
86+
}
87+
})
88+
}
89+
}
90+
91+
func TestExplainRegexWithPositions(t *testing.T) {
92+
explanation, err := ExplainRegex("abc")
93+
if err != nil {
94+
t.Fatalf("ExplainRegex() error = %v", err)
95+
}
96+
97+
if !explanation.IsValid {
98+
t.Fatal("ExplainRegex() should be valid")
99+
}
100+
101+
if explanation.Tree == nil {
102+
t.Fatal("ExplainRegex() Tree should not be nil")
103+
}
104+
105+
if explanation.Tree.Start < 0 {
106+
t.Errorf("ExplainRegex() Tree.Start should be >= 0, got %d", explanation.Tree.Start)
107+
}
108+
if explanation.Tree.End <= explanation.Tree.Start {
109+
t.Errorf("ExplainRegex() Tree.End (%d) should be > Start (%d)", explanation.Tree.End, explanation.Tree.Start)
110+
}
111+
}
112+
113+
func TestExplainRegexWithChildren(t *testing.T) {
114+
explanation, err := ExplainRegex("a|b|c")
115+
if err != nil {
116+
t.Fatalf("ExplainRegex() error = %v", err)
117+
}
118+
119+
if !explanation.IsValid {
120+
t.Fatal("ExplainRegex() should be valid")
121+
}
122+
123+
if explanation.Tree == nil {
124+
t.Fatal("ExplainRegex() Tree should not be nil")
125+
}
126+
127+
if explanation.Tree.NodeType != "Alternation" {
128+
t.Errorf("ExplainRegex() Tree.NodeType should be 'Alternation', got %s", explanation.Tree.NodeType)
129+
}
130+
131+
if len(explanation.Tree.Children) != 3 {
132+
t.Errorf("ExplainRegex() Tree.Children should have 3 elements, got %d", len(explanation.Tree.Children))
133+
}
134+
}

sds-go/go/validation.go

Lines changed: 0 additions & 26 deletions
This file was deleted.

sds-go/go/validation_test.go

Lines changed: 0 additions & 19 deletions
This file was deleted.

sds-go/rust/src/native/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use std::sync::{Arc, Mutex};
99

1010
pub mod create_scanner;
1111
pub mod delete_scanner;
12+
pub mod regex;
1213
pub mod rule;
1314
pub mod scan;
14-
pub mod validation;
1515

1616
pub const ERR_PANIC: i64 = -5;
1717

0 commit comments

Comments
 (0)