From 93fd7363a31f8ba00b34b82eda82c761f18d1deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=B9=BF?= Date: Thu, 19 Jun 2025 17:56:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E5=9F=BA=E6=9C=AC=E7=9A=84=E6=96=87=E6=A1=A3=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了 Document、Array 和 Object 接口的实现 - 实现了 DocumentOperator、ValueOperator 和 FieldOperator 函数 - 添加了 operatorGt 函数作为示例操作符 - 编写了相关测试用例以验证功能正确性 --- case.md | 21 +++++++ coperator.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++ coperator_test.go | 145 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + 4 files changed, 317 insertions(+) create mode 100644 case.md create mode 100644 coperator.go create mode 100644 coperator_test.go create mode 100644 go.mod diff --git a/case.md b/case.md new file mode 100644 index 0000000..9b1f350 --- /dev/null +++ b/case.md @@ -0,0 +1,21 @@ + +filter: + +{ + "name": "case", + "field":{ + "$gt": 0 + } +} + +And Document d={} + +首先使用文档operator处理 + +documentOp(d, filter,nil,nil) + +然后是值operator处理,匹配name + +valueOp(d, nil, key,value) + +字段op处理(d, {$gt:0}, key="field",nil) diff --git a/coperator.go b/coperator.go new file mode 100644 index 0000000..a7fcb5a --- /dev/null +++ b/coperator.go @@ -0,0 +1,148 @@ +package coperator + +import ( + "errors" + "strconv" + "strings" +) + +func isImplementObject(v interface{}) (Object, bool) { + switch a := v.(type) { + case Document: + return &a, true + case Array: + return &a, true + default: + return nil, false + } +} + +type Object interface { + Get(path string) interface{} + GetPathArray(path []string) interface{} +} +type Array []interface{} + +func (a *Array) Get(path string) interface{} { + paths := strings.Split(path, ".") + return a.GetPathArray(paths) +} + +func (a *Array) GetPathArray(path []string) interface{} { + if len(path) == 0 { + return *a + } + index, err := strconv.Atoi(path[0]) + if err != nil { + return nil + } + if len(path) > 1 { + remaining := path[1:] + v := (*a)[index] + if pv, ok := isImplementObject(v); ok { + return pv.GetPathArray(remaining) + } + return nil + } + return (*a)[index] +} + +type Document map[string]interface{} + +func (d *Document) Get(path string) interface{} { + paths := strings.Split(path, ".") + return d.GetPathArray(paths) +} + +func (d *Document) GetPathArray(path []string) interface{} { + if len(path) == 0 { + return *d + } + if len(path) == 1 { + return (*d)[path[0]] + } + xv, xok := (*d)[path[0]] + if !xok { + return nil + } + if v, ok := isImplementObject(xv); ok { + return v.GetPathArray(path[1:]) + } + return nil +} + +type Collection []Document +type Filter map[string]interface{} + +type Operator func(doc *Document, filter Filter, key string, value interface{}) (bool, error) + +var operatorMap = map[string]Operator{} + +func DocumentOperator(doc *Document, filter Filter, key string, value interface{}) (ret bool, err error) { + + for k, v := range filter { + + if strings.HasPrefix(k, "$") { + // top level operator, commonly be $and,$or + opFn, ok := operatorMap[k] + if !ok { + return false, errors.New("operator not supported") + } + ret, err = opFn(doc, v.(Filter), "", nil) + if !ret { + return ret, err + } + continue + } + if f, ok := v.(Filter); ok { + ret, err = FieldOperator(doc, f, k, nil) + if !ret { + return ret, err + } + continue + } + ret, err = ValueOperator(doc, filter, k, v) + if !ret { + return ret, err + } + continue + } + return true, nil +} + +func ValueOperator(doc *Document, filter Filter, key string, value interface{}) (bool, error) { + if doc.Get(key) != value { + return false, nil + } + return true, nil +} + +func FieldOperator(doc *Document, filter Filter, key string, value interface{}) (ret bool, err error) { + for k, v := range filter { + if !strings.HasPrefix(k, "$") { + return false, errors.New("uncorrect grammar") + } + opFn, ok := operatorMap[k] + if !ok { + return false, errors.New("unsupport operator") + } + if fv, ok := v.(Filter); ok { + ret, err = opFn(doc, fv, key, nil) + if !ret { + return ret, err + } + continue + } + ret, err = opFn(doc, nil, key, v) + if !ret { + return ret, err + } + continue + + } + return true, nil +} + +func operatorGt(doc *Document, filter Filter, key string, value interface{}) (ret bool, err error) { + return false, nil +} diff --git a/coperator_test.go b/coperator_test.go new file mode 100644 index 0000000..49801d6 --- /dev/null +++ b/coperator_test.go @@ -0,0 +1,145 @@ +package coperator + +import ( + "strings" + "testing" +) + +func TestArray_GetPathArray(t *testing.T) { + tests := []struct { + name string + array Array + path []string + want interface{} + }{ + { + name: "Get first element", + array: Array{1, 2, 3}, + path: []string{"0"}, + want: 1, + }, + { + name: "Get nested array value", + array: Array{Array{4, 5}}, + path: []string{"0", "1"}, + want: 5, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.array.GetPathArray(tt.path) + if got != tt.want { + t.Errorf("Array.GetPathArray() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDocument_GetPathArray(t *testing.T) { + tests := []struct { + name string + doc Document + path []string + want interface{} + }{ + { + name: "Get top level field", + doc: Document{ + "name": "test", + }, + path: []string{"name"}, + want: "test", + }, + { + name: "Get nested field", + doc: Document{ + "user": Document{ + "id": 1, + }, + }, + path: []string{"user", "id"}, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.doc.GetPathArray(tt.path) + if got != tt.want { + t.Errorf("Document.GetPathArray() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValueOperator(t *testing.T) { + tests := []struct { + name string + doc Document + path []string + value interface{} + want bool + }{ + { + name: "Get top level field", + doc: Document{ + "name": "test", + }, + path: []string{"name"}, + value: "test", + want: true, + }, + { + name: "Get nested field", + doc: Document{ + "user": Document{ + "id": 1, + }, + }, + path: []string{"user", "id"}, + value: 1, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := ValueOperator(&tt.doc, Filter{}, strings.Join(tt.path, "."), tt.value) + if got != tt.want { + t.Errorf("Document.GetPathArray() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDocumentOperator(t *testing.T) { + tests := []struct { + name string + doc Document + filter Filter + want bool + }{ + { + name: "test", + doc: Document{ + "user": Document{ + "id": 1, + }, + }, + filter: Filter{ + "user.id": 1, + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := DocumentOperator(&tt.doc, tt.filter, "", nil) + if got != tt.want { + t.Errorf("Document.GetPathArray() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ddfaff --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.pyer.club/kingecg/coperator + +go 1.23.1