Files
bakery-ia/shared/routing/route_builder.py

311 lines
11 KiB
Python
Raw Permalink Normal View History

2025-10-06 15:27:01 +02:00
"""
Route Builder for standardized URL structure
Ensures consistent API patterns across all microservices
"""
from enum import Enum
from typing import Optional
class RouteCategory(Enum):
"""Categories of API routes with different access patterns"""
BASE = "base" # Atomic CRUD operations on resources
DASHBOARD = "dashboard" # Dashboard data and summaries
ANALYTICS = "analytics" # Analytics endpoints (tier-gated)
OPERATIONS = "operations" # Service-specific operations
class RouteBuilder:
"""
Builder for creating standardized API routes
URL Structure:
- Base: /api/v1/tenants/{tenant_id}/{service}/{resource}
- Dashboard: /api/v1/tenants/{tenant_id}/{service}/dashboard/{operation}
- Analytics: /api/v1/tenants/{tenant_id}/{service}/analytics/{operation}
- Operations: /api/v1/tenants/{tenant_id}/{service}/operations/{operation}
"""
API_VERSION = "v1"
BASE_PATH = f"/api/{API_VERSION}"
def __init__(self, service_name: str):
"""
Initialize route builder for a specific service
Args:
service_name: Name of the service (e.g., 'inventory', 'production')
"""
self.service_name = service_name
def build_base_route(self, resource: str, include_tenant_prefix: bool = True) -> str:
"""
Build base CRUD route for a resource
Args:
resource: Resource name (e.g., 'ingredients', 'batches')
2025-10-27 16:33:26 +01:00
Empty string returns just the service path
2025-10-06 15:27:01 +02:00
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('inventory')
builder.build_base_route('ingredients')
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients'
2025-10-27 16:33:26 +01:00
builder.build_base_route('')
# Returns: '/api/v1/tenants/{tenant_id}/inventory'
2025-10-06 15:27:01 +02:00
"""
2025-10-27 16:33:26 +01:00
if resource:
# Resource provided - add it to the path
if include_tenant_prefix:
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/{resource}"
return f"{self.BASE_PATH}/{self.service_name}/{resource}"
else:
# Empty resource - return just the service path
if include_tenant_prefix:
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}"
return f"{self.BASE_PATH}/{self.service_name}"
2025-10-06 15:27:01 +02:00
def build_dashboard_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
"""
Build dashboard route
Args:
operation: Dashboard operation (e.g., 'summary', 'capacity-status')
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('production')
builder.build_dashboard_route('summary')
# Returns: '/api/v1/tenants/{tenant_id}/production/dashboard/summary'
"""
if include_tenant_prefix:
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/dashboard/{operation}"
return f"{self.BASE_PATH}/{self.service_name}/dashboard/{operation}"
def build_analytics_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
"""
Build analytics route (tier-gated: professional/enterprise)
Args:
operation: Analytics operation (e.g., 'equipment-efficiency', 'trends')
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('production')
builder.build_analytics_route('equipment-efficiency')
# Returns: '/api/v1/tenants/{tenant_id}/production/analytics/equipment-efficiency'
"""
if include_tenant_prefix:
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/analytics/{operation}"
return f"{self.BASE_PATH}/{self.service_name}/analytics/{operation}"
def build_operations_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
"""
Build service operations route
Args:
operation: Operation name (e.g., 'schedule-batch', 'stock-adjustment')
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('production')
builder.build_operations_route('schedule-batch')
# Returns: '/api/v1/tenants/{tenant_id}/production/operations/schedule-batch'
"""
if include_tenant_prefix:
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/operations/{operation}"
return f"{self.BASE_PATH}/{self.service_name}/operations/{operation}"
def build_resource_detail_route(self, resource: str, id_param: str = "id", include_tenant_prefix: bool = True) -> str:
"""
Build route for individual resource details
Args:
resource: Resource name
id_param: Name of the ID parameter
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('inventory')
builder.build_resource_detail_route('ingredients', 'ingredient_id')
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}'
"""
base = self.build_base_route(resource, include_tenant_prefix)
return f"{base}/{{{id_param}}}"
def build_nested_resource_route(
self,
parent_resource: str,
parent_id_param: str,
child_resource: str,
include_tenant_prefix: bool = True
) -> str:
"""
Build route for nested resources
Args:
parent_resource: Parent resource name
parent_id_param: Parent ID parameter name
child_resource: Child resource name
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('inventory')
builder.build_nested_resource_route('ingredients', 'ingredient_id', 'stock')
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}/stock'
"""
base = self.build_resource_detail_route(parent_resource, parent_id_param, include_tenant_prefix)
return f"{base}/{child_resource}"
def build_resource_action_route(
self,
resource: str,
id_param: str,
action: str,
include_tenant_prefix: bool = True
) -> str:
"""
Build route for resource-specific actions
Args:
resource: Resource name
id_param: ID parameter name
action: Action name
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('pos')
builder.build_resource_action_route('configurations', 'config_id', 'test-connection')
# Returns: '/api/v1/tenants/{tenant_id}/pos/configurations/{config_id}/test-connection'
"""
base = self.build_resource_detail_route(resource, id_param, include_tenant_prefix)
return f"{base}/{action}"
def build_global_route(self, path: str) -> str:
"""
Build global route without tenant context
Args:
path: Path after service name
Returns:
Route path
Example:
builder = RouteBuilder('pos')
builder.build_global_route('supported-systems')
# Returns: '/api/v1/pos/supported-systems'
"""
return f"{self.BASE_PATH}/{self.service_name}/{path}"
def build_webhook_route(self, path: str) -> str:
"""
Build webhook route (no tenant context)
Args:
path: Webhook path
Returns:
Route path
Example:
builder = RouteBuilder('pos')
builder.build_webhook_route('{pos_system}')
# Returns: '/api/v1/webhooks/{pos_system}'
"""
return f"{self.BASE_PATH}/webhooks/{path}"
def build_custom_route(
self,
category: RouteCategory,
path_segments: list,
include_tenant_prefix: bool = True
) -> str:
"""
Build custom route with specified category and path segments
Args:
category: Route category
path_segments: List of path segments after category
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
Returns:
Route path
Example:
builder = RouteBuilder('inventory')
builder.build_custom_route(RouteCategory.DASHBOARD, ['food-safety', 'compliance'])
# Returns: '/api/v1/tenants/{tenant_id}/inventory/dashboard/food-safety/compliance'
"""
base_prefix = f"{self.BASE_PATH}/tenants/{{tenant_id}}" if include_tenant_prefix else self.BASE_PATH
# Build path segments string, avoiding trailing slash when empty
segments_str = '/'.join(path_segments) if path_segments else ''
separator = '/' if segments_str else ''
2025-10-06 15:27:01 +02:00
if category == RouteCategory.BASE:
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
2025-10-06 15:27:01 +02:00
elif category == RouteCategory.DASHBOARD:
return f"{base_prefix}/{self.service_name}/dashboard{separator}{segments_str}"
2025-10-06 15:27:01 +02:00
elif category == RouteCategory.ANALYTICS:
return f"{base_prefix}/{self.service_name}/analytics{separator}{segments_str}"
2025-10-06 15:27:01 +02:00
elif category == RouteCategory.OPERATIONS:
return f"{base_prefix}/{self.service_name}/operations{separator}{segments_str}"
2025-10-06 15:27:01 +02:00
# Fallback to base
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
2025-10-06 15:27:01 +02:00
@staticmethod
def get_route_pattern(category: RouteCategory, service_name: Optional[str] = None) -> str:
"""
Get regex pattern for matching routes of a specific category
Args:
category: Route category
service_name: Optional service name to filter
Returns:
Regex pattern for route matching
Example:
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS)
# Returns: r'^/api/v1/tenants/[^/]+/[^/]+/analytics/.*'
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS, 'production')
# Returns: r'^/api/v1/tenants/[^/]+/production/analytics/.*'
"""
service_pattern = service_name if service_name else "[^/]+"
if category == RouteCategory.BASE:
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/(?!dashboard|analytics|operations)[^/]+.*"
elif category == RouteCategory.DASHBOARD:
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/dashboard/.*"
elif category == RouteCategory.ANALYTICS:
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/analytics/.*"
elif category == RouteCategory.OPERATIONS:
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/operations/.*"
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/.*"