feat: implement DashboardWidgetProvider pattern for modular dashboard widgets
Add protocol-based widget system following the MetricsProvider pattern: - Create DashboardWidgetProviderProtocol in contracts/widgets.py - Add WidgetAggregatorService in core to discover and aggregate widgets - Implement MarketplaceWidgetProvider for recent_imports widget - Implement TenancyWidgetProvider for recent_vendors widget - Update admin dashboard to use widget_aggregator - Add widget_provider field to ModuleDefinition Architecture documentation: - Add widget-provider-pattern.md with implementation guide - Add cross-module-import-rules.md enforcing core/optional separation - Update module-system.md with widget_provider and import rules This enables modules to provide rich dashboard widgets without core modules importing from optional modules, maintaining true module independence. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
310
docs/architecture/cross-module-import-rules.md
Normal file
310
docs/architecture/cross-module-import-rules.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Cross-Module Import Rules
|
||||
|
||||
This document defines the strict import rules that ensure the module system remains decoupled, testable, and resilient. These rules are critical for maintaining a truly modular architecture.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Core modules NEVER import from optional modules.**
|
||||
|
||||
This is the fundamental rule that enables optional modules to be truly optional. When a core module imports from an optional module:
|
||||
- The app crashes if that module is disabled
|
||||
- You can't test core functionality in isolation
|
||||
- You create a hidden dependency that violates the architecture
|
||||
|
||||
## Module Classification
|
||||
|
||||
### Core Modules (Always Enabled)
|
||||
- `contracts` - Protocols and interfaces
|
||||
- `core` - Dashboard, settings, profile
|
||||
- `tenancy` - Platform, company, vendor, admin user management
|
||||
- `cms` - Content pages, media library
|
||||
- `customers` - Customer database
|
||||
- `billing` - Subscriptions, tier limits
|
||||
- `payments` - Payment gateway integrations
|
||||
- `messaging` - Email, notifications
|
||||
|
||||
### Optional Modules (Per-Platform)
|
||||
- `analytics` - Reports, dashboards
|
||||
- `cart` - Shopping cart
|
||||
- `catalog` - Product browsing
|
||||
- `checkout` - Cart-to-order conversion
|
||||
- `inventory` - Stock management
|
||||
- `loyalty` - Loyalty programs
|
||||
- `marketplace` - Letzshop integration
|
||||
- `orders` - Order management
|
||||
|
||||
## Import Rules Matrix
|
||||
|
||||
| From \ To | Core | Optional | Contracts |
|
||||
|-----------|------|----------|-----------|
|
||||
| **Core** | :white_check_mark: | :x: FORBIDDEN | :white_check_mark: |
|
||||
| **Optional** | :white_check_mark: | :warning: With care | :white_check_mark: |
|
||||
| **Contracts** | :x: | :x: | :white_check_mark: |
|
||||
|
||||
### Explanation
|
||||
|
||||
1. **Core → Core**: Allowed. Core modules can import from each other (e.g., billing imports from tenancy)
|
||||
|
||||
2. **Core → Optional**: **FORBIDDEN**. This is the most important rule. Core modules must never have direct imports from optional modules.
|
||||
|
||||
3. **Core → Contracts**: Allowed. Contracts define shared protocols and data structures.
|
||||
|
||||
4. **Optional → Core**: Allowed. Optional modules can use core functionality.
|
||||
|
||||
5. **Optional → Optional**: Allowed with care. Check dependencies in `definition.py` to ensure proper ordering.
|
||||
|
||||
6. **Optional → Contracts**: Allowed. This is how optional modules implement protocols.
|
||||
|
||||
7. **Contracts → Anything**: Contracts should only depend on stdlib/typing/Protocol. No module imports.
|
||||
|
||||
## Anti-Patterns (DO NOT DO)
|
||||
|
||||
### Direct Import from Optional Module
|
||||
|
||||
```python
|
||||
# app/modules/core/routes/api/admin_dashboard.py
|
||||
|
||||
# BAD: Core importing from optional module
|
||||
from app.modules.marketplace.services import marketplace_service
|
||||
from app.modules.analytics.services import stats_service
|
||||
|
||||
@router.get("/dashboard")
|
||||
def get_dashboard():
|
||||
# This crashes if marketplace is disabled!
|
||||
imports = marketplace_service.get_recent_imports()
|
||||
```
|
||||
|
||||
### Conditional Import with ismodule_enabled Check
|
||||
|
||||
```python
|
||||
# BAD: Still creates import-time dependency
|
||||
from app.modules.service import module_service
|
||||
|
||||
if module_service.is_module_enabled(db, platform_id, "marketplace"):
|
||||
from app.modules.marketplace.services import marketplace_service # Still loaded!
|
||||
imports = marketplace_service.get_recent_imports()
|
||||
```
|
||||
|
||||
### Type Hints from Optional Modules
|
||||
|
||||
```python
|
||||
# BAD: Type hint forces import
|
||||
from app.modules.marketplace.models import MarketplaceImportJob
|
||||
|
||||
def process_import(job: MarketplaceImportJob) -> None: # Crashes if disabled
|
||||
...
|
||||
```
|
||||
|
||||
## Approved Patterns
|
||||
|
||||
### 1. Provider Protocol Pattern (Metrics & Widgets)
|
||||
|
||||
Use the provider protocol pattern for cross-module data:
|
||||
|
||||
```python
|
||||
# app/modules/core/services/stats_aggregator.py
|
||||
|
||||
# GOOD: Import only protocols and contracts
|
||||
from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue
|
||||
|
||||
class StatsAggregatorService:
|
||||
def _get_enabled_providers(self, db, platform_id):
|
||||
# Discover providers from MODULES registry
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
for module in MODULES.values():
|
||||
if module.has_metrics_provider():
|
||||
# Provider is called through protocol, not direct import
|
||||
provider = module.get_metrics_provider_instance()
|
||||
yield provider
|
||||
```
|
||||
|
||||
### 2. Context Provider Pattern
|
||||
|
||||
Modules contribute context without direct imports:
|
||||
|
||||
```python
|
||||
# app/modules/core/utils.py
|
||||
|
||||
def get_context_for_frontend(frontend_type, request, db, platform):
|
||||
# GOOD: Discover and call providers through registry
|
||||
for module in get_enabled_modules(db, platform.id):
|
||||
if module.has_context_provider(frontend_type):
|
||||
context.update(module.get_context_contribution(...))
|
||||
```
|
||||
|
||||
### 3. Lazy Factory Functions
|
||||
|
||||
Use factory functions in module definitions:
|
||||
|
||||
```python
|
||||
# app/modules/marketplace/definition.py
|
||||
|
||||
def _get_widget_provider():
|
||||
"""Lazy import to avoid circular imports."""
|
||||
from app.modules.marketplace.services.marketplace_widgets import marketplace_widget_provider
|
||||
return marketplace_widget_provider
|
||||
|
||||
marketplace_module = ModuleDefinition(
|
||||
code="marketplace",
|
||||
widget_provider=_get_widget_provider, # Called lazily when needed
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Type Checking Only Imports
|
||||
|
||||
Use `TYPE_CHECKING` for type hints without runtime dependency:
|
||||
|
||||
```python
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.modules.marketplace.models import MarketplaceImportJob
|
||||
|
||||
def process_import(job: "MarketplaceImportJob") -> None:
|
||||
# Works at runtime even if marketplace is disabled
|
||||
...
|
||||
```
|
||||
|
||||
### 5. Registry-Based Discovery
|
||||
|
||||
Discover modules through the registry, not imports:
|
||||
|
||||
```python
|
||||
# GOOD: Discovery through registry
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
def get_module_if_enabled(db, platform_id, module_code):
|
||||
module = MODULES.get(module_code)
|
||||
if module and module_service.is_module_enabled(db, platform_id, module_code):
|
||||
return module
|
||||
return None
|
||||
```
|
||||
|
||||
## Architecture Validation
|
||||
|
||||
The architecture validator (`scripts/validate_architecture.py`) includes rules to detect violations:
|
||||
|
||||
| Rule | Severity | Description |
|
||||
|------|----------|-------------|
|
||||
| IMPORT-001 | ERROR | Core module imports from optional module |
|
||||
| IMPORT-002 | WARNING | Optional module imports from unrelated optional module |
|
||||
| IMPORT-003 | INFO | Consider using protocol pattern instead of direct import |
|
||||
|
||||
Run validation:
|
||||
```bash
|
||||
python scripts/validate_architecture.py
|
||||
```
|
||||
|
||||
## Provider Pattern Summary
|
||||
|
||||
### When to Use Each Pattern
|
||||
|
||||
| Need | Pattern | Location |
|
||||
|------|---------|----------|
|
||||
| Numeric statistics | MetricsProvider | `contracts/metrics.py` |
|
||||
| Dashboard widgets | WidgetProvider | `contracts/widgets.py` |
|
||||
| Page template context | Context Provider | `definition.py` |
|
||||
| Service access | Protocol + DI | `contracts/{module}.py` |
|
||||
|
||||
### Provider Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CONTRACTS LAYER │
|
||||
│ (Shared protocol definitions) │
|
||||
│ │
|
||||
│ MetricsProviderProtocol DashboardWidgetProviderProtocol │
|
||||
│ ContentServiceProtocol ServiceProtocol │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ CORE MODULE │ │ OPTIONAL MODULE 1│ │ OPTIONAL MODULE 2│
|
||||
│ │ │ │ │ │
|
||||
│ StatsAggregator │ │ OrderMetrics │ │ CatalogMetrics │
|
||||
│ WidgetAggregator │ │ (implements) │ │ (implements) │
|
||||
│ (discovers) │ │ │ │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ MODULE REGISTRY │
|
||||
│ │
|
||||
│ MODULES = { │
|
||||
│ "orders": ModuleDefinition(metrics_provider=...), │
|
||||
│ "catalog": ModuleDefinition(metrics_provider=...), │
|
||||
│ } │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing Without Dependencies
|
||||
|
||||
Because core doesn't depend on optional modules, you can test core in isolation:
|
||||
|
||||
```python
|
||||
# tests/unit/core/test_stats_aggregator.py
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from app.modules.contracts.metrics import MetricValue, MetricsProviderProtocol
|
||||
|
||||
class MockMetricsProvider:
|
||||
"""Mock that implements the protocol without needing real modules."""
|
||||
@property
|
||||
def metrics_category(self) -> str:
|
||||
return "test"
|
||||
|
||||
def get_vendor_metrics(self, db, vendor_id, context=None):
|
||||
return [MetricValue(key="test.value", value=42, label="Test", category="test")]
|
||||
|
||||
def test_stats_aggregator_with_mock_provider():
|
||||
# Test core without any optional modules
|
||||
mock_provider = MockMetricsProvider()
|
||||
...
|
||||
```
|
||||
|
||||
## Consequences of Violations
|
||||
|
||||
### If Core Imports Optional Module
|
||||
|
||||
1. **Import Error**: App fails to start if module code has syntax errors
|
||||
2. **Runtime Crash**: App crashes when disabled module is accessed
|
||||
3. **Hidden Dependency**: Module appears optional but isn't
|
||||
4. **Testing Difficulty**: Can't test core without all modules
|
||||
5. **Deployment Issues**: Can't deploy minimal configurations
|
||||
|
||||
### Detection
|
||||
|
||||
Add these checks to CI:
|
||||
|
||||
```bash
|
||||
# Check for forbidden imports
|
||||
grep -r "from app.modules.marketplace" app/modules/core/ && exit 1
|
||||
grep -r "from app.modules.analytics" app/modules/core/ && exit 1
|
||||
grep -r "from app.modules.orders" app/modules/core/ && exit 1
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Rule | Enforcement |
|
||||
|------|-------------|
|
||||
| Core → Optional = FORBIDDEN | Architecture validation, CI checks |
|
||||
| Use Protocol pattern | Code review, documentation |
|
||||
| Lazy factory functions | Required for definition.py |
|
||||
| TYPE_CHECKING imports | Required for type hints across modules |
|
||||
| Registry-based discovery | Required for all cross-module access |
|
||||
|
||||
Following these rules ensures:
|
||||
- Modules can be truly enabled/disabled per platform
|
||||
- Testing can be done in isolation
|
||||
- New modules can be added without modifying core
|
||||
- The app remains stable when modules fail
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Module System Architecture](module-system.md) - Module structure and auto-discovery
|
||||
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Numeric statistics architecture
|
||||
- [Widget Provider Pattern](widget-provider-pattern.md) - Dashboard widgets architecture
|
||||
- [Architecture Violations Status](architecture-violations-status.md) - Current violation tracking
|
||||
Reference in New Issue
Block a user