mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 23:20:52 +08:00
feat: add api-workflow plugin (#1229)
This commit is contained in:
116
plugins/wasm-go/extensions/api-workflow/utils/conditional.go
Normal file
116
plugins/wasm-go/extensions/api-workflow/utils/conditional.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 原子表达式描述:
|
||||
// eq arg1 arg2: arg1 == arg2时为true
|
||||
// ne arg1 arg2: arg1 != arg2时为true
|
||||
// lt arg1 arg2: arg1 < arg2时为true
|
||||
// le arg1 arg2: arg1 <= arg2时为true
|
||||
// gt arg1 arg2: arg1 > arg2时为true
|
||||
// ge arg1 arg2: arg1 >= arg2时为true
|
||||
// and arg1 arg2: arg1 && arg2
|
||||
// or arg1 arg2: arg1 || arg2
|
||||
// contain arg1 arg2: arg1 包含 arg2时为true
|
||||
var operators = map[string]interface{}{
|
||||
"eq": func(a, b interface{}) bool {
|
||||
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
|
||||
},
|
||||
"ge": func(a, b float64) bool { return a >= b },
|
||||
"le": func(a, b float64) bool { return a <= b },
|
||||
"gt": func(a, b float64) bool { return a > b },
|
||||
"lt": func(a, b float64) bool { return a < b },
|
||||
"and": func(a, b bool) bool { return a && b },
|
||||
"or": func(a, b bool) bool { return a || b },
|
||||
"contain": func(a, b string) bool { return strings.Contains(a, b) },
|
||||
}
|
||||
|
||||
// 执行判断条件
|
||||
func ExecConditionalStr(conditionalStr string) (bool, error) {
|
||||
// 正则表达式匹配括号内的表达式
|
||||
re := regexp.MustCompile(`\(([^()]*)\)`)
|
||||
matches := re.FindAllStringSubmatch(conditionalStr, -1)
|
||||
// 找到最里面的(原子表达式)
|
||||
for _, match := range matches {
|
||||
subCondition := match[1]
|
||||
result, err := ExecConditionalStr(subCondition)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 用结果替换原子表达式
|
||||
conditionalStr = strings.ReplaceAll(conditionalStr, match[0], fmt.Sprintf("%t", result))
|
||||
}
|
||||
|
||||
fields := strings.Fields(conditionalStr)
|
||||
// 执行原子表达式
|
||||
if len(fields) == 3 {
|
||||
compareFunc := operators[fields[0]]
|
||||
switch fc := compareFunc.(type) {
|
||||
default:
|
||||
return false, fmt.Errorf("invalid conditional func %v", compareFunc)
|
||||
case func(a, b float64) bool:
|
||||
a, err := strconv.ParseFloat(fields[1], 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid conditional str %s", conditionalStr)
|
||||
}
|
||||
b, err := strconv.ParseFloat(fields[2], 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid conditional str %s", conditionalStr)
|
||||
}
|
||||
return fc(a, b), nil
|
||||
case func(a, b bool) bool:
|
||||
a, err := strconv.ParseBool(fields[1])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid conditional str %s", conditionalStr)
|
||||
}
|
||||
b, err := strconv.ParseBool(fields[2])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid conditional str %s", conditionalStr)
|
||||
}
|
||||
return fc(a, b), nil
|
||||
case func(a, b string) bool:
|
||||
a := fields[1]
|
||||
b := fields[2]
|
||||
return fc(a, b), nil
|
||||
case func(a, b interface{}) bool:
|
||||
a := fields[1]
|
||||
b := fields[2]
|
||||
return fc(a, b), nil
|
||||
}
|
||||
// 继续获取上一层的(原子表达式)
|
||||
} else if strings.Contains(conditionalStr, "(") || strings.Contains(conditionalStr, ")") {
|
||||
return ExecConditionalStr(conditionalStr)
|
||||
// 原子表达式有问题,返回
|
||||
} else {
|
||||
return false, fmt.Errorf("invalid conditional str %s", conditionalStr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 通过正则表达式寻找模板中的 {{foo}} 字符串foo
|
||||
// 返回 {{foo}} : foo
|
||||
func ParseTmplStr(tmpl string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
re := regexp.MustCompile(`\{\{(.*?)\}\}`)
|
||||
matches := re.FindAllStringSubmatch(tmpl, -1)
|
||||
for _, match := range matches {
|
||||
result[match[0]] = match[1]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 使用kv替换模板中的字符
|
||||
// 例如 模板是`hello,{{foo}}` 使用{"{{foo}}":"bot"} 替换后为`hello,bot`
|
||||
func ReplacedStr(tmpl string, kvs map[string]string) string {
|
||||
|
||||
for k, v := range kvs {
|
||||
tmpl = strings.Replace(tmpl, k, v, -1)
|
||||
}
|
||||
|
||||
return tmpl
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecConditionalStr(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"eq int true", "eq 1 1", true, false},
|
||||
{"eq int false", "eq 1 2", false, false},
|
||||
{"eq str true", "eq foo foo", true, false},
|
||||
{"eq str false", "eq foo boo", false, false},
|
||||
{"eq float true", "eq 0.99 0.99", true, false},
|
||||
{"eq float false", "eq 1.1 2.2", false, false},
|
||||
{"eq float int false", "eq 1.0 1", false, false},
|
||||
{"eq float str false", "eq 1.0 foo", false, false},
|
||||
{"lt true", "lt 1.1 2", true, false},
|
||||
{"lt false", "lt 2 1", false, false},
|
||||
{"le true", "le 1 2", true, false},
|
||||
{"le false", "le 2 1", false, false},
|
||||
{"gt true", "gt 2 1", true, false},
|
||||
{"gt false", "gt 1 2", false, false},
|
||||
{"ge true", "ge 2 1", true, false},
|
||||
{"ge false", "ge 1 2", false, false},
|
||||
{"and true", "and true true", true, false},
|
||||
{"and false", "and true false", false, false},
|
||||
{"or true", "or true false", true, false},
|
||||
{"or false", "or false false", false, false},
|
||||
{"contain true", "contain helloworld world", true, false},
|
||||
{"contain false", "contain helloworld moon", false, false},
|
||||
{"invalid input", "invalid", false, true},
|
||||
{"nested expression 1", "and (eq 1 1) (lt 2 3)", true, false},
|
||||
{"nested expression 2", "or (eq 1 2) (and (eq 1 1) (gt 2 3))", false, false},
|
||||
{"nested expression error", "or (eq 1 2) (and (eq 1 1) (gt 2 3)))", false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ExecConditionalStr(tt.args)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ExecConditionalStr() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ExecConditionalStr() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTmplStr(t *testing.T) {
|
||||
type args struct {
|
||||
tmpl string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
want map[string]string
|
||||
}{
|
||||
{"normal", "{{foo}}", map[string]string{"{{foo}}": "foo"}},
|
||||
{"single", "{foo}", map[string]string{}},
|
||||
{"empty", "foo", map[string]string{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ParseTmplStr(tt.args)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseTmplStr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplacedStr(t *testing.T) {
|
||||
type args struct {
|
||||
tmpl string
|
||||
kvs map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"normal", args{tmpl: "hello,{{foo}}", kvs: map[string]string{"{{foo}}": "bot"}}, "hello,bot"},
|
||||
{"empty", args{tmpl: "hello,foo", kvs: map[string]string{"{{foo}}": "bot"}}, "hello,foo"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ReplacedStr(tt.args.tmpl, tt.args.kvs); got != tt.want {
|
||||
t.Errorf("ReplacedStr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
45
plugins/wasm-go/extensions/api-workflow/utils/http.go
Normal file
45
plugins/wasm-go/extensions/api-workflow/utils/http.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package utils
|
||||
|
||||
import "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
const (
|
||||
HeaderContentType = "Content-Type"
|
||||
|
||||
MimeTypeTextPlain = "text/plain"
|
||||
MimeTypeApplicationJson = "application/json"
|
||||
)
|
||||
|
||||
func SendResponse(statusCode uint32, statusCodeDetails string, contentType, body string) error {
|
||||
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetails, CreateHeaders(HeaderContentType, contentType), []byte(body), -1)
|
||||
}
|
||||
|
||||
func CreateHeaders(kvs ...string) [][2]string {
|
||||
headers := make([][2]string, 0, len(kvs)/2)
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
headers = append(headers, [2]string{kvs[i], kvs[i+1]})
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func OverwriteRequestHost(host string) error {
|
||||
if originHost, err := proxywasm.GetHttpRequestHeader(":authority"); err == nil {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("X-ENVOY-ORIGINAL-HOST", originHost)
|
||||
}
|
||||
return proxywasm.ReplaceHttpRequestHeader(":authority", host)
|
||||
}
|
||||
|
||||
func OverwriteRequestPath(path string) error {
|
||||
if originPath, err := proxywasm.GetHttpRequestHeader(":path"); err == nil {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("X-ENVOY-ORIGINAL-PATH", originPath)
|
||||
}
|
||||
return proxywasm.ReplaceHttpRequestHeader(":path", path)
|
||||
}
|
||||
|
||||
func OverwriteRequestAuthorization(credential string) error {
|
||||
if exist, _ := proxywasm.GetHttpRequestHeader("X-HI-ORIGINAL-AUTH"); exist == "" {
|
||||
if originAuth, err := proxywasm.GetHttpRequestHeader("Authorization"); err == nil {
|
||||
_ = proxywasm.AddHttpRequestHeader("X-HI-ORIGINAL-AUTH", originAuth)
|
||||
}
|
||||
}
|
||||
return proxywasm.ReplaceHttpRequestHeader("Authorization", credential)
|
||||
}
|
||||
7
plugins/wasm-go/extensions/api-workflow/utils/tools.go
Normal file
7
plugins/wasm-go/extensions/api-workflow/utils/tools.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func TrimQuote(source string) string {
|
||||
return strings.Trim(source, `"`)
|
||||
}
|
||||
Reference in New Issue
Block a user