feat: Add traffic-editor plugin (#2825)

This commit is contained in:
Kent Dong
2025-12-26 17:29:55 +08:00
committed by GitHub
parent 4babdb6a4f
commit 08a7204085
18 changed files with 3026 additions and 0 deletions

View File

@@ -0,0 +1,515 @@
package pkg
import (
"errors"
"fmt"
"github.com/higress-group/wasm-go/pkg/log"
"github.com/tidwall/gjson"
)
const (
commandTypeSet = "set"
commandTypeConcat = "concat"
commandTypeCopy = "copy"
commandTypeDelete = "delete"
commandTypeRename = "rename"
)
var (
commandFactories = map[string]func(gjson.Result) (Command, error){
"set": newSetCommand,
"concat": newConcatCommand,
"copy": newCopyCommand,
"delete": newDeleteCommand,
"rename": newRenameCommand,
}
)
type CommandSet struct {
DisableReroute bool `json:"disableReroute"`
Commands []Command `json:"commands,omitempty"`
RelatedStages map[Stage]bool `json:"-"`
}
func (s *CommandSet) FromJson(json gjson.Result) error {
relatedStages := map[Stage]bool{}
if commandsJson := json.Get("commands"); commandsJson.Exists() && commandsJson.IsArray() {
for _, item := range commandsJson.Array() {
if command, err := NewCommand(item); err != nil {
return fmt.Errorf("failed to create command from json: %v\n %v", err, item)
} else {
s.Commands = append(s.Commands, command)
for _, ref := range command.GetRefs() {
relatedStages[ref.GetStage()] = true
}
}
}
}
s.RelatedStages = relatedStages
if disableReroute := json.Get("disableReroute"); disableReroute.Exists() {
s.DisableReroute = disableReroute.Bool()
} else {
s.DisableReroute = false
}
return nil
}
func (s *CommandSet) CreatExecutors() []Executor {
executors := make([]Executor, 0, len(s.Commands))
for _, command := range s.Commands {
executor := command.CreateExecutor()
executors = append(executors, executor)
}
return executors
}
type ConditionalCommandSet struct {
ConditionSet
CommandSet
}
func (s *ConditionalCommandSet) FromJson(json gjson.Result) error {
if err := s.ConditionSet.FromJson(json); err != nil {
return err
}
if err := s.CommandSet.FromJson(json); err != nil {
return err
}
return nil
}
type Command interface {
GetType() string
GetRefs() []*Ref
CreateExecutor() Executor
}
type Executor interface {
GetCommand() Command
Run(editorContext EditorContext, stage Stage) error
}
func NewCommand(json gjson.Result) (Command, error) {
t := json.Get("type").String()
if t == "" {
return nil, errors.New("command type is required")
}
if constructor, ok := commandFactories[t]; ok && constructor != nil {
return constructor(json)
} else {
return nil, errors.New("unknown command type: " + t)
}
}
type baseExecutor struct {
finished bool
}
// setCommand
func newSetCommand(json gjson.Result) (Command, error) {
var targetRef *Ref
var err error
if t := json.Get("target"); !t.Exists() {
return nil, errors.New("setCommand: target field is required")
} else {
targetRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("setCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
}
}
var value string
if v := json.Get("value"); !v.Exists() {
return nil, errors.New("setCommand: value field is required")
} else {
value = v.String()
if value == "" {
return nil, errors.New("setCommand: value cannot be empty")
}
}
return &setCommand{
targetRef: targetRef,
value: value,
}, nil
}
type setCommand struct {
targetRef *Ref
value string
}
func (c *setCommand) GetType() string {
return commandTypeSet
}
func (c *setCommand) GetRefs() []*Ref {
return []*Ref{c.targetRef}
}
func (c *setCommand) CreateExecutor() Executor {
return &setExecutor{command: c}
}
type setExecutor struct {
baseExecutor
command *setCommand
}
func (e *setExecutor) GetCommand() Command {
return e.command
}
func (e *setExecutor) Run(editorContext EditorContext, stage Stage) error {
if e.finished {
return nil
}
command := e.command
log.Debugf("setCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
if command.targetRef.GetStage() == stage {
log.Debugf("setCommand: set %s to %s", command.targetRef, command.value)
editorContext.SetRefValue(command.targetRef, command.value)
e.finished = true
}
return nil
}
// concatCommand
func newConcatCommand(json gjson.Result) (Command, error) {
var targetRef *Ref
var err error
if t := json.Get("target"); !t.Exists() {
return nil, errors.New("concatCommand: target field is required")
} else {
targetRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("concatCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
}
}
valuesJson := json.Get("values")
if !valuesJson.Exists() || !valuesJson.IsArray() {
return nil, errors.New("concatCommand: values field is required and must be an array")
}
values := make([]interface{}, 0, len(valuesJson.Array()))
for _, item := range valuesJson.Array() {
var value interface{}
if item.IsObject() {
valueRef, err := NewRef(item)
if err != nil {
return nil, fmt.Errorf("concatCommand: failed to create ref from values field: %v\n %v", err, item.Raw)
}
if valueRef.GetStage() > targetRef.GetStage() {
return nil, fmt.Errorf("concatCommand: the processing stage of value [%s] cannot be after the stage of target [%s]", Stage2String[valueRef.GetStage()], Stage2String[targetRef.GetStage()])
}
value = valueRef
} else {
value = item.String()
}
values = append(values, value)
}
return &concatCommand{
targetRef: targetRef,
values: values,
}, nil
}
type concatCommand struct {
targetRef *Ref
values []interface{}
}
func (c *concatCommand) GetType() string {
return commandTypeConcat
}
func (c *concatCommand) GetRefs() []*Ref {
refs := []*Ref{c.targetRef}
if c.values != nil && len(c.values) != 0 {
for _, value := range c.values {
if ref, ok := value.(*Ref); ok {
refs = append(refs, ref)
}
}
}
return refs
}
func (c *concatCommand) CreateExecutor() Executor {
return &concatExecutor{command: c}
}
type concatExecutor struct {
baseExecutor
command *concatCommand
values []string
}
func (e *concatExecutor) GetCommand() Command {
return e.command
}
func (e *concatExecutor) Run(editorContext EditorContext, stage Stage) error {
if e.finished {
return nil
}
command := e.command
if e.values == nil {
e.values = make([]string, len(command.values))
}
for i, value := range command.values {
if value == nil || e.values[i] != "" {
continue
}
v := ""
if s, ok := value.(string); ok {
v = s
} else if ref, ok := value.(*Ref); ok && ref.GetStage() == stage {
v = editorContext.GetRefValue(ref)
}
e.values[i] = v
}
if command.targetRef.GetStage() == stage {
result := ""
for _, v := range e.values {
if v == "" {
continue
}
result += v
}
log.Debugf("concatCommand: set %s to %s", command.targetRef, result)
editorContext.SetRefValue(command.targetRef, result)
e.finished = true
}
return nil
}
// copyCommand
func newCopyCommand(json gjson.Result) (Command, error) {
var sourceRef *Ref
var targetRef *Ref
var err error
if t := json.Get("source"); !t.Exists() {
return nil, errors.New("copyCommand: source field is required")
} else {
sourceRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("copyCommand: failed to create ref from source field: %v\n %v", err, t.Raw)
}
}
if t := json.Get("target"); !t.Exists() {
return nil, errors.New("copyCommand: target field is required")
} else {
targetRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("copyCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
}
}
if sourceRef.GetStage() > targetRef.GetStage() {
return nil, fmt.Errorf("copyCommand: the processing stage of source [%s] cannot be after the stage of target [%s]", Stage2String[sourceRef.GetStage()], Stage2String[targetRef.GetStage()])
}
return &copyCommand{
sourceRef: sourceRef,
targetRef: targetRef,
}, nil
}
type copyCommand struct {
sourceRef *Ref
targetRef *Ref
}
func (c *copyCommand) GetType() string {
return commandTypeCopy
}
func (c *copyCommand) GetRefs() []*Ref {
return []*Ref{c.sourceRef, c.targetRef}
}
func (c *copyCommand) CreateExecutor() Executor {
return &copyExecutor{command: c}
}
type copyExecutor struct {
baseExecutor
command *copyCommand
valueToCopy string
}
func (e *copyExecutor) GetCommand() Command {
return e.command
}
func (e *copyExecutor) Run(editorContext EditorContext, stage Stage) error {
if e.finished {
return nil
}
command := e.command
if command.sourceRef.GetStage() == stage {
e.valueToCopy = editorContext.GetRefValue(command.sourceRef)
log.Debugf("copyCommand: valueToCopy=%s", e.valueToCopy)
}
if e.valueToCopy == "" {
log.Debug("copyCommand: valueToCopy is empty. skip.")
e.finished = true
return nil
}
if command.targetRef.GetStage() == stage {
editorContext.SetRefValue(command.targetRef, e.valueToCopy)
log.Debugf("copyCommand: set %s to %s", e.valueToCopy, command.targetRef)
e.finished = true
}
return nil
}
// deleteCommand
func newDeleteCommand(json gjson.Result) (Command, error) {
var targetRef *Ref
var err error
if t := json.Get("target"); !t.Exists() {
return nil, errors.New("deleteCommand: target field is required")
} else {
targetRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("deleteCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
}
}
return &deleteCommand{
targetRef: targetRef,
}, nil
}
type deleteCommand struct {
targetRef *Ref
}
func (c *deleteCommand) GetType() string {
return commandTypeDelete
}
func (c *deleteCommand) GetRefs() []*Ref {
return []*Ref{c.targetRef}
}
func (c *deleteCommand) CreateExecutor() Executor {
return &deleteExecutor{command: c}
}
type deleteExecutor struct {
baseExecutor
command *deleteCommand
}
func (e *deleteExecutor) GetCommand() Command {
return e.command
}
func (e *deleteExecutor) Run(editorContext EditorContext, stage Stage) error {
if e.finished {
return nil
}
command := e.command
log.Debugf("deleteCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
if command.targetRef.GetStage() == stage {
log.Debugf("deleteCommand: delete %s", command.targetRef)
editorContext.DeleteRefValues(command.targetRef)
e.finished = true
log.Debugf("deleteCommand: finished deleting %s", command.targetRef)
} else {
log.Debugf("deleteCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
}
return nil
}
// renameCommand
func newRenameCommand(json gjson.Result) (Command, error) {
var targetRef *Ref
var err error
if t := json.Get("target"); !t.Exists() {
return nil, errors.New("renameCommand: target field is required")
} else {
targetRef, err = NewRef(t)
if err != nil {
return nil, fmt.Errorf("renameCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
}
}
newName := json.Get("newName").String()
if newName == "" {
return nil, errors.New("renameCommand: newName field is required")
}
return &renameCommand{
targetRef: targetRef,
newName: newName,
}, nil
}
type renameCommand struct {
targetRef *Ref
newName string
}
func (c *renameCommand) GetType() string {
return commandTypeRename
}
func (c *renameCommand) GetRefs() []*Ref {
return []*Ref{c.targetRef}
}
func (c *renameCommand) CreateExecutor() Executor {
return &renameExecutor{command: c}
}
type renameExecutor struct {
baseExecutor
command *renameCommand
}
func (e *renameExecutor) GetCommand() Command {
return e.command
}
func (e *renameExecutor) Run(editorContext EditorContext, stage Stage) error {
if e.finished {
return nil
}
command := e.command
log.Debugf("renameCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
if command.targetRef.GetStage() == stage {
if command.newName == command.targetRef.Name {
log.Debugf("renameCommand: skip renaming %s to itself", command.targetRef)
} else {
values := editorContext.GetRefValues(command.targetRef)
log.Debugf("renameCommand: rename %s to %s value=%v", command.targetRef, command.newName, values)
editorContext.SetRefValues(&Ref{
Type: command.targetRef.Type,
Name: command.newName,
}, values)
editorContext.DeleteRefValues(command.targetRef)
log.Debugf("renameCommand: finished renaming %s to %s", command.targetRef, command.newName)
}
e.finished = true
} else {
log.Debugf("renameCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
}
return nil
}

View File

@@ -0,0 +1,309 @@
package pkg
import (
"testing"
"github.com/tidwall/gjson"
)
func TestNewSetCommand_Success(t *testing.T) {
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"},"value":"bar"}`
json := gjson.Parse(jsonStr)
cmd, err := newSetCommand(json)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cmd.GetType() != "set" {
t.Errorf("expected type 'set', got %s", cmd.GetType())
}
refs := cmd.GetRefs()
if len(refs) != 1 {
t.Errorf("expected 1 ref, got %d", len(refs))
}
}
func TestNewSetCommand_MissingTarget(t *testing.T) {
jsonStr := `{"type":"set","value":"bar"}`
json := gjson.Parse(jsonStr)
_, err := newSetCommand(json)
if err == nil || err.Error() != "setCommand: target field is required" {
t.Errorf("expected target field error, got %v", err)
}
}
func TestNewSetCommand_MissingValue(t *testing.T) {
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"}}`
json := gjson.Parse(jsonStr)
_, err := newSetCommand(json)
if err == nil || err.Error() != "setCommand: value field is required" {
t.Errorf("expected value field error, got %v", err)
}
}
func TestNewConcatCommand_Success(t *testing.T) {
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"},"values":["a","b"]}`
json := gjson.Parse(jsonStr)
cmd, err := newConcatCommand(json)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cmd.GetType() != "concat" {
t.Errorf("expected type 'concat', got %s", cmd.GetType())
}
refs := cmd.GetRefs()
if len(refs) < 1 {
t.Errorf("expected at least 1 ref, got %d", len(refs))
}
}
func TestNewConcatCommand_MissingTarget(t *testing.T) {
jsonStr := `{"type":"concat","values":["a","b"]}`
json := gjson.Parse(jsonStr)
_, err := newConcatCommand(json)
if err == nil || err.Error() != "concatCommand: target field is required" {
t.Errorf("expected target field error, got %v", err)
}
}
func TestNewConcatCommand_MissingValues(t *testing.T) {
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"}}`
json := gjson.Parse(jsonStr)
_, err := newConcatCommand(json)
if err == nil || err.Error() != "concatCommand: values field is required and must be an array" {
t.Errorf("expected values field error, got %v", err)
}
}
func TestNewCopyCommand_Success(t *testing.T) {
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
json := gjson.Parse(jsonStr)
cmd, err := newCopyCommand(json)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cmd.GetType() != "copy" {
t.Errorf("expected type 'copy', got %s", cmd.GetType())
}
refs := cmd.GetRefs()
if len(refs) != 2 {
t.Errorf("expected 2 refs, got %d", len(refs))
}
}
func TestNewCopyCommand_MissingSource(t *testing.T) {
jsonStr := `{"type":"copy","target":{"type":"request_header","name":"bar"}}`
json := gjson.Parse(jsonStr)
_, err := newCopyCommand(json)
if err == nil || err.Error() != "copyCommand: source field is required" {
t.Errorf("expected source field error, got %v", err)
}
}
func TestNewCopyCommand_MissingTarget(t *testing.T) {
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"}}`
json := gjson.Parse(jsonStr)
_, err := newCopyCommand(json)
if err == nil || err.Error() != "copyCommand: target field is required" {
t.Errorf("expected target field error, got %v", err)
}
}
func TestNewCopyCommand_SourceStageAfterTarget(t *testing.T) {
jsonStr := `{"type":"copy","source":{"type":"response_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
json := gjson.Parse(jsonStr)
_, err := newCopyCommand(json)
if err == nil || err.Error() != "copyCommand: the processing stage of source [response_headers] cannot be after the stage of target [request_headers]" {
t.Errorf("expected source stage field error, got %v", err)
}
}
func TestNewDeleteCommand_Success(t *testing.T) {
jsonStr := `{"type":"delete","target":{"type":"request_header","name":"foo"}}`
json := gjson.Parse(jsonStr)
cmd, err := newDeleteCommand(json)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cmd.GetType() != "delete" {
t.Errorf("expected type 'delete', got %s", cmd.GetType())
}
refs := cmd.GetRefs()
if len(refs) != 1 {
t.Errorf("expected 1 ref, got %d", len(refs))
}
}
func TestNewDeleteCommand_MissingTarget(t *testing.T) {
jsonStr := `{"type":"delete"}`
json := gjson.Parse(jsonStr)
_, err := newDeleteCommand(json)
if err == nil || err.Error() != "deleteCommand: target field is required" {
t.Errorf("expected target field error, got %v", err)
}
}
func TestNewRenameCommand_Success(t *testing.T) {
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"},"newName":"bar"}`
json := gjson.Parse(jsonStr)
cmd, err := newRenameCommand(json)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cmd.GetType() != "rename" {
t.Errorf("expected type 'rename', got %s", cmd.GetType())
}
refs := cmd.GetRefs()
if len(refs) != 1 {
t.Errorf("expected 1 ref, got %d", len(refs))
}
}
func TestNewRenameCommand_MissingTarget(t *testing.T) {
jsonStr := `{"type":"rename","newName":"bar"}`
json := gjson.Parse(jsonStr)
_, err := newRenameCommand(json)
if err == nil || err.Error() != "renameCommand: target field is required" {
t.Errorf("expected target field error, got %v", err)
}
}
func TestNewRenameCommand_MissingNewName(t *testing.T) {
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"}}`
json := gjson.Parse(jsonStr)
_, err := newRenameCommand(json)
if err == nil || err.Error() != "renameCommand: newName field is required" {
t.Errorf("expected newName field error, got %v", err)
}
}
func TestSetExecutor_Run_SingleStage(t *testing.T) {
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
cmd := &setCommand{targetRef: ref, value: "bar"}
executor := cmd.CreateExecutor()
ctx := NewEditorContext()
stage := StageRequestHeaders
err := executor.Run(ctx, stage)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(ref) != "bar" {
t.Errorf("expected value 'bar', got %s", ctx.GetRefValue(ref))
}
}
func TestConcatExecutor_Run_SingleStage(t *testing.T) {
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
executor := cmd.CreateExecutor()
ctx := NewEditorContext()
ctx.SetRefValue(srcRef, "-")
stage := StageRequestHeaders
err := executor.Run(ctx, stage)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(ref) != "a-b" {
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
}
}
func TestConcatExecutor_Run_MultiStages(t *testing.T) {
ref := &Ref{Type: RefTypeResponseHeader, Name: "foo"}
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
executor := cmd.CreateExecutor()
ctx := NewEditorContext()
ctx.SetRefValue(srcRef, "-")
err := executor.Run(ctx, StageRequestHeaders)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
err = executor.Run(ctx, StageResponseHeaders)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(ref) != "a-b" {
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
}
}
func TestCopyExecutor_Run_SingleStage(t *testing.T) {
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
target := &Ref{Type: RefTypeRequestHeader, Name: "bar"}
ctx := NewEditorContext()
ctx.SetRefValue(source, "baz")
cmd := &copyCommand{sourceRef: source, targetRef: target}
executor := cmd.CreateExecutor()
stage := StageRequestHeaders
err := executor.Run(ctx, stage)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(target) != "baz" {
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
}
}
func TestCopyExecutor_Run_MultiStages(t *testing.T) {
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
target := &Ref{Type: RefTypeResponseHeader, Name: "bar"}
ctx := NewEditorContext()
ctx.SetRefValue(source, "baz")
cmd := &copyCommand{sourceRef: source, targetRef: target}
executor := cmd.CreateExecutor()
err := executor.Run(ctx, StageRequestHeaders)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
err = executor.Run(ctx, StageResponseHeaders)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(target) != "baz" {
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
}
}
func TestDeleteExecutor_Run(t *testing.T) {
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
ctx := NewEditorContext()
ctx.SetRefValue(ref, "bar")
cmd := &deleteCommand{targetRef: ref}
executor := cmd.CreateExecutor()
stage := StageRequestHeaders
err := executor.Run(ctx, stage)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ctx.GetRefValue(ref) != "" {
t.Errorf("expected value to be deleted, got %s", ctx.GetRefValue(ref))
}
}
func TestRenameExecutor_Run(t *testing.T) {
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
ctx := NewEditorContext()
ctx.SetRefValue(ref, "bar")
cmd := &renameCommand{targetRef: ref, newName: "baz"}
executor := cmd.CreateExecutor()
stage := StageRequestHeaders
err := executor.Run(ctx, stage)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
newRef := &Ref{Type: ref.Type, Name: "baz"}
if ctx.GetRefValue(newRef) != "bar" {
t.Errorf("expected value 'bar' for new name, got %s", ctx.GetRefValue(newRef))
}
if ctx.GetRefValue(ref) != "" {
t.Errorf("expected old name to be deleted, got %s", ctx.GetRefValue(ref))
}
}

View File

@@ -0,0 +1,325 @@
package pkg
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/higress-group/wasm-go/pkg/log"
"github.com/tidwall/gjson"
)
const (
conditionTypeEquals = "equals"
conditionTypePrefix = "prefix"
conditionTypeSuffix = "suffix"
conditionTypeContains = "contains"
conditionTypeRegex = "regex"
)
var (
conditionFactories = map[string]func(gjson.Result) (Condition, error){
conditionTypeEquals: newEqualsCondition,
conditionTypePrefix: newPrefixCondition,
conditionTypeSuffix: newSuffixCondition,
conditionTypeContains: newContainsCondition,
conditionTypeRegex: newRegexCondition,
}
)
type ConditionSet struct {
Conditions []Condition `json:"conditions,omitempty"`
RelatedStages map[Stage]bool `json:"-"`
}
func (s *ConditionSet) FromJson(json gjson.Result) error {
relatedStages := map[Stage]bool{}
s.Conditions = nil
if conditionsJson := json.Get("conditions"); conditionsJson.Exists() && conditionsJson.IsArray() {
for _, item := range conditionsJson.Array() {
if condition, err := CreateCondition(item); err != nil {
return fmt.Errorf("failed to create condition from json: %v\n %v", err, item)
} else {
s.Conditions = append(s.Conditions, condition)
for _, ref := range condition.GetRefs() {
relatedStages[ref.GetStage()] = true
}
}
}
}
s.RelatedStages = relatedStages
return nil
}
func (s *ConditionSet) Matches(editorContext EditorContext) bool {
if len(s.Conditions) == 0 {
return true
}
for _, condition := range s.Conditions {
if !condition.Evaluate(editorContext) {
return false
}
}
return true
}
type Condition interface {
GetType() string
GetRefs() []*Ref
Evaluate(ctx EditorContext) bool
}
func CreateCondition(json gjson.Result) (Condition, error) {
t := json.Get("type").String()
if t == "" {
return nil, errors.New("condition type is required")
}
if constructor, ok := conditionFactories[t]; !ok || constructor == nil {
return nil, errors.New("unknown condition type: " + t)
} else if condition, err := constructor(json); err != nil {
return nil, fmt.Errorf("failed to create condition with type %s: %v", t, err)
} else {
for _, ref := range condition.GetRefs() {
if ref.GetStage() >= StageResponseHeaders {
return nil, fmt.Errorf("condition only supports request refs")
}
}
return condition, nil
}
}
// equalsCondition
func newEqualsCondition(json gjson.Result) (Condition, error) {
value1 := json.Get("value1")
if value1.Type != gjson.JSON {
return nil, errors.New("equalsCondition: value1 field type must be JSON object")
}
value1Ref, err := NewRef(value1)
if err != nil {
return nil, errors.New("equalsCondition: failed to create value1 ref: " + err.Error())
}
value2 := json.Get("value2").String()
return &equalsCondition{
value1Ref: value1Ref,
value2: value2,
}, nil
}
type equalsCondition struct {
value1Ref *Ref
value2 string
}
func (c *equalsCondition) GetType() string {
return conditionTypeEquals
}
func (c *equalsCondition) GetRefs() []*Ref {
return []*Ref{c.value1Ref}
}
func (c *equalsCondition) Evaluate(ctx EditorContext) bool {
log.Debugf("Evaluating equals condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
ref1Values := ctx.GetRefValues(c.value1Ref)
if len(ref1Values) == 0 {
log.Debugf("No values found for ref1: %v", c.value1Ref)
return false
}
for _, value1 := range ref1Values {
if value1 == c.value2 {
log.Debugf("Condition matched: %s == %s", value1, c.value2)
return true
}
}
log.Debugf("No matches found for condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
return false
}
// prefixCondition
func newPrefixCondition(json gjson.Result) (Condition, error) {
value := json.Get("value")
if value.Type != gjson.JSON {
return nil, errors.New("prefixCondition: value field type must be JSON object")
}
valueRef, err := NewRef(value)
if err != nil {
return nil, errors.New("prefixCondition: failed to create value ref: " + err.Error())
}
prefix := json.Get("prefix").String()
return &prefixCondition{
valueRef: valueRef,
prefix: prefix,
}, nil
}
type prefixCondition struct {
valueRef *Ref
prefix string
}
func (c *prefixCondition) GetType() string {
return conditionTypePrefix
}
func (c *prefixCondition) GetRefs() []*Ref {
return []*Ref{c.valueRef}
}
func (c *prefixCondition) Evaluate(ctx EditorContext) bool {
log.Debugf("Evaluating prefix condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
refValues := ctx.GetRefValues(c.valueRef)
if len(refValues) == 0 {
log.Debugf("No values found for ref: %v", c.valueRef)
return false
}
for _, value := range refValues {
if strings.HasPrefix(value, c.prefix) {
log.Debugf("Condition matched: %s starts with %s", value, c.prefix)
return true
}
}
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
return false
}
// suffixCondition
func newSuffixCondition(json gjson.Result) (Condition, error) {
value := json.Get("value")
if value.Type != gjson.JSON {
return nil, errors.New("suffixCondition: value field type must be JSON object")
}
valueRef, err := NewRef(value)
if err != nil {
return nil, errors.New("suffixCondition: failed to create value ref: " + err.Error())
}
suffix := json.Get("suffix").String()
return &suffixCondition{
valueRef: valueRef,
suffix: suffix,
}, nil
}
type suffixCondition struct {
valueRef *Ref
suffix string
}
func (c *suffixCondition) GetType() string {
return conditionTypeSuffix
}
func (c *suffixCondition) GetRefs() []*Ref {
return []*Ref{c.valueRef}
}
func (c *suffixCondition) Evaluate(ctx EditorContext) bool {
log.Debugf("Evaluating suffix condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
refValues := ctx.GetRefValues(c.valueRef)
if len(refValues) == 0 {
log.Debugf("No values found for ref: %v", c.valueRef)
return false
}
for _, value := range refValues {
if strings.HasSuffix(value, c.suffix) {
log.Debugf("Condition matched: %s ends with %s", value, c.suffix)
return true
}
}
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
return false
}
// containsCondition
func newContainsCondition(json gjson.Result) (Condition, error) {
value := json.Get("value")
if value.Type != gjson.JSON {
return nil, errors.New("containsCondition: value field type must be JSON object")
}
valueRef, err := NewRef(value)
if err != nil {
return nil, errors.New("containsCondition: failed to create value ref: " + err.Error())
}
part := json.Get("part").String()
return &containsCondition{
valueRef: valueRef,
part: part,
}, nil
}
type containsCondition struct {
valueRef *Ref
part string
}
func (c *containsCondition) GetType() string {
return conditionTypeContains
}
func (c *containsCondition) GetRefs() []*Ref {
return []*Ref{c.valueRef}
}
func (c *containsCondition) Evaluate(ctx EditorContext) bool {
refValues := ctx.GetRefValues(c.valueRef)
if len(refValues) == 0 {
return false
}
for _, value := range refValues {
if strings.Contains(value, c.part) {
return true
}
}
return false
}
// regexCondition
func newRegexCondition(json gjson.Result) (Condition, error) {
value := json.Get("value")
if value.Type != gjson.JSON {
return nil, errors.New("regexCondition: value field type must be JSON object")
}
valueRef, err := NewRef(value)
if err != nil {
return nil, errors.New("regexCondition: failed to create value ref: " + err.Error())
}
patternStr := json.Get("pattern").String()
pattern, err := regexp.Compile(patternStr)
if err != nil {
return nil, errors.New("regexCondition: failed to compile pattern: " + err.Error())
}
return &regexCondition{
valueRef: valueRef,
pattern: pattern,
}, nil
}
type regexCondition struct {
valueRef *Ref
pattern *regexp.Regexp
}
func (c *regexCondition) GetType() string {
return conditionTypeRegex
}
func (c *regexCondition) Evaluate(ctx EditorContext) bool {
log.Debugf("Evaluating regex condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
refValues := ctx.GetRefValues(c.valueRef)
if len(refValues) == 0 {
log.Debugf("No values found for ref: %v", c.valueRef)
return false
}
for _, value := range refValues {
if c.pattern.MatchString(value) {
log.Debugf("Condition matched: %s matches %s", value, c.pattern.String())
return true
}
}
log.Debugf("No matches found for condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
return false
}
func (c *regexCondition) GetRefs() []*Ref {
return []*Ref{c.valueRef}
}

View File

@@ -0,0 +1,217 @@
package pkg
import (
"testing"
"github.com/tidwall/gjson"
)
// --- equalsCondition tests ---
func TestEqualsCondition_Match(t *testing.T) {
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
if !cond.Evaluate(ctx) {
t.Error("equalsCondition should match")
}
}
func TestEqualsCondition_NoMatch(t *testing.T) {
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
cond, _ := CreateCondition(json)
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-test": {"def"}})
if cond.Evaluate(ctx) {
t.Error("equalsCondition should not match")
}
}
// --- prefixCondition tests ---
func TestPrefixCondition_Match(t *testing.T) {
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
if !cond.Evaluate(ctx) {
t.Error("prefixCondition should match")
}
}
func TestPrefixCondition_NoMatch(t *testing.T) {
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
cond, _ := CreateCondition(json)
ctx := NewEditorContext()
ctx.SetRequestQueries(map[string][]string{"foo": {"bazbar"}})
if cond.Evaluate(ctx) {
t.Error("prefixCondition should not match")
}
}
// --- suffixCondition tests ---
func TestSuffixCondition_Match(t *testing.T) {
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-end": {"123xyz"}})
if !cond.Evaluate(ctx) {
t.Error("suffixCondition should match")
}
}
func TestSuffixCondition_NoMatch(t *testing.T) {
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
cond, _ := CreateCondition(json)
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-end": {"xyz123"}})
if cond.Evaluate(ctx) {
t.Error("suffixCondition should not match")
}
}
// --- containsCondition tests ---
func TestContainsCondition_Match(t *testing.T) {
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
if !cond.Evaluate(ctx) {
t.Error("containsCondition should match")
}
}
func TestContainsCondition_NoMatch(t *testing.T) {
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
cond, _ := CreateCondition(json)
ctx := NewEditorContext()
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
if cond.Evaluate(ctx) {
t.Error("containsCondition should not match")
}
}
// --- regexCondition tests ---
func TestRegexCondition_Match(t *testing.T) {
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"abcdef"}})
if !cond.Evaluate(ctx) {
t.Error("regexCondition should match")
}
}
func TestRegexCondition_NoMatch(t *testing.T) {
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
cond, _ := CreateCondition(json)
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"defabc"}})
if cond.Evaluate(ctx) {
t.Error("regexCondition should not match")
}
}
// --- CreateCondition error cases ---
func TestCreateCondition_UnknownType(t *testing.T) {
json := gjson.Parse(`{"type":"unknown","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
_, err := CreateCondition(json)
if err == nil {
t.Error("CreateCondition should fail for unknown type")
}
}
func TestCreateCondition_MissingType(t *testing.T) {
json := gjson.Parse(`{"value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
_, err := CreateCondition(json)
if err == nil {
t.Error("CreateCondition should fail for missing type")
}
}
func TestCreateCondition_InvalidRefType(t *testing.T) {
json := gjson.Parse(`{"type":"equals","value1":{"type":"invalid_type","name":"x-test"},"value2":"abc"}`)
_, err := CreateCondition(json)
if err == nil {
t.Error("CreateCondition should fail for invalid ref type")
}
}
func TestCreateCondition_MissingRefName(t *testing.T) {
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header"},"value2":"abc"}`)
_, err := CreateCondition(json)
if err == nil {
t.Error("CreateCondition should fail for missing ref name")
}
}
// --- ConditionSet tests ---
func TestConditionSet_Matches_AllMatch(t *testing.T) {
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
var set ConditionSet
if err := set.FromJson(json); err != nil {
t.Fatalf("FromJson failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
if !set.Matches(ctx) {
t.Error("ConditionSet should match when all conditions match")
}
}
func TestConditionSet_Matches_OneNoMatch(t *testing.T) {
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
var set ConditionSet
if err := set.FromJson(json); err != nil {
t.Fatalf("FromJson failed: %v", err)
}
ctx := NewEditorContext()
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
ctx.SetRequestQueries(map[string][]string{"foo": {"baz"}})
if set.Matches(ctx) {
t.Error("ConditionSet should not match if one condition does not match")
}
}
func TestConditionSet_Matches_Empty(t *testing.T) {
json := gjson.Parse(`{"conditions":[]}`)
var set ConditionSet
if err := set.FromJson(json); err != nil {
t.Fatalf("FromJson failed: %v", err)
}
ctx := NewEditorContext()
if !set.Matches(ctx) {
t.Error("ConditionSet with no conditions should always match")
}
}
// --- GetType/GetRefs coverage ---
func TestCondition_GetTypeAndRefs(t *testing.T) {
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
cond, err := CreateCondition(json)
if err != nil {
t.Fatalf("CreateCondition failed: %v", err)
}
if cond.GetType() != "equals" {
t.Error("GetType should return 'equals'")
}
refs := cond.GetRefs()
if len(refs) != 1 || refs[0].Type != "request_header" || refs[0].Name != "x-test" {
t.Error("GetRefs should return correct ref")
}
}

View File

@@ -0,0 +1,310 @@
package pkg
import (
"maps"
"net/url"
"strings"
"github.com/higress-group/wasm-go/pkg/log"
)
type Stage int
const (
StageInvalid Stage = iota
StageRequestHeaders
StageRequestBody
StageResponseHeaders
StageResponseBody
pathHeader = ":path"
)
var (
OrderedStages = []Stage{
StageRequestHeaders,
StageRequestBody,
StageResponseHeaders,
StageResponseBody,
}
Stage2String = map[Stage]string{
StageRequestHeaders: "request_headers",
StageRequestBody: "request_body",
StageResponseHeaders: "response_headers",
StageResponseBody: "response_body",
}
)
type EditorContext interface {
GetEffectiveCommandSet() *CommandSet
SetEffectiveCommandSet(cmdSet *CommandSet)
GetCommandExecutors() []Executor
SetCommandExecutors(executors []Executor)
GetCurrentStage() Stage
SetCurrentStage(stage Stage)
GetRequestPath() string
SetRequestPath(path string)
GetRequestHeader(key string) []string
GetRequestHeaders() map[string][]string
SetRequestHeaders(map[string][]string)
GetRequestQuery(key string) []string
GetRequestQueries() map[string][]string
SetRequestQueries(map[string][]string)
GetResponseHeader(key string) []string
GetResponseHeaders() map[string][]string
SetResponseHeaders(map[string][]string)
GetRefValue(ref *Ref) string
GetRefValues(ref *Ref) []string
SetRefValue(ref *Ref, value string)
SetRefValues(ref *Ref, values []string)
DeleteRefValues(ref *Ref)
IsRequestHeadersDirty() bool
IsResponseHeadersDirty() bool
ResetDirtyFlags()
}
func NewEditorContext() EditorContext {
return &editorContext{}
}
type editorContext struct {
effectiveCommandSet *CommandSet
commandExecutors []Executor
currentStage Stage
requestPath string
requestHeaders map[string][]string
requestQueries map[string][]string
responseHeaders map[string][]string
requestHeadersDirty bool
responseHeadersDirty bool
}
func (ctx *editorContext) GetEffectiveCommandSet() *CommandSet {
return ctx.effectiveCommandSet
}
func (ctx *editorContext) SetEffectiveCommandSet(cmdSet *CommandSet) {
ctx.effectiveCommandSet = cmdSet
}
func (ctx *editorContext) GetCommandExecutors() []Executor {
return ctx.commandExecutors
}
func (ctx *editorContext) SetCommandExecutors(executors []Executor) {
ctx.commandExecutors = executors
}
func (ctx *editorContext) GetCurrentStage() Stage {
return ctx.currentStage
}
func (ctx *editorContext) SetCurrentStage(stage Stage) {
ctx.currentStage = stage
}
func (ctx *editorContext) GetRequestPath() string {
return ctx.requestPath
}
func (ctx *editorContext) SetRequestPath(path string) {
ctx.requestPath = path
ctx.savePathToHeader()
}
func (ctx *editorContext) GetRequestHeader(key string) []string {
if ctx.requestHeaders == nil {
return nil
}
return ctx.requestHeaders[strings.ToLower(key)]
}
func (ctx *editorContext) GetRequestHeaders() map[string][]string {
return maps.Clone(ctx.requestHeaders)
}
func (ctx *editorContext) SetRequestHeaders(headers map[string][]string) {
ctx.requestHeaders = headers
ctx.loadPathFromHeader()
ctx.requestHeadersDirty = true
}
func (ctx *editorContext) GetRequestQuery(key string) []string {
if ctx.requestQueries == nil {
return nil
}
return ctx.requestQueries[key]
}
func (ctx *editorContext) GetRequestQueries() map[string][]string {
return maps.Clone(ctx.requestQueries)
}
func (ctx *editorContext) SetRequestQueries(queries map[string][]string) {
ctx.requestQueries = queries
ctx.savePathToHeader()
}
func (ctx *editorContext) GetResponseHeader(key string) []string {
if ctx.responseHeaders == nil {
return nil
}
return ctx.responseHeaders[strings.ToLower(key)]
}
func (ctx *editorContext) GetResponseHeaders() map[string][]string {
return maps.Clone(ctx.responseHeaders)
}
func (ctx *editorContext) SetResponseHeaders(headers map[string][]string) {
ctx.responseHeaders = headers
ctx.responseHeadersDirty = true
}
func (ctx *editorContext) GetRefValue(ref *Ref) string {
values := ctx.GetRefValues(ref)
if len(values) == 0 {
return ""
}
return values[0]
}
func (ctx *editorContext) GetRefValues(ref *Ref) []string {
if ref == nil {
return nil
}
switch ref.Type {
case RefTypeRequestHeader:
return ctx.GetRequestHeader(strings.ToLower(ref.Name))
case RefTypeRequestQuery:
return ctx.GetRequestQuery(ref.Name)
case RefTypeResponseHeader:
return ctx.GetResponseHeader(strings.ToLower(ref.Name))
default:
return nil
}
}
func (ctx *editorContext) SetRefValue(ref *Ref, value string) {
if ref == nil {
return
}
ctx.SetRefValues(ref, []string{value})
}
func (ctx *editorContext) SetRefValues(ref *Ref, values []string) {
if ref == nil {
return
}
switch ref.Type {
case RefTypeRequestHeader:
if ctx.requestHeaders == nil {
ctx.requestHeaders = make(map[string][]string)
}
loweredRefName := strings.ToLower(ref.Name)
ctx.requestHeaders[loweredRefName] = values
ctx.requestHeadersDirty = true
if loweredRefName == pathHeader {
ctx.loadPathFromHeader()
}
break
case RefTypeRequestQuery:
if ctx.requestQueries == nil {
ctx.requestQueries = make(map[string][]string)
}
ctx.requestQueries[ref.Name] = values
ctx.savePathToHeader()
break
case RefTypeResponseHeader:
if ctx.responseHeaders == nil {
ctx.responseHeaders = make(map[string][]string)
}
ctx.responseHeaders[strings.ToLower(ref.Name)] = values
ctx.responseHeadersDirty = true
break
}
}
func (ctx *editorContext) DeleteRefValues(ref *Ref) {
if ref == nil {
return
}
switch ref.Type {
case RefTypeRequestHeader:
delete(ctx.requestHeaders, strings.ToLower(ref.Name))
ctx.requestHeadersDirty = true
break
case RefTypeRequestQuery:
delete(ctx.requestQueries, ref.Name)
ctx.savePathToHeader()
break
case RefTypeResponseHeader:
delete(ctx.responseHeaders, strings.ToLower(ref.Name))
ctx.responseHeadersDirty = true
break
}
}
func (ctx *editorContext) IsRequestHeadersDirty() bool {
return ctx.requestHeadersDirty
}
func (ctx *editorContext) IsResponseHeadersDirty() bool {
return ctx.responseHeadersDirty
}
func (ctx *editorContext) ResetDirtyFlags() {
ctx.requestHeadersDirty = false
ctx.responseHeadersDirty = false
}
func (ctx *editorContext) savePathToHeader() {
u, err := url.Parse(ctx.requestPath)
if err != nil {
log.Errorf("failed to build the new path with query strings: %v", err)
return
}
query := url.Values{}
for k, vs := range ctx.requestQueries {
for _, v := range vs {
query.Add(k, v)
}
}
u.RawQuery = query.Encode()
ctx.SetRefValue(&Ref{Type: RefTypeRequestHeader, Name: pathHeader}, u.String())
}
func (ctx *editorContext) loadPathFromHeader() {
paths := ctx.GetRequestHeader(pathHeader)
if len(paths) == 0 || paths[0] == "" {
log.Warn("the request has an empty path")
ctx.requestPath = ""
ctx.requestQueries = make(map[string][]string)
return
}
path := paths[0]
queries := make(map[string][]string)
u, err := url.Parse(path)
if err != nil {
log.Warnf("unable to parse the request path: %s", path)
ctx.requestPath = ""
ctx.requestQueries = make(map[string][]string)
return
}
ctx.requestPath = u.Path
for k, vs := range u.Query() {
queries[k] = vs
}
ctx.requestQueries = queries
}

View File

@@ -0,0 +1,218 @@
package pkg
import (
"reflect"
"testing"
)
func newTestRef(t, name string) *Ref {
return &Ref{Type: t, Name: name}
}
func TestEditorContext_CommandSetAndExecutors(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
cmdSet := &CommandSet{}
ctx.SetEffectiveCommandSet(cmdSet)
if ctx.GetEffectiveCommandSet() != cmdSet {
t.Errorf("EffectiveCommandSet not set/get correctly")
}
executors := []Executor{nil, nil}
ctx.SetCommandExecutors(executors)
if !reflect.DeepEqual(ctx.GetCommandExecutors(), executors) {
t.Errorf("CommandExecutors not set/get correctly")
}
}
func TestEditorContext_Stage(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
ctx.SetCurrentStage(StageRequestHeaders)
if ctx.GetCurrentStage() != StageRequestHeaders {
t.Errorf("CurrentStage not set/get correctly")
}
}
func TestEditorContext_RequestPath(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
ctx.SetRequestPath("/foo/bar")
if ctx.GetRequestPath() != "/foo/bar" {
t.Errorf("RequestPath not set/get correctly")
}
}
func TestEditorContext_RequestHeaders(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
ctx.SetRequestHeaders(headers)
if !reflect.DeepEqual(ctx.GetRequestHeaders(), headers) {
t.Errorf("RequestHeaders not set/get correctly")
}
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty not set correctly")
}
if got := ctx.GetRequestHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
t.Errorf("GetRequestHeader failed")
}
}
func TestEditorContext_RequestQueries(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
queries := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
ctx.SetRequestQueries(queries)
if !reflect.DeepEqual(ctx.GetRequestQueries(), queries) {
t.Errorf("RequestQueries not set/get correctly")
}
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty not set correctly")
}
if got := ctx.GetRequestQuery("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
t.Errorf("GetRequestQuery failed")
}
}
func TestEditorContext_ResponseHeaders(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
ctx.SetResponseHeaders(headers)
if !reflect.DeepEqual(ctx.GetResponseHeaders(), headers) {
t.Errorf("ResponseHeaders not set/get correctly")
}
if !ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty not set correctly")
}
if got := ctx.GetResponseHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
t.Errorf("GetResponseHeader failed")
}
}
func TestEditorContext_RefValueAndValues(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
rh := newTestRef(RefTypeRequestHeader, "foo")
rq := newTestRef(RefTypeRequestQuery, "bar")
rh2 := newTestRef(RefTypeResponseHeader, "baz")
ctx.SetRefValue(rh, "v1")
ctx.SetRefValues(rq, []string{"v2", "v3"})
ctx.SetRefValues(rh2, []string{"v4"})
if v := ctx.GetRefValue(rh); v != "v1" {
t.Errorf("GetRefValue(RequestHeader) failed: %v", v)
}
if v := ctx.GetRefValues(rq); !reflect.DeepEqual(v, []string{"v2", "v3"}) {
t.Errorf("GetRefValues(RequestQuery) failed: %v", v)
}
if v := ctx.GetRefValues(rh2); !reflect.DeepEqual(v, []string{"v4"}) {
t.Errorf("GetRefValues(ResponseHeader) failed: %v", v)
}
}
func TestEditorContext_DeleteRefValues(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
rh := newTestRef(RefTypeRequestHeader, "foo")
rq := newTestRef(RefTypeRequestQuery, "bar")
rh2 := newTestRef(RefTypeResponseHeader, "baz")
ctx.SetRefValue(rh, "v1")
ctx.SetRefValues(rq, []string{"v2", "v3"})
ctx.SetRefValues(rh2, []string{"v4"})
ctx.DeleteRefValues(rh)
ctx.DeleteRefValues(rq)
ctx.DeleteRefValues(rh2)
if v := ctx.GetRefValues(rh); len(v) != 0 {
t.Errorf("DeleteRefValues(RequestHeader) failed: %v", v)
}
if v := ctx.GetRefValues(rq); len(v) != 0 {
t.Errorf("DeleteRefValues(RequestQuery) failed: %v", v)
}
if v := ctx.GetRefValues(rh2); len(v) != 0 {
t.Errorf("DeleteRefValues(ResponseHeader) failed: %v", v)
}
}
func TestEditorContext_ResetDirtyFlags(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
ctx.ResetDirtyFlags()
if ctx.IsRequestHeadersDirty() || ctx.IsRequestHeadersDirty() || ctx.IsResponseHeadersDirty() {
t.Errorf("ResetDirtyFlags failed")
}
}
func TestEditorContext_IsRequestHeadersDirty_SetHeaders(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
if ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty should be false initially")
}
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty should be true after SetRequestHeaders")
}
ctx.ResetDirtyFlags()
if ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty should be false after ResetDirtyFlags")
}
ref := newTestRef(RefTypeRequestHeader, "foo")
ctx.SetRefValue(ref, "baz")
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty should be true after SetRefValue")
}
ctx.ResetDirtyFlags()
ctx.DeleteRefValues(ref)
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestHeadersDirty should be true after DeleteRefValues")
}
}
func TestEditorContext_IsRequestHeadersDirty_SetQueries(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
if ctx.IsRequestHeadersDirty() {
t.Errorf("RequestQueriesDirty should be false initially")
}
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestQueriesDirty should be true after SetRequestQueries")
}
ctx.ResetDirtyFlags()
if ctx.IsRequestHeadersDirty() {
t.Errorf("RequestQueriesDirty should be false after ResetDirtyFlags")
}
ref := newTestRef(RefTypeRequestQuery, "foo")
ctx.SetRefValues(ref, []string{"baz"})
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestQueriesDirty should be true after SetRefValues")
}
ctx.ResetDirtyFlags()
ctx.DeleteRefValues(ref)
if !ctx.IsRequestHeadersDirty() {
t.Errorf("RequestQueriesDirty should be true after DeleteRefValues")
}
}
func TestEditorContext_IsResponseHeadersDirty(t *testing.T) {
ctx := NewEditorContext().(*editorContext)
if ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty should be false initially")
}
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
if !ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty should be true after SetResponseHeaders")
}
ctx.ResetDirtyFlags()
if ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty should be false after ResetDirtyFlags")
}
ref := newTestRef(RefTypeResponseHeader, "foo")
ctx.SetRefValues(ref, []string{"baz"})
if !ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty should be true after SetRefValues")
}
ctx.ResetDirtyFlags()
ctx.DeleteRefValues(ref)
if !ctx.IsResponseHeadersDirty() {
t.Errorf("ResponseHeadersDirty should be true after DeleteRefValues")
}
}

