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')
|
|
|
|
|
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'
|
|
|
|
|
"""
|
|
|
|
|
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}"
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-10-12 18:47:33 +02:00
|
|
|
# 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:
|
2025-10-12 18:47:33 +02:00
|
|
|
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
|
2025-10-06 15:27:01 +02:00
|
|
|
elif category == RouteCategory.DASHBOARD:
|
2025-10-12 18:47:33 +02:00
|
|
|
return f"{base_prefix}/{self.service_name}/dashboard{separator}{segments_str}"
|
2025-10-06 15:27:01 +02:00
|
|
|
elif category == RouteCategory.ANALYTICS:
|
2025-10-12 18:47:33 +02:00
|
|
|
return f"{base_prefix}/{self.service_name}/analytics{separator}{segments_str}"
|
2025-10-06 15:27:01 +02:00
|
|
|
elif category == RouteCategory.OPERATIONS:
|
2025-10-12 18:47:33 +02:00
|
|
|
return f"{base_prefix}/{self.service_name}/operations{separator}{segments_str}"
|
2025-10-06 15:27:01 +02:00
|
|
|
|
|
|
|
|
# Fallback to base
|
2025-10-12 18:47:33 +02:00
|
|
|
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}/.*"
|