Skip to content

Commit 9b50fcd

Browse files
committed
uplift validation rules and create utility used in expression checker
1 parent 2493245 commit 9b50fcd

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

lambdas/shared/src/common/validator/expression_checker.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import datetime
22
import re
33
import uuid
4+
from typing import Optional
45

56
from common.validator.constants.enums import MESSAGES, ExceptionLevels, MessageLabel
67
from common.validator.error_report.record_error import ErrorReport, RecordError
78
from common.validator.lookup_expressions.key_data import KeyData
89
from common.validator.lookup_expressions.lookup_data import LookUpData
10+
from common.validator.validation_utils import check_if_future_date
911

1012

1113
class ExpressionChecker:
@@ -28,8 +30,10 @@ def validate_expression(
2830
return self._validate_datetime(expression_rule, field_name, field_value, row)
2931
case "STRING":
3032
return self._validate_for_string_values(expression_rule, field_name, field_value, row)
33+
case "LIST":
34+
return self._validate_for_list_values(expression_rule, field_name, field_value, row)
3135
case "DATE":
32-
return self._validate_datetime(expression_rule, field_name, field_value, row)
36+
return self.validate_for_date(expression_rule, field_name, field_value, row)
3337
case "UUID":
3438
return self._validate_uuid(expression_rule, field_name, field_value, row)
3539
case "INT":
@@ -97,6 +101,23 @@ def _validate_datetime(self, _expression_rule, field_name, field_value, row) ->
97101
message = MESSAGES[ExceptionLevels.UNEXPECTED_EXCEPTION] % (e.__class__.__name__, e)
98102
return ErrorReport(ExceptionLevels.UNEXPECTED_EXCEPTION, message, row, field_name, "", self.summarise)
99103

104+
def validate_for_date(self, _expression_rule, field_name, field_value, row, future_date_allowed: bool = False):
105+
"""
106+
Apply pre-validation to a date field to ensure that it is a string (JSON dates must be
107+
written as strings) containing a valid date in the format "YYYY-MM-DD"
108+
"""
109+
if not isinstance(field_value, str):
110+
raise TypeError(f"{field_name} must be a string")
111+
112+
try:
113+
parsed_date = datetime.strptime(field_value, "%Y-%m-%d").date()
114+
except ValueError as value_error:
115+
raise ValueError(f'{field_name} must be a valid date string in the format "YYYY-MM-DD"') from value_error
116+
117+
# Enforce future date rule using central checker after successful parse
118+
if not future_date_allowed and check_if_future_date(parsed_date):
119+
raise ValueError(f"{field_name} must not be in the future")
120+
100121
# UUID validate
101122
def _validate_uuid(self, _expression_rule: str, field_name: str, field_value: str, row: dict) -> ErrorReport:
102123
try:
@@ -206,6 +227,42 @@ def _validate_equal(self, expression_rule: str, field_name: str, field_value: st
206227
message = MESSAGES[ExceptionLevels.UNEXPECTED_EXCEPTION] % (e.__class__.__name__, e)
207228
return ErrorReport(ExceptionLevels.UNEXPECTED_EXCEPTION, message, row, field_name, "", self.summarise)
208229

230+
def for_list(self, expression_rule: str, field_name: str, field_value: list, row: dict):
231+
"""
232+
Apply validation to a list field to ensure it is a non-empty list which meets the length requirements and
233+
requirements, if applicable, for each list element to be a non-empty string or non-empty dictionary
234+
"""
235+
defined_length: Optional[int] = (None,)
236+
max_length: Optional[int] = (None,)
237+
elements_are_strings: bool = (False,)
238+
string_element_max_length: Optional[int] = (None,)
239+
elements_are_dicts: bool = (False,)
240+
if not isinstance(field_value, list):
241+
raise TypeError(f"{field_name} must be an array")
242+
243+
if defined_length:
244+
if len(field_value) != defined_length:
245+
raise ValueError(f"{field_name} must be an array of length {defined_length}")
246+
else:
247+
if len(field_value) == 0:
248+
raise ValueError(f"{field_name} must be a non-empty array")
249+
250+
if max_length is not None and len(field_value) > max_length:
251+
raise ValueError(f"{field_name} must be an array of maximum length {max_length}")
252+
253+
if elements_are_strings:
254+
for idx, element in enumerate(field_value):
255+
self._validate_for_string_values.for_string(
256+
element, f"{field_name}[{idx}]", max_length=string_element_max_length
257+
)
258+
259+
if elements_are_dicts:
260+
for element in field_value:
261+
if not isinstance(element, dict):
262+
raise TypeError(f"{field_name} must be an array of objects")
263+
if len(element) == 0:
264+
raise ValueError(f"{field_name} must be an array of non-empty objects")
265+
209266
# Not Equal Validate
210267
def _validate_not_equal(self, expression_rule: str, field_name: str, field_value: str, row: dict) -> ErrorReport:
211268
try:
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from datetime import date, datetime
2+
3+
from stdnum.verhoeff import validate
4+
5+
6+
def check_if_future_date(parsed_value: date | datetime):
7+
"""
8+
Ensure a parsed date or datetime object is not in the future.
9+
"""
10+
if isinstance(parsed_value, datetime):
11+
now = datetime.now(parsed_value.tzinfo) if parsed_value.tzinfo else datetime.now()
12+
elif isinstance(parsed_value, date):
13+
now = datetime.now().date()
14+
if parsed_value > now:
15+
return True
16+
return False
17+
18+
19+
def is_valid_simple_snomed(simple_snomed: str) -> bool:
20+
"check the snomed code valid or not."
21+
min_snomed_length = 6
22+
max_snomed_length = 18
23+
return (
24+
simple_snomed is not None
25+
and simple_snomed.isdigit()
26+
and simple_snomed[0] != "0"
27+
and min_snomed_length <= len(simple_snomed) <= max_snomed_length
28+
and validate(simple_snomed)
29+
and (simple_snomed[-3:-1] in ("00", "10"))
30+
)
31+
32+
33+
def nhs_number_mod11_check(nhs_number: str) -> bool:
34+
"""
35+
Parameters:-
36+
nhs_number: str
37+
The NHS number to be checked.
38+
Returns:-
39+
True if the nhs number passes the mod 11 check, False otherwise.
40+
41+
Definition of NHS number can be found at:
42+
https://www.datadictionary.nhs.uk/attributes/nhs_number.html
43+
"""
44+
is_mod11 = False
45+
if nhs_number.isdigit() and len(nhs_number) == 10:
46+
# Create a reversed list of weighting factors
47+
weighting_factors = list(range(2, 11))[::-1]
48+
# Multiply each of the first nine digits by the weighting factor and add the results of each multiplication
49+
# together
50+
total = sum(int(digit) * weight for digit, weight in zip(nhs_number[:-1], weighting_factors))
51+
# Divide the total by 11 and establish the remainder and subtract the remainder from 11 to give the check digit.
52+
# If the result is 11 then a check digit of 0 is used. If the result is 10 then the NHS NUMBER is invalid and
53+
# not used.
54+
check_digit = 0 if (total % 11 == 0) else (11 - (total % 11))
55+
# Check the remainder matches the check digit. If it does not, the NHS NUMBER is invalid.
56+
is_mod11 = check_digit == int(nhs_number[-1])
57+
58+
return is_mod11

lambdas/shared/tests/test_common/validator/test_schemas/test_schema.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"errorLevel": 0,
2626
"expression": {
2727
"expressionName": "Person Forname Not Empty Check",
28-
"expressionType": "NOTEMPTY",
28+
"expressionType": "LIST",
2929
"expressionRule": ""
3030
},
3131
"errorGroup": "completeness"
@@ -39,7 +39,7 @@
3939
"parentExpression": "01K5EGR0C7Y1WJ0BC803SQDWK4",
4040
"expression": {
4141
"expressionName": "Person Surname Not Empty Check",
42-
"expressionType": "NOTEMPTY",
42+
"expressionType": "STRING",
4343
"expressionRule": ""
4444
},
4545
"errorGroup": "completeness"
@@ -52,7 +52,7 @@
5252
"errorLevel": 1,
5353
"expression": {
5454
"expressionName": "Date of Birth Not Empty Check",
55-
"expressionType": "NOTEMPTY",
55+
"expressionType": "DATE",
5656
"expressionRule": ""
5757
},
5858
"errorGroup": "consistency"
@@ -65,7 +65,7 @@
6565
"errorLevel": 1,
6666
"expression": {
6767
"expressionName": "Gender Valid Check",
68-
"expressionType": "NOTEMPTY",
68+
"expressionType": "STRING",
6969
"expressionRule": ""
7070
},
7171
"errorGroup": "consistency"

0 commit comments

Comments
 (0)