mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 05:29:51 +00:00
Compare commits
2 Commits
b546cf3ad0
...
faad7cb6d7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
faad7cb6d7 | ||
![]() |
97d692910b |
522
internal/domain/expr.go
Normal file
522
internal/domain/expr.go
Normal file
@ -0,0 +1,522 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Value any
|
||||
|
||||
type (
|
||||
ComparisonOperator string
|
||||
LogicalOperator string
|
||||
)
|
||||
|
||||
const (
|
||||
GreaterThan ComparisonOperator = ">"
|
||||
LessThan ComparisonOperator = "<"
|
||||
GreaterOrEqual ComparisonOperator = ">="
|
||||
LessOrEqual ComparisonOperator = "<="
|
||||
Equal ComparisonOperator = "=="
|
||||
NotEqual ComparisonOperator = "!="
|
||||
Is ComparisonOperator = "is"
|
||||
|
||||
And LogicalOperator = "and"
|
||||
Or LogicalOperator = "or"
|
||||
Not LogicalOperator = "not"
|
||||
)
|
||||
|
||||
type EvalResult struct {
|
||||
Type string
|
||||
Value any
|
||||
}
|
||||
|
||||
func (e *EvalResult) GetFloat64() (float64, error) {
|
||||
if e.Type != "number" {
|
||||
return 0, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
switch v := e.Value.(type) {
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case float64:
|
||||
return v, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported type: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left > right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) > other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left >= right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) >= other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left < right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) < other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left <= right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) <= other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left == right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) == other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "number":
|
||||
left, err := e.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetFloat64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: left != right,
|
||||
}, nil
|
||||
case "string":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(string) != other.Value.(string),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "boolean":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(bool) && other.Value.(bool),
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "boolean":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(bool) || other.Value.(bool),
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvalResult) Not() (*EvalResult, error) {
|
||||
if e.Type != "boolean" {
|
||||
return nil, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: !e.Value.(bool),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) {
|
||||
if e.Type != other.Type {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
|
||||
}
|
||||
switch e.Type {
|
||||
case "boolean":
|
||||
return &EvalResult{
|
||||
Type: "boolean",
|
||||
Value: e.Value.(bool) == other.Value.(bool),
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type Expr interface {
|
||||
GetType() string
|
||||
Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||
}
|
||||
|
||||
type ConstExpr struct {
|
||||
Type string `json:"type"`
|
||||
Value Value `json:"value"`
|
||||
ValueType string `json:"valueType"`
|
||||
}
|
||||
|
||||
func (c ConstExpr) GetType() string { return c.Type }
|
||||
|
||||
func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
return &EvalResult{
|
||||
Type: c.ValueType,
|
||||
Value: c.Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type VarExpr struct {
|
||||
Type string `json:"type"`
|
||||
Selector WorkflowNodeIOValueSelector `json:"selector"`
|
||||
}
|
||||
|
||||
func (v VarExpr) GetType() string { return v.Type }
|
||||
|
||||
func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
if v.Selector.Id == "" {
|
||||
return nil, fmt.Errorf("node id is empty")
|
||||
}
|
||||
if v.Selector.Name == "" {
|
||||
return nil, fmt.Errorf("name is empty")
|
||||
}
|
||||
|
||||
if _, ok := variables[v.Selector.Id]; !ok {
|
||||
return nil, fmt.Errorf("node %s not found", v.Selector.Id)
|
||||
}
|
||||
|
||||
if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {
|
||||
return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id)
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: v.Selector.Type,
|
||||
Value: variables[v.Selector.Id][v.Selector.Name],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CompareExpr struct {
|
||||
Type string `json:"type"` // compare
|
||||
Op ComparisonOperator `json:"op"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (c CompareExpr) GetType() string { return c.Type }
|
||||
|
||||
func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := c.Left.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := c.Right.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
case GreaterThan:
|
||||
return left.GreaterThan(right)
|
||||
case LessThan:
|
||||
return left.LessThan(right)
|
||||
case GreaterOrEqual:
|
||||
return left.GreaterOrEqual(right)
|
||||
case LessOrEqual:
|
||||
return left.LessOrEqual(right)
|
||||
case Equal:
|
||||
return left.Equal(right)
|
||||
case NotEqual:
|
||||
return left.NotEqual(right)
|
||||
case Is:
|
||||
return left.Is(right)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", c.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type LogicalExpr struct {
|
||||
Type string `json:"type"` // logical
|
||||
Op LogicalOperator `json:"op"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (l LogicalExpr) GetType() string { return l.Type }
|
||||
|
||||
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := l.Left.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := l.Right.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch l.Op {
|
||||
case And:
|
||||
return left.And(right)
|
||||
case Or:
|
||||
return left.Or(right)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator: %s", l.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type NotExpr struct {
|
||||
Type string `json:"type"` // not
|
||||
Expr Expr `json:"expr"`
|
||||
}
|
||||
|
||||
func (n NotExpr) GetType() string { return n.Type }
|
||||
|
||||
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
inner, err := n.Expr.Eval(variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inner.Not()
|
||||
}
|
||||
|
||||
type rawExpr struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func MarshalExpr(e Expr) ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func UnmarshalExpr(data []byte) (Expr, error) {
|
||||
var typ rawExpr
|
||||
if err := json.Unmarshal(data, &typ); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch typ.Type {
|
||||
case "const":
|
||||
var e ConstExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case "var":
|
||||
var e VarExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case "compare":
|
||||
var e CompareExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToCompareExpr()
|
||||
case "logical":
|
||||
var e LogicalExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToLogicalExpr()
|
||||
case "not":
|
||||
var e NotExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToNotExpr()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown expr type: %s", typ.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type CompareExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Op ComparisonOperator `json:"op"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
}
|
||||
|
||||
func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) {
|
||||
leftExpr, err := UnmarshalExpr(r.Left)
|
||||
if err != nil {
|
||||
return CompareExpr{}, err
|
||||
}
|
||||
rightExpr, err := UnmarshalExpr(r.Right)
|
||||
if err != nil {
|
||||
return CompareExpr{}, err
|
||||
}
|
||||
return CompareExpr{
|
||||
Type: r.Type,
|
||||
Op: r.Op,
|
||||
Left: leftExpr,
|
||||
Right: rightExpr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LogicalExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Op LogicalOperator `json:"op"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
}
|
||||
|
||||
func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
|
||||
left, err := UnmarshalExpr(r.Left)
|
||||
if err != nil {
|
||||
return LogicalExpr{}, err
|
||||
}
|
||||
right, err := UnmarshalExpr(r.Right)
|
||||
if err != nil {
|
||||
return LogicalExpr{}, err
|
||||
}
|
||||
return LogicalExpr{
|
||||
Type: r.Type,
|
||||
Op: r.Op,
|
||||
Left: left,
|
||||
Right: right,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type NotExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Expr json.RawMessage `json:"expr"`
|
||||
}
|
||||
|
||||
func (r NotExprRaw) ToNotExpr() (NotExpr, error) {
|
||||
inner, err := UnmarshalExpr(r.Expr)
|
||||
if err != nil {
|
||||
return NotExpr{}, err
|
||||
}
|
||||
return NotExpr{
|
||||
Type: r.Type,
|
||||
Expr: inner,
|
||||
}, nil
|
||||
}
|
127
internal/domain/expr_test.go
Normal file
127
internal/domain/expr_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogicalEval(t *testing.T) {
|
||||
// 测试逻辑表达式 and
|
||||
logicalExpr := LogicalExpr{
|
||||
Left: ConstExpr{
|
||||
Type: "const",
|
||||
Value: true,
|
||||
ValueType: "boolean",
|
||||
},
|
||||
Op: And,
|
||||
Right: ConstExpr{
|
||||
Type: "const",
|
||||
Value: true,
|
||||
ValueType: "boolean",
|
||||
},
|
||||
}
|
||||
result, err := logicalExpr.Eval(nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||
}
|
||||
if result.Value != true {
|
||||
t.Errorf("expected true, got %v", result)
|
||||
}
|
||||
|
||||
// 测试逻辑表达式 or
|
||||
orExpr := LogicalExpr{
|
||||
Left: ConstExpr{
|
||||
Type: "const",
|
||||
Value: true,
|
||||
ValueType: "boolean",
|
||||
},
|
||||
Op: Or,
|
||||
Right: ConstExpr{
|
||||
Type: "const",
|
||||
Value: true,
|
||||
ValueType: "boolean",
|
||||
},
|
||||
}
|
||||
result, err = orExpr.Eval(nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to evaluate logical expression: %v", err)
|
||||
}
|
||||
if result.Value != true {
|
||||
t.Errorf("expected true, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalExpr(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Expr
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := UnmarshalExpr(tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpr_Eval(t *testing.T) {
|
||||
type args struct {
|
||||
variables map[string]map[string]any
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *EvalResult
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
variables: map[string]map[string]any{
|
||||
"ODnYSOXB6HQP2_vz6JcZE": {
|
||||
"certificate.validated": true,
|
||||
"certificate.daysLeft": 2,
|
||||
},
|
||||
},
|
||||
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c, err := UnmarshalExpr(tt.args.data)
|
||||
if err != nil {
|
||||
t.Errorf("UnmarshalExpr() error = %v", err)
|
||||
return
|
||||
}
|
||||
got, err := c.Eval(tt.args.variables)
|
||||
t.Log("got:", got)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got.Value != true {
|
||||
t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
|
||||
@ -81,6 +82,10 @@ type WorkflowNodeConfigForApply struct {
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForCondition struct {
|
||||
Expression Expr `json:"expression"` // 条件表达式
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
@ -104,6 +109,24 @@ type WorkflowNodeConfigForNotify struct {
|
||||
Message string `json:"message"` // 通知内容
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
|
||||
expression := n.Config["expression"]
|
||||
if expression == nil {
|
||||
return WorkflowNodeConfigForCondition{}
|
||||
}
|
||||
|
||||
raw, _ := json.Marshal(expression)
|
||||
|
||||
expr, err := UnmarshalExpr([]byte(raw))
|
||||
if err != nil {
|
||||
return WorkflowNodeConfigForCondition{}
|
||||
}
|
||||
|
||||
return WorkflowNodeConfigForCondition{
|
||||
Expression: expr,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||
skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays")
|
||||
if skipBeforeExpiryDays == 0 {
|
||||
@ -171,6 +194,7 @@ type WorkflowNodeIO struct {
|
||||
type WorkflowNodeIOValueSelector struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
const WorkflowNodeIONameCertificate string = "certificate"
|
||||
|
@ -98,16 +98,26 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
|
||||
|
||||
procErr = processor.Process(ctx)
|
||||
if procErr != nil {
|
||||
if current.Type != domain.WorkflowNodeTypeCondition {
|
||||
processor.GetLogger().Error(procErr.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
nodeOutputs := processor.GetOutputs()
|
||||
if len(nodeOutputs) > 0 {
|
||||
ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: 优化可读性
|
||||
if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
|
||||
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
|
||||
current = nil
|
||||
procErr = nil
|
||||
return nil
|
||||
} else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
|
||||
return procErr
|
||||
} else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch {
|
||||
current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure)
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
type applyNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
|
||||
certRepo certificateRepository
|
||||
outputRepo workflowOutputRepository
|
||||
@ -25,6 +26,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
||||
return &applyNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
|
||||
certRepo: repository.NewCertificateRepository(),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
@ -71,6 +73,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
n.logger.Warn("failed to parse certificate, may be the CA responded error")
|
||||
return err
|
||||
}
|
||||
|
||||
certificate := &domain.Certificate{
|
||||
Source: domain.CertificateSourceTypeWorkflow,
|
||||
Certificate: applyResult.CertificateFullChain,
|
||||
@ -96,6 +99,10 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加中间结果
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
|
||||
|
||||
n.logger.Info("apply completed")
|
||||
|
||||
return nil
|
||||
@ -139,6 +146,10 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
||||
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||
if expirationTime > renewalInterval {
|
||||
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24)
|
||||
|
||||
return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
@ -9,16 +10,42 @@ import (
|
||||
type conditionNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
}
|
||||
|
||||
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
|
||||
return &conditionNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *conditionNode) Process(ctx context.Context) error {
|
||||
// 此类型节点不需要执行任何操作,直接返回
|
||||
n.logger.Info("enter condition node: " + n.node.Name)
|
||||
|
||||
nodeConfig := n.node.GetConfigForCondition()
|
||||
if nodeConfig.Expression == nil {
|
||||
n.logger.Info("no condition found, continue to next node")
|
||||
return nil
|
||||
}
|
||||
|
||||
rs, err := n.eval(ctx, nodeConfig.Expression)
|
||||
if err != nil {
|
||||
n.logger.Warn("failed to eval expression: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if rs.Value == false {
|
||||
n.logger.Info("condition not met, skip this branch")
|
||||
return errors.New("condition not met")
|
||||
}
|
||||
|
||||
n.logger.Info("condition met, continue to next node")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (*domain.EvalResult, error) {
|
||||
variables := GetNodeOutputs(ctx)
|
||||
return expression.Eval(variables)
|
||||
}
|
||||
|
126
internal/workflow/node-processor/context.go
Normal file
126
internal/workflow/node-processor/context.go
Normal file
@ -0,0 +1,126 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 定义上下文键类型,避免键冲突
|
||||
type workflowContextKey string
|
||||
|
||||
const (
|
||||
nodeOutputsKey workflowContextKey = "node_outputs"
|
||||
)
|
||||
|
||||
// 带互斥锁的节点输出容器
|
||||
type nodeOutputsContainer struct {
|
||||
sync.RWMutex
|
||||
outputs map[string]map[string]any
|
||||
}
|
||||
|
||||
// 创建新的并发安全的节点输出容器
|
||||
func newNodeOutputsContainer() *nodeOutputsContainer {
|
||||
return &nodeOutputsContainer{
|
||||
outputs: make(map[string]map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// 添加节点输出到上下文
|
||||
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
container = newNodeOutputsContainer()
|
||||
}
|
||||
|
||||
container.Lock()
|
||||
defer container.Unlock()
|
||||
|
||||
// 创建输出的深拷贝以避免后续修改
|
||||
outputCopy := make(map[string]any, len(output))
|
||||
for k, v := range output {
|
||||
outputCopy[k] = v
|
||||
}
|
||||
|
||||
container.outputs[nodeId] = outputCopy
|
||||
return context.WithValue(ctx, nodeOutputsKey, container)
|
||||
}
|
||||
|
||||
// 从上下文获取节点输出
|
||||
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
container.RLock()
|
||||
defer container.RUnlock()
|
||||
|
||||
output, exists := container.outputs[nodeId]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
outputCopy := make(map[string]any, len(output))
|
||||
for k, v := range output {
|
||||
outputCopy[k] = v
|
||||
}
|
||||
|
||||
return outputCopy
|
||||
}
|
||||
|
||||
// 获取特定节点的特定输出项
|
||||
func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) {
|
||||
output := GetNodeOutput(ctx, nodeId)
|
||||
if output == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, exists := output[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// 获取所有节点输出
|
||||
func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
container.RLock()
|
||||
defer container.RUnlock()
|
||||
|
||||
// 创建所有输出的深拷贝
|
||||
allOutputs := make(map[string]map[string]any, len(container.outputs))
|
||||
for nodeId, output := range container.outputs {
|
||||
nodeCopy := make(map[string]any, len(output))
|
||||
for k, v := range output {
|
||||
nodeCopy[k] = v
|
||||
}
|
||||
allOutputs[nodeId] = nodeCopy
|
||||
}
|
||||
|
||||
return allOutputs
|
||||
}
|
||||
|
||||
// 获取节点输出容器
|
||||
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
|
||||
value := ctx.Value(nodeOutputsKey)
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.(*nodeOutputsContainer)
|
||||
}
|
||||
|
||||
// 检查节点是否有输出
|
||||
func HasNodeOutput(ctx context.Context, nodeId string) bool {
|
||||
container := getNodeOutputsContainer(ctx)
|
||||
if container == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
container.RLock()
|
||||
defer container.RUnlock()
|
||||
|
||||
_, exists := container.outputs[nodeId]
|
||||
return exists
|
||||
}
|
@ -15,6 +15,7 @@ import (
|
||||
type deployNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
|
||||
certRepo certificateRepository
|
||||
outputRepo workflowOutputRepository
|
||||
@ -24,6 +25,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
||||
return &deployNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
|
||||
certRepo: repository.NewCertificateRepository(),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
|
@ -9,12 +9,14 @@ import (
|
||||
type executeFailureNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
}
|
||||
|
||||
func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode {
|
||||
return &executeFailureNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,12 +9,14 @@ import (
|
||||
type executeSuccessNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
}
|
||||
|
||||
func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode {
|
||||
return &executeSuccessNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
type notifyNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
|
||||
settingsRepo settingsRepository
|
||||
}
|
||||
@ -20,6 +21,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
|
||||
return ¬ifyNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
|
||||
settingsRepo: repository.NewSettingsRepository(),
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ type NodeProcessor interface {
|
||||
SetLogger(*slog.Logger)
|
||||
|
||||
Process(ctx context.Context) error
|
||||
|
||||
GetOutputs() map[string]any
|
||||
}
|
||||
|
||||
type nodeProcessor struct {
|
||||
@ -32,6 +34,20 @@ func (n *nodeProcessor) SetLogger(logger *slog.Logger) {
|
||||
n.logger = logger
|
||||
}
|
||||
|
||||
type nodeOutputer struct {
|
||||
outputs map[string]any
|
||||
}
|
||||
|
||||
func newNodeOutputer() *nodeOutputer {
|
||||
return &nodeOutputer{
|
||||
outputs: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nodeOutputer) GetOutputs() map[string]any {
|
||||
return n.outputs
|
||||
}
|
||||
|
||||
type certificateRepository interface {
|
||||
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ import (
|
||||
type startNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
}
|
||||
|
||||
func NewStartNode(node *domain.WorkflowNode) *startNode {
|
||||
return &startNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
type uploadNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*nodeProcessor
|
||||
*nodeOutputer
|
||||
|
||||
certRepo certificateRepository
|
||||
outputRepo workflowOutputRepository
|
||||
@ -21,6 +23,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
|
||||
return &uploadNode{
|
||||
node: node,
|
||||
nodeProcessor: newNodeProcessor(node),
|
||||
nodeOutputer: newNodeOutputer(),
|
||||
|
||||
certRepo: repository.NewCertificateRepository(),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
@ -66,6 +69,9 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
|
||||
|
||||
n.logger.Info("upload completed")
|
||||
|
||||
return nil
|
||||
@ -85,6 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl
|
||||
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id)
|
||||
if lastCertificate != nil {
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
|
||||
return true, "the certificate has already been uploaded"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd";
|
||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||
import AddNode from "./AddNode";
|
||||
import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
|
||||
import { Expr, WorkflowNodeIoValueType } from "@/domain/workflow";
|
||||
import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow";
|
||||
import { produce } from "immer";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
@ -30,15 +30,34 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
// 创建单个条件的表达式
|
||||
const createComparisonExpr = (condition: ConditionItem): Expr => {
|
||||
const selectors = condition.leftSelector.split("#");
|
||||
const t = selectors[2] as WorkflowNodeIoValueType;
|
||||
const left: Expr = {
|
||||
type: "var",
|
||||
selector: {
|
||||
id: selectors[0],
|
||||
name: selectors[1],
|
||||
type: selectors[2] as WorkflowNodeIoValueType,
|
||||
type: t,
|
||||
},
|
||||
};
|
||||
const right: Expr = { type: "const", value: condition.rightValue || "" };
|
||||
|
||||
let value: Value = condition.rightValue;
|
||||
switch (t) {
|
||||
case "boolean":
|
||||
if (value === "true") {
|
||||
value = true;
|
||||
} else if (value === "false") {
|
||||
value = false;
|
||||
}
|
||||
break;
|
||||
case "number":
|
||||
value = parseInt(value as string);
|
||||
break;
|
||||
case "string":
|
||||
value = value as string;
|
||||
break;
|
||||
}
|
||||
|
||||
const right: Expr = { type: "const", value: value, valueType: t };
|
||||
|
||||
return {
|
||||
type: "compare",
|
||||
|
@ -318,6 +318,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
|
||||
const right: Expr = {
|
||||
type: "const",
|
||||
value: rightValue,
|
||||
valueType: type,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -232,13 +232,13 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => {
|
||||
|
||||
// #region Condition expression
|
||||
|
||||
type Value = string | number | boolean;
|
||||
export type Value = string | number | boolean;
|
||||
|
||||
export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is";
|
||||
|
||||
export type LogicalOperator = "and" | "or" | "not";
|
||||
|
||||
export type ConstExpr = { type: "const"; value: Value };
|
||||
export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType };
|
||||
export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector };
|
||||
export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr };
|
||||
export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr };
|
||||
|
Loading…
x
Reference in New Issue
Block a user