View File

@@ -0,0 +1,26 @@
package pkg
import (
"github.com/higress-group/wasm-go/pkg/log"
)
func init() {
// Initialize mock logger for testing
log.SetPluginLog(&mockLogger{})
}
type mockLogger struct{}
func (m *mockLogger) Trace(msg string) {}
func (m *mockLogger) Tracef(format string, args ...interface{}) {}
func (m *mockLogger) Debug(msg string) {}
func (m *mockLogger) Debugf(format string, args ...interface{}) {}
func (m *mockLogger) Info(msg string) {}
func (m *mockLogger) Infof(format string, args ...interface{}) {}
func (m *mockLogger) Warn(msg string) {}
func (m *mockLogger) Warnf(format string, args ...interface{}) {}
func (m *mockLogger) Error(msg string) {}
func (m *mockLogger) Errorf(format string, args ...interface{}) {}
func (m *mockLogger) Critical(msg string) {}
func (m *mockLogger) Criticalf(format string, args ...interface{}) {}
func (m *mockLogger) ResetID(pluginID string) {}

View File

@@ -0,0 +1,64 @@
package pkg
import (
"errors"
"fmt"
"github.com/tidwall/gjson"
)
const (
RefTypeRequestHeader = "request_header"
RefTypeRequestQuery = "request_query"
RefTypeResponseHeader = "response_header"
)
var (
refType2Stage = map[string]Stage{
RefTypeRequestHeader: StageRequestHeaders,
RefTypeRequestQuery: StageRequestHeaders,
RefTypeResponseHeader: StageResponseHeaders,
}
)
type Ref struct {
Type string `json:"type"`
Name string `json:"name,omitempty"`
stage Stage
}
func NewRef(json gjson.Result) (*Ref, error) {
ref := &Ref{}
if t := json.Get("type").String(); t != "" {
ref.Type = t
} else {
return nil, errors.New("missing type field")
}
if _, ok := refType2Stage[ref.Type]; !ok {
return nil, fmt.Errorf("unknown ref type: %s", ref.Type)
}
if name := json.Get("name").String(); name != "" {
ref.Name = name
} else {
return nil, errors.New("missing name field")
}
return ref, nil
}
func (r *Ref) GetStage() Stage {
if r.stage == 0 {
if stage, ok := refType2Stage[r.Type]; ok {
r.stage = stage
}
}
return r.stage
}
func (r *Ref) String() string {
return fmt.Sprintf("%s/%s", r.Type, r.Name)
}