"""Schemas for Workspace API endpoints.""" from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field, field_validator, model_validator class WorkspaceBase(BaseModel): name: str icon: str = "LayoutGrid" description: str | None = None is_default: bool = False is_active: bool = True class WorkspaceCreate(WorkspaceBase): pass class WorkspaceUpdate(BaseModel): name: str | None = None icon: str | None = None description: str | None = None is_default: bool | None = None is_active: bool | None = None class WorkspaceResponse(WorkspaceBase): id: str created_by: str | None = None created_at: str | None = None updated_at: str | None = None model_config = {"from_attributes": True} # ─── N1: Workspace-Scope-Registry (Phase N) ─────────────────── class ScopeOption(BaseModel): """Ein auswählbarer Wert einer Scope-Dimension.""" value: str label: str class ScopeValueSource(BaseModel): """Deklariert, woher der Editor wählbare Werte lädt. Security (fail-closed): nur interne ``/api/v1/...``-Pfade sind erlaubt — keine absoluten URLs, keine protokoll-relativen Pfade, keine externen Hosts (SSRF-sicher per Konstruktion). """ endpoint: str items_path: str = "items" value_key: str = "id" label_key: str = "name" @field_validator("endpoint") @classmethod def _must_be_internal_api_path(cls, v: str) -> str: if not v.startswith("/api/v1/"): raise ValueError( "value_source endpoint must be an internal /api/v1/ path" ) return v class WorkspaceScopeDimension(BaseModel): """Eine filterbare Dimension eines Moduls im Workspace (N1). ``multiselect``/``select`` brauchen zwingend ``options`` oder ``value_source`` (fail-closed gegen leere Filter-UI); ``toggle`` ist ein reiner Schalter mit ``default``. """ key: str = Field(..., min_length=1, max_length=100) label: str = Field(..., min_length=1, max_length=200) control: Literal["multiselect", "select", "toggle"] options: list[ScopeOption] = Field(default_factory=list) value_source: ScopeValueSource | None = None default: bool | str | None = None @model_validator(mode="after") def _options_or_source_required(self) -> WorkspaceScopeDimension: if self.control in ("multiselect", "select"): if not self.options and self.value_source is None: raise ValueError( f"scope dimension '{self.key}': control '{self.control}' " "requires options or value_source" ) return self class WorkspaceModuleScopes(BaseModel): """Scope-Contribution eines Plugins für ein Modul.""" module_key: str = Field(..., min_length=1, max_length=100) dimensions: list[WorkspaceScopeDimension] = Field(..., min_length=1) class WorkspaceModuleResponse(BaseModel): id: str workspace_id: str module_key: str is_visible: bool menu_order: int config: dict = {} model_config = {"from_attributes": True}