Source code for pyx12.map_if._element

######################################################################
# Copyright (c)
# All rights reserved.
#
# This software is licensed as described in the file LICENSE.txt, which
# you should have received as part of this distribution.
#
######################################################################
"""
Element interface - leaf node validator.
"""

from __future__ import annotations

import re
import sys
from typing import TYPE_CHECKING, cast
from xml.etree.ElementTree import Element

import pyx12.segment

from .. import validation
from ..dataele import _DataEle
from ..error_item import EleError
from ..errors import EngineError
from ._base import _required_attr, x12_node

if TYPE_CHECKING:
    from ._root import map_if


############################################################
# Element Interface
############################################################
[docs] class element_if(x12_node): """ Element Interface """ root: map_if base_name: str valid_codes: list[str | None] _valid_codes_set: frozenset[str | None] external_codes: str | None rec: re.Pattern[str] | None refdes: str | None data_ele: str | None _data_ele: _DataEle | None seq: int max_use: str | None res: str | None def __init__(self, root: map_if, parent: x12_node, elem: Element) -> None: """ :param parent: parent node """ x12_node.__init__(self) self.children = [] self.root = root self.parent = parent self.base_name = "element" self.valid_codes = [] self.external_codes = None self.rec = None self.id = elem.get("xid") self.refdes = self.id self.data_ele = elem.get("data_ele") if elem.get("data_ele") else elem.findtext("data_ele") # Eagerly cache the data element definition; a map that references an # undefined data_ele fails at validation time, matching legacy behavior. try: self._data_ele = ( self.root.data_elements.get_by_elem_num(self.data_ele) if self.data_ele else None ) except EngineError: self._data_ele = None self.usage = elem.get("usage") if elem.get("usage") else elem.findtext("usage") self.name = elem.get("name") if elem.get("name") else elem.findtext("name") self.seq = int(_required_attr(elem, "seq")) self.path = elem.get("seq") if elem.get("seq") else (elem.findtext("seq") or "") # type: ignore[assignment] self.max_use = elem.get("max_use") if elem.get("max_use") else elem.findtext("max_use") self.res = elem.findtext("regex") try: if self.res is not None and self.res != "": self.rec = re.compile(self.res, re.S) except Exception: raise EngineError('Element regex "%s" failed to compile' % (self.res)) from None v = elem.find("valid_codes") if v is not None: self.external_codes = v.get("external") for c in v.findall("code"): self.valid_codes.append(c.text) # Parallel frozenset for O(1) membership checks. The list is kept # because callers (loop_if path disambiguation, map_if._get_icvn) # rely on a stable indexed order from the XML. self._valid_codes_set = frozenset(self.valid_codes)
[docs] def debug_print(self) -> None: sys.stdout.write(self.__repr__()) for node in self.children: node.debug_print()
def __repr__(self) -> str: """ :rtype: string """ data_ele = self._resolve_data_ele() out = '%s "%s"' % (self.refdes, self.name) if self.data_ele: out += " data_ele: %s" % (self.data_ele) if self.usage: out += " usage: %s" % (self.usage) if self.seq: out += " seq: %i" % (self.seq) out += " %s(%i, %i)" % (data_ele["data_type"], data_ele["min_len"], data_ele["max_len"]) if self.external_codes: out += " external codes: %s" % (self.external_codes) out += "\n" return out def _resolve_data_ele(self) -> _DataEle: if self._data_ele is not None: return self._data_ele return self.root.data_elements.get_by_elem_num(self.data_ele) def _ele_error(self, err_cde: str, err_str: str, err_val: str | None) -> EleError: return EleError( err_cde=err_cde, err_str=err_str, err_val=err_val, refdes=self.refdes, map_node=self, ) def _valid_code(self, code: str | None) -> bool: """ Verify the x12 element value is in the given list of valid codes :return: True if found, else False :rtype: boolean """ return code in self._valid_codes_set
[docs] def get_parent(self) -> x12_node | None: """ :return: ref to parent class instance """ return self.parent
[docs] def is_match(self) -> bool: """ :return: :rtype: boolean """ # match also by ID raise NotImplementedError("Override in sub-class")
[docs] def is_valid_errors( self, elem: pyx12.segment.Element | pyx12.segment.Composite | None, type_list: list[str | None] | None = None, ) -> tuple[bool, list[EleError]]: """ Pure validator: returns (ok, errors) without touching an error handler. """ if type_list is None: type_list = [] errors: list[EleError] = [] if elem and elem.is_composite(): err_str = 'Data element "%s" (%s) is an invalid composite' % (self.name, self.refdes) errors.append(self._ele_error("6", err_str, elem.__repr__())) return False, errors if elem is None or elem.get_value() == "": empty_errors = self._validate_when_empty() return (not empty_errors, empty_errors) if self.usage == "N" and elem.get_value() != "": err_str = 'Data element "%s" (%s) is marked as Not Used' % (self.name, self.refdes) errors.append(self._ele_error("10", err_str, None)) return False, errors elem_val = elem.get_value() errors += self._validate_length(elem_val) ctrl_errors = self._validate_control_chars(elem_val) errors += ctrl_errors if ctrl_errors: # control char errors trump later checks return False, errors errors += self._validate_trailing_spaces(elem_val) errors += self._is_valid_code(elem_val) errors += self._validate_data_type(elem_val) if type_list: errors += self._validate_type_list(elem_val, type_list) errors += self._validate_regex(elem_val) return (not errors, errors)
def _validate_when_empty(self) -> list[EleError]: if self.usage in ("N", "S"): return [] # An element's parent is a composite_if or a segment_if at runtime. assert self.parent is not None if self.usage == "R" and ( self.seq != 1 or not self.parent.is_composite() or self.parent.usage == "R" ): err_str = 'Mandatory data element "%s" (%s) is missing' % (self.name, self.refdes) return [self._ele_error("1", err_str, None)] return [] def _validate_length(self, elem_val: str) -> list[EleError]: data_ele = self._resolve_data_ele() data_type = data_ele["data_type"] min_len = data_ele["min_len"] max_len = data_ele["max_len"] # Numeric types ignore "-" and "." for length purposes. if data_type is not None and (data_type == "R" or data_type[0] == "N"): measured = elem_val.replace("-", "").replace(".", "") else: measured = elem_val elem_len = len(measured) out: list[EleError] = [] if elem_len < min_len: err_str = 'Data element "%s" (%s) is too short: len("%s") = %i < %i (min_len)' % ( self.name, self.refdes, elem_val, elem_len, min_len, ) out.append(self._ele_error("4", err_str, elem_val)) if elem_len > max_len: err_str = 'Data element "%s" (%s) is too long: len("%s") = %i > %i (max_len)' % ( self.name, self.refdes, elem_val, elem_len, max_len, ) out.append(self._ele_error("5", err_str, elem_val)) return out def _validate_control_chars(self, elem_val: str) -> list[EleError]: res, bad_string = validation.contains_control_character(elem_val) if not res: return [] err_str = 'Data element "%s" (%s), contains an invalid control character(%s)' % ( self.name, self.refdes, bad_string, ) return [self._ele_error("6", err_str, bad_string)] def _validate_trailing_spaces(self, elem_val: str) -> list[EleError]: data_ele = self._resolve_data_ele() if data_ele["data_type"] not in ("AN", "ID") or elem_val[-1] != " ": return [] if len(elem_val.rstrip()) < data_ele["min_len"]: return [] err_str = 'Data element "%s" (%s) has unnecessary trailing spaces. (%s)' % ( self.name, self.refdes, elem_val, ) return [self._ele_error("6", err_str, elem_val)] def _validate_data_type(self, elem_val: str) -> list[EleError]: data_type = self._resolve_data_ele()["data_type"] if validation.IsValidDataType( elem_val, cast(str, data_type), self.root.param.get("charset"), self.root.icvn or "00401", ): return [] if data_type in ("RD8", "DT", "D8", "D6"): err_str = 'Data element "%s" (%s) contains an invalid date (%s)' % ( self.name, self.refdes, elem_val, ) return [self._ele_error("8", err_str, elem_val)] if data_type == "TM": err_str = 'Data element "%s" (%s) contains an invalid time (%s)' % ( self.name, self.refdes, elem_val, ) return [self._ele_error("9", err_str, elem_val)] err_str = 'Data element "%s" (%s) is type %s, contains an invalid character(%s)' % ( self.name, self.refdes, data_type, elem_val, ) return [self._ele_error("6", err_str, elem_val)] def _validate_type_list(self, elem_val: str, type_list: list[str | None]) -> list[EleError]: valid_type = False for dtype in type_list: if dtype is not None: valid_type |= validation.IsValidDataType( elem_val, dtype, self.root.param.get("charset") ) if valid_type: return [] if "TM" in type_list: err_str = 'Data element "%s" (%s) contains an invalid time (%s)' % ( self.name, self.refdes, elem_val, ) return [self._ele_error("9", err_str, elem_val)] if any(t in type_list for t in ("RD8", "DT", "D8", "D6")): err_str = 'Data element "%s" (%s) contains an invalid date (%s)' % ( self.name, self.refdes, elem_val, ) return [self._ele_error("8", err_str, elem_val)] return [] def _validate_regex(self, elem_val: str) -> list[EleError]: if self.rec is None or self.rec.search(elem_val): return [] err_str = 'Data element "%s" with a value of (%s)' % (self.name, elem_val) err_str += ' failed to match the regular expression "%s"' % (self.res) return [self._ele_error("7", err_str, elem_val)] def _is_valid_code(self, elem_val: str) -> list[EleError]: if not self._valid_codes_set and self.external_codes is None: return [] if elem_val in self._valid_codes_set: return [] if self.external_codes is not None and self.root.ext_codes.isValid( self.external_codes, elem_val ): return [] err_str = "(%s) is not a valid code for %s (%s)" % (elem_val, self.name, self.refdes) return [self._ele_error("7", err_str, elem_val)]
[docs] def get_data_type(self) -> str | None: return self._resolve_data_ele()["data_type"]
@property def data_type(self) -> str | None: return self._resolve_data_ele()["data_type"] @property def min_len(self) -> int: return self._resolve_data_ele()["min_len"] @property def max_len(self) -> int: return self._resolve_data_ele()["max_len"] @property def data_element_name(self) -> str | None: return self._resolve_data_ele()["name"]
[docs] def get_seg_count(self) -> None: """ """ pass
[docs] def is_element(self) -> bool: """ :rtype: boolean """ return True
[docs] def get_path(self) -> str: """ :return: path - XPath style :rtype: string """ if self._fullpath: return self._fullpath # get enclosing loop seg = self.get_parent_segment() assert seg.parent is not None parent_path = seg.parent.get_path() # add the segment, element, and sub-element path self._fullpath = parent_path + "/" + (self.id or "") return self._fullpath
[docs] def get_parent_segment(self) -> x12_node: # An element is always nested inside a segment (directly, or via a composite). p = self.parent assert p is not None while not p.is_segment(): p = p.parent assert p is not None return p