"""Canonical MAS v0.1 local-part generator. Generators are stricter than parsers: they never emit both a facet and an interaction, and they enforce the 64-octet SMTP limit after encoding. """ from __future__ import annotations from typing import Optional from .parser import MasSyntaxError, parse_local_part def generate_local_part(target: str, *, facet: Optional[str] = None, interaction: Optional[str] = None) -> str: if facet is not None and interaction is not None: raise MasSyntaxError("generator-facet-and-interaction", "v0.1 generators MUST NOT emit both '+' and '!'") s = target if facet is not None: s += "+" + facet if interaction is not None: s += "!" + interaction parsed = parse_local_part(s) # enforces grammar, emptiness, reserved ops, length if str(parsed.target) != target or (parsed.facet and parsed.facet.value) != facet \ or (parsed.interaction and parsed.interaction.mode) != interaction: raise MasSyntaxError("generator-component", "a component contains a MAS operator") return s def resource_local_part(resource_type: str, resource_id: str, *, facet: Optional[str] = None, interaction: Optional[str] = None) -> str: if not resource_type or not resource_id: raise MasSyntaxError("generator-empty", "resource type and id must be non-empty") if "=" in resource_type: raise MasSyntaxError("invalid-resource-type", "resource type must not contain '='") return generate_local_part(f"{resource_type}={resource_id}", facet=facet, interaction=interaction)