diff --git a/MODULE.bazel b/MODULE.bazel index fb1a4fd6..ee54e0c0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,6 +21,7 @@ bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True) bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) bazel_dep(name = "rules_cc", version = "0.0.17", dev_dependency = True) bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True) +bazel_dep(name = "bazel_features", version = "1.32.0", dev_dependency = True) # Needed for bazelci and for building distribution tarballs. # If using an unreleased version of bazel_skylib via git_override, apply diff --git a/WORKSPACE b/WORKSPACE index d9d5d66b..a2fb9f2f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -44,6 +44,17 @@ rules_shell_dependencies() rules_shell_toolchains() +http_archive( + name = "bazel_features", + sha256 = "07bd2b18764cdee1e0d6ff42c9c0a6111ffcbd0c17f0de38e7f44f1519d1c0cd", + strip_prefix = "bazel_features-1.32.0", + url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.32.0/bazel_features-v1.32.0.tar.gz", +) + +load("@bazel_features//:deps.bzl", "bazel_features_deps") + +bazel_features_deps() + maybe( http_archive, name = "io_bazel_stardoc", diff --git a/docs/structs_doc.md b/docs/structs_doc.md index 00585f55..78db98eb 100755 --- a/docs/structs_doc.md +++ b/docs/structs_doc.md @@ -2,6 +2,31 @@ Skylib module containing functions that operate on structs. + + +## structs.merge + +
+load("@bazel_skylib//lib:structs.bzl", "structs")
+
+structs.merge(first, *rest)
+
+ +Merges multiple `struct` instances together. Later `struct` keys overwrite early `struct` keys. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| first | The initial `struct` to merge keys/values into. | none | +| rest | Other `struct` instances to merge. | none | + +**RETURNS** + +A merged `struct`. + + ## structs.to_dict diff --git a/lib/structs.bzl b/lib/structs.bzl index 78066ad9..b0335858 100644 --- a/lib/structs.bzl +++ b/lib/structs.bzl @@ -14,6 +14,20 @@ """Skylib module containing functions that operate on structs.""" +_built_in_function = type(str) + +def _is_built_in_function(v): + """Returns True if v is an instance of a built-in function. + + Args: + v: The value whose type should be checked. + + Returns: + True if v is an instance of a built-in function, False otherwise. + """ + + return type(v) == _built_in_function + def _to_dict(s): """Converts a `struct` to a `dict`. @@ -31,9 +45,25 @@ def _to_dict(s): return { key: getattr(s, key) for key in dir(s) - if key != "to_json" and key != "to_proto" + if not ((key == "to_json" or key == "to_proto") and _is_built_in_function(getattr(s, key))) } +def _merge(first, *rest): + """Merges multiple `struct` instances together. Later `struct` keys overwrite early `struct` keys. + + Args: + first: The initial `struct` to merge keys/values into. + *rest: Other `struct` instances to merge. + + Returns: + A merged `struct`. + """ + map = _to_dict(first) + for r in rest: + map |= _to_dict(r) + return struct(**map) + structs = struct( to_dict = _to_dict, + merge = _merge, ) diff --git a/tests/structs_tests.bzl b/tests/structs_tests.bzl index 79de7ad1..8313d32f 100644 --- a/tests/structs_tests.bzl +++ b/tests/structs_tests.bzl @@ -14,11 +14,15 @@ """Unit tests for structs.bzl.""" +load("@bazel_features//:features.bzl", "bazel_features") load("//lib:structs.bzl", "structs") load("//lib:unittest.bzl", "asserts", "unittest") -def _add_test(ctx): - """Unit tests for dicts.add.""" +def _placeholder(): + pass + +def _to_dict_test(ctx): + """Unit tests for structs.to_dict.""" env = unittest.begin(ctx) # Test zero- and one-argument behavior. @@ -42,13 +46,55 @@ def _add_test(ctx): structs.to_dict(struct(a = 1, b = struct(bb = 1))), ) + # Older Bazel denied creating `struct` with `to_json`/`to_proto` + if not bazel_features.rules.no_struct_field_denylist: + return unittest.end(env) + + # Test `to_json`/`to_proto` values are propagated + asserts.equals( + env, + {"to_json": 1, "to_proto": 2}, + structs.to_dict(struct(to_json = 1, to_proto = 2)), + ) + + # Test `to_json`/`to_proto` functions are propagated + asserts.equals( + env, + {"to_json": _placeholder, "to_proto": _placeholder}, + structs.to_dict(struct(to_json = _placeholder, to_proto = _placeholder)), + ) + + return unittest.end(env) + +to_dict_test = unittest.make(_to_dict_test) + +def _merge_test(ctx): + """Unit tests for structs.merge.""" + env = unittest.begin(ctx) + + # Fixtures + a = struct(a = 1) + b = struct(b = 2) + c = struct(a = 3) + + # Test one argument + asserts.equals(env, {"a": 1}, structs.to_dict(structs.merge(a))) + + # Test two arguments + asserts.equals(env, {"a": 1}, structs.to_dict(structs.merge(a, a))) + asserts.equals(env, {"a": 1, "b": 2}, structs.to_dict(structs.merge(a, b))) + + # Test overwrite + asserts.equals(env, {"a": 3}, structs.to_dict(structs.merge(a, c))) + return unittest.end(env) -add_test = unittest.make(_add_test) +merge_test = unittest.make(_merge_test) def structs_test_suite(): """Creates the test targets and test suite for structs.bzl tests.""" unittest.suite( "structs_tests", - add_test, + to_dict_test, + merge_test, )