Skip to content

Commit 88d1e48

Browse files
authoredApr 26, 2024··
feat:(fieldmask) support black-list mode (#192)
1 parent 9aed032 commit 88d1e48

File tree

15 files changed

+1252
-456
lines changed

15 files changed

+1252
-456
lines changed
 

‎fieldmask/README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ $ | the root object,every path must start with it.
5959
</byte-sheet-html-origin><!--EndFragment-->
6060

6161
#### Agreement Of Implementation
62-
- A field in mask means "PASS" (**will be** serialized/deserialized), and the other field not in mask means "Filtered" ((**won't be** serialized/deserialized))
6362
- A empty mask means "PASS ALL" (all field is "PASS")
6463
- For map of neither-string-nor-integer typed key, only syntax token of all '*' (see above) is allowed in.
6564
- For safty, required fields which are not in mask ("Filtered") will still be written into message:
@@ -68,6 +67,10 @@ $ | the root object,every path must start with it.
6867
- FieldMask settings must start from the root object.
6968
- Tips: If you want to set FieldMask from a non-root object and make it effective, you need to add `field_mask_halfway` option and regenerate the codes. However, there is a latent risk: if different parent objects reference the same child object, and these two parent objects set different fieldmasks, only one parent object's fieldmask relative to this child object will be effective.
7069

70+
#### Visibility
71+
By default, a field in mask means "PASS" (**will be** serialized/deserialized), and the other fields not in mask means "REJECT" (**won't be** serialized/deserialized) -- which is so-called **"White List"**
72+
However, we allow user to use fieldmask as a **"Black List"**, as long as enable option `Options.BlackList` mode. Under such mode, a field in the mask means "REJECT", and the other fields means "PASS". See [main_test.go](https://github.com/cloudwego/kitex-tests/blob/feat/fieldmask_test/thriftrpc/fieldmask/main_test.go) for detailed usage.
73+
7174
### Type Descriptor
7275
Type descriptor is the runtime representation of a message definition, in aligned with [Protobuf Descriptor](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto). To get a type descriptor, you must enable thrift reflection feature first, which was introduced in thriftgo [v0.3.0](https://github.com/cloudwego/thriftgo/pull/83). you can generate related codes for this feature using option `with_reflection`.
7376

@@ -102,6 +105,13 @@ func init() {
102105
fieldmaskCache.Store("Mask1ForBase", fm)
103106
}
104107
```
108+
- If you want to enable black-list mode of fieldmask, you can create fieldmask like this:
109+
```go
110+
fm, err := fieldmask.Options{
111+
BlackListMode: true,
112+
}.NewFieldMask(desc, "$.Addr")
113+
```
114+
105115
3. Now you can use fieldmask in either client-side or server-side
106116
- For server-side, you can set fieldmask with generated API `Set_FieldMask()` on your response object. Then the object itself will notice the fieldmask and using it during its serialization
107117
```go
@@ -123,6 +133,7 @@ func init() {
123133
```
124134
- For client-side: related to the deserialization process of framework. For kitex, it's WIP.
125135

136+
126137
## How to pass fieldmask between programs?
127138
Generally, you can add one binary field on your request definition to carry fieldmask, and explicitly serialize/deserialize the fieldmask you are using into/from this field. We provide two encapsulated API for serialization/deserialization:
128139
- [FieldMask.MarshalJSON()/UnmarshalJSON()](serdes.go): Object methods, serialize/deserialize fieldmask into/from json bytes

‎fieldmask/api_test.go

+203-9
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ struct MetaInfo {
6767
6868
typedef Val Key
6969
70+
enum Ex {
71+
A = 1,
72+
B = 2,
73+
C = 3
74+
}
75+
7076
struct BaseResp {
7177
1: required string StatusMessage = "",
7278
2: required i32 StatusCode = 0,
@@ -96,22 +102,186 @@ struct BaseResp {
96102
}
97103
`
98104

99-
func GetDescriptor(IDL string, root string) *thrift_reflection.TypeDescriptor {
105+
func GetDescriptor(IDL string, root string) (ret *thrift_reflection.TypeDescriptor) {
100106
ast, err := parser.ParseString("a.thrift", IDL)
101107
if err != nil {
102108
panic(err.Error())
103109
}
104110
_, fd := thrift_reflection.RegisterAST(ast)
105111
st := fd.GetStructDescriptor(root)
106-
return &thrift_reflection.TypeDescriptor{
112+
ret = &thrift_reflection.TypeDescriptor{
107113
Filepath: st.Filepath,
108114
Name: st.Name,
109115
Extra: map[string]string{thrift_reflection.GLOBAL_UUID_EXTRA_KEY: st.Extra[thrift_reflection.GLOBAL_UUID_EXTRA_KEY]},
110116
}
117+
return
118+
}
119+
120+
func TestFieldMask_Single(t *testing.T) {
121+
type args struct {
122+
opts Options
123+
IDL string
124+
rootStruct string
125+
paths []string
126+
inMasks [][]interface{}
127+
notInMasks [][]interface{}
128+
err []error
129+
}
130+
tests := []struct {
131+
name string
132+
args args
133+
want *FieldMask
134+
}{
135+
{
136+
name: "Base",
137+
args: args{
138+
IDL: baseIDL,
139+
rootStruct: "Base",
140+
paths: []string{
141+
"$.LogID",
142+
"$.TrafficEnv.Open",
143+
"$.Extra[0]",
144+
"$.Extra[1].List",
145+
"$.Extra[1].Set[1].A",
146+
"$.Extra[3].IntMap{1}",
147+
"$.Extra[3].IntMap{3}.A",
148+
"$.Extra[3].StrMap{\"x\"}",
149+
"$.Extra[3].StrMap{\"y\"}.A",
150+
},
151+
inMasks: [][]interface{}{
152+
{int16(1)},
153+
{int16(5), int16(1)},
154+
{int16(6), 0},
155+
{int16(6), 1, int16(3)},
156+
{int16(6), 1, int16(4), 1, int16(1)},
157+
{int16(6), 3, int16(1), 1},
158+
{int16(6), 3, int16(1), 3, int16(1)},
159+
{int16(6), 3, int16(2), "x"},
160+
{int16(6), 3, int16(2), "y", int16(1)},
161+
},
162+
notInMasks: [][]interface{}{
163+
{int16(0)},
164+
{int16(2)},
165+
{int16(256)},
166+
{int16(5), int16(0)},
167+
{int16(5), int16(2)},
168+
{int16(5), int16(256)},
169+
{int16(6), 2},
170+
{int16(6), 1, int16(1)},
171+
{int16(6), 1, int16(2)},
172+
{int16(6), 1, int16(4), 1, int16(2)},
173+
{int16(6), 3, int16(1), 0},
174+
{int16(6), 3, int16(1), 2},
175+
{int16(6), 3, int16(1), 3, int16(2)},
176+
{int16(6), 3, int16(2), "z"},
177+
{int16(6), 3, int16(2), "y", int16(2)},
178+
},
179+
},
180+
},
181+
}
182+
183+
for _, tt := range tests {
184+
t.Run(tt.name, func(t *testing.T) {
185+
st := GetDescriptor(tt.args.IDL, tt.args.rootStruct)
186+
BLACK_MODE := false
187+
188+
retry:
189+
println("Black:", BLACK_MODE)
190+
opts := tt.args.opts
191+
opts.BlackListMode = BLACK_MODE
192+
got, err := opts.NewFieldMask(st, tt.args.paths...)
193+
if tt.args.err != nil {
194+
if err == nil {
195+
t.Fatal(err)
196+
}
197+
return
198+
}
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
203+
out, err := got.MarshalJSON()
204+
if err != nil {
205+
t.Fatal(err)
206+
}
207+
// println(string(out))
208+
if !json.Valid(out) {
209+
t.Fatal("not invalid json")
210+
}
211+
212+
// test unmarshal json
213+
nn := &FieldMask{}
214+
if err := nn.UnmarshalJSON(out); err != nil {
215+
t.Fatal(err)
216+
}
217+
218+
if BLACK_MODE {
219+
tt.args.inMasks, tt.args.notInMasks = tt.args.notInMasks, tt.args.inMasks
220+
}
221+
222+
for _, path := range tt.args.inMasks {
223+
cur := got
224+
ok := false
225+
for _, elem := range path {
226+
// cj, err := cur.MarshalJSON()
227+
// if err != nil {
228+
// t.Fatal(err)
229+
// }
230+
// fmt.Printf("for elem %#v, cur %v, ok %v\n", elem, string(cj), ok)
231+
switch p := elem.(type) {
232+
case string:
233+
cur, ok = cur.Str(p)
234+
case int:
235+
cur, ok = cur.Int(p)
236+
case int16:
237+
cur, ok = cur.Field(p)
238+
default:
239+
panic("elem type should be int or string or int16")
240+
}
241+
242+
if !ok {
243+
t.Fatalf("path %#v not exist!", path)
244+
}
245+
}
246+
}
247+
248+
for _, path := range tt.args.notInMasks {
249+
cur := got
250+
ok := false
251+
for i, elem := range path {
252+
switch p := elem.(type) {
253+
case string:
254+
cur, ok = cur.Str(p)
255+
case int:
256+
cur, ok = cur.Int(p)
257+
case int16:
258+
cur, ok = cur.Field(p)
259+
default:
260+
panic("elem type should be int or string or int16")
261+
}
262+
if i < len(path)-1 {
263+
if !ok {
264+
t.Fatalf("path %#v not exist!", path)
265+
}
266+
} else {
267+
if ok {
268+
t.Fatalf("path %#v exist!", path)
269+
}
270+
}
271+
}
272+
}
273+
274+
if !BLACK_MODE {
275+
BLACK_MODE = true
276+
goto retry
277+
}
278+
})
279+
}
111280
}
112281

113282
func TestNewFieldMask(t *testing.T) {
114283
type args struct {
284+
opts Options
115285
IDL string
116286
rootStruct string
117287
paths []string
@@ -124,14 +294,23 @@ func TestNewFieldMask(t *testing.T) {
124294
args args
125295
want *FieldMask
126296
}{
297+
{
298+
name: "Enum Key Map",
299+
args: args{
300+
IDL: baseIDL,
301+
rootStruct: "BaseResp",
302+
paths: []string{"$.F7{1}"},
303+
notInMasks: []string{"$.F7{2}"},
304+
},
305+
},
127306
{
128307
name: "Neither-string-nor-integer-key Map",
129308
args: args{
130309
IDL: baseIDL,
131310
rootStruct: "BaseResp",
132-
paths: []string{"$.F10{*}.A", "$.F5{*}.A"},
311+
paths: []string{"$.F10{*}.A", "$.F5{*}.A", "$.F7{0}"},
133312
inMasks: []string{"$.F10{\"a\"}.A", "$.F5{0}.A"},
134-
notInMasks: []string{`$.F10{"a"}.B`, "$.F10{*}.B", "$.F5{0}.B", "$.F5{*}.B"},
313+
notInMasks: []string{`$.F10{"a"}.B`, "$.F10{*}.B", "$.F5{0}.B", "$.F5{*}.B", "$.F7{1}"},
135314
},
136315
},
137316
{
@@ -208,7 +387,7 @@ func TestNewFieldMask(t *testing.T) {
208387
// }()
209388

210389
st := GetDescriptor(tt.args.IDL, tt.args.rootStruct)
211-
got, err := NewFieldMask(st, tt.args.paths...)
390+
got, err := tt.args.opts.NewFieldMask(st, tt.args.paths...)
212391
if tt.args.err != nil {
213392
if err == nil {
214393
t.Fatal(err)
@@ -222,9 +401,8 @@ func TestNewFieldMask(t *testing.T) {
222401
retry := true
223402
begin:
224403

225-
// println("fieldmask:")
226-
// println(got.String(st))
227-
// spew.Dump(got)
404+
println("fieldmask:")
405+
println(got.String(st))
228406

229407
// test marshal json
230408
// println("marshal:")
@@ -297,7 +475,23 @@ func TestMarshalJSONStable(t *testing.T) {
297475
t.Fatal(err)
298476
}
299477
println(string(jo))
300-
if string(jo) != (`{"path":"$","type":"Struct","children":[{"path":1,"type":"StrMap","children":[{"path":"a","type":"Struct"},{"path":"b","type":"Struct"},{"path":"c","type":"Struct"},{"path":"d","type":"Struct"}]},{"path":2,"type":"IntMap","children":[{"path":0,"type":"Struct"},{"path":1,"type":"Struct"},{"path":2,"type":"Struct"},{"path":3,"type":"Struct"},{"path":4,"type":"Struct"}]}]}`) {
478+
act := new(FieldMask)
479+
if err := act.UnmarshalJSON(jo); err != nil {
480+
t.Fatal(err)
481+
}
482+
if !act.PathInMask(st, "$.F2{4,1,3,0,2}") {
483+
t.Fail()
484+
}
485+
if !act.PathInMask(st, `$.F1{"c","d","b","a"}`) {
486+
t.Fail()
487+
}
488+
if act.PathInMask(st, `$.F2{5,100}`) {
489+
t.Fail()
490+
}
491+
if act.PathInMask(st, `$.F1{"5","100ab11"}`) {
492+
t.Fail()
493+
}
494+
if string(jo) != (`{"path":"$","type":"Struct","is_black":false,"children":[{"path":1,"type":"StrMap","is_black":false,"children":[{"path":"a","type":"Struct","is_black":false},{"path":"b","type":"Struct","is_black":false},{"path":"c","type":"Struct","is_black":false},{"path":"d","type":"Struct","is_black":false}]},{"path":2,"type":"IntMap","is_black":false,"children":[{"path":0,"type":"Struct","is_black":false},{"path":1,"type":"Struct","is_black":false},{"path":2,"type":"Struct","is_black":false},{"path":3,"type":"Struct","is_black":false},{"path":4,"type":"Struct","is_black":false}]}]}`) {
301495
t.Fatal(string(jo))
302496
}
303497
}

0 commit comments

Comments
 (0)
Please sign in to comment.