"""API routes for searching and fetching souls-directory markdown entries.""" from __future__ import annotations import re from fastapi import APIRouter, Depends, HTTPException, Query, status from app.api.deps import ActorContext, require_user_or_agent from app.schemas.souls_directory import ( SoulsDirectoryMarkdownResponse, SoulsDirectorySearchResponse, SoulsDirectorySoulRef, ) from app.services import souls_directory router = APIRouter(prefix="/souls-directory", tags=["souls-directory"]) USER_OR_AGENT_DEP = Depends(require_user_or_agent) _SAFE_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$") _SAFE_SLUG_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$") def _validate_segment(value: str, *, field: str) -> str: cleaned = value.strip().strip("/") if not cleaned: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=f"{field} is required", ) if field == "handle": ok = bool(_SAFE_SEGMENT_RE.match(cleaned)) else: ok = bool(_SAFE_SLUG_RE.match(cleaned)) if not ok: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=f"{field} contains unsupported characters", ) return cleaned @router.get("/search", response_model=SoulsDirectorySearchResponse) async def search( q: str = Query(default="", min_length=0), limit: int = Query(default=20, ge=1, le=100), _actor: ActorContext = USER_OR_AGENT_DEP, ) -> SoulsDirectorySearchResponse: """Search souls-directory entries by handle/slug query text.""" refs = await souls_directory.list_souls_directory_refs() matches = souls_directory.search_souls(refs, query=q, limit=limit) items = [ SoulsDirectorySoulRef( handle=ref.handle, slug=ref.slug, page_url=ref.page_url, raw_md_url=ref.raw_md_url, ) for ref in matches ] return SoulsDirectorySearchResponse(items=items) @router.get("/{handle}/{slug}.md", response_model=SoulsDirectoryMarkdownResponse) @router.get("/{handle}/{slug}", response_model=SoulsDirectoryMarkdownResponse) async def get_markdown( handle: str, slug: str, _actor: ActorContext = USER_OR_AGENT_DEP, ) -> SoulsDirectoryMarkdownResponse: """Fetch markdown content for a validated souls-directory handle and slug.""" safe_handle = _validate_segment(handle, field="handle") safe_slug = _validate_segment(slug.removesuffix(".md"), field="slug") try: content = await souls_directory.fetch_soul_markdown( handle=safe_handle, slug=safe_slug, ) except Exception as exc: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc), ) from exc return SoulsDirectoryMarkdownResponse( handle=safe_handle, slug=safe_slug, content=content, )