Skip to content

Commit 00603ef

Browse files
authored
Merge pull request #54 from edanalytics/fix/validate_uniqueness_by_identity
validate uniqueness by identity
2 parents d36c903 + bfea9c6 commit 00603ef

4 files changed

Lines changed: 54 additions & 25 deletions

File tree

lightbeam/api.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ async def load_descriptors_values(self):
304304
for key in v.keys():
305305
if key.endswith("Id"): descriptor = key[0:-2]
306306
self.descriptor_values.append([descriptor, v["namespace"], v["codeValue"], v["shortDescription"], v.get("description", "")])
307-
307+
308308
# save
309309
if self.lightbeam.track_state:
310310
self.logger.debug(f"saving descriptor values to {cache_file}...")
@@ -331,33 +331,47 @@ async def load_descriptors_values(self):
331331
# }
332332
# (The first element is a required attribute of the assessmentItem; the other two are required elements
333333
# of the required nested assessmentReference.)
334-
def get_params_for_endpoint(self, endpoint):
334+
def get_params_for_endpoint(self, endpoint, type='required'):
335335
if "Descriptor" in endpoint: swagger = self.descriptors_swagger
336336
else: swagger = self.resources_swagger
337-
definition = util.camel_case(self.lightbeam.config["namespace"]) + "_" + util.singularize_endpoint(endpoint)
338-
return self.get_required_params_from_swagger(swagger, definition)
337+
definition = util.get_swagger_ref_for_endpoint(self.lightbeam.config["namespace"], swagger, endpoint)
338+
if type=='required':
339+
return self.get_required_params_from_swagger(swagger, definition)
340+
else:
341+
# descriptor endpoints all have the same structure and identity fields:
342+
if "Descriptor" in endpoint:
343+
return { 'namespace':'namespace', 'codeValue':'codeValue', 'shortDescription':'shortDescription'}
344+
else:
345+
return self.get_identity_params_from_swagger(swagger, definition)
339346

340347
def get_required_params_from_swagger(self, swagger, definition, prefix=""):
341348
params = {}
342-
use_definitions = False
343-
if "definitions" in swagger.keys():
344-
schema = swagger["definitions"][definition]
345-
use_definitions = True
346-
elif "components" in swagger.keys() and "schemas" in swagger["components"].keys():
347-
schema = swagger["components"]["schemas"][definition]
348-
else:
349+
schema = util.resolve_swagger_ref(swagger, definition)
350+
if not schema:
349351
self.logger.critical(f"Swagger contains neither `definitions` nor `components.schemas` - check that the Swagger is valid.")
350352

351-
for requiredProperty in schema["required"]:
352-
if "$ref" in schema["properties"][requiredProperty].keys():
353-
sub_definition = schema["properties"][requiredProperty]["$ref"]
354-
if use_definitions:
355-
sub_definition = sub_definition.replace("#/definitions/", "")
356-
else:
357-
sub_definition = sub_definition.replace("#/components/schemas/", "")
358-
sub_params = self.get_required_params_from_swagger(swagger, sub_definition, prefix=requiredProperty+".")
353+
for prop in schema["required"]:
354+
if "$ref" in schema["properties"][prop].keys():
355+
sub_definition = schema["properties"][prop]["$ref"]
356+
sub_params = self.get_required_params_from_swagger(swagger, sub_definition, prefix=prop+".")
357+
for k,v in sub_params.items():
358+
params[k] = v
359+
elif schema["properties"][prop]["type"]!="array":
360+
params[prop] = prefix + prop
361+
return params
362+
363+
def get_identity_params_from_swagger(self, swagger, definition, prefix=""):
364+
params = {}
365+
schema = util.resolve_swagger_ref(swagger, definition)
366+
if not schema:
367+
self.logger.critical(f"Swagger contains neither `definitions` nor `components.schemas` - check that the Swagger is valid.")
368+
369+
for prop in schema["properties"]:
370+
if prop.endswith("Reference") and "required" in schema.keys() and prop in schema['required'] and "$ref" in schema["properties"][prop].keys():
371+
sub_definition = schema["properties"][prop]["$ref"]
372+
sub_params = self.get_identity_params_from_swagger(swagger, sub_definition, prefix=prop+".")
359373
for k,v in sub_params.items():
360374
params[k] = v
361-
elif schema["properties"][requiredProperty]["type"]!="array":
362-
params[requiredProperty] = prefix + requiredProperty
375+
elif "type" in schema["properties"][prop].keys() and schema["properties"][prop]["type"]!="array" and "x-Ed-Fi-isIdentity" in schema["properties"][prop].keys():
376+
params[prop] = prefix + prop
363377
return params

lightbeam/lightbeam.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ def write_structured_output(self, command):
166166
# failures.line_numbers are split each on their own line; here we remove those line breaks
167167
content = re.sub(r'"line_numbers": \[(\d|,|\s|\n)*\]', self.replace_linebreaks, content)
168168
fp.write(content)
169-
self.logger.info(f"results written to {self.results_file}")
169+
170+
self.logger.info(f"results written to {self.results_file}")
170171

171172

172173
def load_config_file(self) -> dict:

lightbeam/util.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,18 @@ def keys_match(key, wildcard_key):
7979
if key==wildcard_key: return True
8080
if wildcard_key.startswith("*") and key.endswith(wildcard_key.lstrip("*")): return True
8181
if wildcard_key.endswith("*") and key.startswith(wildcard_key.rstrip("*")): return True
82-
return False
82+
return False
83+
84+
def get_swagger_ref_for_endpoint(namespace, swagger, endpoint):
85+
if "definitions" in swagger.keys():
86+
return "#/definitions/" + camel_case(namespace) + "_" + singularize_endpoint(endpoint)
87+
elif "components" in swagger.keys() and "schemas" in swagger["components"].keys():
88+
return "#/components/schemas/" + camel_case(namespace) + "_" + singularize_endpoint(endpoint)
89+
90+
def resolve_swagger_ref(swagger, ref):
91+
if "definitions" in swagger.keys():
92+
definition = ref.replace("#/definitions/", "")
93+
return swagger["definitions"][definition]
94+
elif "components" in swagger.keys() and "schemas" in swagger["components"].keys():
95+
definition = ref.replace("#/components/schemas/", "")
96+
return swagger["components"]["schemas"][definition]

lightbeam/validate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ async def do_validate_payload(self, endpoint, file_name, data, line_counter):
251251

252252
resolver = RefResolver("test", swagger, swagger)
253253
validator = Draft4Validator(resource_schema, resolver=resolver)
254-
params_structure = self.lightbeam.api.get_params_for_endpoint(endpoint)
254+
identity_params_structure = self.lightbeam.api.get_params_for_endpoint(endpoint, type='identity')
255255
distinct_params = []
256256

257257
# check payload is valid JSON
@@ -281,7 +281,7 @@ async def do_validate_payload(self, endpoint, file_name, data, line_counter):
281281

282282
# check natural keys are unique
283283
if "uniqueness" in self.validation_methods:
284-
params = json.dumps(util.interpolate_params(params_structure, payload))
284+
params = json.dumps(util.interpolate_params(identity_params_structure, payload))
285285
params_hash = hashlog.get_hash(params)
286286
if params_hash in distinct_params:
287287
self.log_validation_error(endpoint, file_name, line_counter, "uniqueness", "duplicate value(s) for natural key(s): {params}")

0 commit comments

Comments
 (0)