"""Explicit row values for ARR processing XML, not an OHIP mapping policy. Callers must already have resolved every value, note/trace selection and row order from independently accepted source rules. No fields have defaults. """ from __future__ import annotations from dataclasses import dataclass from datetime import date import re from .collect_arr_source import require VERSION = "arr-processing-xml/v1" # Match the existing frozen handoff/upload boundary, not the larger diagnostic # native-XML comparator limit. A serialization result must fit that entrypoint. MAX_BYTES = 25 * 1024 * 1024 MAX_ROWS = 10000 MAX_ELEMENTS = 500000 # Declarative correspondence only; shared by serializer and independent parser. SCALARS = ( ("block_code", "BLOCK_CODE"), ("adults", "ADULTS"), ("children", "CF_CHILDREN"), ("company_name", "COMPANY_NAME"), ("confirmation_no", "CONFIRMATION_NO"), ("room_no", "DISP_ROOM_NO"), ("effective_rate", "EFFECTIVE_RATE_AMOUNT"), ("full_name", "FULL_NAME"), ("no_of_rooms", "NO_OF_ROOMS"), ("products", "PRODUCTS"), ("rate_code", "RATE_CODE"), ("room_category_label", "ROOM_CATEGORY_LABEL"), ("arrival", "TRUNC_BEGIN"), ("departure", "TRUNC_END"), ) @dataclass(frozen=True) class SourceRow: reservation_id: str block_code: str adults: str children: str company_name: str confirmation_no: str room_no: str effective_rate: str full_name: str no_of_rooms: str products: str rate_code: str room_category_label: str arrival: str departure: str notes: tuple[str, ...] traces: tuple[str, ...] @dataclass(frozen=True) class SourceDocument: hotel_id: str report_date: date rows: tuple[SourceRow, ...] def _text(value): require(type(value) is str, "arr_xml_explicit_text_required") require(all(char in "\t\n\r" or 0x20 <= ord(char) <= 0xD7FF or 0xE000 <= ord(char) <= 0xFFFD or 0x10000 <= ord(char) <= 0x10FFFF for char in value), "arr_xml_invalid_character") return len(value.encode("utf-8")) def validate_document(document: SourceDocument) -> None: """Validate transport shape only; existing processor owns business rules. An explicit blank is retained even on a required whitelist field. A missing value/None is rejected. This preserves invalid and non-whitelist source rows so serialization cannot silently remove records to make processing succeed. """ require(type(document) is SourceDocument, "arr_xml_document_required") require(type(document.hotel_id) is str and bool(re.fullmatch(r"[A-Za-z0-9_-]{1,64}", document.hotel_id)), "arr_xml_hotel_required") require(type(document.report_date) is date, "arr_xml_explicit_date_required") require(type(document.rows) is tuple and 1 <= len(document.rows) <= MAX_ROWS, "arr_xml_record_count") size, elements = 0, 6 for row in document.rows: require(type(row) is SourceRow, "arr_xml_row_required") size += _text(row.reservation_id) require(bool(row.reservation_id.strip()), "arr_xml_reservation_identity_required") for attribute, _ in SCALARS: size += _text(getattr(row, attribute)) for collection in (row.notes, row.traces): require(type(collection) is tuple, "arr_xml_explicit_collection_required") for value in collection: size += _text(value) elements += 19 + 2 * (len(row.notes) + len(row.traces)) require(size <= MAX_BYTES and elements <= MAX_ELEMENTS, "arr_xml_input_budget_exceeded")