strat-gameplay-webapp/backend/app/models/roster_models.py
Cal Corum 3c5055dbf6 CLAUDE: Implement polymorphic RosterLink for both PD and SBA leagues
Added league-agnostic roster tracking with single-table design:

Database Changes:
- Modified RosterLink model with surrogate primary key (id)
- Added nullable card_id (PD) and player_id (SBA) columns
- Added CHECK constraint ensuring exactly one ID populated (XOR logic)
- Added unique constraints for (game_id, card_id) and (game_id, player_id)
- Imported CheckConstraint and UniqueConstraint from SQLAlchemy

New Files:
- app/models/roster_models.py: Pydantic models for type safety
  - BaseRosterLinkData: Abstract base class
  - PdRosterLinkData: PD league card-based rosters
  - SbaRosterLinkData: SBA league player-based rosters
  - RosterLinkCreate: Request validation model

- tests/unit/models/test_roster_models.py: 24 unit tests (all passing)
  - Tests for PD/SBA roster link creation and validation
  - Tests for RosterLinkCreate XOR validation
  - Tests for polymorphic behavior

Database Operations:
- add_pd_roster_card(): Add PD card to game roster
- add_sba_roster_player(): Add SBA player to game roster
- get_pd_roster(): Get PD cards with optional team filter
- get_sba_roster(): Get SBA players with optional team filter
- remove_roster_entry(): Remove roster entry by ID

Tests:
- Added 12 integration tests for roster operations
- Fixed setup_database fixture scope (module → function)

Documentation:
- Updated backend/CLAUDE.md with RosterLink documentation
- Added usage examples and design rationale
- Updated Game model relationship description

Design Pattern:
Single table with application-layer type safety rather than SQLAlchemy
polymorphic inheritance. Simpler queries, database-enforced integrity,
and Pydantic type safety at application layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:45:44 -05:00

122 lines
3.5 KiB
Python

"""Pydantic models for roster link type safety
Provides league-specific type-safe models for roster operations:
- PdRosterLinkData: PD league card-based rosters
- SbaRosterLinkData: SBA league player-based rosters
"""
from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, field_validator
class BaseRosterLinkData(BaseModel, ABC):
"""Abstract base for roster link data
Common fields shared across all leagues
"""
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)
id: Optional[int] = None # Database ID (populated after save)
game_id: UUID
team_id: int
@abstractmethod
def get_entity_id(self) -> int:
"""Get the entity ID (card_id or player_id)"""
pass
@abstractmethod
def get_entity_type(self) -> str:
"""Get entity type identifier ('card' or 'player')"""
pass
class PdRosterLinkData(BaseRosterLinkData):
"""PD league roster link - tracks cards
Used for Paper Dynasty league games where rosters are composed of cards.
Each card represents a player with detailed scouting data.
"""
card_id: int
@field_validator("card_id")
@classmethod
def validate_card_id(cls, v: int) -> int:
if v <= 0:
raise ValueError("card_id must be positive")
return v
def get_entity_id(self) -> int:
return self.card_id
def get_entity_type(self) -> str:
return "card"
class SbaRosterLinkData(BaseRosterLinkData):
"""SBA league roster link - tracks players
Used for SBA league games where rosters are composed of players.
Players are identified directly by player_id without a card system.
"""
player_id: int
@field_validator("player_id")
@classmethod
def validate_player_id(cls, v: int) -> int:
if v <= 0:
raise ValueError("player_id must be positive")
return v
def get_entity_id(self) -> int:
return self.player_id
def get_entity_type(self) -> str:
return "player"
class RosterLinkCreate(BaseModel):
"""Request model for creating a roster link"""
game_id: UUID
team_id: int
card_id: Optional[int] = None
player_id: Optional[int] = None
@field_validator("team_id")
@classmethod
def validate_team_id(cls, v: int) -> int:
if v <= 0:
raise ValueError("team_id must be positive")
return v
def model_post_init(self, __context) -> None:
"""Validate that exactly one ID is populated"""
has_card = self.card_id is not None
has_player = self.player_id is not None
if has_card == has_player: # XOR check (both True or both False = invalid)
raise ValueError("Exactly one of card_id or player_id must be provided")
def to_pd_data(self) -> PdRosterLinkData:
"""Convert to PD roster data (validates card_id is present)"""
if self.card_id is None:
raise ValueError("card_id required for PD roster")
return PdRosterLinkData(
game_id=self.game_id, team_id=self.team_id, card_id=self.card_id
)
def to_sba_data(self) -> SbaRosterLinkData:
"""Convert to SBA roster data (validates player_id is present)"""
if self.player_id is None:
raise ValueError("player_id required for SBA roster")
return SbaRosterLinkData(
game_id=self.game_id, team_id=self.team_id, player_id=self.player_id
)