Skip to content

Commit c122832

Browse files
committed
Refactor attributes_not_null into attributes_validation CHECK function to better handle reference attributes check; Add heatingdemand_profile_is_set validation rule to the optimizer schema
1 parent 4afd0e0 commit c122832

4 files changed

Lines changed: 301 additions & 78 deletions

File tree

esdlvalidator/validation/functions/check_attributes_not_null.py

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import jsonschema
2+
from jsonschema.exceptions import ValidationError
3+
4+
from esdlvalidator.validation.functions import utils
5+
from esdlvalidator.validation.functions.function import (
6+
FunctionFactory,
7+
FunctionCheck,
8+
FunctionDefinition,
9+
ArgDefinition,
10+
FunctionType,
11+
CheckResult,
12+
)
13+
14+
from pyecore.ecore import EOrderedSet
15+
16+
17+
@FunctionFactory.register(FunctionType.CHECK, "attributes_validation")
18+
class AttributesValidation(FunctionCheck):
19+
"""
20+
Validates attributes of an ESDL entity or its nested reference.
21+
22+
This function performs two types of checks:
23+
1. Null checks: Ensures specified attributes are set and not equal to any value listed in `count_as_null`.
24+
2. Validity checks: Ensures specified attributes match expected values defined in `count_as_valid`.
25+
Only string values are supported for validity checks.
26+
27+
Supports checking attributes directly on the entity or through a reference path, including filtered lists.
28+
"""
29+
30+
def get_function_definition(self):
31+
return FunctionDefinition(
32+
"attributes_validation",
33+
"Check attributes of an ESDL entity or its nested reference.",
34+
[
35+
ArgDefinition(
36+
"ref",
37+
"An optional dictionary specifying the reference path (Dot-separated string. For instance costInformation.investmentCosts), "
38+
"an optional filter can be specified to locate the target reference entity when reference is of EOrderedSet type.",
39+
False,
40+
),
41+
ArgDefinition(
42+
"null_checks",
43+
"A list of dictionaries with keys:"
44+
" - attribute: the attribute name to check"
45+
" - count_as_null: list of values considered null (e.g., 0.0).",
46+
True,
47+
),
48+
ArgDefinition(
49+
"valid_checks",
50+
"A list of dictionaries with keys: "
51+
" - attribute: the attribute name to validate"
52+
" - count_as_valid: either a string or list of strings considered valid",
53+
True,
54+
),
55+
ArgDefinition("resultMsgJSON", "If True, returns results in structured JSON format.", False),
56+
ArgDefinition("resultMsgAtParentNode", "If True, attaches result to the parent entity's ID.", False),
57+
],
58+
)
59+
60+
NOT_FOUND = "Not found"
61+
UNSET = "Unset"
62+
63+
args_schema = {
64+
"type": "object",
65+
"required": ["null_checks", "valid_checks"],
66+
"properties": {
67+
# If ref is not specified, it refers to the input entity.
68+
"ref": {
69+
"type": "object",
70+
"required": ["path"],
71+
"properties": {
72+
"path": {
73+
"type": "string",
74+
"description": "Dot-separated reference path (e.g., costInformation.investmentCosts)",
75+
},
76+
# To locate the target reference entity when reference is of EOrderedSet type.
77+
"ref_list_filter": {
78+
"type": "object",
79+
"required": ["is_type"],
80+
"properties": {
81+
"is_type": {"type": "string"},
82+
"match": {
83+
"type": "object",
84+
"description": "Optional attribute filters",
85+
},
86+
},
87+
},
88+
},
89+
"additionalProperties": False,
90+
},
91+
"null_checks": {
92+
"type": "array",
93+
"items": {"$ref": "#/$defs/nullCheckItem"},
94+
},
95+
"valid_checks": {
96+
"type": "array",
97+
"items": {"$ref": "#/$defs/validCheckItem"},
98+
},
99+
"resultMsgJSON": {"type": "boolean"},
100+
"resultMsgAtParentNode": {"type": "boolean"},
101+
},
102+
"additionalProperties": False,
103+
"$defs": {
104+
"nullCheckItem": {
105+
"type": "object",
106+
"required": ["attribute", "count_as_null"],
107+
"properties": {
108+
"attribute": {"type": "string", "description": "Attribute name to check for null-like values"},
109+
"count_as_null": {
110+
"type": "array",
111+
"items": {"anyOf": [{"type": "string"}, {"type": "number"}]},
112+
"description": "Values considered null; can be empty",
113+
},
114+
},
115+
"additionalProperties": False,
116+
},
117+
"validCheckItem": {
118+
"type": "object",
119+
"required": ["attribute", "count_as_valid"],
120+
"properties": {
121+
"attribute": {"type": "string", "description": "Attribute name to validate"},
122+
"count_as_valid": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
123+
},
124+
"additionalProperties": False,
125+
},
126+
},
127+
}
128+
129+
def execute(self):
130+
entity = self.value
131+
args_dict = self.args
132+
results = []
133+
134+
try:
135+
jsonschema.validate(instance=args_dict, schema=self.args_schema)
136+
except ValidationError as e:
137+
raise ValueError(f"Schema validation failed at {list(e.path)}: {e.message}")
138+
139+
ref = utils.get_attribute(self.args, "ref")
140+
null_checks = utils.get_attribute(self.args, "null_checks")
141+
valid_checks = utils.get_attribute(self.args, "valid_checks")
142+
resultMsgJSON = utils.get_attribute(self.args, "resultMsgJSON")
143+
resultMsgAtParentNode = utils.get_attribute(self.args, "resultMsgAtParentNode")
144+
145+
entity_to_check = self._resolve_reference(entity, ref, results)
146+
147+
if entity_to_check:
148+
results.extend(self._run_null_checks(entity_to_check, null_checks))
149+
results.extend(self._run_valid_checks(entity_to_check, valid_checks))
150+
151+
if results:
152+
target_id = (
153+
utils.get_attribute(entity.eContainer(), "id")
154+
if resultMsgAtParentNode
155+
else utils.get_attribute(entity, "id")
156+
)
157+
msg = {"offending_asset": target_id, "message": results} if resultMsgJSON else results
158+
return CheckResult(False, msg)
159+
160+
return CheckResult(True)
161+
162+
def _resolve_reference(self, entity: any, ref: dict, results: list):
163+
if not ref:
164+
return entity
165+
166+
ref_path = ref["path"]
167+
reference = utils.get_attr_or_ref_attr(entity, ref_path)
168+
169+
if reference == self.NOT_FOUND:
170+
raise ValueError(f"[{ref_path}] not found.")
171+
if reference == self.UNSET:
172+
results.append(f"[{ref_path}] should be defined, but is unset.")
173+
return None
174+
175+
if isinstance(reference, EOrderedSet):
176+
if len(reference) > 1 and ref.get("ref_list_filter"):
177+
entity_to_check = utils.get_ref(reference, ref["ref_list_filter"])
178+
else:
179+
entity_to_check = reference[0]
180+
181+
if entity_to_check is None:
182+
match_str = ", ".join(f"[{k}] = '{v}'" for k, v in ref["ref_list_filter"].get("match", {}).items())
183+
results.append(
184+
f"[{ref_path}] should contain a [{ref['ref_list_filter']['is_type']}] ({match_str}), but not found."
185+
)
186+
return entity_to_check
187+
188+
return reference
189+
190+
def _run_null_checks(self, entity: any, checks: list):
191+
results = []
192+
for check in checks:
193+
attr = check["attribute"]
194+
null_values = check["count_as_null"]
195+
value = utils.get_attr_or_ref_attr(entity, attr)
196+
197+
if value == self.NOT_FOUND:
198+
raise ValueError(f"Attribute [{attr}] not found.")
199+
if value == self.UNSET:
200+
results.append(f"[{attr}] should be defined, but is unset.")
201+
else:
202+
for null_val in null_values:
203+
if (isinstance(null_val, str) and str(null_val).lower() == str(value).lower()) or null_val == value:
204+
results.append(f"[{attr}] value cannot be {null_val}.")
205+
break
206+
return results
207+
208+
def _run_valid_checks(self, entity: object, checks: list):
209+
results = []
210+
for check in checks:
211+
attr = check["attribute"]
212+
valid_values = check["count_as_valid"]
213+
valid_list = valid_values if isinstance(valid_values, list) else [valid_values]
214+
215+
value = utils.get_attr_or_ref_attr(entity, attr)
216+
217+
if value == self.NOT_FOUND:
218+
raise ValueError(f"Attribute [{attr}] not found.")
219+
220+
if value == self.UNSET:
221+
if self.UNSET not in valid_list:
222+
results.append(
223+
f"[{attr}] should be {'one of ' if len(valid_list) > 1 else ''}[{', '.join(valid_list)}], but is unset."
224+
)
225+
else:
226+
value_str = getattr(value, "name", str(value)).lower()
227+
if value_str not in [v.lower() for v in valid_list]:
228+
results.append(
229+
f"[{attr}] should be {'one of ' if len(valid_list) > 1 else ''}[{', '.join(valid_list)}], but found [{value_str}]."
230+
)
231+
return results

