Skip to content

Commit d10dd4c

Browse files
config validator gives verbose result now
1 parent 336f90d commit d10dd4c

File tree

12 files changed

+116
-26
lines changed

12 files changed

+116
-26
lines changed

index.html

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,28 @@ <h3>Visualiser Output</h3>
187187
const jsonInput = document.getElementById("jsonfile");
188188
const runBtn = document.getElementById("run");
189189

190-
function log(text) {
191-
let cleanText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
192-
// Handle ANSI Colors for Pydantic output
193-
cleanText = cleanText
194-
.replace(/\x1b\[92m/g, '<span class="ansi-green">')
195-
.replace(/\x1b\[93m/g, '<span class="ansi-yellow">')
196-
.replace(/\x1b\[91m/g, '<span class="ansi-red">')
197-
.replace(/\x1b\[0m/g, '</span>');
190+
function log(text) {
191+
let cleanText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
192+
// ANSI color replacements
193+
cleanText = cleanText
194+
.replace(/\x1b\[92m/g, '<span class="ansi-grey">') // validator/method green
195+
.replace(/\x1b\[93m/g, '<span class="ansi-yellow">') // general yellow
196+
.replace(/\x1b\[34m/g, '<span style="color:#005eb8;font-weight:bold">') // blue
197+
.replace(/\x1b\[35m/g, '<span style="color:#800080;font-weight:bold">') // magenta
198+
.replace(/\x1b\[36m/g, '<span style="color:#008080;font-weight:bold">') // cyan
199+
.replace(/\x1b\[94m/g, '<span style="color:#1E90FF;font-weight:bold">') // light blue
200+
.replace(/\x1b\[95m/g, '<span style="color:#EE82EE;font-weight:bold">') // light magenta
201+
.replace(/\x1b\[96m/g, '<span style="color:#20B2AA;font-weight:bold">') // light cyan
202+
.replace(/\x1b\[37m/g, '<span style="color:#CCCCCC;font-weight:bold">') // white/light grey
203+
.replace(/\x1b\[33m/g, '<span class="ansi-yellow">') // colon yellow
204+
.replace(/\x1b\[0m/g, '</span>'); // reset
198205

199-
output.innerHTML += cleanText + "\n";
200-
output.scrollTop = output.scrollHeight;
206+
if (cleanText.includes("Valid Config")) {
207+
cleanText = cleanText.replace(/Valid Config/g, '<span style="font-size:2em;font-weight:bold;color:#007f3b">Valid Config</span>');
201208
}
209+
output.innerHTML += cleanText + "\n";
210+
output.scrollTop = output.scrollHeight;
211+
}
202212

203213
function clearLog() { output.innerHTML = ""; } // Changed to innerHTML for spans
204214

@@ -217,6 +227,8 @@ <h3>Visualiser Output</h3>
217227
"src/eligibility_signposting_api/model/campaign_config.py",
218228
"src/eligibility_signposting_api/config/__init__.py",
219229
"src/eligibility_signposting_api/config/constants.py",
230+
"src/rules_validation_api/decorators/__init__.py",
231+
"src/rules_validation_api/decorators/tracker.py",
220232
"src/rules_validation_api/__init__.py",
221233
"src/rules_validation_api/validators/__init__.py",
222234
"src/rules_validation_api/validators/rules_validator.py",

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ class Iteration(BaseModel):
269269

270270
model_config = {"populate_by_name": True, "arbitrary_types_allowed": True, "extra": "ignore"}
271271

272+
def __init__(self, **data: dict[str, typing.Any]) -> None:
273+
super().__init__(**data)
274+
# Ensure each rule knows its parent iteration
275+
for rule in self.iteration_rules:
276+
rule.set_parent(self)
277+
272278
@field_validator("iteration_date", mode="before")
273279
@classmethod
274280
def parse_dates(cls, v: str | date) -> date:
@@ -292,12 +298,6 @@ def parse_dates(cls, v: str | date) -> date:
292298
def serialize_dates(v: date, _info: SerializationInfo) -> str:
293299
return v.strftime("%Y%m%d")
294300

295-
@model_validator(mode="after")
296-
def attach_rule_parents(self) -> Iteration:
297-
for rule in self.iteration_rules:
298-
rule.set_parent(self)
299-
return self
300-
301301
def __str__(self) -> str:
302302
return json.dumps(self.model_dump(by_alias=True), indent=2)
303303

src/rules_validation_api/app.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,38 @@
22
import json
33
import logging
44
import sys
5+
from collections import defaultdict
56
from pathlib import Path
67

8+
from rules_validation_api.decorators.tracker import VALIDATORS_CALLED
79
from rules_validation_api.validators.rules_validator import RulesValidation
810

911
logging.basicConfig(
10-
level=logging.INFO, # or DEBUG for more detail
12+
level=logging.INFO,
1113
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
1214
force=True,
1315
)
1416

15-
GREEN = "\033[92m" # pragma: no cover
16-
RESET = "\033[0m" # pragma: no cover
17-
YELLOW = "\033[93m" # pragma: no cover
18-
RED = "\033[91m" # pragma: no cover
17+
GREEN = "\033[92m"
18+
RESET = "\033[0m"
19+
YELLOW = "\033[93m"
20+
RED = "\033[91m"
1921

22+
# ANSI color codes
23+
LEFT_COLOR = "\033[34m" # Blue for class name
24+
COLON_COLOR = "\033[33m" # Yellow for colon
25+
RIGHT_COLOR = "\033[92m" # Milk green for validator
26+
CLASS_COLORS = [
27+
"\033[34m", # blue
28+
"\033[35m", # magenta
29+
"\033[36m", # cyan
30+
"\033[94m", # light blue
31+
"\033[95m", # light magenta
32+
"\033[96m", # light cyan
33+
"\033[37m", # white/light grey
34+
]
2035

21-
def main() -> None: # pragma: no cover
36+
def main() -> None:
2237
parser = argparse.ArgumentParser(description="Validate campaign configuration.")
2338
parser.add_argument("--config_path", required=True, help="Path to the campaign config JSON file")
2439
args = parser.parse_args()
@@ -28,9 +43,33 @@ def main() -> None: # pragma: no cover
2843
json_data = json.load(file)
2944
RulesValidation(**json_data)
3045
sys.stdout.write(f"{GREEN}Valid Config{RESET}\n")
46+
47+
# Group by class
48+
grouped = defaultdict(list)
49+
for v in VALIDATORS_CALLED:
50+
cls, method = v.split(":", 1)
51+
grouped[cls].append(method.strip())
52+
53+
# Assign colors to classes
54+
cls_color_map = {}
55+
for i, cls_name in enumerate(sorted(grouped.keys(), reverse=True)):
56+
cls_color_map[cls_name] = CLASS_COLORS[i % len(CLASS_COLORS)]
57+
58+
# Print grouped
59+
for cls_name in sorted(grouped.keys(), reverse=True):
60+
methods = sorted(grouped[cls_name])
61+
# First method prints class name
62+
first = methods[0]
63+
colored = f"{cls_color_map[cls_name]}{cls_name}{RESET}{COLON_COLOR}:{RESET}{RIGHT_COLOR}{first}{RESET}"
64+
print(colored)
65+
# Rest methods indented
66+
for method_name in methods[1:]:
67+
colored = f"{' ' * len(cls_name)}{COLON_COLOR}:{RESET}{RIGHT_COLOR}{method_name}{RESET}"
68+
print(colored)
69+
3170
except ValueError as e:
3271
sys.stderr.write(f"{YELLOW}Validation Error:{RESET} {RED}{e}{RESET}\n")
3372

3473

35-
if __name__ == "__main__": # pragma: no cover
74+
if __name__ == "__main__":
3675
main()

src/rules_validation_api/decorators/__init__.py

Whitespace-only changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import Self
2+
3+
from pydantic import model_validator
4+
5+
VALIDATORS_CALLED: list[str] = []
6+
7+
8+
# --- Mixin and decorator to track validators ---
9+
class TrackValidatorsMixin:
10+
"""
11+
Mixin to track all validator names in a Pydantic model.
12+
"""
13+
14+
@model_validator(mode="after")
15+
def _track_validators(self) -> Self:
16+
for name in dir(self):
17+
if name.startswith(("validate_", "check_")) and callable(getattr(self, name)):
18+
full_name = f"{self.__class__.__name__}:{name}"
19+
if full_name not in VALIDATORS_CALLED:
20+
VALIDATORS_CALLED.append(full_name)
21+
return self
22+
23+
24+
def track_validators(cls) -> type: # noqa:ANN001
25+
"""
26+
Decorator to add the tracking mixin to a Pydantic model.
27+
"""
28+
return type(cls.__name__, (TrackValidatorsMixin, cls), {})

src/rules_validation_api/validators/actions_mapper_validator.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from pydantic import ValidationError, model_validator
22

33
from eligibility_signposting_api.model.campaign_config import ActionsMapper
4+
from rules_validation_api.decorators.tracker import track_validators
45
from rules_validation_api.validators.available_action_validator import AvailableActionValidation
56

67

8+
@track_validators
79
class ActionsMapperValidation(ActionsMapper):
810
@model_validator(mode="after")
911
def validate_keys(self) -> "ActionsMapperValidation":
@@ -16,17 +18,14 @@ def validate_keys(self) -> "ActionsMapperValidation":
1618
@model_validator(mode="after")
1719
def validate_values(self) -> "ActionsMapperValidation":
1820
error_report = []
19-
2021
for key, value in self.root.items():
2122
try:
2223
AvailableActionValidation.model_validate(value.model_dump())
2324
except ValidationError as e:
2425
for err in e.errors():
2526
msg = err.get("msg", "Unknown error").replace("Value error, ", "")
2627
error_report.append(f"\n Action '{key}': {msg}")
27-
2828
if error_report:
2929
final_msg = "Markdown Validation Issues:".join(error_report)
3030
raise ValueError(final_msg)
31-
3231
return self

src/rules_validation_api/validators/available_action_validator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
from pydantic import field_validator
44

55
from eligibility_signposting_api.model.campaign_config import AvailableAction
6+
from rules_validation_api.decorators.tracker import track_validators
67

78

9+
@track_validators
810
class AvailableActionValidation(AvailableAction):
911
@field_validator("action_description")
1012
@classmethod

src/rules_validation_api/validators/campaign_config_validator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from pydantic import field_validator, model_validator
66

77
from eligibility_signposting_api.model.campaign_config import CampaignConfig, Iteration
8+
from rules_validation_api.decorators.tracker import track_validators
89
from rules_validation_api.validators.iteration_validator import IterationValidation
910

1011

12+
@track_validators
1113
class CampaignConfigValidation(CampaignConfig):
1214
@field_validator("iterations")
1315
@classmethod
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from eligibility_signposting_api.model.campaign_config import IterationCohort
2+
from rules_validation_api.decorators.tracker import track_validators
23

34

5+
@track_validators
46
class IterationCohortValidation(IterationCohort):
57
pass

src/rules_validation_api/validators/iteration_rules_validator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
RuleAttributeName,
99
RuleType,
1010
)
11+
from rules_validation_api.decorators.tracker import track_validators
1112

1213

14+
@track_validators
1315
class IterationRuleValidation(IterationRule):
1416
@model_validator(mode="after")
1517
def check_cohort_attribute_name(self) -> typing.Self:

0 commit comments

Comments
 (0)