diff --git a/capellambse_context_diagrams/collectors/custom.py b/capellambse_context_diagrams/collectors/custom.py index 7787445..06d2e14 100644 --- a/capellambse_context_diagrams/collectors/custom.py +++ b/capellambse_context_diagrams/collectors/custom.py @@ -4,10 +4,12 @@ """This module defines the collector for the CustomDiagram.""" from __future__ import annotations +import builtins import collections.abc as cabc import copy import typing as t +import capellambse import capellambse.model as m from .. import _elkjs, context @@ -116,9 +118,98 @@ def _fix_box_heights(self) -> None: box = self.boxes[uuid] box.height = max([box.height] + list(min_heights.values())) + def _safely_eval_filter(self, obj: m.ModelElement, filter: str) -> bool: + if not filter.startswith("lambda"): + raise ValueError(f"Filter '{filter}' is not a lambda expression.") + + safe_builtins = { + "abs", + "all", + "any", + "ascii", + "bin", + "bool", + "bytearray", + "bytes", + "callable", + "chr", + "classmethod", + "complex", + "dict", + "divmod", + "enumerate", + "filter", + "float", + "format", + "frozenset", + "getattr", + "hasattr", + "hash", + "hex", + "id", + "int", + "isinstance", + "issubclass", + "iter", + "len", + "list", + "map", + "max", + "memoryview", + "min", + "next", + "object", + "oct", + "ord", + "pow", + "print", + "property", + "range", + "repr", + "reversed", + "round", + "set", + "slice", + "sorted", + "staticmethod", + "str", + "sum", + "tuple", + "type", + "vars", + "zip", + } + allowed_builtins = { + name: getattr(builtins, name) for name in safe_builtins + } + allowed_builtins.update( + { + "True": True, + "False": False, + "capellambse": capellambse, + } + ) + + try: + # pylint: disable=eval-used + result = eval(filter, {"__builtins__": allowed_builtins})(obj) + except Exception as e: + raise ValueError( + f"Filter '{filter}' raised an exception: {e}" + ) from e + + if not isinstance(result, bool): + raise ValueError( + f"Filter '{filter}' did not return a boolean value." + ) + + return result + def _matches_filters( - self, obj: m.ModelElement, filters: dict[str, t.Any] + self, obj: m.ModelElement, filters: dict[str, t.Any] | str ) -> bool: + if isinstance(filters, str): + return self._safely_eval_filter(obj, filters) for key, value in filters.items(): if getattr(obj, key) != value: return False diff --git a/docs/custom_diagram.md b/docs/custom_diagram.md index 3c175a1..0a49da5 100644 --- a/docs/custom_diagram.md +++ b/docs/custom_diagram.md @@ -34,7 +34,7 @@ In the example above, we first `get` all the inputs of our target element and it ### `filter` -Whenever you have a list of elements and you want to filter them, you can use the `filter` keyword. The `filter` keyword takes a dictionary as an argument. The dictionary should have the key as the attribute name and the value as the value you want to filter on. +Whenever you have a list of elements and you want to filter them, you can use the `filter` keyword. The `filter` keyword takes a dictionary or a string as an argument. The dictionary should have the key as the attribute name and the value as the value you want to filter on. ```yaml get: @@ -47,6 +47,18 @@ get: In the example above, we get all the inputs of our target element and include all the exchanges that are of kind `FunctionalExchange` in the resulting diagram. +For a string, the filter should be a lambda expression that takes the element as an argument and returns a boolean. + +```yaml +get: + - name: inputs + filter: "lambda x: isinstance(x, capellambse.metamodel.fa.FunctionPort)" + include: + - name: exchanges +``` + +In the example above, we get all the inputs that are of type `FunctionPort` and include all it's exchanges in the resulting diagram. + ### `repeat` With the `repeat` keyword, you can repeat the collection. The value of `repeat` should be an integer. If the value is -1, the collection will repeat until no new elements are found. If the value is 0, the collection will not repeat. If the value is 1, the collection will repeat once and so on.