feat: Condition/ConditionExpr 支持 JSON 序列化/反序列化
给 Condition/ConditionExpr 加 MarshalJSON/UnmarshalJSON,中间过一层 DTO (conditionDTO/exprDTO)避免 UnmarshalJSON 递归。FromMap(m map[string]any) 方便从已经解析好的 map(比如请求参数)直接构造 Condition,不用先序列化 成 JSON 字符串再解析一遍。
This commit is contained in:
@@ -0,0 +1,161 @@
|
|||||||
|
package condition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conditionDTO 是 Condition 的 JSON 中间结构,用于避免 UnmarshalJSON 递归
|
||||||
|
type conditionDTO struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Describe string `json:"describe,omitempty"`
|
||||||
|
Exprs []*exprDTO `json:"exprs,omitempty"`
|
||||||
|
Childrens []*conditionDTO `json:"childrens,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type exprDTO struct {
|
||||||
|
Operator string `json:"operator"`
|
||||||
|
Field string `json:"field"`
|
||||||
|
FieldResource string `json:"fieldResource"`
|
||||||
|
FieldSqlFunc string `json:"fieldSqlFunc,omitempty"`
|
||||||
|
FieldSqlFuncParam string `json:"fieldSqlFuncParam,omitempty"`
|
||||||
|
IgnoreEmptyParam bool `json:"ignoreEmptyParam,omitempty"`
|
||||||
|
TokenType TokenType `json:"tokenType"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Condition) UnmarshalJSON(data []byte) error {
|
||||||
|
var dto conditionDTO
|
||||||
|
if err := json.Unmarshal(data, &dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conditionFromDTO(this, &dto)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this Condition) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(conditionToDTO(&this))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ConditionExpr) UnmarshalJSON(data []byte) error {
|
||||||
|
var dto exprDTO
|
||||||
|
if err := json.Unmarshal(data, &dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*this = *exprFromDTO(&dto)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this ConditionExpr) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(exprToDTO(&this))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromMap 将 map[string]any 转换为 *Condition
|
||||||
|
func FromMap(m map[string]any) (*Condition, error) {
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cond := New()
|
||||||
|
if err := json.Unmarshal(data, cond); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cond, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionFromDTO(c *Condition, dto *conditionDTO) {
|
||||||
|
if strings.EqualFold(dto.Type, "OR") {
|
||||||
|
c.typ = OR
|
||||||
|
} else {
|
||||||
|
c.typ = AND
|
||||||
|
}
|
||||||
|
c.describe = dto.Describe
|
||||||
|
for _, e := range dto.Exprs {
|
||||||
|
c.SetExpr(exprFromDTO(e))
|
||||||
|
}
|
||||||
|
for _, child := range dto.Childrens {
|
||||||
|
childCond := &Condition{}
|
||||||
|
conditionFromDTO(childCond, child)
|
||||||
|
c.SetCondition(childCond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionToDTO(c *Condition) *conditionDTO {
|
||||||
|
typ := "AND"
|
||||||
|
if c.typ == OR {
|
||||||
|
typ = "OR"
|
||||||
|
}
|
||||||
|
dto := &conditionDTO{Type: typ, Describe: c.describe}
|
||||||
|
for _, e := range c.exprs {
|
||||||
|
dto.Exprs = append(dto.Exprs, exprToDTO(e))
|
||||||
|
}
|
||||||
|
for _, child := range c.childrens {
|
||||||
|
dto.Childrens = append(dto.Childrens, conditionToDTO(child))
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func exprFromDTO(dto *exprDTO) *ConditionExpr {
|
||||||
|
tokenType := dto.TokenType
|
||||||
|
if tokenType == "" {
|
||||||
|
tokenType = STRING
|
||||||
|
}
|
||||||
|
return &ConditionExpr{
|
||||||
|
operator: ToConditionOperator(dto.Operator),
|
||||||
|
field: dto.Field,
|
||||||
|
fieldResource: dto.FieldResource,
|
||||||
|
fieldSqlFunc: dto.FieldSqlFunc,
|
||||||
|
fieldSqlFuncParam: dto.FieldSqlFuncParam,
|
||||||
|
ignoreEmptyParma: dto.IgnoreEmptyParam,
|
||||||
|
tokenType: tokenType,
|
||||||
|
token: dto.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exprToDTO(e *ConditionExpr) *exprDTO {
|
||||||
|
return &exprDTO{
|
||||||
|
Operator: operatorToString(e.operator),
|
||||||
|
Field: e.field,
|
||||||
|
FieldResource: e.fieldResource,
|
||||||
|
FieldSqlFunc: e.fieldSqlFunc,
|
||||||
|
FieldSqlFuncParam: e.fieldSqlFuncParam,
|
||||||
|
IgnoreEmptyParam: e.ignoreEmptyParma,
|
||||||
|
TokenType: e.tokenType,
|
||||||
|
Token: e.token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operatorToString(op ConditionOperator) string {
|
||||||
|
switch op {
|
||||||
|
case IS_NULL:
|
||||||
|
return "IS NULL"
|
||||||
|
case IS_NOT_NULL:
|
||||||
|
return "IS NOT NULL"
|
||||||
|
case EQ:
|
||||||
|
return "="
|
||||||
|
case NE:
|
||||||
|
return "!="
|
||||||
|
case GT:
|
||||||
|
return ">"
|
||||||
|
case GE:
|
||||||
|
return ">="
|
||||||
|
case LT:
|
||||||
|
return "<"
|
||||||
|
case LE:
|
||||||
|
return "<="
|
||||||
|
case LIKE:
|
||||||
|
return "LIKE"
|
||||||
|
case NOT_LIKE:
|
||||||
|
return "NOT LIKE"
|
||||||
|
case IN:
|
||||||
|
return "IN"
|
||||||
|
case NOT_IN:
|
||||||
|
return "NOT IN"
|
||||||
|
case REGEXP:
|
||||||
|
return "REGEXP"
|
||||||
|
case NOT_REGEXP:
|
||||||
|
return "NOT REGEXP"
|
||||||
|
default:
|
||||||
|
return "="
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package condition_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.fsdpf.net/go/condition"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type conditionJSONTest struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConditionJSONSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(conditionJSONTest))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造一个有代表性的多层条件
|
||||||
|
func buildTestCondition() *condition.Condition {
|
||||||
|
cond := condition.New(condition.Describe("测试"))
|
||||||
|
cond.SetExpr(condition.NewExpr("User", "name",
|
||||||
|
condition.Operator(condition.LIKE),
|
||||||
|
condition.Token("张三", condition.STRING),
|
||||||
|
))
|
||||||
|
cond.SetExpr(condition.NewExpr("User", "age",
|
||||||
|
condition.Operator(condition.GE),
|
||||||
|
condition.Token("18", condition.STRING),
|
||||||
|
))
|
||||||
|
|
||||||
|
sub := condition.New(condition.Type(condition.OR))
|
||||||
|
sub.SetExpr(condition.NewExpr("User", "status",
|
||||||
|
condition.Operator(condition.IN),
|
||||||
|
condition.Token("active", condition.STRING),
|
||||||
|
))
|
||||||
|
sub.SetExpr(condition.NewExpr("User", "roles",
|
||||||
|
condition.FieldSqlFn("json_contains", ""),
|
||||||
|
condition.Token("admin", condition.STRING),
|
||||||
|
))
|
||||||
|
cond.SetCondition(sub)
|
||||||
|
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *conditionJSONTest) TestMarshalUnmarshal() {
|
||||||
|
orig := buildTestCondition()
|
||||||
|
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.T().Log("JSON:", string(data))
|
||||||
|
|
||||||
|
got := condition.New()
|
||||||
|
t.Require().NoError(json.Unmarshal(data, got))
|
||||||
|
|
||||||
|
// 二次序列化结果应一致
|
||||||
|
data2, err := json.Marshal(got)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.Equal(string(data), string(data2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *conditionJSONTest) TestFromMap() {
|
||||||
|
m := map[string]any{
|
||||||
|
"type": "AND",
|
||||||
|
"describe": "来自Map",
|
||||||
|
"exprs": []any{
|
||||||
|
map[string]any{
|
||||||
|
"operator": "=",
|
||||||
|
"field": "name",
|
||||||
|
"fieldResource": "User",
|
||||||
|
"tokenType": "string",
|
||||||
|
"token": "张三",
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"operator": ">=",
|
||||||
|
"field": "age",
|
||||||
|
"fieldResource": "User",
|
||||||
|
"tokenType": "string",
|
||||||
|
"token": "18",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"childrens": []any{
|
||||||
|
map[string]any{
|
||||||
|
"type": "OR",
|
||||||
|
"exprs": []any{
|
||||||
|
map[string]any{
|
||||||
|
"operator": "IS NULL",
|
||||||
|
"field": "deleted_at",
|
||||||
|
"fieldResource": "User",
|
||||||
|
"tokenType": "string",
|
||||||
|
"token": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cond, err := condition.FromMap(m)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.False(cond.IsEmpty())
|
||||||
|
|
||||||
|
data, err := json.Marshal(cond)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.T().Log("FromMap JSON:", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *conditionJSONTest) TestRoundTrip() {
|
||||||
|
orig := buildTestCondition()
|
||||||
|
|
||||||
|
// Condition → JSON → Condition → JSON,两次 JSON 应一致
|
||||||
|
data1, err := json.Marshal(orig)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
|
||||||
|
cond2 := condition.New()
|
||||||
|
t.Require().NoError(json.Unmarshal(data1, cond2))
|
||||||
|
|
||||||
|
data2, err := json.Marshal(cond2)
|
||||||
|
t.Require().NoError(err)
|
||||||
|
|
||||||
|
t.JSONEq(string(data1), string(data2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *conditionJSONTest) TestFromMapEmpty() {
|
||||||
|
cond, err := condition.FromMap(map[string]any{})
|
||||||
|
t.Require().NoError(err)
|
||||||
|
t.True(cond.IsEmpty())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user