esdlvalidator/validation/functions/utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import builtins
22

3-
from pyecore.ecore import EValue
3+
from esdlvalidator.core.esdl import utils as esdlUtils
4+
from pyecore.ecore import EValue, EOrderedSet
45

56

67
def get_attr_or_ref_attr(obj, attr_path: str):
@@ -35,6 +36,31 @@ def get_attr_or_ref_attr(obj, attr_path: str):
3536
return get_attr_or_ref_attr(value, remaining[0])
3637

3738

39+
def get_ref(references: EOrderedSet, ref_filter: dict):
40+
"""
41+
Filters a set of ESDL references to find the first entity matching the given type and attribute criteria.
42+
43+
Args:
44+
references: A collection of ESDL entities.
45+
ref_filter: A dictionary with keys:
46+
- 'is_type': the expected ESDL type as a string.
47+
- 'match': [optional] a dictionary of attribute names and expected values.
48+
49+
Returns:
50+
The first matching entity, or None if no match is found.
51+
"""
52+
ref_type = ref_filter["is_type"]
53+
match_dict = ref_filter.get("match", {})
54+
esdl_type = esdlUtils.get_esdl_class_from_string(ref_type)[0]
55+
56+
for entity in references:
57+
if not isinstance(entity, esdl_type):
58+
continue
59+
if all(get_attr_or_ref_attr(entity, key) == value for key, value in match_dict.items()):
60+
return entity
61+
62+
return None
63+
3864
def has_attribute(obj, name: str) -> bool:
3965
# give a default "nothing_found" since None can be the actual returned value
4066
result = get_attribute(obj, name, "nothing_found")

0 commit comments

Comments
 (0)