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,
)