diff --git a/hl7apy/core.py b/hl7apy/core.py index c66d6e0..960d11b 100644 --- a/hl7apy/core.py +++ b/hl7apy/core.py @@ -740,15 +740,16 @@ def to_er7(self, encoding_chars=None, trailing_children=False): return separator.join(s) - def validate(self, report_file=None): + def validate(self, report_file=None, return_errors=False): """ Validate the HL7 element using the :attr:`STRICT ` validation level. It calls the :func:`Validator.validate ` method passing the reference used in the instantiation of the element. :param: report_file: the report file to pass to the validator + :param: return_errors: return errors and warnings instead of raising """ - return Validator.validate(self, reference=self.reference, report_file=report_file) + return Validator.validate(self, reference=self.reference, report_file=report_file, return_errors=return_errors) def is_z_element(self): return False diff --git a/hl7apy/validation.py b/hl7apy/validation.py index 591fe29..ea4028e 100644 --- a/hl7apy/validation.py +++ b/hl7apy/validation.py @@ -22,11 +22,18 @@ from __future__ import absolute_import import traceback +from collections import namedtuple + from hl7apy import load_reference from hl7apy.consts import VALIDATION_LEVEL from hl7apy.exceptions import ChildNotFound, ValidationError, ValidationWarning +ErrorsAndWarnings = namedtuple( + "ErrorsAndWarnings", + ("is_valid", "errors", "warnings")) + + class Validator(object): """ Class that handles validation. It defines validation levels and validate @@ -38,7 +45,7 @@ def __init__(self, level): self.level = level @staticmethod - def validate(element, reference=None, report_file=None): + def validate(element, reference=None, report_file=None, return_errors=False): """ Checks if the :class:`Element ` is a valid HL7 message according to the reference specified. If the reference is not specified, it will be used the official HL7 structures for the @@ -50,13 +57,18 @@ def validate(element, reference=None, report_file=None): * the datatype of fields, components and subcomponents * the values, in particular the length and the adherence with the HL7 table, if one is specified - It raises the first exception that it finds. + All errors that occur will be written to a file, if any of the two following conditions are met: + 1. either the :attr:`report_file` is an instance (file-like object) with a .write() method, then report_file.write() is called; + 2. or the :attr:`report_file` is a serializable (string-like object) + value that will be used to as the path to create a new_file, then new_file.write() is called; if the file already exists, it will be overwritten. - If :attr:`report_file` is specified, it will create a file with all the errors that occur. + If :attr:`return_errors` is False, the functions raises the first exception that it finds. + If :attr:`return_errors` is True, errors and warnings are returned using a namedtuple with "errors" and "warnings" list fields. + Else the funtion returns True. :param element: :class:`Element `: The element to validate :param reference: the reference to use. Usually is None or a message profile object - :param report_file: the name of the report file to create + :param report_file: the name of the report file to create, or an instance having a write() method :return: The True if everything is ok :raises: :exc:`ValidationError `: when errors occur @@ -197,11 +209,25 @@ def _is_valid(el, ref, errs, warns): _is_valid(element, reference, errors, warnings) if report_file is not None: - with open(report_file, "w") as f: + try: + write = report_file.write + except AttributeError: + with open(report_file, "w") as f: + for e in errors: + f.write("Error: {}\n".format(e)) + for w in warnings: + f.write("Warning: {}\n".format(w)) + else: for e in errors: - f.write("Error: {}\n".format(e)) + write("Error: {}\n".format(e)) for w in warnings: - f.write("Warning: {}\n".format(w)) + write("Warning: {}\n".format(w)) + + if return_errors: + return ErrorsAndWarnings( + is_valid=not errors, + errors=errors, + warnings=warnings) if errors: raise errors[0] diff --git a/tests/test_validation.py b/tests/test_validation.py index faa2a14..26de2b9 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -21,6 +21,7 @@ from __future__ import absolute_import +from io import StringIO import os import re import sys @@ -388,6 +389,33 @@ def test_wd_type_field(self): parsed_s = parse_segment(s, version='2.7') self.assertRaises(ValidationError, parsed_s.validate) + def test_validate_with_param_report_file_as_string_io(self): + """ + Tests that, if you use stringIO to validate, warning and errors are written there + """ + string_io = StringIO() + msg = self._create_message(self.oml_o33) + self.assertTrue(msg.validate(report_file=string_io)) + self.assertGreater(string_io.tell(), 0) + + def test_validate_with_param_return_errors(self): + """ + Tests message is valid with warnings using return_errors=True + """ + msg = self._create_message(self.oml_o33) + errors_and_warnings = msg.validate(return_errors=True) + self.assertTrue(errors_and_warnings.is_valid) + self.assertEqual(len(errors_and_warnings.warnings), 2) + + def test_well_structured_message_using_return_error(self): + """ + Tests that a valid message is validated using return_error + """ + msg = self._create_message(self.adt_a01) + errors_and_warnings = msg.validate(return_errors=True) + self.assertTrue(errors_and_warnings.is_valid) + self.assertEqual(len(errors_and_warnings.errors), 0) + self.assertEqual(len(errors_and_warnings.warnings), 0) class TestMessageProfile(unittest.TestCase):