Skip to content

Commit bec7963

Browse files
wip
1 parent 453a998 commit bec7963

File tree

11 files changed

+97
-29
lines changed

11 files changed

+97
-29
lines changed

index.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<meta charset="UTF-8"/>
4+
<meta charset="UTF-8" />
55
<title>Campaign Config Validator - NHS Digital</title>
66
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
77
<style>
@@ -225,9 +225,12 @@ <h3>Visualiser Output</h3>
225225
.replace(/\x1b\[91m/g, '<span class="ansi-red">')
226226
.replace(/\x1b\[0m/g, '</span>');
227227

228-
output.innerHTML += cleanText + "\n";
229-
output.scrollTop = output.scrollHeight;
228+
if (cleanText.includes("Valid Config")) {
229+
cleanText = cleanText.replace(/Valid Config/g, '<span style="font-size:2em;font-weight:bold;color:#007f3b">Valid Config</span>');
230230
}
231+
output.innerHTML += cleanText + "\n";
232+
output.scrollTop = output.scrollHeight;
233+
}
231234

232235
function clearLog() {
233236
output.innerHTML = "";
@@ -248,6 +251,8 @@ <h3>Visualiser Output</h3>
248251
"src/eligibility_signposting_api/model/campaign_config.py",
249252
"src/eligibility_signposting_api/config/__init__.py",
250253
"src/eligibility_signposting_api/config/constants.py",
254+
"src/rules_validation_api/decorators/__init__.py",
255+
"src/rules_validation_api/decorators/tracker.py",
251256
"src/rules_validation_api/__init__.py",
252257
"src/rules_validation_api/validators/__init__.py",
253258
"src/rules_validation_api/validators/rules_validator.py",

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class StatusText(BaseModel):
238238
not_actionable: str | None = Field(None, alias="NotActionable")
239239
actionable: str | None = Field(None, alias="Actionable")
240240

241-
model_config = {"populate_by_name": True}
241+
model_config = {"populate_by_name": True, "extra": "ignore"}
242242

243243

244244
class RuleEntry(BaseModel):
@@ -277,6 +277,12 @@ class Iteration(BaseModel):
277277

278278
model_config = {"populate_by_name": True, "arbitrary_types_allowed": True, "extra": "ignore"}
279279

280+
def __init__(self, **data: dict[str, typing.Any]) -> None:
281+
super().__init__(**data)
282+
# Ensure each rule knows its parent iteration
283+
for rule in self.iteration_rules:
284+
rule.set_parent(self)
285+
280286
@field_validator("iteration_date", mode="before")
281287
@classmethod
282288
def parse_dates(cls, v: str | date) -> date:
@@ -300,12 +306,6 @@ def parse_dates(cls, v: str | date) -> date:
300306
def serialize_dates(v: date, _info: SerializationInfo) -> str:
301307
return v.strftime("%Y%m%d")
302308

303-
@model_validator(mode="after")
304-
def attach_rule_parents(self) -> Iteration:
305-
for rule in self.iteration_rules:
306-
rule.set_parent(self)
307-
return self
308-
309309
def __str__(self) -> str:
310310
return json.dumps(self.model_dump(by_alias=True), indent=2)
311311

src/rules_validation_api/app.py

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

7-
from pydantic import ValidationError
8-
8+
from rules_validation_api.decorators.tracker import VALIDATORS_CALLED
99
from rules_validation_api.validators.rules_validator import RulesValidation
1010

1111
logging.basicConfig(
@@ -19,20 +19,19 @@
1919
YELLOW = "\033[93m"
2020
RED = "\033[91m"
2121

22-
23-
def refine_error(e: ValidationError) -> str:
24-
"""Return a very short, single-line error message."""
25-
lines = [f"Validation Error: {len(e.errors())} validation error(s)"]
26-
27-
for err in e.errors():
28-
loc = ".".join(str(x) for x in err["loc"])
29-
msg = err["msg"]
30-
type_ = err["type"]
31-
32-
lines.append(f"{loc} : {msg} [type={type_}]")
33-
34-
return "\n".join(lines)
35-
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+
]
3635

3736
def main() -> None:
3837
parser = argparse.ArgumentParser(description="Validate campaign configuration.")
@@ -45,9 +44,31 @@ def main() -> None:
4544
RulesValidation(**json_data)
4645
sys.stdout.write(f"{GREEN}Valid Config{RESET}\n")
4746

48-
except ValidationError as e:
49-
clean = refine_error(e)
50-
sys.stderr.write(f"{YELLOW}{clean}{RESET}\n")
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+
70+
except ValueError as e:
71+
sys.stderr.write(f"{YELLOW}Validation Error:{RESET} {RED}{e}{RESET}\n")
5172

5273

5374
if __name__ == "__main__":
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 & 0 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":

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
@@ -9,8 +9,10 @@
99
RuleAttributeName,
1010
RuleType,
1111
)
12+
from rules_validation_api.decorators.tracker import track_validators
1213

1314

15+
@track_validators
1416
class IterationRuleValidation(IterationRule):
1517
@field_validator("attribute_target")
1618
@classmethod

src/rules_validation_api/validators/iteration_validator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
IterationRule,
1212
RuleType,
1313
)
14+
from rules_validation_api.decorators.tracker import track_validators
1415
from rules_validation_api.validators.actions_mapper_validator import ActionsMapperValidation
1516
from rules_validation_api.validators.available_action_validator import AvailableActionValidation
1617
from rules_validation_api.validators.iteration_cohort_validator import IterationCohortValidation
1718
from rules_validation_api.validators.iteration_rules_validator import IterationRuleValidation
1819

1920

21+
@track_validators
2022
class IterationValidation(Iteration):
2123
@field_validator("iteration_rules")
2224
@classmethod

0 commit comments

Comments
 (0)