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

311 lines
11 KiB
Python
Executable File

"""
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')
Empty string returns just the service path
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'
builder.build_base_route('')
# Returns: '/api/v1/tenants/{tenant_id}/inventory'
"""
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}"
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 ''
if category == RouteCategory.BASE:
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
elif category == RouteCategory.DASHBOARD:
return f"{base_prefix}/{self.service_name}/dashboard{separator}{segments_str}"
elif category == RouteCategory.ANALYTICS:
return f"{base_prefix}/{self.service_name}/analytics{separator}{segments_str}"
elif category == RouteCategory.OPERATIONS:
return f"{base_prefix}/{self.service_name}/operations{separator}{segments_str}"
# Fallback to base
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
@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}/.*"