diff --git a/schematic/schemas/constants.py b/schematic/schemas/constants.py index 33bc90fe6..102dab99c 100644 --- a/schematic/schemas/constants.py +++ b/schematic/schemas/constants.py @@ -3,9 +3,12 @@ from enum import Enum -class ValidationRule(Enum): - """Validation rules that are used to create JSON Schema""" +class ValidationRuleName(Enum): + """Names of validation rules that are used to create JSON Schema""" + LIST = "list" + DATE = "date" + URL = "url" REGEX = "regex" IN_RANGE = "inRange" STR = "str" @@ -16,7 +19,7 @@ class ValidationRule(Enum): class JSONSchemaType(Enum): - """This enum is allowed values type values for a JSON Schema in a data model""" + """This enum is the currently supported JSON Schema types""" STRING = "string" NUMBER = "number" @@ -24,18 +27,15 @@ class JSONSchemaType(Enum): BOOLEAN = "boolean" +class JSONSchemaFormat(Enum): + """This enum is the currently supported JSON Schema formats""" + + DATE = "date" + URI = "uri" + + class RegexModule(Enum): """This enum are allowed modules for the regex validation rule""" SEARCH = "search" MATCH = "match" - - -# A dict where the keys are type validation rules, and the values are their JSON Schema equivalent -TYPE_RULES = { - ValidationRule.STR.value: JSONSchemaType.STRING.value, - ValidationRule.NUM.value: JSONSchemaType.NUMBER.value, - ValidationRule.FLOAT.value: JSONSchemaType.NUMBER.value, - ValidationRule.INT.value: JSONSchemaType.INTEGER.value, - ValidationRule.BOOL.value: JSONSchemaType.BOOLEAN.value, -} diff --git a/schematic/schemas/create_json_schema.py b/schematic/schemas/create_json_schema.py index 24f4c2938..c491d757b 100644 --- a/schematic/schemas/create_json_schema.py +++ b/schematic/schemas/create_json_schema.py @@ -15,13 +15,22 @@ from schematic.schemas.data_model_graph import DataModelGraphExplorer from schematic.utils.schema_utils import get_json_schema_log_file_path -from schematic.utils.validate_utils import rule_in_rule_list from schematic.utils.io_utils import export_json +from schematic.schemas.json_schema_validation_rule_functions import ( + filter_unused_inputted_rules, + check_for_conflicting_inputted_rules, + check_for_duplicate_inputted_rules, + get_in_range_parameters_from_inputted_rule, + get_regex_parameters_from_inputted_rule, + get_js_type_from_inputted_rules, + get_rule_from_inputted_rules, + get_validation_rule_names_from_inputted_rules, + get_names_from_inputted_rules, +) from schematic.schemas.constants import ( - ValidationRule, JSONSchemaType, - RegexModule, - TYPE_RULES, + JSONSchemaFormat, + ValidationRuleName, ) @@ -142,11 +151,12 @@ class Node: # pylint: disable=too-many-instance-attributes is_required: Whether or not this node is required dependencies: This nodes dependencies description: This nodes description, gotten from the comment in the data model - type: The type of the property (inferred from validation_rules) is_array: Whether or not the property is an array (inferred from validation_rules) + type: The type of the property (inferred from validation_rules) + format: The format of the property (inferred from validation_rules) minimum: The minimum value of the property (if numeric) (inferred from validation_rules) maximum: The maximum value of the property (if numeric) (inferred from validation_rules) - pattern: The regex pattern of the property + pattern: The regex pattern of the property (inferred from validation_rules) """ name: str @@ -158,8 +168,9 @@ class Node: # pylint: disable=too-many-instance-attributes is_required: bool = field(init=False) dependencies: list[str] = field(init=False) description: str = field(init=False) - type: Optional[str] = field(init=False) is_array: bool = field(init=False) + type: Optional[JSONSchemaType] = field(init=False) + format: Optional[JSONSchemaFormat] = field(init=False) minimum: Optional[float] = field(init=False) maximum: Optional[float] = field(init=False) pattern: Optional[str] = field(init=False) @@ -191,8 +202,9 @@ def __post_init__(self) -> None: ) ( - self.type, self.is_array, + self.type, + self.format, self.minimum, self.maximum, self.pattern, @@ -201,168 +213,84 @@ def __post_init__(self) -> None: def _get_validation_rule_based_fields( validation_rules: list[str], -) -> tuple[Optional[str], bool, Optional[float], Optional[float], Optional[str]]: +) -> tuple[ + bool, + Optional[JSONSchemaType], + Optional[JSONSchemaFormat], + Optional[float], + Optional[float], + Optional[str], +]: """ Gets the fields for the Node class that are based on the validation rules - Args: - validation_rules: A list of validation rules - - Raises: - ValueError: If both the inRange and regex rule are present - ValueError: If the inRange rule and a type validation rule other than 'int' or 'num' - are present - ValueError: If the regex rule and a type validation rule other than 'str' are present - - Returns: - A tuple containing the type, is_array, minimum, maximum, and pattern fields for - a Node object - """ - prop_type: Optional[str] = None - is_array = False - minimum: Optional[float] = None - maximum: Optional[float] = None - pattern: Optional[str] = None - - if validation_rules: - if rule_in_rule_list("list", validation_rules): - is_array = True - - type_rule = _get_type_rule_from_rule_list(validation_rules) - if type_rule: - prop_type = TYPE_RULES.get(type_rule) - - regex_rule = _get_rule_from_rule_list(ValidationRule.REGEX, validation_rules) - range_rule = _get_rule_from_rule_list(ValidationRule.IN_RANGE, validation_rules) - if range_rule and regex_rule: - raise ValueError( - "regex and inRange rules are incompatible: ", validation_rules - ) - - if range_rule: - if prop_type not in [ - JSONSchemaType.NUMBER.value, - JSONSchemaType.INTEGER.value, - None, - ]: - raise ValueError( - "Validation rules must be either 'int' or 'num' when using the inRange rule" - ) - prop_type = prop_type or JSONSchemaType.NUMBER.value - minimum, maximum = _get_range_from_in_range_rule(range_rule) - - if regex_rule: - if prop_type not in (None, JSONSchemaType.STRING.value): - raise ValueError("Type must be 'string' when using a regex rule") - prop_type = JSONSchemaType.STRING.value - pattern = _get_pattern_from_regex_rule(regex_rule) - - return ( - prop_type, - is_array, - minimum, - maximum, - pattern, - ) - - -def _get_range_from_in_range_rule( - rule: str, -) -> tuple[Optional[float], Optional[float]]: - """ - Returns the min and max from an inRange rule if they exist - - Arguments: - rule: The inRange rule - - Returns: - The min and max from the rule - """ - range_min: Optional[float] = None - range_max: Optional[float] = None - parameters = rule.split(" ") - if len(parameters) > 1 and parameters[1].isnumeric(): - range_min = float(parameters[1]) - if len(parameters) > 2 and parameters[2].isnumeric(): - range_max = float(parameters[2]) - return (range_min, range_max) + JSON Schema docs: - -def _get_pattern_from_regex_rule(rule: str) -> Optional[str]: - """Gets the pattern from the regex rule + Array: https://json-schema.org/understanding-json-schema/reference/array + Types: https://json-schema.org/understanding-json-schema/reference/type#type-specific-keywords + Format: https://json-schema.org/understanding-json-schema/reference/type#format + Pattern: https://json-schema.org/understanding-json-schema/reference/string#regexp + Min/max: https://json-schema.org/understanding-json-schema/reference/numeric#range Arguments: - rule: The full regex rule + validation_rules: A list of input validation rules Returns: - If the module parameter is search or match, and the pattern parameter exists - the pattern is returned - Otherwise None - """ - parameters = rule.split(" ") - if len(parameters) != 3: - return None - _, module, pattern = parameters - # Do not translate other modules - if module not in [item.value for item in RegexModule]: - return None - # Match is just search but only at the beginning of the string - if module == RegexModule.MATCH.value and not pattern.startswith("^"): - return f"^{pattern}" - return pattern - - -def _get_type_rule_from_rule_list(rule_list: list[str]) -> Optional[str]: - """ - Returns the type rule from a list of rules if there is only one - Returns None if there are no type rules + A tuple containing fields for a Node object: + - js_is_array: Whether or not the Node should be an array in JSON Schema + - js_type: The JSON Schema type + - js_format: The JSON Schema format + - js_minimum: If the type is numeric the JSON Schema minimum + - js_maximum: If the type is numeric the JSON Schema maximum + - js_pattern: If the type is string the JSON Schema pattern + """ + js_is_array = False + js_type = None + js_format = None + js_minimum = None + js_maximum = None + js_pattern = None - Arguments: - rule_list: A list of validation rules - - Raises: - ValueError: When more than one type rule is found - - Returns: - The type rule if one is found, or None - """ - rule_list = [rule.split(" ")[0] for rule in rule_list] - rule_list = [rule for rule in rule_list if rule in TYPE_RULES] - if len(rule_list) > 1: - raise ValueError( - "Found more than one type rule in validation rules: ", rule_list + if validation_rules: + validation_rules = filter_unused_inputted_rules(validation_rules) + validation_rule_name_strings = get_names_from_inputted_rules(validation_rules) + check_for_duplicate_inputted_rules(validation_rule_name_strings) + check_for_conflicting_inputted_rules(validation_rule_name_strings) + validation_rule_names = get_validation_rule_names_from_inputted_rules( + validation_rules ) - if len(rule_list) == 0: - return None - return rule_list[0] + js_is_array = ValidationRuleName.LIST in validation_rule_names -def _get_rule_from_rule_list( - rule: ValidationRule, rule_list: list[str] -) -> Optional[str]: - """ - Returns the a rule from a list of rules if there is only one + js_type = get_js_type_from_inputted_rules(validation_rules) - Arguments: - rule: A ValidationRule enum - rule_list: A list of validation rules + if ValidationRuleName.URL in validation_rule_names: + js_format = JSONSchemaFormat.URI + elif ValidationRuleName.DATE in validation_rule_names: + js_format = JSONSchemaFormat.DATE - Raises: - ValueError: When more than one of the rule is found + in_range_rule = get_rule_from_inputted_rules( + ValidationRuleName.IN_RANGE, validation_rules + ) + if in_range_rule: + js_minimum, js_maximum = get_in_range_parameters_from_inputted_rule( + in_range_rule + ) - Returns: - The rule if one is found, otherwise None is returned - """ - rule_value = rule.value - rule_list = [rule for rule in rule_list if rule.startswith(rule_value)] - if len(rule_list) > 1: - msg = ( - f"Found more than one '{rule_value}' rule in validation rules: {rule_list}" + regex_rule = get_rule_from_inputted_rules( + ValidationRuleName.REGEX, validation_rules ) - raise ValueError(msg) - if len(rule_list) == 0: - return None - return rule_list[0] + if regex_rule: + js_pattern = get_regex_parameters_from_inputted_rule(regex_rule) + + return ( + js_is_array, + js_type, + js_format, + js_minimum, + js_maximum, + js_pattern, + ) @dataclass @@ -911,7 +839,7 @@ def _create_array_property(node: Node) -> Property: items: Items = {} if node.type: - items["type"] = node.type + items["type"] = node.type.value _set_type_specific_keywords(items, node) array_type_dict: TypeDict = {"type": "array", "title": "array"} @@ -984,10 +912,10 @@ def _create_simple_property(node: Node) -> Property: if node.type: if node.is_required: - prop["type"] = node.type + prop["type"] = node.type.value else: prop["oneOf"] = [ - {"type": node.type, "title": node.type}, + {"type": node.type.value, "title": node.type.value}, {"type": "null", "title": "null"}, ] elif node.is_required: @@ -1005,9 +933,10 @@ def _set_type_specific_keywords(schema: dict[str, Any], node: Node) -> None: schema: The schema to set keywords on node (Node): The node the corresponds to the property which is being set in the JSON Schema """ - if node.minimum is not None: - schema["minimum"] = node.minimum - if node.maximum is not None: - schema["maximum"] = node.maximum - if node.pattern is not None: - schema["pattern"] = node.pattern + for attr in ["minimum", "maximum", "pattern"]: + value = getattr(node, attr) + if value is not None: + schema[attr] = value + + if node.format is not None: + schema["format"] = node.format.value diff --git a/schematic/schemas/json_schema_validation_rule_functions.py b/schematic/schemas/json_schema_validation_rule_functions.py new file mode 100644 index 000000000..ae5790a5e --- /dev/null +++ b/schematic/schemas/json_schema_validation_rule_functions.py @@ -0,0 +1,413 @@ +""" +This module contains functions for interacting with validation rules for JSON Schema creation + +JSON Schema docs: + +Array: https://json-schema.org/understanding-json-schema/reference/array +Types: https://json-schema.org/understanding-json-schema/reference/type#type-specific-keywords +Format: https://json-schema.org/understanding-json-schema/reference/type#format +Pattern: https://json-schema.org/understanding-json-schema/reference/string#regexp +Min/max: https://json-schema.org/understanding-json-schema/reference/numeric#range + +""" +import warnings +from dataclasses import dataclass +from typing import Optional +from schematic.schemas.constants import JSONSchemaType, ValidationRuleName, RegexModule + + +@dataclass +class ValidationRule: + """ + This class represents a Schematic validation rule to be used for creating JSON Schemas + + Attributes: + name: The name of the validation rule + js_type: The JSON Schema type this rule indicates. + For example type rules map over to their equivalent JSON Schema type: str -> string + Other rules have an implicit type. For example the regex rule maps to the JSON + Schema pattern keyword. The pattern keyword requires the type to be string + incompatible_rules: Other validation rules this rule can not be paired with + parameters: Parameters for the validation rule that need to be collected for the JSON Schema + """ + + name: ValidationRuleName + js_type: Optional[JSONSchemaType] + incompatible_rules: list[ValidationRuleName] + parameters: Optional[list[str]] = None + + +# A dictionary of current Schematic validation rules +# where the keys are name of the rule in Schematic +# and the values are ValidationRule objects +_VALIDATION_RULES = { + "list": ValidationRule( + name=ValidationRuleName.LIST, + js_type=None, + incompatible_rules=[], + ), + "date": ValidationRule( + name=ValidationRuleName.DATE, + js_type=JSONSchemaType.STRING, + incompatible_rules=[ + ValidationRuleName.IN_RANGE, + ValidationRuleName.URL, + ValidationRuleName.INT, + ValidationRuleName.FLOAT, + ValidationRuleName.BOOL, + ValidationRuleName.NUM, + ], + ), + "url": ValidationRule( + name=ValidationRuleName.URL, + js_type=JSONSchemaType.STRING, + incompatible_rules=[ + ValidationRuleName.IN_RANGE, + ValidationRuleName.DATE, + ValidationRuleName.INT, + ValidationRuleName.FLOAT, + ValidationRuleName.BOOL, + ValidationRuleName.NUM, + ], + ), + "regex": ValidationRule( + name=ValidationRuleName.REGEX, + js_type=JSONSchemaType.STRING, + incompatible_rules=[ + ValidationRuleName.IN_RANGE, + ValidationRuleName.INT, + ValidationRuleName.FLOAT, + ValidationRuleName.BOOL, + ValidationRuleName.NUM, + ], + parameters=["module", "pattern"], + ), + "inRange": ValidationRule( + name=ValidationRuleName.IN_RANGE, + js_type=JSONSchemaType.NUMBER, + incompatible_rules=[ + ValidationRuleName.URL, + ValidationRuleName.DATE, + ValidationRuleName.REGEX, + ValidationRuleName.STR, + ValidationRuleName.BOOL, + ], + parameters=["minimum", "maximum"], + ), + "str": ValidationRule( + name=ValidationRuleName.STR, + js_type=JSONSchemaType.STRING, + incompatible_rules=[ + ValidationRuleName.IN_RANGE, + ValidationRuleName.INT, + ValidationRuleName.FLOAT, + ValidationRuleName.NUM, + ValidationRuleName.BOOL, + ], + ), + "float": ValidationRule( + name=ValidationRuleName.FLOAT, + js_type=JSONSchemaType.NUMBER, + incompatible_rules=[ + ValidationRuleName.URL, + ValidationRuleName.DATE, + ValidationRuleName.REGEX, + ValidationRuleName.STR, + ValidationRuleName.BOOL, + ValidationRuleName.INT, + ValidationRuleName.NUM, + ], + ), + "int": ValidationRule( + name=ValidationRuleName.INT, + js_type=JSONSchemaType.INTEGER, + incompatible_rules=[ + ValidationRuleName.URL, + ValidationRuleName.DATE, + ValidationRuleName.REGEX, + ValidationRuleName.STR, + ValidationRuleName.BOOL, + ValidationRuleName.NUM, + ValidationRuleName.FLOAT, + ], + ), + "num": ValidationRule( + name=ValidationRuleName.NUM, + js_type=JSONSchemaType.NUMBER, + incompatible_rules=[ + ValidationRuleName.URL, + ValidationRuleName.DATE, + ValidationRuleName.REGEX, + ValidationRuleName.STR, + ValidationRuleName.BOOL, + ValidationRuleName.INT, + ValidationRuleName.FLOAT, + ], + ), +} + + +def filter_unused_inputted_rules(inputted_rules: list[str]) -> list[str]: + """Filters a list of validation rules for only those used to create JSON Schemas + + Arguments: + inputted_rules: A list of validation rules + + Raises: + warning: When any of the inputted rules are not used for JSON Schema creation + + Returns: + A filtered list of validation rules + """ + unused_rules = [ + rule + for rule in inputted_rules + if _get_name_from_inputted_rule(rule) + not in [e.value for e in ValidationRuleName] + ] + if unused_rules: + msg = f"These validation rules will be ignored in creating the JSON Schema: {unused_rules}" + warnings.warn(msg) + + return [ + rule + for rule in inputted_rules + if _get_name_from_inputted_rule(rule) in [e.value for e in ValidationRuleName] + ] + + +def check_for_duplicate_inputted_rules(inputted_rules: list[str]) -> None: + """Checks that there are no rules with duplicate names + + Arguments: + inputted_rules: A list of validation rules + + Raises: + ValueError: If there are multiple rules with the same name + """ + rule_names = get_names_from_inputted_rules(inputted_rules) + if sorted(rule_names) != sorted(list(set(rule_names))): + raise ValueError(f"Validation Rules contains duplicates: {inputted_rules}") + + +def check_for_conflicting_inputted_rules(inputted_rules: list[str]) -> None: + """Checks that each rule has no conflicts with any other rule + + Arguments: + inputted_rules: A list of validation rules + + Raises: + ValueError: If a rule is in conflict with any other rule + """ + rule_names = get_names_from_inputted_rules(inputted_rules) + rules: list[ValidationRule] = _get_rules_by_names(rule_names) + for rule in rules: + incompatible_rule_names = [rule.value for rule in rule.incompatible_rules] + conflicting_rule_names = sorted( + list(set(rule_names).intersection(incompatible_rule_names)) + ) + if conflicting_rule_names: + msg = ( + f"Validation rule: {rule.name.value} " + f"has conflicting rules: {conflicting_rule_names}" + ) + raise ValueError(msg) + + +def get_rule_from_inputted_rules( + rule_name: ValidationRuleName, inputted_rules: list[str] +) -> Optional[str]: + """Returns a rule from a list of rules + + Arguments: + rule_name: A ValidationRuleName + inputted_rules: A list of validation rules + + Raises: + ValueError: If there are multiple of the rule in the list + + Returns: + The rule if one is found, otherwise None is returned + """ + inputted_rules = [ + rule for rule in inputted_rules if rule.startswith(rule_name.value) + ] + if len(inputted_rules) > 1: + raise ValueError(f"Found duplicates of rule in rules: {inputted_rules}") + if len(inputted_rules) == 0: + return None + return inputted_rules[0] + + +def get_js_type_from_inputted_rules( + inputted_rules: list[str], +) -> Optional[JSONSchemaType]: + """Gets the JSON Schema type from a list of rules + + Arguments: + inputted_rules: A list of inputted validation rules + + Raises: + ValueError: If there are multiple type rules in the list + + Returns: + The JSON Schema type if a type rule is found, otherwise None + """ + rule_names = get_names_from_inputted_rules(inputted_rules) + validation_rules = _get_rules_by_names(rule_names) + # A set of js_types of the validation rules + json_schema_types = { + rule.js_type for rule in validation_rules if rule.js_type is not None + } + if len(json_schema_types) > 1: + raise ValueError( + f"Validation rules contain more than one implied JSON Schema type: {inputted_rules}" + ) + if len(json_schema_types) == 0: + return None + return list(json_schema_types)[0] + + +def get_in_range_parameters_from_inputted_rule( + inputted_rule: str, +) -> tuple[Optional[float], Optional[float]]: + """ + Returns the min and max from an inRange rule if they exist + + Arguments: + inputted_rule: The inRange rule + + Returns: + The min and max from the rule + """ + minimum: Optional[float] = None + maximum: Optional[float] = None + parameters = _get_parameters_from_inputted_rule(inputted_rule) + if parameters: + if ( + "minimum" in parameters + and parameters["minimum"] is not None + and parameters["minimum"].isnumeric() + ): + minimum = float(parameters["minimum"]) + if ( + "maximum" in parameters + and parameters["maximum"] is not None + and parameters["maximum"].isnumeric() + ): + maximum = float(parameters["maximum"]) + return (minimum, maximum) + + +def get_regex_parameters_from_inputted_rule( + inputted_rule: str, +) -> Optional[str]: + """ + Gets the pattern from the regex rule + + Arguments: + inputted_rule: The full regex rule + + Returns: + If the module parameter is search or match, and the pattern parameter exists + the pattern is returned + Otherwise None + """ + module: Optional[str] = None + pattern: Optional[str] = None + parameters = _get_parameters_from_inputted_rule(inputted_rule) + if parameters: + if "module" in parameters: + module = parameters["module"] + if "pattern" in parameters: + pattern = parameters["pattern"] + if module is None or pattern is None: + return None + # Do not translate other modules + if module not in [item.value for item in RegexModule]: + return None + # Match is just search but only at the beginning of the string + if module == RegexModule.MATCH.value and not pattern.startswith("^"): + return f"^{pattern}" + return pattern + + +def get_validation_rule_names_from_inputted_rules( + inputted_rules: list[str], +) -> list[ValidationRuleName]: + """Gets a list of ValidationRuleNames from a list of inputted validation rules + + Arguments: + inputted_rules: A list of inputted validation rules from a data model + + Returns: + A list of ValidationRuleNames + """ + rule_names = get_names_from_inputted_rules(inputted_rules) + rules = _get_rules_by_names(rule_names) + return [rule.name for rule in rules] + + +def get_names_from_inputted_rules(inputted_rules: list[str]) -> list[str]: + """Gets the names from a list of inputted rules + + Arguments: + inputted_rules: A list of inputted validation rules from a data model + + Returns: + The names of the inputted rules + """ + return [_get_name_from_inputted_rule(rule) for rule in inputted_rules] + + +def _get_parameters_from_inputted_rule(inputted_rule: str) -> Optional[dict[str, str]]: + """Creates a dictionary of parameters and values from an input rule string + + Arguments: + inputted_rule: An inputted validation rule from a data model + + Returns: + If the rule exists, a dictionary where + the keys are the rule parameters + the values are the input rule parameter values + Else None + """ + rule_name = _get_name_from_inputted_rule(inputted_rule) + rule_values = inputted_rule.split(" ")[1:] + rule = _VALIDATION_RULES.get(rule_name) + if rule and rule.parameters: + return dict(zip(rule.parameters, rule_values)) + return None + + +def _get_name_from_inputted_rule(inputted_rule: str) -> str: + """Gets the name from an inputted rule + + Arguments: + inputted_rule: An inputted validation rule from a data model + + Returns: + The name of the inputted rule + """ + return inputted_rule.split(" ")[0] + + +def _get_rules_by_names(names: list[str]) -> list[ValidationRule]: + """Gets a list of ValidationRules by name if they exist + + Arguments: + names: A list of names of ValidationRules + + Raises: + ValueError: If any of the input names don't correspond to actual rules + + Returns: + A list of ValidationRules + """ + rule_dict = {name: _VALIDATION_RULES.get(name) for name in names} + invalid_rule_names = [ + rule_name for (rule_name, rule) in rule_dict.items() if rule is None + ] + if invalid_rule_names: + raise ValueError("Some input rule names are invalid:", invalid_rule_names) + return [rule for rule in rule_dict.values() if rule is not None] diff --git a/tests/data/example.model.column_type_component.csv b/tests/data/example.model.column_type_component.csv index e5555782d..d5703b856 100644 --- a/tests/data/example.model.column_type_component.csv +++ b/tests/data/example.model.column_type_component.csv @@ -53,13 +53,15 @@ MockRDB,,,"Component, MockRDB_id, SourceManifest",,FALSE,DataType,,,, MockRDB_id,,,,,TRUE,DataProperty,,,int, SourceManifest,,,,,TRUE,DataProperty,,,, MockFilename,,,"Component, Filename",,FALSE,DataType,,,, -JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, Enum, Enum Not Required, List String, List String, List InRange, List Not Required, List Enum Not Required, String Not Required, ",,FALSE,DataType,,,, +JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, String, String Not Required, Enum, Enum Not Required, Date, URL, InRange, Regex, List, List Not Required, List Enum, List Enum Not Required, List String, List InRange",,FALSE,DataType,,,, No Rules,,,,,TRUE,DataProperty,,,, No Rules Not Required,,,,,FALSE,DataProperty,,,, String,,,,,TRUE,DataProperty,,,str error, String Not Required,,,,,FALSE,DataProperty,,,str error, Enum,,"ab, cd, ef, gh",,,TRUE,DataProperty,,,, Enum Not Required,,"ab, cd, ef, gh",,,FALSE,DataProperty,,,, +Date,,,,,TRUE,DataProperty,,,date,string +URL,,,,,TRUE,DataProperty,,,url,string Range,,,,,TRUE,DataProperty,,,inRange 50 100, List,,,,,TRUE,DataProperty,,,list, List Not Required,,,,,FALSE,DataProperty,,,list, diff --git a/tests/data/example.model.column_type_component.invalid.csv b/tests/data/example.model.column_type_component.invalid.csv index 4e7b33e95..8788fde3d 100644 --- a/tests/data/example.model.column_type_component.invalid.csv +++ b/tests/data/example.model.column_type_component.invalid.csv @@ -53,13 +53,16 @@ MockRDB,,,"Component, MockRDB_id, SourceManifest",,FALSE,DataType,,,, MockRDB_id,,,,,TRUE,DataProperty,,,int, SourceManifest,,,,,TRUE,DataProperty,,,, MockFilename,,,"Component, Filename",,FALSE,DataType,,,, -JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, Enum, Enum Not Required, List String, List String, List InRange, List Not Required, List Enum Not Required, String Not Required, ",,FALSE,DataType,,,, +JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, String, String Not Required, Enum, Enum Not Required, Date, URL, InRange, Regex, List, List Not Required, List Enum, List Enum Not Required, List String, List InRange",,FALSE,DataType,,,, No Rules,,,,,TRUE,DataProperty,,,, No Rules Not Required,,,,,FALSE,DataProperty,,,, String,,,,,TRUE,DataProperty,,,str error, String Not Required,,,,,FALSE,DataProperty,,,str error, Enum,,"ab, cd, ef, gh",,,TRUE,DataProperty,,,, Enum Not Required,,"ab, cd, ef, gh",,,FALSE,DataProperty,,,, +Date,,,,,TRUE,DataProperty,,,date +URL,,,,,TRUE,DataProperty,,,url +InRange,,,,,TRUE,DataProperty,,,inRange 50 100 Range,,,,,TRUE,DataProperty,,,inRange 50 100, List,,,,,TRUE,DataProperty,,,list, List Not Required,,,,,FALSE,DataProperty,,,list, diff --git a/tests/data/example.model.column_type_component.invalid.jsonld b/tests/data/example.model.column_type_component.invalid.jsonld index be5d53b7c..8341b97c3 100644 --- a/tests/data/example.model.column_type_component.invalid.jsonld +++ b/tests/data/example.model.column_type_component.invalid.jsonld @@ -1860,6 +1860,15 @@ }, { "@id": "bts:StringNotRequired" + }, + { + "@id": "bts:Date" + }, + { + "@id": "bts:URL" + }, + { + "@id": "bts:InRange" } ], "sms:validationRules": [] @@ -2089,6 +2098,63 @@ "sms:validationRules": [ "str error" ] + }, + { + "@id": "bts:Date", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "Date", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "Date", + "sms:required": "sms:true", + "sms:validationRules": [ + "date" + ] + }, + { + "@id": "bts:URL", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "URL", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "URL", + "sms:required": "sms:true", + "sms:validationRules": [ + "url" + ] + }, + { + "@id": "bts:InRange", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "InRange", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "InRange", + "sms:required": "sms:true", + "sms:validationRules": [ + "inRange 50 100" + ] }, { "@id": "bts:Range", diff --git a/tests/data/example.model.column_type_component.jsonld b/tests/data/example.model.column_type_component.jsonld index 3df973959..75f516f47 100644 --- a/tests/data/example.model.column_type_component.jsonld +++ b/tests/data/example.model.column_type_component.jsonld @@ -1840,6 +1840,12 @@ { "@id": "bts:NoRulesNotRequired" }, + { + "@id": "bts:String" + }, + { + "@id": "bts:StringNotRequired" + }, { "@id": "bts:Enum" }, @@ -1847,19 +1853,34 @@ "@id": "bts:EnumNotRequired" }, { - "@id": "bts:ListString" + "@id": "bts:Date" }, { - "@id": "bts:ListInRange" + "@id": "bts:URL" + }, + { + "@id": "bts:InRange" + }, + { + "@id": "bts:Regex" + }, + { + "@id": "bts:List" }, { "@id": "bts:ListNotRequired" }, + { + "@id": "bts:ListEnum" + }, { "@id": "bts:ListEnumNotRequired" }, { - "@id": "bts:StringNotRequired" + "@id": "bts:ListString" + }, + { + "@id": "bts:ListInRange" } ], "sms:validationRules": [] @@ -1898,6 +1919,44 @@ "sms:required": "sms:false", "sms:validationRules": [] }, + { + "@id": "bts:String", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "String", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "String", + "sms:required": "sms:true", + "sms:validationRules": [ + "str error" + ] + }, + { + "@id": "bts:StringNotRequired", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "StringNotRequired", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "String Not Required", + "sms:required": "sms:false", + "sms:validationRules": [ + "str error" + ] + }, { "@id": "bts:Enum", "@type": "rdfs:Class", @@ -1961,10 +2020,10 @@ "sms:validationRules": [] }, { - "@id": "bts:ListString", + "@id": "bts:Date", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListString", + "rdfs:label": "Date", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -1973,18 +2032,18 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List String", + "sms:columnType": "string", + "sms:displayName": "Date", "sms:required": "sms:true", "sms:validationRules": [ - "list", - "str" + "date" ] }, { - "@id": "bts:ListInRange", + "@id": "bts:URL", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListInRange", + "rdfs:label": "URL", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -1993,11 +2052,64 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List InRange", + "sms:columnType": "string", + "sms:displayName": "URL", "sms:required": "sms:true", "sms:validationRules": [ - "list", - "inRange 50 100" + "url" + ] + }, + { + "@id": "bts:InRange", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "InRange", + "rdfs:subClassOf": [ + { + "@id": "bts:Thing" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "InRange", + "sms:required": "sms:false", + "sms:validationRules": [] + }, + { + "@id": "bts:Regex", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "Regex", + "rdfs:subClassOf": [ + { + "@id": "bts:Thing" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "Regex", + "sms:required": "sms:false", + "sms:validationRules": [] + }, + { + "@id": "bts:List", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "List", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "List", + "sms:required": "sms:true", + "sms:validationRules": [ + "list" ] }, { @@ -2020,10 +2132,10 @@ ] }, { - "@id": "bts:ListEnumNotRequired", + "@id": "bts:ListEnum", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListEnumNotRequired", + "rdfs:label": "ListEnum", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2046,17 +2158,17 @@ "@id": "bts:Gh" } ], - "sms:displayName": "List Enum Not Required", - "sms:required": "sms:false", + "sms:displayName": "List Enum", + "sms:required": "sms:true", "sms:validationRules": [ "list" ] }, { - "@id": "bts:StringNotRequired", + "@id": "bts:ListEnumNotRequired", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "StringNotRequired", + "rdfs:label": "ListEnumNotRequired", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2065,36 +2177,31 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "String Not Required", - "sms:required": "sms:false", - "sms:validationRules": [ - "str error" - ] - }, - { - "@id": "bts:String", - "@type": "rdfs:Class", - "rdfs:comment": "TBD", - "rdfs:label": "String", - "rdfs:subClassOf": [ + "schema:rangeIncludes": [ { - "@id": "bts:DataProperty" + "@id": "bts:Ab" + }, + { + "@id": "bts:Cd" + }, + { + "@id": "bts:Ef" + }, + { + "@id": "bts:Gh" } ], - "schema:isPartOf": { - "@id": "http://schema.biothings.io" - }, - "sms:displayName": "String", - "sms:required": "sms:true", + "sms:displayName": "List Enum Not Required", + "sms:required": "sms:false", "sms:validationRules": [ - "str error" + "list" ] }, { - "@id": "bts:Range", + "@id": "bts:ListString", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "Range", + "rdfs:label": "ListString", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2103,17 +2210,18 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "Range", + "sms:displayName": "List String", "sms:required": "sms:true", "sms:validationRules": [ - "inRange 50 100" + "list", + "str" ] }, { - "@id": "bts:List", + "@id": "bts:ListInRange", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "List", + "rdfs:label": "ListInRange", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2122,17 +2230,18 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List", + "sms:displayName": "List InRange", "sms:required": "sms:true", "sms:validationRules": [ - "list" + "list", + "inRange 50 100" ] }, { - "@id": "bts:ListEnum", + "@id": "bts:Range", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListEnum", + "rdfs:label": "Range", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2141,24 +2250,10 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "schema:rangeIncludes": [ - { - "@id": "bts:Ab" - }, - { - "@id": "bts:Cd" - }, - { - "@id": "bts:Ef" - }, - { - "@id": "bts:Gh" - } - ], - "sms:displayName": "List Enum", + "sms:displayName": "Range", "sms:required": "sms:true", "sms:validationRules": [ - "list" + "inRange 50 100" ] }, { diff --git a/tests/data/example.model.csv b/tests/data/example.model.csv index 096930b1b..fb65ffa8c 100644 --- a/tests/data/example.model.csv +++ b/tests/data/example.model.csv @@ -53,14 +53,16 @@ MockRDB,,,"Component, MockRDB_id, SourceManifest",,FALSE,DataType,,, MockRDB_id,,,,,TRUE,DataProperty,,,int SourceManifest,,,,,TRUE,DataProperty,,, MockFilename,,,"Component, Filename",,FALSE,DataType,,, -JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, Enum, Enum Not Required, List String, List String, List InRange, List Not Required, List Enum Not Required, String Not Required, ",,FALSE,DataType,,, +JSONSchemaComponent,Component to hold attributes for testing JSON Schemas,,"Component, No Rules, No Rules Not Required, String, String Not Required, Enum, Enum Not Required, Date, URL, InRange, Regex, List, List Not Required, List Enum, List Enum Not Required, List String, List InRange",,FALSE,DataType,,, No Rules,,,,,TRUE,DataProperty,,, No Rules Not Required,,,,,FALSE,DataProperty,,, String,,,,,TRUE,DataProperty,,,str error String Not Required,,,,,FALSE,DataProperty,,,str error Enum,,"ab, cd, ef, gh",,,TRUE,DataProperty,,, Enum Not Required,,"ab, cd, ef, gh",,,FALSE,DataProperty,,, -Range,,,,,TRUE,DataProperty,,,inRange 50 100 +Date,,,,,TRUE,DataProperty,,,date +URL,,,,,TRUE,DataProperty,,,url +InRange,,,,,TRUE,DataProperty,,,inRange 50 100 Regex,,,,,TRUE,DataProperty,,,regex search [a-f] List,,,,,TRUE,DataProperty,,,list List Not Required,,,,,FALSE,DataProperty,,,list diff --git a/tests/data/example.model.jsonld b/tests/data/example.model.jsonld index db5e7fd92..b3c3e0da4 100644 --- a/tests/data/example.model.jsonld +++ b/tests/data/example.model.jsonld @@ -1840,6 +1840,12 @@ { "@id": "bts:NoRulesNotRequired" }, + { + "@id": "bts:String" + }, + { + "@id": "bts:StringNotRequired" + }, { "@id": "bts:Enum" }, @@ -1847,19 +1853,34 @@ "@id": "bts:EnumNotRequired" }, { - "@id": "bts:ListString" + "@id": "bts:Date" }, { - "@id": "bts:ListInRange" + "@id": "bts:URL" + }, + { + "@id": "bts:InRange" + }, + { + "@id": "bts:Regex" + }, + { + "@id": "bts:List" }, { "@id": "bts:ListNotRequired" }, + { + "@id": "bts:ListEnum" + }, { "@id": "bts:ListEnumNotRequired" }, { - "@id": "bts:StringNotRequired" + "@id": "bts:ListString" + }, + { + "@id": "bts:ListInRange" } ], "sms:validationRules": [] @@ -1898,6 +1919,44 @@ "sms:required": "sms:false", "sms:validationRules": [] }, + { + "@id": "bts:String", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "String", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "String", + "sms:required": "sms:true", + "sms:validationRules": [ + "str error" + ] + }, + { + "@id": "bts:StringNotRequired", + "@type": "rdfs:Class", + "rdfs:comment": "TBD", + "rdfs:label": "StringNotRequired", + "rdfs:subClassOf": [ + { + "@id": "bts:DataProperty" + } + ], + "schema:isPartOf": { + "@id": "http://schema.biothings.io" + }, + "sms:displayName": "String Not Required", + "sms:required": "sms:false", + "sms:validationRules": [ + "str error" + ] + }, { "@id": "bts:Enum", "@type": "rdfs:Class", @@ -1961,10 +2020,10 @@ "sms:validationRules": [] }, { - "@id": "bts:ListString", + "@id": "bts:Date", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListString", + "rdfs:label": "Date", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -1973,18 +2032,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List String", + "sms:displayName": "Date", "sms:required": "sms:true", "sms:validationRules": [ - "list", - "str" + "date" ] }, { - "@id": "bts:ListInRange", + "@id": "bts:URL", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListInRange", + "rdfs:label": "URL", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -1993,18 +2051,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List InRange", + "sms:displayName": "URL", "sms:required": "sms:true", "sms:validationRules": [ - "list", - "inRange 50 100" + "url" ] }, { - "@id": "bts:ListNotRequired", + "@id": "bts:InRange", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListNotRequired", + "rdfs:label": "InRange", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2013,17 +2070,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List Not Required", - "sms:required": "sms:false", + "sms:displayName": "InRange", + "sms:required": "sms:true", "sms:validationRules": [ - "list" + "inRange 50 100" ] }, { - "@id": "bts:ListEnumNotRequired", + "@id": "bts:Regex", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListEnumNotRequired", + "rdfs:label": "Regex", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2032,31 +2089,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "schema:rangeIncludes": [ - { - "@id": "bts:Ab" - }, - { - "@id": "bts:Cd" - }, - { - "@id": "bts:Ef" - }, - { - "@id": "bts:Gh" - } - ], - "sms:displayName": "List Enum Not Required", - "sms:required": "sms:false", + "sms:displayName": "Regex", + "sms:required": "sms:true", "sms:validationRules": [ - "list" + "regex search [a-f]" ] }, { - "@id": "bts:StringNotRequired", + "@id": "bts:List", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "StringNotRequired", + "rdfs:label": "List", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2065,17 +2108,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "String Not Required", - "sms:required": "sms:false", + "sms:displayName": "List", + "sms:required": "sms:true", "sms:validationRules": [ - "str error" + "list" ] }, { - "@id": "bts:String", + "@id": "bts:ListNotRequired", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "String", + "rdfs:label": "ListNotRequired", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2084,17 +2127,17 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "String", - "sms:required": "sms:true", + "sms:displayName": "List Not Required", + "sms:required": "sms:false", "sms:validationRules": [ - "str error" + "list" ] }, { - "@id": "bts:Range", + "@id": "bts:ListEnum", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "Range", + "rdfs:label": "ListEnum", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2103,17 +2146,31 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "Range", + "schema:rangeIncludes": [ + { + "@id": "bts:Ab" + }, + { + "@id": "bts:Cd" + }, + { + "@id": "bts:Ef" + }, + { + "@id": "bts:Gh" + } + ], + "sms:displayName": "List Enum", "sms:required": "sms:true", "sms:validationRules": [ - "inRange 50 100" + "list" ] }, { - "@id": "bts:Regex", + "@id": "bts:ListEnumNotRequired", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "Regex", + "rdfs:label": "ListEnumNotRequired", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2122,17 +2179,31 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "Regex", - "sms:required": "sms:true", + "schema:rangeIncludes": [ + { + "@id": "bts:Ab" + }, + { + "@id": "bts:Cd" + }, + { + "@id": "bts:Ef" + }, + { + "@id": "bts:Gh" + } + ], + "sms:displayName": "List Enum Not Required", + "sms:required": "sms:false", "sms:validationRules": [ - "regex search [a-f]" + "list" ] }, { - "@id": "bts:List", + "@id": "bts:ListString", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "List", + "rdfs:label": "ListString", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2141,17 +2212,18 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "sms:displayName": "List", + "sms:displayName": "List String", "sms:required": "sms:true", "sms:validationRules": [ - "list" + "list", + "str" ] }, { - "@id": "bts:ListEnum", + "@id": "bts:ListInRange", "@type": "rdfs:Class", "rdfs:comment": "TBD", - "rdfs:label": "ListEnum", + "rdfs:label": "ListInRange", "rdfs:subClassOf": [ { "@id": "bts:DataProperty" @@ -2160,24 +2232,11 @@ "schema:isPartOf": { "@id": "http://schema.biothings.io" }, - "schema:rangeIncludes": [ - { - "@id": "bts:Ab" - }, - { - "@id": "bts:Cd" - }, - { - "@id": "bts:Ef" - }, - { - "@id": "bts:Gh" - } - ], - "sms:displayName": "List Enum", + "sms:displayName": "List InRange", "sms:required": "sms:true", "sms:validationRules": [ - "list" + "list", + "inRange 50 100" ] } ], diff --git a/tests/data/expected_jsonschemas/expected.JSONSchemaComponent.schema.json b/tests/data/expected_jsonschemas/expected.JSONSchemaComponent.schema.json index f2127e3c3..b378f46d2 100644 --- a/tests/data/expected_jsonschemas/expected.JSONSchemaComponent.schema.json +++ b/tests/data/expected_jsonschemas/expected.JSONSchemaComponent.schema.json @@ -10,6 +10,12 @@ }, "title": "Component" }, + "Date": { + "description": "TBD", + "format": "date", + "title": "Date", + "type": "string" + }, "Enum": { "description": "TBD", "oneOf": [ @@ -44,6 +50,41 @@ ], "title": "Enum Not Required" }, + "InRange": { + "description": "TBD", + "maximum": 100.0, + "minimum": 50.0, + "title": "InRange", + "type": "number" + }, + "List": { + "description": "TBD", + "oneOf": [ + { + "title": "array", + "type": "array" + } + ], + "title": "List" + }, + "ListEnum": { + "description": "TBD", + "oneOf": [ + { + "items": { + "enum": [ + "ab", + "cd", + "ef", + "gh" + ] + }, + "title": "array", + "type": "array" + } + ], + "title": "List Enum" + }, "ListEnumNotRequired": { "description": "TBD", "oneOf": [ @@ -119,6 +160,17 @@ "description": "TBD", "title": "No Rules Not Required" }, + "Regex": { + "description": "TBD", + "pattern": "[a-f]", + "title": "Regex", + "type": "string" + }, + "String": { + "description": "TBD", + "title": "String", + "type": "string" + }, "StringNotRequired": { "description": "TBD", "oneOf": [ @@ -132,14 +184,27 @@ } ], "title": "String Not Required" + }, + "URL": { + "description": "TBD", + "format": "uri", + "title": "URL", + "type": "string" } }, "required": [ "Component", + "Date", "Enum", + "InRange", + "List", + "ListEnum", "ListInRange", "ListString", - "NoRules" + "NoRules", + "Regex", + "String", + "URL" ], "title": "JSONSchemaComponent_validation", "type": "object" diff --git a/tests/data/expected_jsonschemas/expected.MockComponent.schema.json b/tests/data/expected_jsonschemas/expected.MockComponent.schema.json index 5fd69c98e..157363526 100644 --- a/tests/data/expected_jsonschemas/expected.MockComponent.schema.json +++ b/tests/data/expected_jsonschemas/expected.MockComponent.schema.json @@ -12,9 +12,8 @@ }, "CheckDate": { "description": "TBD", - "not": { - "type": "null" - }, + "type": "string", + "format": "date", "title": "Check Date" }, "CheckFloat": { @@ -241,9 +240,8 @@ }, "CheckURL": { "description": "TBD", - "not": { - "type": "null" - }, + "type": "string", + "format": "uri", "title": "Check URL" }, "CheckUnique": { diff --git a/tests/unit/test_create_json_schema.py b/tests/unit/test_create_json_schema.py index 2cb377ec9..d69968175 100644 --- a/tests/unit/test_create_json_schema.py +++ b/tests/unit/test_create_json_schema.py @@ -15,12 +15,7 @@ from schematic.schemas.data_model_graph import DataModelGraphExplorer from schematic.schemas.create_json_schema import ( - ValidationRule, _get_validation_rule_based_fields, - _get_range_from_in_range_rule, - _get_pattern_from_regex_rule, - _get_type_rule_from_rule_list, - _get_rule_from_rule_list, JSONSchema, Node, GraphTraversalState, @@ -34,7 +29,8 @@ _create_simple_property, _set_type_specific_keywords, ) -from tests.utils import json_files_equal + +from schematic.schemas.constants import JSONSchemaType, JSONSchemaFormat # pylint: disable=protected-access # pylint: disable=too-many-arguments @@ -66,8 +62,10 @@ def fixture_test_nodes( "StringNotRequired", "Enum", "EnumNotRequired", - "Range", + "InRange", "Regex", + "Date", + "URL", "List", "ListNotRequired", "ListEnum", @@ -148,38 +146,44 @@ def test_update_property(self) -> None: @pytest.mark.parametrize( - "node_name, expected_type, expected_is_array, expected_min, expected_max, expected_pattern", + "node_name, expected_type, expected_is_array, expected_min, expected_max, expected_pattern, expected_format", [ # If there are no type validation rules the type is None - ("NoRules", None, False, None, None, None), + ("NoRules", None, False, None, None, None, None), # If there is one type validation rule the type is set to the # JSON Schema equivalent of the validation rule - ("String", "string", False, None, None, None), + ("String", JSONSchemaType.STRING, False, None, None, None, None), # If there are any list type validation rules then is_array is set to True - ("List", None, True, None, None, None), + ("List", None, True, None, None, None, None), # If there are any list type validation rules and one type validation rule # then is_array is set to True, and the type is set to the # JSON Schema equivalent of the validation rule - ("ListString", "string", True, None, None, None), + ("ListString", JSONSchemaType.STRING, True, None, None, None, None), # If there is an inRange rule the min and max will be set - ("Range", "number", False, 50, 100, None), + ("InRange", JSONSchemaType.NUMBER, False, 50, 100, None, None), # If there is a regex rule, then the pattern should be set - ("Regex", "string", False, None, None, "[a-f]"), + ("Regex", JSONSchemaType.STRING, False, None, None, "[a-f]", None), + # If there is a date rule, then the format should be set to "date" + ("Date", JSONSchemaType.STRING, False, None, None, None, JSONSchemaFormat.DATE), + # If there is a URL rule, then the format should be set to "uri" + ("URL", JSONSchemaType.STRING, False, None, None, None, JSONSchemaFormat.URI), ], - ids=["None", "String", "List", "ListString", "Range", "Regex"], + ids=["None", "String", "List", "ListString", "InRange", "Regex", "Date", "URI"], ) def test_node_init( node_name: str, - expected_type: Optional[str], + expected_type: Optional[JSONSchemaType], expected_is_array: bool, expected_min: Optional[float], expected_max: Optional[float], expected_pattern: Optional[str], + expected_format: Optional[JSONSchemaFormat], test_nodes: dict[str, Node], ) -> None: """Tests for Node class""" node = test_nodes[node_name] assert node.type == expected_type + assert node.format == expected_format assert node.is_array == expected_is_array assert node.minimum == expected_min assert node.maximum == expected_max @@ -187,186 +191,72 @@ def test_node_init( @pytest.mark.parametrize( - "validation_rules, expected_type, expected_is_array, expected_min, expected_max, expected_pattern", + "validation_rules, expected_type, expected_is_array, expected_min, expected_max, expected_pattern, expected_format", [ # If there are no type validation rules the type is None - ([], None, False, None, None, None), + ([], None, False, None, None, None, None), # If there is one type validation rule the type is set to the # JSON Schema equivalent of the validation rule - (["str"], "string", False, None, None, None), + (["str"], JSONSchemaType.STRING, False, None, None, None, None), # If there are any list type validation rules then is_array is set to True - (["list"], None, True, None, None, None), + (["list"], None, True, None, None, None, None), # If there are any list type validation rules and one type validation rule # then is_array is set to True, and the type is set to the # JSON Schema equivalent of the validation rule - (["list", "str"], "string", True, None, None, None), + (["list", "str"], JSONSchemaType.STRING, True, None, None, None, None), # If there is an inRange rule the min and max will be set - (["inRange 50 100"], "number", False, 50, 100, None), + (["inRange 50 100"], JSONSchemaType.NUMBER, False, 50, 100, None, None), # If there is a regex rule, then the pattern should be set - (["regex search [a-f]"], "string", False, None, None, "[a-f]"), + ( + ["regex search [a-f]"], + JSONSchemaType.STRING, + False, + None, + None, + "[a-f]", + None, + ), + # If there is a date rule, then the format should be set to "date" + ( + ["date"], + JSONSchemaType.STRING, + False, + None, + None, + None, + JSONSchemaFormat.DATE, + ), + # If there is a URL rule, then the format should be set to "uri" + (["url"], JSONSchemaType.STRING, False, None, None, None, JSONSchemaFormat.URI), ], - ids=["No rules", "String", "List", "ListString", "InRange", "Regex"], + ids=["No rules", "String", "List", "ListString", "InRange", "Regex", "Date", "URL"], ) def test_get_validation_rule_based_fields( validation_rules: list[str], - expected_type: Optional[str], + expected_type: Optional[JSONSchemaType], expected_is_array: bool, expected_min: Optional[float], expected_max: Optional[float], expected_pattern: Optional[str], + expected_format: Optional[JSONSchemaFormat], ) -> None: """Tests for _get_validation_rule_based_fields""" ( - prop_type, is_array, + property_type, + property_format, minimum, maximum, pattern, ) = _get_validation_rule_based_fields(validation_rules) - assert prop_type == expected_type + assert property_type == expected_type + assert property_format == expected_format assert is_array == expected_is_array assert minimum == expected_min assert maximum == expected_max assert pattern == expected_pattern -@pytest.mark.parametrize( - "input_rule, expected_tuple", - [ - ("", (None, None)), - ("inRange", (None, None)), - ("inRange x x", (None, None)), - ("inRange 0", (0, None)), - ("inRange 0 x", (0, None)), - ("inRange 0 1", (0, 1)), - ("inRange 0 1 x", (0, 1)), - ], - ids=[ - "No rules", - "inRange with no params", - "inRange with bad params", - "inRange with minimum", - "inRange with minimum, bad maximum", - "inRange with minimum, maximum", - "inRange with minimum, maximum, extra param", - ], -) -def test_get_range_from_in_range_rule( - input_rule: str, - expected_tuple: tuple[Optional[str], Optional[str]], -) -> None: - """Test for _get_range_from_in_range_rule""" - result = _get_range_from_in_range_rule(input_rule) - assert result == expected_tuple - - -@pytest.mark.parametrize( - "input_rule, expected_pattern", - [ - ("", None), - ("regex search [a-f]", "[a-f]"), - ("regex match [a-f]", "^[a-f]"), - ("regex match ^[a-f]", "^[a-f]"), - ("regex split ^[a-f]", None), - ], - ids=[ - "No rules, None returned", - "Search module, Pattern returned", - "Match module, Pattern returned with carrot added", - "Match module, Pattern returned with no carrot added", - "Unallowed module, None returned", - ], -) -def test_get_pattern_from_regex_rule( - input_rule: str, - expected_pattern: Optional[str], -) -> None: - """Test for _get_pattern_from_regex_rule""" - result = _get_pattern_from_regex_rule(input_rule) - assert result == expected_pattern - - -@pytest.mark.parametrize( - "rule, input_rules, expected_rule", - [ - (ValidationRule.IN_RANGE, [], None), - (ValidationRule.IN_RANGE, ["regex match [a-f]"], None), - (ValidationRule.IN_RANGE, ["inRange 0 1"], "inRange 0 1"), - (ValidationRule.IN_RANGE, ["str error", "inRange 0 1"], "inRange 0 1"), - (ValidationRule.REGEX, ["inRange 0 1"], None), - (ValidationRule.REGEX, ["regex match [a-f]"], "regex match [a-f]"), - ], - ids=[ - "inRange: No rules", - "inRange: No inRange rules", - "inRange: Rule present", - "inRange: Rule present, multiple rules", - "regex: No regex rules", - "regex: Rule present", - ], -) -def test_get_rule_from_rule_list( - rule: ValidationRule, - input_rules: list[str], - expected_rule: Optional[str], -) -> None: - """Test for _get_rule_from_rule_list""" - result = _get_rule_from_rule_list(rule, input_rules) - assert result == expected_rule - - -@pytest.mark.parametrize( - "rule, input_rules", - [ - (ValidationRule.IN_RANGE, ["inRange", "inRange"]), - (ValidationRule.IN_RANGE, ["inRange 0", "inRange 0"]), - ], - ids=["Multiple inRange rules", "Multiple inRange rules with params"], -) -def test_get_rule_from_rule_list_exceptions( - rule: ValidationRule, - input_rules: list[str], -) -> None: - """Test for __get_rule_from_rule_list with exceptions""" - with pytest.raises( - ValueError, match="Found more than one 'inRange' rule in validation rules" - ): - _get_rule_from_rule_list(rule, input_rules) - - -@pytest.mark.parametrize( - "input_rules, expected_rule", - [([], None), (["list strict"], None), (["str"], "str"), (["str error"], "str")], - ids=["No rules", "List", "String", "String with error param"], -) -def test_get_type_rule_from_rule_list( - input_rules: list[str], - expected_rule: Optional[str], -) -> None: - """Test for _get_type_rule_from_rule_list""" - result = _get_type_rule_from_rule_list(input_rules) - assert result == expected_rule - - -@pytest.mark.parametrize( - "input_rules", - [(["str", "int"]), (["str", "str", "str"]), (["str", "str error", "str warning"])], - ids=[ - "Multiple type rules", - "Repeated str rules", - "Repeated str rules with parameters", - ], -) -def test_get_type_rule_from_rule_list_exceptions( - input_rules: list[str], -) -> None: - """Test for _get_type_rule_from_rule_list with exceptions""" - with pytest.raises( - ValueError, match="Found more than one type rule in validation rules" - ): - _get_type_rule_from_rule_list(input_rules) - - class TestGraphTraversalState: """Tests for GraphTraversalState class""" @@ -569,7 +459,12 @@ def test_create_json_schema_with_class_label( schema_path=test_path, use_property_display_names=False, ) - assert json_files_equal(expected_path, test_path) + with open(expected_path, encoding="utf-8") as file1, open( + test_path, encoding="utf-8" + ) as file2: + expected_json = json.load(file1) + test_json = json.load(file2) + assert expected_json == test_json @pytest.mark.parametrize( @@ -595,32 +490,12 @@ def test_create_json_schema_with_display_names( schema_name=f"{datatype}_validation", schema_path=test_path, ) - assert json_files_equal(expected_path, test_path) - - -@pytest.mark.parametrize( - "datatype", - [ - ("BulkRNA-seqAssay"), - ("Patient"), - ], -) -def test_create_json_schema_with_display_names( - dmge: DataModelGraphExplorer, datatype: str, test_directory: str -) -> None: - """Tests for JSONSchemaGenerator.create_json_schema""" - test_file = f"test.{datatype}.display_names_schema.json" - test_path = os.path.join(test_directory, test_file) - expected_path = ( - f"tests/data/expected_jsonschemas/expected.{datatype}.display_names_schema.json" - ) - create_json_schema( - dmge=dmge, - datatype=datatype, - schema_name=f"{datatype}_validation", - schema_path=test_path, - ) - assert json_files_equal(expected_path, test_path) + with open(expected_path, encoding="utf-8") as file1, open( + test_path, encoding="utf-8" + ) as file2: + expected_json = json.load(file1) + test_json = json.load(file2) + assert expected_json == test_json @pytest.mark.parametrize( @@ -1142,7 +1017,7 @@ def test_create_enum_property( [None], ), ( - "Range", + "InRange", { "type": "number", "minimum": 50, @@ -1183,12 +1058,12 @@ def test_create_simple_property( "node_name, expected_schema", [ ("NoRules", {}), - ("Range", {"minimum": 50, "maximum": 100}), + ("InRange", {"minimum": 50, "maximum": 100}), ("Regex", {"pattern": "[a-f]"}), ], ids=[ "NoRules", - "Range", + "InRange", "Regex", ], ) diff --git a/tests/unit/test_json_schema_validation_rule_functions.py b/tests/unit/test_json_schema_validation_rule_functions.py new file mode 100644 index 000000000..fc1ef9291 --- /dev/null +++ b/tests/unit/test_json_schema_validation_rule_functions.py @@ -0,0 +1,423 @@ +"""Unit tests for validation rule functions""" + +from typing import Optional, Union +import pytest +from schematic.schemas.json_schema_validation_rule_functions import ( + ValidationRuleName, + filter_unused_inputted_rules, + check_for_duplicate_inputted_rules, + check_for_conflicting_inputted_rules, + get_rule_from_inputted_rules, + get_js_type_from_inputted_rules, + get_in_range_parameters_from_inputted_rule, + get_regex_parameters_from_inputted_rule, + get_validation_rule_names_from_inputted_rules, + _get_parameters_from_inputted_rule, + get_names_from_inputted_rules, + _get_name_from_inputted_rule, + _get_rules_by_names, +) +from schematic.schemas.constants import JSONSchemaType + + +@pytest.mark.parametrize( + "input_list, expected_list", + [ + ([], []), + (["str"], ["str"]), + (["str", "unused_rule"], ["str"]), + ], + ids=["No rules", "Str rule", "Contains an unused rule"], +) +def test_filter_unused_inputted_rules( + input_list: list[str], expected_list: list[str] +) -> None: + """ + Test for filter_unused_inputted_rules + Tests that only rules used to create JSON Schemas are left + """ + result = filter_unused_inputted_rules(input_list) + assert result == expected_list + + +@pytest.mark.parametrize( + "input_list", + [ + ([]), + (["str"]), + (["str", "int", "bool"]), + ], + ids=["No rules", "One rule", "Multiple rules"], +) +def test_check_for_duplicate_inputted_rules( + input_list: list[str], +) -> None: + """ + Test for check_for_duplicate_inputted_rules + Tests that no duplicate rules were found and no exceptions raised + """ + check_for_duplicate_inputted_rules(input_list) + + +@pytest.mark.parametrize( + "input_list", + [(["str", "str"]), (["str warning", "str error"])], + ids=["Duplicate string rules", "Duplicate string rules with different parameters"], +) +def test_check_for_duplicate_inputted_rules_with_duplicates( + input_list: list[str], +) -> None: + """ + Test for check_for_duplicate_inputted_rules + Tests that duplicate rules were found and a ValueError was raised + """ + with pytest.raises(ValueError, match="Validation Rules contains duplicates"): + check_for_duplicate_inputted_rules(input_list) + + +@pytest.mark.parametrize( + "input_list", + [([]), (["str", "regex"]), (["inRange", "int"])], + ids=["No rules", "Str and regex rules", "InRange and int rules"], +) +def test_check_for_conflicting_inputted_rules( + input_list: list[str], +) -> None: + """ + Test for check_for_conflicting_inputted_rules + Tests that no rules are in conflict with each other + """ + check_for_conflicting_inputted_rules(input_list) + + +@pytest.mark.parametrize( + "input_list, expected_msg", + [ + (["str", "int"], "Validation rule: str has conflicting rules: \\['int'\\]"), + (["date", "url"], "Validation rule: date has conflicting rules: \\['url'\\]"), + (["regex", "int"], "Validation rule: regex has conflicting rules: \\['int'\\]"), + ( + ["inRange", "str"], + "Validation rule: inRange has conflicting rules: \\['str'\\]", + ), + ( + ["inRange", "str", "regex"], + "Validation rule: inRange has conflicting rules: \\['regex', 'str'\\]", + ), + ], + ids=[ + "Multiple type rules", + "Multiple format rules", + "Regex and int rules", + "InRange and str rules", + "InRange and multiple conflicting rules", + ], +) +def test_check_for_conflicting_inputted_rules_with_conflicts( + input_list: list[str], expected_msg: str +) -> None: + """ + Test for check_for_conflicting_inputted_rules + Tests that rules are in conflict with each other and a ValueError is raised + """ + with pytest.raises(ValueError, match=expected_msg): + check_for_conflicting_inputted_rules(input_list) + + +@pytest.mark.parametrize( + "rule, input_rules, expected_rule", + [ + (ValidationRuleName.IN_RANGE, [], None), + (ValidationRuleName.IN_RANGE, ["regex match [a-f]"], None), + (ValidationRuleName.IN_RANGE, ["inRange 0 1"], "inRange 0 1"), + (ValidationRuleName.IN_RANGE, ["str error", "inRange 0 1"], "inRange 0 1"), + (ValidationRuleName.REGEX, ["inRange 0 1"], None), + (ValidationRuleName.REGEX, ["regex match [a-f]"], "regex match [a-f]"), + ], + ids=[ + "inRange: No rules", + "inRange: No inRange rules", + "inRange: Rule present", + "inRange: Rule present, multiple rules", + "regex: No regex rules", + "regex: Rule present", + ], +) +def test_get_rule_from_inputted_rules( + rule: ValidationRuleName, + input_rules: list[str], + expected_rule: Optional[str], +) -> None: + """ + Test for get_rule_from_inputted_rules + Tests that None is returned if there are no matches + or the rule is returned if there is a match + """ + result = get_rule_from_inputted_rules(rule, input_rules) + assert result == expected_rule + + +def test_get_rule_from_inputted_rules_with_exception() -> None: + """ + Test for get_rule_from_inputted_rules + Tests that when the requested rule has multiple matches, a ValueError is raised + """ + with pytest.raises(ValueError): + get_rule_from_inputted_rules( + ValidationRuleName.IN_RANGE, ["inRange", "inRange 0 1"] + ) + + +@pytest.mark.parametrize( + "input_rules, expected_js_type", + [ + ([], None), + (["list strict"], None), + (["str"], JSONSchemaType.STRING), + (["str error"], JSONSchemaType.STRING), + ], + ids=["No rules", "List", "String", "String with error param"], +) +def test_get_js_type_from_inputted_rules( + input_rules: list[str], + expected_js_type: Optional[JSONSchemaType], +) -> None: + """ + Test for get_js_type_from_inputted_rules + Tests that if theres only one JSON Schema type amongst all the rules it will be returned + Otherwise None will be returned + """ + result = get_js_type_from_inputted_rules(input_rules) + assert result == expected_js_type + + +def test_get_js_type_from_inputted_rules_with_exception() -> None: + """ + Test for get_js_type_from_inputted_rules + Tests that if there are multiple JSON Schema types amongst all the rules + a ValueError will be raised + """ + with pytest.raises(ValueError): + get_js_type_from_inputted_rules(["str", "int"]) + + +@pytest.mark.parametrize( + "input_rule, expected_tuple", + [ + ("inRange", (None, None)), + ("inRange x x", (None, None)), + ("inRange 0", (0, None)), + ("inRange 0 x", (0, None)), + ("inRange 0 1", (0, 1)), + ("inRange 0 1 x", (0, 1)), + ], + ids=[ + "inRange with no params", + "inRange with bad params", + "inRange with minimum", + "inRange with minimum, bad maximum", + "inRange with minimum, maximum", + "inRange with minimum, maximum, extra param", + ], +) +def test_get_in_range_parameters_from_inputted_rule( + input_rule: str, + expected_tuple: tuple[Optional[str], Optional[str]], +) -> None: + """ + Test for get_in_range_parameters_from_inputted_rule + Tests that if the minimum and maximum parameters exist and are numeric they are returned + """ + result = get_in_range_parameters_from_inputted_rule(input_rule) + assert result == expected_tuple + + +@pytest.mark.parametrize( + "input_rule, expected_pattern", + [ + ("regex search [a-f]", "[a-f]"), + ("regex match [a-f]", "^[a-f]"), + ("regex match ^[a-f]", "^[a-f]"), + ("regex split ^[a-f]", None), + ], + ids=[ + "Search module, Pattern returned", + "Match module, Pattern returned with carrot added", + "Match module, Pattern returned with no carrot added", + "Unallowed module, None returned", + ], +) +def test_get_regex_parameters_from_inputted_rule( + input_rule: str, + expected_pattern: Optional[str], +) -> None: + """ + Test for get_regex_parameters_from_inputted_rule + Tests that if the module parameter exists and is one of the allowed values + the pattern is returned + """ + result = get_regex_parameters_from_inputted_rule(input_rule) + assert result == expected_pattern + + +@pytest.mark.parametrize( + "input_rules, expected_rule_names", + [ + ([], []), + (["str"], [ValidationRuleName.STR]), + (["str error"], [ValidationRuleName.STR]), + ( + ["str", "regex"], + [ValidationRuleName.STR, ValidationRuleName.REGEX], + ), + ], + ids=[ + "Empty list", + "String rule", + "String rule with parameters", + "Multiple rules", + ], +) +def test_get_validation_rule_names_from_inputted_rules( + input_rules: list[str], expected_rule_names: list[ValidationRuleName] +) -> None: + """ + Test for get_validation_rule_names_from_inputted_rules + Tests that for each input rule, the ValidationRuleName is returned + """ + result = get_validation_rule_names_from_inputted_rules(input_rules) + assert result == expected_rule_names + + +@pytest.mark.parametrize( + "input_rules", + [ + (["not_a_rule"]), + (["str", "regex", "not_a_rule"]), + ], + ids=["Non-rule", "Non-rule with actual rules"], +) +def test_get_validation_rule_names_from_inputted_rules_exception( + input_rules: list[str], +) -> None: + """ + Test for get_validation_rule_names_from_inputted_rules + Tests that if any fo the rules are invalid, a ValueError will be raised + """ + with pytest.raises(ValueError): + get_validation_rule_names_from_inputted_rules(input_rules) + + +@pytest.mark.parametrize( + "input_rule, expected_dict", + [ + ("not a rule", None), + ("str", None), + ("str error", None), + ("regex", {}), + ("regex search", {"module": "search"}), + ("regex search [a-f]", {"module": "search", "pattern": "[a-f]"}), + ], + ids=[ + "Not a rule", + "Str rule no parameters", + "Str rule parameters, but not collected", + "Regex rule no parameters", + "Regex rule module parameter", + "Regex rule module parameter and pattern parameter", + ], +) +def test_get_parameters_from_inputted_rule( + input_rule: str, + expected_dict: Optional[dict[str, Union[str, float]]], +) -> None: + """ + Test for _get_parameters_from_inputted_rule + Tests that if the validation rule has parameters to collect, and the input rule has them + they will be collected as a dict. + """ + result = _get_parameters_from_inputted_rule(input_rule) + assert result == expected_dict + + +@pytest.mark.parametrize( + "rules, expected_rule_names", + [ + ([], []), + (["str"], ["str"]), + (["str warning"], ["str"]), + (["str warning", "regex search [a-f]"], ["str", "regex"]), + ], + ids=[ + "Empty", + "One string rule, no parameters", + "One string rule, with parameters", + "two rules with parameters", + ], +) +def test_get_names_from_inputted_rules( + rules: list[str], expected_rule_names: list[str] +) -> None: + """ + Test for get_names_from_inputted_rules + Tests that the rule name is returned for each rule + (A rule is a string, that when split by spaces, the first item is the name) + """ + result = get_names_from_inputted_rules(rules) + assert result == expected_rule_names + + +@pytest.mark.parametrize( + "rules, expected_rule_names", + [("str", "str"), ("str warning", "str"), ("regex search [a-f]", "regex")], + ids=[ + "String rule, no parameters", + "String rule, with parameters", + "Regex rule, with parameters", + ], +) +def test_get_name_from_inputted_rule( + rules: list[str], expected_rule_names: list[str] +) -> None: + """ + Test for _get_name_from_inputted_rule + Tests that the rule name is returned + (A rule is a string, that when split by spaces, the first item is the name) + """ + result = _get_name_from_inputted_rule(rules) + assert result == expected_rule_names + + +@pytest.mark.parametrize( + "rule_names, expected_rule_names", + [ + ([], []), + (["str"], [ValidationRuleName.STR]), + (["str", "regex"], [ValidationRuleName.STR, ValidationRuleName.REGEX]), + ], + ids=["No rules", "Str rule", "Str + regex rules"], +) +def test_get_rules_by_names( + rule_names: list[str], expected_rule_names: list[ValidationRuleName] +) -> None: + """ + Test for _get_rules_by_names + Tests that for every rule name in the input list the rule is returned + """ + result = _get_rules_by_names(rule_names) + result_rules_names = [rule.name for rule in result] + assert result_rules_names == expected_rule_names + + +@pytest.mark.parametrize( + "rule_names", + [(["not_a_rule"]), (["not_a_rule", "str"]), (["int", "not_a_rule"])], + ids=["Non-rule", "Non-rule + str", "Non-rule + int"], +) +def test_get_rules_by_names_exceptions(rule_names: list[str]) -> None: + """ + Test for _get_rules_by_names + Tests when an a name with no actual real is given, a ValueError is raised + """ + with pytest.raises(ValueError): + _get_rules_by_names(rule_names)