...

Source file src/sigs.k8s.io/structured-merge-diff/v4/typed/remove_test.go

Documentation: sigs.k8s.io/structured-merge-diff/v4/typed

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package typed_test
    18  
    19  import (
    20  	"fmt"
    21  	"testing"
    22  
    23  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    24  	"sigs.k8s.io/structured-merge-diff/v4/typed"
    25  	"sigs.k8s.io/structured-merge-diff/v4/value"
    26  )
    27  
    28  type removeTestCase struct {
    29  	name         string
    30  	rootTypeName string
    31  	schema       typed.YAMLObject
    32  	quadruplets  []removeQuadruplet
    33  }
    34  
    35  type removeQuadruplet struct {
    36  	object        typed.YAMLObject
    37  	set           *fieldpath.Set
    38  	removeOutput  typed.YAMLObject
    39  	extractOutput typed.YAMLObject
    40  }
    41  
    42  var simplePairSchema = `types:
    43  - name: stringPair
    44    map:
    45      fields:
    46      - name: key
    47        type:
    48          scalar: string
    49      - name: value
    50        type:
    51          namedType: __untyped_atomic_
    52  - name: __untyped_atomic_
    53    scalar: untyped
    54    list:
    55      elementType:
    56        namedType: __untyped_atomic_
    57      elementRelationship: atomic
    58    map:
    59      elementType:
    60        namedType: __untyped_atomic_
    61      elementRelationship: atomic
    62  `
    63  
    64  var structGrabBagSchema = `types:
    65  - name: myStruct
    66    map:
    67      fields:
    68      - name: numeric
    69        type:
    70          scalar: numeric
    71      - name: string
    72        type:
    73          scalar: string
    74      - name: bool
    75        type:
    76          scalar: boolean
    77      - name: setStr
    78        type:
    79          list:
    80            elementType:
    81              scalar: string
    82            elementRelationship: associative
    83      - name: setBool
    84        type:
    85          list:
    86            elementType:
    87              scalar: boolean
    88            elementRelationship: associative
    89      - name: setNumeric
    90        type:
    91          list:
    92            elementType:
    93              scalar: numeric
    94            elementRelationship: associative
    95  `
    96  
    97  var associativeAndAtomicSchema = `types:
    98  - name: myRoot
    99    map:
   100      fields:
   101      - name: list
   102        type:
   103          namedType: myList
   104      - name: atomicList
   105        type:
   106          namedType: mySequence
   107      - name: atomicMap
   108        type:
   109          namedType: myAtomicMap
   110  - name: myList
   111    list:
   112      elementType:
   113        namedType: myElement
   114      elementRelationship: associative
   115      keys:
   116      - key
   117      - id
   118  - name: myAtomicMap
   119    map:
   120      elementType:
   121        scalar: string
   122      elementRelationship: atomic
   123  - name: mySequence
   124    list:
   125      elementType:
   126        scalar: string
   127      elementRelationship: atomic
   128  - name: myElement
   129    map:
   130      fields:
   131      - name: key
   132        type:
   133          scalar: string
   134      - name: id
   135        type:
   136          scalar: numeric
   137      - name: value
   138        type:
   139          namedType: myValue
   140      - name: bv
   141        type:
   142          scalar: boolean
   143      - name: nv
   144        type:
   145          scalar: numeric
   146  - name: myValue
   147    map:
   148      elementType:
   149        scalar: string
   150  `
   151  var atomicTypesSchema = `types:
   152  - name: myRoot
   153    map:
   154      fields:
   155      - name: atomicMap
   156        type:
   157          namedType: myAtomicMap
   158      - name: atomicList
   159        type:
   160          namedType: mySequence
   161  - name: myAtomicMap
   162    map:
   163      elementType:
   164        scalar: string
   165      elementRelationship: atomic
   166  - name: mySequence
   167    list:
   168      elementType:
   169        scalar: string
   170      elementRelationship: atomic
   171  `
   172  
   173  var nestedTypesSchema = `types:
   174  - name: type
   175    map:
   176      fields:
   177        - name: listOfLists
   178          type:
   179            namedType: listOfLists
   180        - name: listOfMaps
   181          type:
   182            namedType: listOfMaps
   183        - name: mapOfLists
   184          type:
   185            namedType: mapOfLists
   186        - name: mapOfMaps
   187          type:
   188            namedType: mapOfMaps
   189        - name: mapOfMapsRecursive
   190          type:
   191            namedType: mapOfMapsRecursive
   192        - name: struct
   193          type:
   194            namedType: struct
   195  - name: struct
   196    map:
   197      fields:
   198      - name: name
   199        type:
   200          scalar: string
   201      - name: value
   202        type:
   203          scalar: number
   204  - name: listOfLists
   205    list:
   206      elementType:
   207        map:
   208          fields:
   209          - name: name
   210            type:
   211              scalar: string
   212          - name: value
   213            type:
   214              namedType: list
   215      elementRelationship: associative
   216      keys:
   217      - name
   218  - name: list
   219    list:
   220      elementType:
   221        scalar: string
   222      elementRelationship: associative
   223  - name: listOfMaps
   224    list:
   225      elementType:
   226        map:
   227          fields:
   228          - name: name
   229            type:
   230              scalar: string
   231          - name: value
   232            type:
   233              namedType: map
   234      elementRelationship: associative
   235      keys:
   236      - name
   237  - name: map
   238    map:
   239      elementType:
   240        scalar: string
   241      elementRelationship: associative
   242  - name: mapOfLists
   243    map:
   244      elementType:
   245        namedType: list
   246      elementRelationship: associative
   247  - name: mapOfMaps
   248    map:
   249      elementType:
   250        namedType: map
   251      elementRelationship: associative
   252  - name: mapOfMapsRecursive
   253    map:
   254      elementType:
   255        namedType: mapOfMapsRecursive
   256      elementRelationship: associative
   257  `
   258  
   259  var removeCases = []removeTestCase{{
   260  	name:         "simple pair",
   261  	rootTypeName: "stringPair",
   262  	schema:       typed.YAMLObject(simplePairSchema),
   263  	quadruplets: []removeQuadruplet{{
   264  		`{"key":"foo"}`,
   265  		_NS(_P("key")),
   266  		``,
   267  		`{"key":"foo"}`,
   268  	}, {
   269  		`{"key":"foo"}`,
   270  		_NS(),
   271  		`{"key":"foo"}`,
   272  		``,
   273  	}, {
   274  		`{"key":"foo","value":true}`,
   275  		_NS(_P("key")),
   276  		`{"value":true}`,
   277  		`{"key":"foo"}`,
   278  	}, {
   279  		`{"key":"foo","value":{"a": "b"}}`,
   280  		_NS(_P("value")),
   281  		`{"key":"foo"}`,
   282  		`{"value":{"a": "b"}}`,
   283  	}},
   284  }, {
   285  	name:         "struct grab bag",
   286  	rootTypeName: "myStruct",
   287  	schema:       typed.YAMLObject(structGrabBagSchema),
   288  	quadruplets: []removeQuadruplet{{
   289  		`{"setBool":[false]}`,
   290  		_NS(_P("setBool", _V(false))),
   291  		`{"setBool":null}`,
   292  		`{"setBool":[false]}`,
   293  	}, {
   294  		`{"setBool":[false]}`,
   295  		_NS(_P("setBool", _V(true))),
   296  		`{"setBool":[false]}`,
   297  		`{"setBool":null}`,
   298  	}, {
   299  		`{"setBool":[true,false]}`,
   300  		_NS(_P("setBool", _V(true))),
   301  		`{"setBool":[false]}`,
   302  		`{"setBool":[true]}`,
   303  	}, {
   304  		`{"setBool":[true,false]}`,
   305  		_NS(_P("setBool")),
   306  		``,
   307  		`{"setBool":null}`,
   308  	}, {
   309  		`{"setNumeric":[1,2,3,4.5]}`,
   310  		_NS(_P("setNumeric", _V(1)), _P("setNumeric", _V(4.5))),
   311  		`{"setNumeric":[2,3]}`,
   312  		`{"setNumeric":[1,4.5]}`,
   313  	}, {
   314  		`{"setStr":["a","b","c"]}`,
   315  		_NS(_P("setStr", _V("a"))),
   316  		`{"setStr":["b","c"]}`,
   317  		`{"setStr":["a"]}`,
   318  	}},
   319  }, {
   320  	name:         "associative and atomic",
   321  	rootTypeName: "myRoot",
   322  	schema:       typed.YAMLObject(associativeAndAtomicSchema),
   323  	quadruplets: []removeQuadruplet{{
   324  		// extract a struct from an associative list
   325  		`{"list":[{"key":"a","id":1},{"key":"a","id":2},{"key":"b","id":1}]}`,
   326  		_NS(
   327  			_P("list", _KBF("key", "a", "id", 1), "key"),
   328  			_P("list", _KBF("key", "a", "id", 1), "id"),
   329  		),
   330  		`unparseable`,
   331  		`{"list":[{"key":"a","id":1}]}`,
   332  	}, {
   333  		// remove structs from an associative list
   334  		`{"list":[{"key":"a","id":1},{"key":"a","id":2},{"key":"b","id":1}]}`,
   335  		_NS(
   336  			_P("list", _KBF("key", "a", "id", 1)),
   337  		),
   338  		`{"list":[{"key":"a","id":2},{"key":"b","id":1}]}`,
   339  		`unparseable`,
   340  	}, {
   341  		`{"atomicList":["a", "a", "a"]}`,
   342  		_NS(_P("atomicList")),
   343  		``,
   344  		// atomic lists should still return everything in the list
   345  		`{"atomicList":["a", "a", "a"]}`,
   346  	}, {
   347  		`{"atomicMap":{"a": "c", "b": "d"}}`,
   348  		_NS(_P("atomicMap")),
   349  		``,
   350  		// atomic maps should still return everything in the map
   351  		`{"atomicMap":{"a": "c", "b": "d"}}`,
   352  	}},
   353  }, {
   354  	name:         "nested types",
   355  	rootTypeName: "type",
   356  	schema:       typed.YAMLObject(nestedTypesSchema),
   357  	quadruplets: []removeQuadruplet{{
   358  		// extract everything
   359  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   360  		_NS(
   361  			_P("listOfLists", _KBF("name", "a"), "name"),
   362  			_P("listOfLists", _KBF("name", "a"), "value", _V("b")),
   363  			_P("listOfLists", _KBF("name", "a"), "value", _V("c")),
   364  			_P("listOfLists", _KBF("name", "d"), "name"),
   365  		),
   366  		`unparseable`,
   367  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   368  	}, {
   369  		// path to root type
   370  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   371  		_NS(
   372  			_P("listOfLists"),
   373  		),
   374  		``,
   375  		`{"listOfLists": null}`,
   376  	}, {
   377  		// path to a top-level element (extract)
   378  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   379  		_NS(_P("listOfLists", _KBF("name", "d"), "name")),
   380  		//`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, null]}`,
   381  		`unparseable`,
   382  		`{"listOfLists": [{"name": "d"}]}`,
   383  	}, {
   384  		// path to a top-level element (remove)
   385  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   386  		_NS(_P("listOfLists", _KBF("name", "d"))),
   387  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}]}`,
   388  		`unparseable`,
   389  	}, {
   390  		// same as previous with the other top-level element containing nested elements. (extract)
   391  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   392  		_NS(
   393  			_P("listOfLists", _KBF("name", "a"), "name"),
   394  		),
   395  		`unparseable`,
   396  		`{"listOfLists": [{"name": "a"}]}`,
   397  	}, {
   398  		// same as previous with the other top-level element containing nested elements. (remove)
   399  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   400  		_NS(
   401  			_P("listOfLists", _KBF("name", "a")),
   402  		),
   403  		`{"listOfLists": [{"name": "d"}]}`,
   404  		`unparseable`,
   405  	}, {
   406  		// just one path to leaf element
   407  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   408  		_NS(
   409  			_P("listOfLists", _KBF("name", "a"), "value", _V("b")),
   410  		),
   411  		`{"listOfLists": [{"name":"a", "value": ["c"]}, {"name": "d"}]}`,
   412  		`unparseable`, // cannot extract leaf element without path to top-level element as well
   413  	}, {
   414  		// paths to leaf and top level element
   415  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   416  		_NS(
   417  			_P("listOfLists", _KBF("name", "a"), "name"),
   418  			_P("listOfLists", _KBF("name", "a"), "value", _V("b")),
   419  		),
   420  		`unparseable`, // cannot remove a top-level list and a single element from the list within
   421  		`{"listOfLists": [{"name": "a", "value": ["b"]}]}`,
   422  	}, {
   423  		// path to non-existant top-level element
   424  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   425  		_NS(
   426  			_P("listOfLists", _KBF("name", "x")),
   427  		),
   428  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`, // doesn't remove anything
   429  		`{"listOfLists":null}`, // extract only the root type
   430  	}, {
   431  		// path with existant top-level but non-existant leaf element
   432  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   433  		_NS(
   434  			_P("listOfLists", _KBF("name", "a"), "value", _V("x")),
   435  		),
   436  		`{"listOfLists": [{"name":"a", "value": ["b","c"]}, {"name": "d"}]}`, // nothing removed since the path doesn't exist.
   437  		`unparseable`, //`{"listOfLists":[{"value":null}]}`, // unparseable because name cannot be missing
   438  	}, {
   439  		// paths with existant top-level but non-existant leaf element
   440  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   441  		_NS(
   442  			_P("listOfLists", _KBF("name", "a"), "name"),
   443  			_P("listOfLists", _KBF("name", "a"), "value", _V("x")),
   444  		),
   445  		`unparseable`, // unparseable because remove cannot operate on a top-level element and a leaf within
   446  		`unparseable`, //`{"listOfLists":[{"name: "a","value": null}]}`, // unparseable because value (list type) cannot be null
   447  	}, {
   448  		// invalid path to just a leaf
   449  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   450  		_NS(
   451  			_P(_V("b")),
   452  		),
   453  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   454  		``,
   455  	}, {
   456  		// extract everything
   457  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   458  		_NS(
   459  			_P("listOfMaps", _KBF("name", "a"), "name"),
   460  			_P("listOfMaps", _KBF("name", "a"), "value", "b"),
   461  			_P("listOfMaps", _KBF("name", "a"), "value", "c"),
   462  			_P("listOfMaps", _KBF("name", "d"), "name"),
   463  			_P("listOfMaps", _KBF("name", "d"), "value", "e"),
   464  		),
   465  		`unparseable`,
   466  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   467  	}, {
   468  		// path to root type
   469  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   470  		_NS(
   471  			_P("listOfMaps"),
   472  		),
   473  		``,
   474  		`{"listOfMaps"}`,
   475  	}, {
   476  		// path to a top-level element (extract)
   477  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   478  		_NS(
   479  			_P("listOfMaps", _KBF("name", "a"), "name"),
   480  		),
   481  		`unparseable`,
   482  		`{"listOfMaps": [{"name": "a"}]}`,
   483  	}, {
   484  		// path to a top-level element (remove)
   485  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   486  		_NS(
   487  			_P("listOfMaps", _KBF("name", "a")),
   488  		),
   489  		`{"listOfMaps": [{"name": "d", "value": {"e":"z"}}]}`,
   490  		`unparseable`,
   491  	}, {
   492  		// just one path to leaf element
   493  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   494  		_NS(
   495  			_P("listOfMaps", _KBF("name", "a"), "value", "b"),
   496  		),
   497  		`{"listOfMaps": [{"name": "a", "value": {"c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   498  		`unparseable`, // cannot extract leaf element without path to top-level element as well
   499  	}, {
   500  		// paths to leaf and top level element
   501  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   502  		_NS(
   503  			_P("listOfMaps", _KBF("name", "a"), "name"),
   504  			_P("listOfMaps", _KBF("name", "a"), "value", "b"),
   505  		),
   506  		`unparseable`, // cannot remove a top-lvel list and a single element from the list within
   507  		`{"listOfMaps": [{"name": "a", "value": {"b":"x"}}]}`,
   508  	}, {
   509  		// path to non-existant top-level element
   510  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   511  		_NS(
   512  			_P("listOfMaps", _KBF("name", "q"), "name"),
   513  		),
   514  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`, // doesn't remove anything
   515  		`{"listOfMaps":null}`, // extract only the root type
   516  	}, {
   517  		// path with existant top-level but non-existant leaf element.
   518  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   519  		_NS(
   520  			_P("listOfMaps", _KBF("name", "a"), "value", "q"),
   521  		),
   522  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`, // doesn't remove anything
   523  		`unparseable`, //`{"listOfMaps": [{"value": null}]}`, // unparseable because name cannot be missing
   524  	}, {
   525  		// paths with existant top-level but non-existant leaf element
   526  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   527  		_NS(
   528  			_P("listOfMaps", _KBF("name", "a"), "name"),
   529  			_P("listOfMaps", _KBF("name", "a"), "value", "q"),
   530  		),
   531  		`unparseable`, // unparseable because remove cannot operate on a top-level element and a leaf within
   532  		`{"listOfMaps": [{"name":"a", "value": null}]}`, // parseable because value (map type) CAN be null
   533  	}, {
   534  		// extract everything
   535  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   536  		_NS(
   537  			_P("mapOfLists", "b", _V("a")),
   538  			_P("mapOfLists", "b", _V("c")),
   539  			_P("mapOfLists", "d", _V("e")),
   540  			_P("mapOfLists", "d", _V("f")),
   541  		),
   542  		`unparseable`,
   543  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   544  	}, {
   545  		// path to root type
   546  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   547  		_NS(
   548  			_P("mapOfLists"),
   549  		),
   550  		``,
   551  		`{"mapOfLists":null}`,
   552  	}, {
   553  		// path to a top-level element
   554  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   555  		_NS(
   556  			_P("mapOfLists", "b"),
   557  		),
   558  		`{"mapOfLists": {"d":["e", "f"]}}`,
   559  		`{"mapOfLists": {"b":null}}`,
   560  	}, {
   561  		// just one path to leaf element
   562  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   563  		_NS(
   564  			_P("mapOfLists", "b", _V("a")),
   565  		),
   566  		`{"mapOfLists":{"b":["c"],"d":["e", "f"]}}`,
   567  		`{"mapOfLists":{"b":["a"]}}`,
   568  	}, {
   569  		// path to non-existant top-level element
   570  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   571  		_NS(
   572  			_P("mapOfLists", "q"),
   573  		),
   574  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   575  		`{"mapOfLists":null}`,
   576  	}, {
   577  		// path with existant top-level but non-existant leaf element
   578  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   579  		_NS(
   580  			_P("mapOfLists", "b", _V("q")),
   581  		),
   582  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   583  		`{"mapOfLists":{"b":null}}`,
   584  	}, {
   585  		// path with existant top-level but non-existant leaf element
   586  		`{"mapOfLists": {"b":null, "d":["e", "f"]}}`,
   587  		_NS(
   588  			_P("mapOfLists", "b"),
   589  		),
   590  		`{"mapOfLists": {"d":["e", "f"]}}`,
   591  		`{"mapOfLists":{"b":null}}`, // same output as previous case, but can be differentiated by input fieldpath.Set
   592  	}, {
   593  		// invalid path to just a leaf
   594  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   595  		_NS(
   596  			_P(_V("a")),
   597  		),
   598  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   599  		``,
   600  	}, {
   601  		// extract everything
   602  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   603  		_NS(
   604  			_P("mapOfMaps", "b", "a"),
   605  			_P("mapOfMaps", "b", "c"),
   606  			_P("mapOfMaps", "d", "e"),
   607  			_P("mapOfMaps", "d", "f"),
   608  		),
   609  		`unparseable`,
   610  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   611  	}, {
   612  		// path to root type
   613  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   614  		_NS(
   615  			_P("mapOfMaps"),
   616  		),
   617  		``,
   618  		`{"mapOfMaps":null}`,
   619  	}, {
   620  		// path to a top-level element
   621  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   622  		_NS(
   623  			_P("mapOfMaps", "b"),
   624  		),
   625  		`{"mapOfMaps": {"d":{"e":"y", "f":"w"}}}`,
   626  		`{"mapOfMaps": {"b":null}}`,
   627  	}, {
   628  		// just one path to leaf element
   629  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   630  		_NS(
   631  			_P("mapOfMaps", "b", "a"),
   632  		),
   633  		`{"mapOfMaps": {"b":{"c":"z"},"d":{"e":"y", "f":"w"}}}`,
   634  		`{"mapOfMaps": {"b":{"a":"x"}}}`,
   635  	}, {
   636  		// path to non-existant top-level element
   637  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   638  		_NS(
   639  			_P("mapOfMaps", "q"),
   640  		),
   641  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   642  		`{"mapOfMaps": null}`,
   643  	}, {
   644  		// path with existant top-level but non-existant leaf element
   645  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   646  		_NS(
   647  			_P("mapOfMaps", "b", "q"),
   648  		),
   649  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   650  		`{"mapOfMaps": {"b":null}}`,
   651  	}, {
   652  		// top-level element with null leaf elements
   653  		`{"mapOfMaps": {"b":null, "d":{"e":"y", "f":"w"}}}`,
   654  		_NS(
   655  			_P("mapOfMaps", "b"),
   656  		),
   657  		`{"mapOfMaps": {"d":{"e":"y", "f":"w"}}}`,
   658  		`{"mapOfMaps": {"b":null}}`, // same output as previous case, but can be differentiated by input fieldpath.Set
   659  	}, {
   660  		// invalid path to just a leaf
   661  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   662  		_NS(
   663  			_P("a"),
   664  		),
   665  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   666  		``,
   667  	}, {
   668  		// root element
   669  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   670  		_NS(
   671  			_P("mapOfMapsRecursive"),
   672  		),
   673  		``,
   674  		`{"mapOfMapsRecursive":null}`,
   675  	}, {
   676  		// top-level map
   677  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   678  		_NS(
   679  			_P("mapOfMapsRecursive", "a"),
   680  		),
   681  		`{"mapOfMapsRecursive"}`,
   682  		`{"mapOfMapsRecursive": {"a":null}}`,
   683  	}, {
   684  		// second-level map
   685  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   686  		_NS(
   687  			_P("mapOfMapsRecursive", "a", "b"),
   688  		),
   689  		`{"mapOfMapsRecursive":{"a":null}}`,
   690  		`{"mapOfMapsRecursive": {"a":{"b":null}}}`,
   691  	}, {
   692  		// third-level map
   693  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   694  		_NS(
   695  			_P("mapOfMapsRecursive", "a", "b", "c"),
   696  		),
   697  		`{"mapOfMapsRecursive":{"a":{"b":null}}}`,
   698  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   699  	}},
   700  }}
   701  
   702  func (tt removeTestCase) test(t *testing.T) {
   703  	parser, err := typed.NewParser(tt.schema)
   704  	if err != nil {
   705  		t.Fatalf("failed to create schema: %v", err)
   706  	}
   707  	pt := parser.Type(tt.rootTypeName)
   708  
   709  	for i, quadruplet := range tt.quadruplets {
   710  		quadruplet := quadruplet
   711  		t.Run(fmt.Sprintf("%v-valid-%v", tt.name, i), func(t *testing.T) {
   712  			t.Parallel()
   713  
   714  			tv, err := pt.FromYAML(quadruplet.object)
   715  			if err != nil {
   716  				t.Fatalf("unable to parser/validate object yaml: %v\n%v", err, quadruplet.object)
   717  			}
   718  
   719  			// test RemoveItems
   720  			if quadruplet.removeOutput != "unparseable" {
   721  				rmOut, err := pt.FromYAML(quadruplet.removeOutput)
   722  				if err != nil {
   723  					t.Fatalf("unable to parser/validate removeOutput yaml: %v\n%v", err, quadruplet.removeOutput)
   724  				}
   725  
   726  				rmGot := tv.RemoveItems(quadruplet.set)
   727  				if !value.Equals(rmGot.AsValue(), rmOut.AsValue()) {
   728  					t.Errorf("RemoveItems expected\n%v\nbut got\n%v\n",
   729  						value.ToString(rmOut.AsValue()), value.ToString(rmGot.AsValue()),
   730  					)
   731  				}
   732  			}
   733  
   734  			// test ExtractItems
   735  			if quadruplet.extractOutput != "unparseable" {
   736  				exOut, err := pt.FromYAML(quadruplet.extractOutput)
   737  				if err != nil {
   738  					t.Fatalf("unable to parser/validate extractOutput yaml: %v\n%v", err, quadruplet.extractOutput)
   739  				}
   740  				exGot := tv.ExtractItems(quadruplet.set)
   741  				if !value.Equals(exGot.AsValue(), exOut.AsValue()) {
   742  					t.Errorf("ExtractItems expected\n%v\nbut got\n%v\n",
   743  						value.ToString(exOut.AsValue()), value.ToString(exGot.AsValue()),
   744  					)
   745  				}
   746  
   747  			}
   748  		})
   749  	}
   750  }
   751  
   752  func TestRemove(t *testing.T) {
   753  	for _, tt := range removeCases {
   754  		tt := tt
   755  		t.Run(tt.name, func(t *testing.T) {
   756  			t.Parallel()
   757  			tt.test(t)
   758  		})
   759  	}
   760  }
   761  
   762  type reversibleExtractTestCase struct {
   763  	name         string
   764  	rootTypeName string
   765  	schema       typed.YAMLObject
   766  	pairs        []reversibleExtractPair
   767  }
   768  
   769  type reversibleExtractPair struct {
   770  	object typed.YAMLObject
   771  	pso    typed.YAMLObject
   772  }
   773  
   774  var reversibleExtractCases = []reversibleExtractTestCase{{
   775  	name:         "nested types",
   776  	rootTypeName: "type",
   777  	schema:       typed.YAMLObject(nestedTypesSchema),
   778  	pairs: []reversibleExtractPair{{
   779  		// add to top level element
   780  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   781  		`{"listOfLists": [{"name": "f", "value": ["j", "k"]},]}`,
   782  	}, {
   783  		// add to leaf element
   784  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   785  		`{"listOfLists": [{"name": "a", "value": ["j", "k"]},]}`,
   786  	}, {
   787  		// apply empty structure
   788  		`{"listOfLists": [{"name": "a", "value": ["b", "c"]}, {"name": "d"}]}`,
   789  		`{"listOfLists": [{"name": "a", "value": null},]}`,
   790  	}, {
   791  		// add to top level element
   792  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   793  		`{"listOfMaps": [{"name": "f", "value": {"q":"p"}}]}`,
   794  	}, {
   795  		// add to leaf element
   796  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   797  		`{"listOfMaps": [{"name": "a", "value": {"f":"p"}}]}`,
   798  	}, {
   799  		// replace leaf element
   800  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   801  		`{"listOfMaps": [{"name": "a", "value": {"b":"p"}}]}`,
   802  	}, {
   803  		// apply empty structure
   804  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   805  		`{"listOfMaps": [{"name": "a", "value": null}]}`,
   806  	}, {
   807  		// add to top level element
   808  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   809  		`{"mapOfLists": {"x":["y","z"]}}`,
   810  	}, {
   811  		// add to leaf element
   812  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   813  		`{"mapOfLists": {"b":["y","z"]}}`,
   814  	}, {
   815  		// apply empty structure
   816  		`{"mapOfLists": {"b":["a","c"], "d":["e", "f"]}}`,
   817  		`{"mapOfLists": {"b":null}}`,
   818  	}, {
   819  		// add to top level element
   820  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   821  		`{"mapOfMaps": {"i":{"j":"k"}}}`,
   822  	}, {
   823  		// add to leaf element
   824  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   825  		`{"mapOfMaps": {"b":{"j":"k"}}}`,
   826  	}, {
   827  		// replace leaf element
   828  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   829  		`{"mapOfMaps": {"b":{"a":"k"}}}`,
   830  	}, {
   831  		// apply empty structure
   832  		`{"mapOfMaps": {"b":{"a":"x","c":"z"}, "d":{"e":"y", "f":"w"}}}`,
   833  		`{"mapOfMaps": {"b": null}}`,
   834  	}, {
   835  		// misc: add another root type
   836  		`{"listOfMaps": [{"name": "a", "value": {"b":"x", "c":"y"}}, {"name": "d", "value": {"e":"z"}}]}`,
   837  		`{"mapOfLists": {"b":["y","z"]}}`,
   838  	}, {
   839  		// misc: recursive deeply nested leaves
   840  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}, "d":{"e":{"f":null}, "g":null}}}}`,
   841  		`{"mapOfMapsRecursive": {"a":{"d":{"e":{"f":{"q":null}, "p":null}}}}}`,
   842  	}, {
   843  		// misc: recursive deeply nested empty structure
   844  		`{"mapOfMapsRecursive": {"a":{"b":{"c":{"d":{"e":{"f":null}}, "g":{"h":null}, "i":null}}}}}`,
   845  		`{"mapOfMapsRecursive": {"a":{"b":{"c":null}}}}`,
   846  	}},
   847  }}
   848  
   849  func (tt reversibleExtractTestCase) test(t *testing.T) {
   850  	parser, err := typed.NewParser(tt.schema)
   851  	if err != nil {
   852  		t.Fatalf("failed to create schema: %v", err)
   853  	}
   854  	pt := parser.Type(tt.rootTypeName)
   855  
   856  	for i, pair := range tt.pairs {
   857  		pair := pair
   858  		t.Run(fmt.Sprintf("%v-valid-%v", tt.name, i), func(t *testing.T) {
   859  			t.Parallel()
   860  			// Generate initial typed obj
   861  			initialObj, err := pt.FromYAML(pair.object)
   862  			if err != nil {
   863  				t.Fatalf("unable to parser/validate initial object yaml: %v\n%v", err, pair.object)
   864  			}
   865  			// Generate PSO
   866  			pso, err := pt.FromYAML(pair.pso)
   867  			if err != nil {
   868  				t.Fatalf("unable to parser/validate PSO yaml: %v\n%v", err, pair.pso)
   869  			}
   870  			// Merge PSO with base object
   871  			mergedObj, err := initialObj.Merge(pso)
   872  			if err != nil {
   873  				t.Fatalf("unable to merge PSO into initial object: %v\n", err)
   874  			}
   875  			// convert PSO to fieldset
   876  			fieldSet, err := pso.ToFieldSet()
   877  			if err != nil {
   878  				t.Fatalf("unable to convert pso to fieldset: %v\n%v", err, pso)
   879  			}
   880  			// trying to extract the fieldSet directly will return everything
   881  			// under the first path in the set, so we must filter out all
   882  			// the non-leaf nodes from the fieldSet
   883  			extractSet := fieldSet.Leaves()
   884  			// extract  PSO fieldset from result object
   885  			extracted := mergedObj.ExtractItems(extractSet)
   886  			// confirm extract object is initial PSO
   887  			if !value.Equals(pso.AsValue(), extracted.AsValue()) {
   888  				t.Errorf("ExtractItems not reversible expected\n%v\nbut got\n%v\n",
   889  					value.ToString(pso.AsValue()), value.ToString(extracted.AsValue()),
   890  				)
   891  			}
   892  		})
   893  	}
   894  }
   895  
   896  // TestReversibleExtract ensures that when you apply a
   897  // partially specified object (PSO) to an existing object
   898  // and then Extract the fieldset from the resulting object
   899  // you receive back the initial partially specified object.
   900  func TestReversibleExtract(t *testing.T) {
   901  	for _, tt := range reversibleExtractCases {
   902  		tt := tt
   903  		t.Run(tt.name, func(t *testing.T) {
   904  			t.Parallel()
   905  			tt.test(t)
   906  		})
   907  	}
   908  }
   909  

View as plain text