OPEN SOURCE
software/src/utils/validation.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Input validation utilities for Bible references."""23from __future__ import annotations45import re6from pathlib import Path7from typing import TYPE_CHECKING89from src.exceptions import InvalidReferenceError, VerseNotFoundError1011if TYPE_CHECKING:12from src.data.loader import DataLoader1314# Valid book names (canonical list)15# New Testament books16NT_BOOKS = [17"matthew", "mark", "luke", "john", "acts",18"romans", "1corinthians", "2corinthians", "galatians", "ephesians",19"philippians", "colossians", "1thessalonians", "2thessalonians",20"1timothy", "2timothy", "titus", "philemon",21"hebrews", "james", "1peter", "2peter",22"1john", "2john", "3john", "jude", "revelation",23]2425# Old Testament books26OT_BOOKS = [27"genesis", "exodus", "leviticus", "numbers", "deuteronomy",28"joshua", "judges", "ruth", "1samuel", "2samuel",29"1kings", "2kings", "1chronicles", "2chronicles",30"ezra", "nehemiah", "esther", "job", "psalms",31"proverbs", "ecclesiastes", "songofsolomon",32"isaiah", "jeremiah", "lamentations", "ezekiel", "daniel",33"hosea", "joel", "amos", "obadiah", "jonah",34"micah", "nahum", "habakkuk", "zephaniah", "haggai",35"zechariah", "malachi",36]3738ALL_BOOKS = OT_BOOKS + NT_BOOKS3940# Book name aliases for common variations41BOOK_ALIASES: dict[str, str] = {42"gen": "genesis",43"ex": "exodus",44"exod": "exodus",45"lev": "leviticus",46"num": "numbers",47"deut": "deuteronomy",48"josh": "joshua",49"judg": "judges",50"1sam": "1samuel",51"2sam": "2samuel",52"1kgs": "1kings",53"2kgs": "2kings",54"1chr": "1chronicles",55"2chr": "2chronicles",56"neh": "nehemiah",57"est": "esther",58"ps": "psalms",59"psa": "psalms",60"psalm": "psalms",61"prov": "proverbs",62"eccl": "ecclesiastes",63"song": "songofsolomon",64"sos": "songofsolomon",65"isa": "isaiah",66"jer": "jeremiah",67"lam": "lamentations",68"ezek": "ezekiel",69"dan": "daniel",70"hos": "hosea",71"ob": "obadiah",72"mic": "micah",73"nah": "nahum",74"hab": "habakkuk",75"zeph": "zephaniah",76"hag": "haggai",77"zech": "zechariah",78"mal": "malachi",79"matt": "matthew",80"mat": "matthew",81"mk": "mark",82"lk": "luke",83"jn": "john",84"jhn": "john",85"rom": "romans",86"1cor": "1corinthians",87"2cor": "2corinthians",88"gal": "galatians",89"eph": "ephesians",90"phil": "philippians",91"col": "colossians",92"1thess": "1thessalonians",93"2thess": "2thessalonians",94"1tim": "1timothy",95"2tim": "2timothy",96"tit": "titus",97"phm": "philemon",98"phlm": "philemon",99"heb": "hebrews",100"jas": "james",101"1pet": "1peter",102"2pet": "2peter",103"1jn": "1john",104"2jn": "2john",105"3jn": "3john",106"rev": "revelation",107}108109110def normalize_book_name(book: str) -> str:111"""Normalize a book name to its canonical form.112113Args:114book: Book name (may be alias or variant).115116Returns:117Canonical lowercase book name.118119Raises:120InvalidReferenceError: If book name is not recognized.121"""122book_lower = book.lower().strip()123124# Remove common separators125book_lower = book_lower.replace(" ", "").replace("-", "").replace("_", "")126127# Check if it's already canonical128if book_lower in ALL_BOOKS:129return book_lower130131# Check aliases132if book_lower in BOOK_ALIASES:133return BOOK_ALIASES[book_lower]134135raise InvalidReferenceError(book, f"Unknown book name: {book}")136137138def validate_chapter(book: str, chapter: int) -> None:139"""Validate that a chapter number is valid for a book.140141Args:142book: Canonical book name.143chapter: Chapter number.144145Raises:146InvalidReferenceError: If chapter is invalid.147"""148if chapter < 1:149raise InvalidReferenceError(150f"{book} {chapter}",151f"Chapter must be positive, got {chapter}",152)153154# Maximum chapters per book (approximate, will be validated against data)155max_chapters = {156"genesis": 50, "exodus": 40, "leviticus": 27, "numbers": 36,157"deuteronomy": 34, "joshua": 24, "judges": 21, "ruth": 4,158"1samuel": 31, "2samuel": 24, "1kings": 22, "2kings": 25,159"1chronicles": 29, "2chronicles": 36, "ezra": 10, "nehemiah": 13,160"esther": 10, "job": 42, "psalms": 150, "proverbs": 31,161"ecclesiastes": 12, "songofsolomon": 8, "isaiah": 66, "jeremiah": 52,162"lamentations": 5, "ezekiel": 48, "daniel": 12, "hosea": 14,163"joel": 3, "amos": 9, "obadiah": 1, "jonah": 4, "micah": 7,164"nahum": 3, "habakkuk": 3, "zephaniah": 3, "haggai": 2,165"zechariah": 14, "malachi": 4, "matthew": 28, "mark": 16,166"luke": 24, "john": 21, "acts": 28, "romans": 16,167"1corinthians": 16, "2corinthians": 13, "galatians": 6,168"ephesians": 6, "philippians": 4, "colossians": 4,169"1thessalonians": 5, "2thessalonians": 3, "1timothy": 6,170"2timothy": 4, "titus": 3, "philemon": 1, "hebrews": 13,171"james": 5, "1peter": 5, "2peter": 3, "1john": 5,172"2john": 1, "3john": 1, "jude": 1, "revelation": 22,173}174175book_lower = book.lower()176if book_lower in max_chapters and chapter > max_chapters[book_lower]:177raise InvalidReferenceError(178f"{book} {chapter}",179f"{book} only has {max_chapters[book_lower]} chapters",180)181182183def validate_verse(book: str, chapter: int, verse: int) -> None:184"""Validate that a verse number is plausible.185186Args:187book: Canonical book name.188chapter: Chapter number.189verse: Verse number.190191Raises:192InvalidReferenceError: If verse is obviously invalid.193"""194if verse < 1:195raise InvalidReferenceError(196f"{book} {chapter}:{verse}",197f"Verse must be positive, got {verse}",198)199200# Most chapters have fewer than 200 verses (Psalm 119 has 176)201if verse > 200:202raise InvalidReferenceError(203f"{book} {chapter}:{verse}",204f"Verse number {verse} is implausibly high",205)206207208def validate_reference(book: str, chapter: int, verse: int) -> str:209"""Validate a complete verse reference.210211Args:212book: Book name (may be alias).213chapter: Chapter number.214verse: Verse number.215216Returns:217Canonical book name.218219Raises:220InvalidReferenceError: If reference is invalid.221"""222canonical_book = normalize_book_name(book)223validate_chapter(canonical_book, chapter)224validate_verse(canonical_book, chapter, verse)225return canonical_book226227228def validate_reference_exists(229loader: DataLoader,230book: str,231chapter: int,232verse: int,233) -> None:234"""Validate that a verse actually exists in the data.235236Args:237loader: Data loader to check against.238book: Canonical book name.239chapter: Chapter number.240verse: Verse number.241242Raises:243VerseNotFoundError: If verse doesn't exist in data.244"""245# Load the verse and check if it has any content246from src.models.verse import VerseReference247ref = VerseReference(book=book, chapter=chapter, verse=verse)248loaded = loader.load_verse(ref)249250# Check if any source text is available251has_content = (252loaded.greek_text253or loaded.hebrew_text254or loaded.english_kjv255or loaded.english_asv256or loaded.japanese_meiji257or loaded.japanese_taisho258)259260if not has_content:261raise VerseNotFoundError(book, chapter, verse)262263264def parse_reference_string(ref_str: str) -> tuple[str, int, int]:265"""Parse a reference string like 'John 3:16' into components.266267Supports formats:268- 'John 3:16'269- 'john 3:16'270- 'john:3:16'271- 'jn 3:16'272- '1john 1:1'273- '1 john 1:1'274275Args:276ref_str: Reference string to parse.277278Returns:279Tuple of (book, chapter, verse).280281Raises:282InvalidReferenceError: If format is invalid.283"""284ref_str = ref_str.strip()285286# Pattern for references like "John 3:16" or "1 John 1:1"287pattern = r"^(\d?\s*[a-zA-Z]+)\s+(\d+):(\d+)$"288match = re.match(pattern, ref_str)289290if match:291book_part = match.group(1).strip()292chapter = int(match.group(2))293verse = int(match.group(3))294book = normalize_book_name(book_part)295return book, chapter, verse296297# Try alternate pattern "book:chapter:verse"298pattern2 = r"^(\d?\s*[a-zA-Z]+):(\d+):(\d+)$"299match2 = re.match(pattern2, ref_str)300301if match2:302book_part = match2.group(1).strip()303chapter = int(match2.group(2))304verse = int(match2.group(3))305book = normalize_book_name(book_part)306return book, chapter, verse307308raise InvalidReferenceError(309ref_str,310"Expected format like 'John 3:16' or 'john:3:16'",311)312313314def sanitize_book_name_for_path(book: str) -> str:315"""Sanitize a book name for safe use in file paths.316317Prevents directory traversal attacks.318319Args:320book: Book name to sanitize.321322Returns:323Safe book name for paths.324325Raises:326InvalidReferenceError: If book name contains unsafe characters.327"""328canonical = normalize_book_name(book)329330# Additional safety check: ensure no path separators331if "/" in canonical or "\\" in canonical or ".." in canonical:332raise InvalidReferenceError(book, "Book name contains invalid path characters")333334return canonical335336337def is_valid_book(book: str) -> bool:338"""Check if a book name is valid without raising an exception.339340Args:341book: Book name to check.342343Returns:344True if valid, False otherwise.345"""346try:347normalize_book_name(book)348return True349except InvalidReferenceError:350return False351