docs: add AI code generation requirements and comprehensive Google-style docstrings
- Add AI code generation requirements to AGENTS.md - Add module-level docstrings to all 46 Python modules - Add detailed Google-style docstrings to all classes and functions - Remove all inline comments following self-documenting code principle - Include Args, Returns, Raises sections in function docstrings - Add Attributes and Examples sections to class docstrings
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
"""Domain layer exports."""
|
||||
"""Domain layer exports.
|
||||
|
||||
This module re-exports all domain layer components including
|
||||
entities, value objects, repositories, and exceptions.
|
||||
"""
|
||||
|
||||
from app.domain.entities import BaseEntity, Post
|
||||
from app.domain.exceptions import (
|
||||
@@ -13,18 +17,14 @@ from app.domain.repositories import PostRepository, Repository
|
||||
from app.domain.value_objects import Content, Slug, Title, ValueObject
|
||||
|
||||
__all__ = [
|
||||
# Entities
|
||||
"BaseEntity",
|
||||
"Post",
|
||||
# Value Objects
|
||||
"ValueObject",
|
||||
"Title",
|
||||
"Content",
|
||||
"Slug",
|
||||
# Repositories
|
||||
"Repository",
|
||||
"PostRepository",
|
||||
# Exceptions
|
||||
"DomainException",
|
||||
"ValidationException",
|
||||
"NotFoundException",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Domain entities."""
|
||||
"""Domain entities.
|
||||
|
||||
This module re-exports all domain entities that represent
|
||||
core business objects with identity.
|
||||
"""
|
||||
|
||||
from app.domain.entities.base import BaseEntity
|
||||
from app.domain.entities.post import Post
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Base entity for DDD domain layer."""
|
||||
"""Base entity for DDD domain layer.
|
||||
|
||||
This module provides the foundational BaseEntity class that all domain
|
||||
entities must inherit from. It implements common entity patterns including
|
||||
identity management, equality comparison, and timestamp tracking.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
@@ -9,25 +14,62 @@ from uuid import UUID, uuid4
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BaseEntity(ABC):
|
||||
"""Base class for all domain entities."""
|
||||
"""Base class for all domain entities.
|
||||
|
||||
Provides common functionality for domain entities including unique
|
||||
identification, creation/update timestamps, and equality comparison
|
||||
based on identity.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the entity, automatically generated.
|
||||
created_at: Timestamp when the entity was created.
|
||||
updated_at: Timestamp when the entity was last updated.
|
||||
|
||||
Example:
|
||||
>>> class User(BaseEntity):
|
||||
... name: str
|
||||
... def to_dict(self) -> dict[str, Any]:
|
||||
... return {"id": str(self.id), "name": self.name}
|
||||
"""
|
||||
|
||||
id: UUID = field(default_factory=uuid4)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare entities by identity.
|
||||
|
||||
Args:
|
||||
other: Another object to compare with.
|
||||
|
||||
Returns:
|
||||
True if both objects are BaseEntity instances with same ID.
|
||||
"""
|
||||
if not isinstance(other, BaseEntity):
|
||||
return NotImplemented
|
||||
return self.id == other.id
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Get hash based on entity identity.
|
||||
|
||||
Returns:
|
||||
Hash of the entity ID.
|
||||
"""
|
||||
return hash(self.id)
|
||||
|
||||
def touch(self) -> None:
|
||||
"""Update the updated_at timestamp."""
|
||||
"""Update the updated_at timestamp.
|
||||
|
||||
Should be called whenever the entity is modified to track
|
||||
the last modification time.
|
||||
"""
|
||||
self.updated_at = datetime.now(UTC)
|
||||
|
||||
@abstractmethod
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert entity to dictionary."""
|
||||
"""Convert entity to dictionary representation.
|
||||
|
||||
Returns:
|
||||
Dictionary containing all entity attributes.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Domain entity for Blog Post."""
|
||||
"""Domain entity for Blog Post.
|
||||
|
||||
This module defines the Post aggregate root entity that encapsulates
|
||||
all business logic related to blog posts including publishing, content
|
||||
management, and tag operations.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
@@ -11,7 +16,28 @@ from app.domain.value_objects.title import Title
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Post(BaseEntity):
|
||||
"""Blog post domain entity."""
|
||||
"""Blog post domain entity.
|
||||
|
||||
Represents a blog post with title, content, slug, and metadata.
|
||||
Encapsulates business logic for post lifecycle management.
|
||||
|
||||
Attributes:
|
||||
title: Post title value object with validation.
|
||||
content: Post content value object with validation.
|
||||
slug: URL-friendly identifier generated from title.
|
||||
author_id: Identifier of the post author.
|
||||
published: Publication status flag.
|
||||
tags: List of tags associated with the post.
|
||||
|
||||
Example:
|
||||
>>> post = Post.create(
|
||||
... title_str="My First Post",
|
||||
... content_str="This is the content...",
|
||||
... author_id="user-123",
|
||||
... tags=["python", "fastapi"]
|
||||
... )
|
||||
>>> post.publish()
|
||||
"""
|
||||
|
||||
title: Title
|
||||
content: Content
|
||||
@@ -21,40 +47,66 @@ class Post(BaseEntity):
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
def publish(self) -> None:
|
||||
"""Publish the post."""
|
||||
"""Publish the post.
|
||||
|
||||
Sets the published flag to True and updates the timestamp.
|
||||
"""
|
||||
self.published = True
|
||||
self.touch()
|
||||
|
||||
def unpublish(self) -> None:
|
||||
"""Unpublish the post."""
|
||||
"""Unpublish the post.
|
||||
|
||||
Sets the published flag to False and updates the timestamp.
|
||||
"""
|
||||
self.published = False
|
||||
self.touch()
|
||||
|
||||
def update_content(self, content: Content) -> None:
|
||||
"""Update post content."""
|
||||
"""Update post content.
|
||||
|
||||
Args:
|
||||
content: New content value object.
|
||||
"""
|
||||
self.content = content
|
||||
self.touch()
|
||||
|
||||
def update_title(self, title: Title) -> None:
|
||||
"""Update post title and regenerate slug."""
|
||||
"""Update post title and regenerate slug.
|
||||
|
||||
Args:
|
||||
title: New title value object.
|
||||
"""
|
||||
self.title = title
|
||||
self.slug = Slug.from_title(title.value)
|
||||
self.touch()
|
||||
|
||||
def add_tag(self, tag: str) -> None:
|
||||
"""Add a tag to the post."""
|
||||
"""Add a tag to the post.
|
||||
|
||||
Args:
|
||||
tag: Tag string to add. Only adds if not already present.
|
||||
"""
|
||||
if tag not in self.tags:
|
||||
self.tags.append(tag)
|
||||
self.touch()
|
||||
|
||||
def remove_tag(self, tag: str) -> None:
|
||||
"""Remove a tag from the post."""
|
||||
"""Remove a tag from the post.
|
||||
|
||||
Args:
|
||||
tag: Tag string to remove. Only removes if present.
|
||||
"""
|
||||
if tag in self.tags:
|
||||
self.tags.remove(tag)
|
||||
self.touch()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert entity to dictionary."""
|
||||
"""Convert entity to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation with all post attributes.
|
||||
"""
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"title": self.title.value,
|
||||
@@ -75,7 +127,17 @@ class Post(BaseEntity):
|
||||
author_id: str,
|
||||
tags: list[str] | None = None,
|
||||
) -> "Post":
|
||||
"""Factory method to create a new post."""
|
||||
"""Factory method to create a new post.
|
||||
|
||||
Args:
|
||||
title_str: Title string for the post.
|
||||
content_str: Content string for the post.
|
||||
author_id: Identifier of the post author.
|
||||
tags: Optional list of tags.
|
||||
|
||||
Returns:
|
||||
New Post instance with validated value objects.
|
||||
"""
|
||||
title = Title(title_str)
|
||||
content = Content(content_str)
|
||||
slug = Slug.from_title(title_str)
|
||||
|
||||
@@ -1,39 +1,78 @@
|
||||
"""Domain exceptions."""
|
||||
"""Domain exceptions for business logic errors.
|
||||
|
||||
This module defines the exception hierarchy for domain layer errors.
|
||||
All domain exceptions inherit from DomainException base class.
|
||||
"""
|
||||
|
||||
|
||||
class DomainException(Exception):
|
||||
"""Base exception for domain layer."""
|
||||
"""Base exception for domain layer.
|
||||
|
||||
All domain-specific exceptions should inherit from this class.
|
||||
Provides a consistent interface for error messages.
|
||||
|
||||
Attributes:
|
||||
message: Human-readable error description.
|
||||
|
||||
Example:
|
||||
>>> raise DomainException("Business rule violated")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
"""Initialize domain exception.
|
||||
|
||||
Args:
|
||||
message: Error message describing the exception.
|
||||
"""
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ValidationException(DomainException):
|
||||
"""Raised when validation fails."""
|
||||
"""Raised when validation fails.
|
||||
|
||||
pass
|
||||
Used when entity or value object validation does not pass.
|
||||
|
||||
Example:
|
||||
>>> raise ValidationException("Title is too long")
|
||||
"""
|
||||
|
||||
|
||||
class NotFoundException(DomainException):
|
||||
"""Raised when an entity is not found."""
|
||||
"""Raised when an entity is not found.
|
||||
|
||||
pass
|
||||
Used when requesting an entity that does not exist in the repository.
|
||||
|
||||
Example:
|
||||
>>> raise NotFoundException("Post with id 123 not found")
|
||||
"""
|
||||
|
||||
|
||||
class AlreadyExistsException(DomainException):
|
||||
"""Raised when trying to create an entity that already exists."""
|
||||
"""Raised when trying to create an entity that already exists.
|
||||
|
||||
pass
|
||||
Used when attempting to create a duplicate entity.
|
||||
|
||||
Example:
|
||||
>>> raise AlreadyExistsException("Post with this slug already exists")
|
||||
"""
|
||||
|
||||
|
||||
class UnauthorizedException(DomainException):
|
||||
"""Raised when user is not authorized."""
|
||||
"""Raised when user is not authorized.
|
||||
|
||||
pass
|
||||
Used when authentication is required but not provided or invalid.
|
||||
|
||||
Example:
|
||||
>>> raise UnauthorizedException("Authentication required")
|
||||
"""
|
||||
|
||||
|
||||
class ForbiddenException(DomainException):
|
||||
"""Raised when access is forbidden."""
|
||||
"""Raised when access is forbidden.
|
||||
|
||||
pass
|
||||
Used when authenticated user lacks required permissions.
|
||||
|
||||
Example:
|
||||
>>> raise ForbiddenException("Only admins can delete posts")
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Repository interfaces."""
|
||||
"""Repository interfaces.
|
||||
|
||||
This module re-exports all repository interfaces that define
|
||||
the contract for data access operations.
|
||||
"""
|
||||
|
||||
from app.domain.repositories.base import Repository
|
||||
from app.domain.repositories.post import PostRepository
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Base repository interface for DDD."""
|
||||
"""Base repository interface for DDD.
|
||||
|
||||
This module defines the generic repository pattern interface that all
|
||||
repository implementations must follow. Provides standard CRUD operations.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar
|
||||
@@ -10,34 +14,76 @@ T = TypeVar("T", bound=BaseEntity)
|
||||
|
||||
|
||||
class Repository(ABC, Generic[T]):
|
||||
"""Generic repository interface."""
|
||||
"""Generic repository interface.
|
||||
|
||||
Defines the contract for repository implementations. All repositories
|
||||
must provide standard CRUD operations for their entity type.
|
||||
|
||||
Type Parameters:
|
||||
T: Entity type that must inherit from BaseEntity.
|
||||
|
||||
Example:
|
||||
>>> class PostRepository(Repository[Post]):
|
||||
... async def get_by_id(self, entity_id: UUID) -> Post | None:
|
||||
... ...
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, entity_id: UUID) -> T | None:
|
||||
"""Get entity by ID."""
|
||||
"""Get entity by ID.
|
||||
|
||||
Args:
|
||||
entity_id: Unique identifier of the entity.
|
||||
|
||||
Returns:
|
||||
Entity instance if found, None otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_all(self) -> list[T]:
|
||||
"""Get all entities."""
|
||||
"""Get all entities.
|
||||
|
||||
Returns:
|
||||
List of all entity instances.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def add(self, entity: T) -> None:
|
||||
"""Add new entity."""
|
||||
"""Add new entity.
|
||||
|
||||
Args:
|
||||
entity: Entity instance to add.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, entity: T) -> None:
|
||||
"""Update existing entity."""
|
||||
"""Update existing entity.
|
||||
|
||||
Args:
|
||||
entity: Entity instance with updated data.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, entity_id: UUID) -> None:
|
||||
"""Delete entity by ID."""
|
||||
"""Delete entity by ID.
|
||||
|
||||
Args:
|
||||
entity_id: Unique identifier of the entity to delete.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def exists(self, entity_id: UUID) -> bool:
|
||||
"""Check if entity exists."""
|
||||
"""Check if entity exists.
|
||||
|
||||
Args:
|
||||
entity_id: Unique identifier of the entity.
|
||||
|
||||
Returns:
|
||||
True if entity exists, False otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Post repository interface."""
|
||||
"""Post repository interface.
|
||||
|
||||
This module extends the base repository interface with post-specific
|
||||
query methods including slug lookup, author filtering, and search.
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
@@ -7,11 +11,27 @@ from app.domain.repositories.base import Repository
|
||||
|
||||
|
||||
class PostRepository(Repository[Post]):
|
||||
"""Repository interface for Blog Posts."""
|
||||
"""Repository interface for Blog Posts.
|
||||
|
||||
Extends the generic repository with post-specific operations
|
||||
including slug-based lookup, author filtering, tag filtering,
|
||||
and full-text search capabilities.
|
||||
|
||||
Example:
|
||||
>>> posts = await repo.get_by_author("user-123", limit=10)
|
||||
>>> exists = await repo.slug_exists("my-first-post")
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_slug(self, slug: str) -> Post | None:
|
||||
"""Get post by slug."""
|
||||
"""Get post by slug.
|
||||
|
||||
Args:
|
||||
slug: URL-friendly slug identifier.
|
||||
|
||||
Returns:
|
||||
Post instance if found, None otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
@@ -21,7 +41,16 @@ class PostRepository(Repository[Post]):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Get all posts by author."""
|
||||
"""Get all posts by author.
|
||||
|
||||
Args:
|
||||
author_id: Identifier of the author.
|
||||
limit: Maximum number of posts to return.
|
||||
offset: Number of posts to skip.
|
||||
|
||||
Returns:
|
||||
List of posts by the specified author.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
@@ -30,7 +59,15 @@ class PostRepository(Repository[Post]):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Get all published posts."""
|
||||
"""Get all published posts.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of posts to return.
|
||||
offset: Number of posts to skip.
|
||||
|
||||
Returns:
|
||||
List of published posts.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
@@ -40,12 +77,28 @@ class PostRepository(Repository[Post]):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Get posts by tag."""
|
||||
"""Get posts by tag.
|
||||
|
||||
Args:
|
||||
tag: Tag to filter by.
|
||||
limit: Maximum number of posts to return.
|
||||
offset: Number of posts to skip.
|
||||
|
||||
Returns:
|
||||
List of posts with the specified tag.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def slug_exists(self, slug: str) -> bool:
|
||||
"""Check if slug already exists."""
|
||||
"""Check if slug already exists.
|
||||
|
||||
Args:
|
||||
slug: Slug to check for existence.
|
||||
|
||||
Returns:
|
||||
True if slug exists, False otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
@@ -55,5 +108,14 @@ class PostRepository(Repository[Post]):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Search posts by query string."""
|
||||
"""Search posts by query string.
|
||||
|
||||
Args:
|
||||
query: Search query string.
|
||||
limit: Maximum number of posts to return.
|
||||
offset: Number of posts to skip.
|
||||
|
||||
Returns:
|
||||
List of posts matching the search query.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Role-based access control definitions."""
|
||||
"""Role-based access control definitions.
|
||||
|
||||
This module provides role and permission definitions for the application
|
||||
along with utility functions and decorators for permission checking.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
@@ -9,7 +13,20 @@ from app.domain.exceptions import ForbiddenException
|
||||
|
||||
|
||||
class Role(str, Enum):
|
||||
"""User roles in the system."""
|
||||
"""User roles in the system.
|
||||
|
||||
Defines the available user roles with hierarchical permissions.
|
||||
ADMIN has full access, USER has standard access, GUEST has read-only.
|
||||
|
||||
Attributes:
|
||||
ADMIN: Administrator with full system access.
|
||||
USER: Regular authenticated user.
|
||||
GUEST: Unauthenticated or limited access user.
|
||||
|
||||
Example:
|
||||
>>> if role == Role.ADMIN:
|
||||
... grant_full_access()
|
||||
"""
|
||||
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
@@ -17,9 +34,16 @@ class Role(str, Enum):
|
||||
|
||||
|
||||
class Permission:
|
||||
"""Permission definitions."""
|
||||
"""Permission definitions.
|
||||
|
||||
Contains string constants for all available permissions in the system.
|
||||
Used for role-based access control checks.
|
||||
|
||||
Example:
|
||||
>>> if has_permission(role, Permission.POST_CREATE):
|
||||
... allow_post_creation()
|
||||
"""
|
||||
|
||||
# Post permissions
|
||||
POST_CREATE = "post:create"
|
||||
POST_READ = "post:read"
|
||||
POST_READ_UNPUBLISHED = "post:read_unpublished"
|
||||
@@ -28,7 +52,6 @@ class Permission:
|
||||
POST_PUBLISH = "post:publish"
|
||||
|
||||
|
||||
# Role-based permission mapping
|
||||
ROLE_PERMISSIONS: dict[Role, list[str]] = {
|
||||
Role.ADMIN: [
|
||||
Permission.POST_CREATE,
|
||||
@@ -52,24 +75,52 @@ ROLE_PERMISSIONS: dict[Role, list[str]] = {
|
||||
|
||||
|
||||
def has_permission(role: Role, permission: str) -> bool:
|
||||
"""Check if role has specific permission."""
|
||||
"""Check if role has specific permission.
|
||||
|
||||
Args:
|
||||
role: User role to check.
|
||||
permission: Permission string to verify.
|
||||
|
||||
Returns:
|
||||
True if role has the permission, False otherwise.
|
||||
|
||||
Example:
|
||||
>>> has_permission(Role.ADMIN, Permission.POST_DELETE)
|
||||
True
|
||||
"""
|
||||
return permission in ROLE_PERMISSIONS.get(role, [])
|
||||
|
||||
|
||||
def require_permission(
|
||||
permission: str,
|
||||
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
"""Decorator to require specific permission."""
|
||||
"""Decorator to require specific permission.
|
||||
|
||||
Creates a decorator that checks if the user has the required permission
|
||||
before executing the decorated function.
|
||||
|
||||
Args:
|
||||
permission: Permission string required for execution.
|
||||
|
||||
Returns:
|
||||
Decorator function for permission checking.
|
||||
|
||||
Raises:
|
||||
ForbiddenException: If user lacks the required permission.
|
||||
|
||||
Example:
|
||||
>>> @require_permission(Permission.POST_CREATE)
|
||||
... async def create_post():
|
||||
... ...
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(func)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
# Get token_info from kwargs
|
||||
token_info = kwargs.get("token_info")
|
||||
if not token_info:
|
||||
raise ForbiddenException("Authentication required")
|
||||
|
||||
# Determine role from token or default to guest
|
||||
roles = getattr(token_info, "roles", [])
|
||||
if Role.ADMIN.value in roles:
|
||||
role = Role.ADMIN
|
||||
@@ -93,7 +144,18 @@ def require_permission(
|
||||
def get_effective_role(roles: list[str]) -> Role:
|
||||
"""Determine effective role from list of roles.
|
||||
|
||||
Priority: admin > user > guest
|
||||
Evaluates multiple roles and returns the highest privilege role.
|
||||
Priority order: admin > user > guest.
|
||||
|
||||
Args:
|
||||
roles: List of role strings from token.
|
||||
|
||||
Returns:
|
||||
Highest privilege Role enum value.
|
||||
|
||||
Example:
|
||||
>>> get_effective_role(["user", "admin"])
|
||||
<Role.ADMIN: 'admin'>
|
||||
"""
|
||||
if Role.ADMIN.value in roles:
|
||||
return Role.ADMIN
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Value objects."""
|
||||
"""Value objects.
|
||||
|
||||
This module re-exports all domain value objects that represent
|
||||
immutable validated domain concepts.
|
||||
"""
|
||||
|
||||
from app.domain.value_objects.base import ValueObject
|
||||
from app.domain.value_objects.content import Content
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Base value object for DDD domain layer."""
|
||||
"""Base value object for DDD domain layer.
|
||||
|
||||
This module provides the foundational ValueObject class that all domain
|
||||
value objects must inherit from. Implements equality, hashing, and
|
||||
validation patterns for immutable value objects.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
@@ -9,29 +14,78 @@ T = TypeVar("T")
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ValueObject(ABC, Generic[T]):
|
||||
"""Base class for all value objects."""
|
||||
"""Base class for all value objects.
|
||||
|
||||
Value objects are immutable objects defined by their attributes rather
|
||||
than identity. They are validated on creation and provide type safety.
|
||||
|
||||
Attributes:
|
||||
value: The underlying value wrapped by the value object.
|
||||
|
||||
Type Parameters:
|
||||
T: Type of the wrapped value.
|
||||
|
||||
Example:
|
||||
>>> class Email(ValueObject[str]):
|
||||
... def _validate(self) -> None:
|
||||
... if "@" not in self.value:
|
||||
... raise ValueError("Invalid email")
|
||||
"""
|
||||
|
||||
value: T
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate value object after initialization.
|
||||
|
||||
Automatically called by dataclass after __init__. Triggers
|
||||
the validation method to ensure value integrity.
|
||||
"""
|
||||
self._validate()
|
||||
|
||||
@abstractmethod
|
||||
def _validate(self) -> None:
|
||||
"""Validate the value object. Raise ValueError if invalid."""
|
||||
"""Validate the value object.
|
||||
|
||||
Must be implemented by subclasses to enforce value constraints.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value does not meet validation criteria.
|
||||
"""
|
||||
...
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare value objects by value.
|
||||
|
||||
Args:
|
||||
other: Another object to compare with.
|
||||
|
||||
Returns:
|
||||
True if both are ValueObjects with equal values.
|
||||
"""
|
||||
if not isinstance(other, ValueObject):
|
||||
return False
|
||||
return bool(self.value == other.value)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Get hash based on wrapped value.
|
||||
|
||||
Returns:
|
||||
Hash of the underlying value.
|
||||
"""
|
||||
return hash(self.value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Convert value object to string.
|
||||
|
||||
Returns:
|
||||
String representation of the wrapped value.
|
||||
"""
|
||||
return str(self.value)
|
||||
|
||||
def to_primitive(self) -> Any:
|
||||
"""Convert value object to primitive type."""
|
||||
"""Convert value object to primitive type.
|
||||
|
||||
Returns:
|
||||
The underlying primitive value.
|
||||
"""
|
||||
return self.value
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Content value object."""
|
||||
"""Content value object.
|
||||
|
||||
This module defines the Content value object for blog post content
|
||||
with validation for minimum and maximum length constraints.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -7,12 +11,35 @@ from app.domain.value_objects.base import ValueObject
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Content(ValueObject[str]):
|
||||
"""Blog post content value object."""
|
||||
"""Blog post content value object.
|
||||
|
||||
Wraps and validates blog post content ensuring it meets length
|
||||
requirements and is not empty.
|
||||
|
||||
Attributes:
|
||||
value: The content string value.
|
||||
MIN_LENGTH: Minimum allowed content length (10 characters).
|
||||
MAX_LENGTH: Maximum allowed content length (50000 characters).
|
||||
|
||||
Raises:
|
||||
ValueError: If content is empty, too short, or too long.
|
||||
|
||||
Example:
|
||||
>>> content = Content("This is valid content...")
|
||||
>>> print(content.value)
|
||||
"""
|
||||
|
||||
MIN_LENGTH: int = 10
|
||||
MAX_LENGTH: int = 50000
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Validate content string.
|
||||
|
||||
Checks that content is a non-empty string within length bounds.
|
||||
|
||||
Raises:
|
||||
ValueError: If content fails validation criteria.
|
||||
"""
|
||||
if not isinstance(self.value, str):
|
||||
raise ValueError("Content must be a string")
|
||||
if not self.value.strip():
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Slug value object for URL-friendly identifiers."""
|
||||
"""Slug value object for URL-friendly identifiers.
|
||||
|
||||
This module defines the Slug value object for generating and validating
|
||||
URL-friendly slugs from titles. Enforces lowercase, alphanumeric, and
|
||||
hyphen-only format.
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
@@ -8,12 +13,36 @@ from app.domain.value_objects.base import ValueObject
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Slug(ValueObject[str]):
|
||||
"""URL slug value object."""
|
||||
"""URL slug value object.
|
||||
|
||||
Represents a URL-friendly identifier generated from titles.
|
||||
Validates format and provides factory method for slug generation.
|
||||
|
||||
Attributes:
|
||||
value: The slug string value.
|
||||
MAX_LENGTH: Maximum allowed slug length (200 characters).
|
||||
SLUG_PATTERN: Regex pattern for valid slug format.
|
||||
|
||||
Raises:
|
||||
ValueError: If slug format is invalid.
|
||||
|
||||
Example:
|
||||
>>> slug = Slug.from_title("My First Post!")
|
||||
>>> print(slug.value)
|
||||
'my-first-post'
|
||||
"""
|
||||
|
||||
MAX_LENGTH: int = 200
|
||||
SLUG_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Validate slug format.
|
||||
|
||||
Ensures slug contains only lowercase letters, numbers, and hyphens.
|
||||
|
||||
Raises:
|
||||
ValueError: If slug format is invalid.
|
||||
"""
|
||||
if not isinstance(self.value, str):
|
||||
raise ValueError("Slug must be a string")
|
||||
if len(self.value) > self.MAX_LENGTH:
|
||||
@@ -23,17 +52,22 @@ class Slug(ValueObject[str]):
|
||||
|
||||
@classmethod
|
||||
def from_title(cls, title: str) -> "Slug":
|
||||
"""Generate slug from title."""
|
||||
# Convert to lowercase, replace spaces with hyphens
|
||||
"""Generate slug from title.
|
||||
|
||||
Converts title to URL-friendly format by lowercasing, removing
|
||||
special characters, and replacing spaces with hyphens.
|
||||
|
||||
Args:
|
||||
title: Source title string.
|
||||
|
||||
Returns:
|
||||
New Slug instance with generated value.
|
||||
"""
|
||||
slug = title.lower().strip()
|
||||
# Keep only alphanumeric, spaces, and hyphens
|
||||
slug = re.sub(r"[^a-z0-9\s-]", "", slug)
|
||||
# Replace spaces and multiple hyphens with single hyphen
|
||||
slug = re.sub(r"[-\s]+", "-", slug)
|
||||
# Limit length and strip hyphens
|
||||
max_len = 200 # Same as MAX_LENGTH
|
||||
max_len = 200
|
||||
slug = slug[:max_len].strip("-")
|
||||
# Ensure we have at least one character
|
||||
if not slug:
|
||||
slug = "post"
|
||||
return cls(value=slug)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Title value object."""
|
||||
"""Title value object.
|
||||
|
||||
This module defines the Title value object for blog post titles
|
||||
with validation for minimum and maximum length constraints.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -7,12 +11,35 @@ from app.domain.value_objects.base import ValueObject
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Title(ValueObject[str]):
|
||||
"""Blog post title value object."""
|
||||
"""Blog post title value object.
|
||||
|
||||
Wraps and validates blog post titles ensuring they meet length
|
||||
requirements and are not empty.
|
||||
|
||||
Attributes:
|
||||
value: The title string value.
|
||||
MIN_LENGTH: Minimum allowed title length (3 characters).
|
||||
MAX_LENGTH: Maximum allowed title length (200 characters).
|
||||
|
||||
Raises:
|
||||
ValueError: If title is empty, too short, or too long.
|
||||
|
||||
Example:
|
||||
>>> title = Title("My Blog Post")
|
||||
>>> print(title.value)
|
||||
"""
|
||||
|
||||
MIN_LENGTH: int = 3
|
||||
MAX_LENGTH: int = 200
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Validate title string.
|
||||
|
||||
Checks that title is a non-empty string within length bounds.
|
||||
|
||||
Raises:
|
||||
ValueError: If title fails validation criteria.
|
||||
"""
|
||||
if not isinstance(self.value, str):
|
||||
raise ValueError("Title must be a string")
|
||||
if len(self.value) < self.MIN_LENGTH:
|
||||
|
||||
Reference in New Issue
Block a user