go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/proto/structmask/structmask_test.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package structmask 16 17 import ( 18 "strings" 19 "testing" 20 21 "google.golang.org/protobuf/encoding/protojson" 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/types/known/structpb" 24 ) 25 26 func TestStructMask(t *testing.T) { 27 t.Parallel() 28 29 cases := []struct { 30 name string 31 mask []*StructMask 32 input string 33 output string 34 }{ 35 { 36 "noop", 37 makeMask(), 38 `{"a": "b"}`, 39 `{"a": "b"}`, 40 }, 41 42 { 43 "all star 1", 44 makeMask(`*`), 45 `{}`, 46 `{}`, 47 }, 48 { 49 "all star 2", 50 makeMask(`*`, `*`), 51 `{"a": "b", "c": {"d": ["e"]}}`, 52 `{"a": "b", "c": {"d": ["e"]}}`, 53 }, 54 55 { 56 "field selector", 57 makeMask(`a`), 58 `{"a": "b", "c": {"d": ["e"]}}`, 59 `{"a": "b"}`, 60 }, 61 { 62 "nested field selector", 63 makeMask(`a.b`), 64 `{ 65 "a": {"b": 1, "z": "..."}, 66 "b": 2, 67 "c": {"b": 3} 68 }`, 69 `{"a": {"b": 1}}`, 70 }, 71 72 { 73 "dict star last", 74 makeMask(`a.*`), 75 `{ 76 "a": {"b": 1, "c": 2}, 77 "b": "zzz" 78 }`, 79 `{ 80 "a": {"b": 1, "c": 2} 81 }`, 82 }, 83 { 84 "dict star not last", 85 makeMask(`*.b`), 86 `{ 87 "a": {"b": 1, "c": 2}, 88 "b": {"z": 123}, 89 "c": 123, 90 "d": [] 91 }`, 92 `{ 93 "a": {"b": 1} 94 }`, 95 }, 96 { 97 "dict star nested", 98 makeMask(`*.b.*`), 99 `{ 100 "f1": 1, 101 "f2": {"z": []}, 102 "f3": {"b": [1, 2, 3]}, 103 "f4": {"b": 123} 104 }`, 105 `{ 106 "f3": {"b": [1, 2, 3]} 107 }`, 108 }, 109 110 { 111 "list star last", 112 makeMask(`a.*`), 113 `{ 114 "a": [{"a": "b"}, 2, {"a": "b"}], 115 "b": "zzz" 116 }`, 117 `{ 118 "a": [{"a": "b"}, 2, {"a": "b"}] 119 }`, 120 }, 121 { 122 "list star not last", 123 makeMask(`a.*.b`), 124 `{ 125 "a": [{"b": "c"}, 2, {"a": "c"}, null], 126 "b": "zzz" 127 }`, 128 `{ 129 "a": [{"b": "c"}, null, null, null] 130 }`, 131 }, 132 { 133 "list star nested", 134 makeMask(`a.*.*`), 135 `{ 136 "a": [ 137 "skip", 138 null, 139 123, 140 {"a": "b"}, 141 {"a": {"b": "c"}}, 142 [1, 2, 3] 143 ] 144 }`, 145 `{ 146 "a": [ 147 null, 148 null, 149 null, 150 {"a": "b"}, 151 {"a": {"b": "c"}}, 152 [1, 2, 3] 153 ] 154 }`, 155 }, 156 157 { 158 "multiple field selectors, no stars", 159 makeMask(`a.a`, `a.b`, `b`), 160 `{ 161 "a": { 162 "a": 1, 163 "b": 2, 164 "c": 3 165 }, 166 "b": [1, 2, 3], 167 "c": 5 168 }`, 169 `{ 170 "a": { 171 "a": 1, 172 "b": 2 173 }, 174 "b": [1, 2, 3] 175 }`, 176 }, 177 178 { 179 "early leaf nodes, fields", 180 makeMask(`a.b.c`, `a.b`), 181 `{ 182 "a": { 183 "b": {"c": 1, "d": 2, "e": {"f": 3}}, 184 "c": 2 185 } 186 }`, 187 `{ 188 "a": { 189 "b": {"c": 1, "d": 2, "e": {"f": 3}} 190 } 191 }`, 192 }, 193 { 194 "early leaf nodes, fields, reversed", 195 makeMask(`a.b`, `a.b.c`), 196 `{ 197 "a": { 198 "b": {"c": 1, "d": 2, "e": {"f": 3}}, 199 "c": 2 200 } 201 }`, 202 `{ 203 "a": { 204 "b": {"c": 1, "d": 2, "e": {"f": 3}} 205 } 206 }`, 207 }, 208 209 { 210 "early leaf nodes, stars", 211 makeMask(`a.*.c`, `a.*`), 212 `{ 213 "a": { 214 "b": {"c": 1, "d": 2, "e": {"f": 3}}, 215 "c": 2 216 } 217 }`, 218 `{ 219 "a": { 220 "b": {"c": 1, "d": 2, "e": {"f": 3}}, 221 "c": 2 222 } 223 }`, 224 }, 225 226 { 227 "star + field selector merging, simple", 228 makeMask(`*.a`, `a.b`), 229 `{ 230 "a": {"a": 1, "b": 2, "c": 3}, 231 "b": {"a": 1, "b": 2, "c": 3}, 232 "c": 123 233 }`, 234 `{ 235 "a": {"a": 1, "b": 2}, 236 "b": {"a": 1} 237 }`, 238 }, 239 { 240 "star + field selector merging, deeper", 241 makeMask(`*.a.a`, `a.a.b`), 242 `{ 243 "a": {"a": {"a": 1, "b": 2}}, 244 "b": {"a": {"a": 1, "b": 2}} 245 }`, 246 `{ 247 "a": {"a": {"a": 1, "b": 2}}, 248 "b": {"a": {"a": 1}} 249 }`, 250 }, 251 { 252 "star + field selector merging, deeper, reverse order", 253 makeMask(`a.a.b`, `*.a.a`), 254 `{ 255 "a": {"a": {"a": 1, "b": 2}}, 256 "b": {"a": {"a": 1, "b": 2}} 257 }`, 258 `{ 259 "a": {"a": {"a": 1, "b": 2}}, 260 "b": {"a": {"a": 1}} 261 }`, 262 }, 263 264 { 265 "list merges, simple", 266 makeMask(`*.*.a`, `a.*.b`), 267 `{ 268 "a": [ 269 {"a": 1, "b": 1}, 270 {"b": 1}, 271 {"c": 1} 272 ], 273 "b": [ 274 {"a": 1, "b": 1}, 275 {"b": 1}, 276 {"c": 1} 277 ] 278 }`, 279 `{ 280 "a": [ 281 {"a": 1, "b": 1}, 282 {"b": 1}, 283 null 284 ], 285 "b": [ 286 {"a": 1}, 287 null, 288 null 289 ] 290 }`, 291 }, 292 { 293 "list merges with nulls", 294 makeMask(`a.*.a`, `*.*.b`), 295 `{ 296 "a": [ 297 {"b": 1, "c": 1}, 298 {"a": 1, "c": 1}, 299 {"c": 1} 300 ] 301 }`, 302 `{ 303 "a": [ 304 {"b": 1}, 305 {"a": 1}, 306 null 307 ] 308 }`, 309 }, 310 { 311 "list merges with nulls, deeper", 312 makeMask(`*.*.*.b`, `x.*.a.c`), 313 `{ 314 "x": [ 315 {"a": {"c": 1}}, 316 {"y": {"b": 1}} 317 ] 318 }`, 319 `{ 320 "x": [ 321 {"a": {"c": 1}}, 322 {"y": {"b": 1}} 323 ] 324 }`, 325 }, 326 327 { 328 "merging exact same values", 329 makeMask(`a.*`, `*.b`), 330 `{"a": {"b": {"c": 1}}}`, 331 `{"a": {"b": {"c": 1}}}`, 332 }, 333 334 { 335 "merging scalars into nils", 336 makeMask(`a.*.a`, `*.*`), 337 `{ 338 "a": [ 339 {"b": 1}, 340 1 341 ] 342 }`, 343 `{ 344 "a": [ 345 {"b": 1}, 346 1 347 ] 348 }`, 349 }, 350 } 351 352 for _, cs := range cases { 353 t.Run(cs.name, func(t *testing.T) { 354 filter, err := NewFilter(cs.mask) 355 if err != nil { 356 t.Errorf("bad filter: %s", err) 357 return 358 } 359 expected := asProto(cs.output) 360 output := filter.Apply(asProto(cs.input)) 361 if !proto.Equal(output, expected) { 362 t.Errorf("got:\n---------\n%s\n---------\nbut want\n---------\n%s\n---------", asJSON(output), asJSON(expected)) 363 } 364 }) 365 } 366 } 367 368 func makeMask(masks ...string) []*StructMask { 369 out := make([]*StructMask, len(masks)) 370 for idx, m := range masks { 371 out[idx] = &StructMask{Path: strings.Split(m, ".")} 372 } 373 return out 374 } 375 376 func asProto(json string) *structpb.Struct { 377 s := &structpb.Struct{} 378 if err := protojson.Unmarshal([]byte(json), s); err != nil { 379 panic(err) 380 } 381 return s 382 } 383 384 func asJSON(s *structpb.Struct) string { 385 blob, err := (protojson.MarshalOptions{ 386 Multiline: true, 387 Indent: " ", 388 }).Marshal(s) 389 if err != nil { 390 panic(err) 391 } 392 return string(blob) 393 }