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  }