Framework Registries Reference¶
Overview¶
The Substation framework uses the Registry pattern to enable a modular, decentralized architecture. Registries act as central coordination points where modules register their capabilities, allowing the TUI to discover and utilize functionality dynamically without hardcoded dependencies.
Purpose of the Registry Pattern¶
The registry pattern provides: - Decoupled Architecture: Modules can be added or removed without modifying core code - Dynamic Discovery: The TUI discovers available functionality at runtime - Centralized Access: Single point of access for each type of functionality - Module Independence: Modules don't need to know about each other
Registry Lifecycle¶
- Initialization: Registries are created as singletons at application startup
- Registration: Modules register their components during module loading
- Discovery: The TUI queries registries to find available functionality
- Execution: The TUI invokes functionality through registry lookups
- Cleanup: Registries can be cleared for testing or module unloading
Architecture¶
flowchart TB
subgraph Modules
M1[ServersModule]
M2[NetworksModule]
M3[VolumesModule]
end
subgraph Registries
MR[ModuleRegistry]
VR[ViewRegistry]
DR[DataProviderRegistry]
AR[ActionRegistry]
BR[BatchOperationRegistry]
FR[FormRegistry]
RR[ResourceRegistry]
end
subgraph TUI
UI[User Interface]
NAV[Navigation]
DATA[Data Layer]
ACT[Actions]
end
M1 --> MR
M2 --> MR
M3 --> MR
MR --> VR
MR --> DR
MR --> AR
MR --> BR
MR --> FR
VR --> UI
DR --> DATA
AR --> ACT
BR --> ACT
FR --> UI
RR --> NAV
ModuleRegistry¶
Purpose¶
Central registry for managing all loaded OpenStack modules. Controls module lifecycle, dependency resolution, and component integration.
Key Features¶
- Module loading in dependency order
- Configuration management per module
- Hot-reload support for development
- Health check coordination
Registration Process¶
// Module registers itself with dependencies
let module = ServersModule(tui: tui)
try await ModuleRegistry.shared.register(module)
Module Integration Flow¶
- Validate module dependencies
- Load module configuration
- Call module.configure()
- Register module components with other registries
- Track module for lifecycle management
Usage Examples¶
// Get navigation provider for current view
if let provider = ModuleRegistry.shared.navigationProvider(for: .servers) {
// Use provider for navigation
}
// Health check all modules
let healthStatus = await ModuleRegistry.shared.healthCheckAll()
// Hot reload a module
let result = await ModuleRegistry.shared.reloadModule("servers")
ViewRegistry¶
Purpose¶
Manages all view handlers and metadata, enabling dynamic view registration and discovery.
View Metadata Structure¶
- identifier: Unique view ID
- title: Display title
- category: View category for organization
- parentViewId: Navigation hierarchy
- supportsMultiSelect: Bulk selection capability
- isDetailView: Whether this is a detail view
Registration Process¶
// Register view with metadata
let metadata = ViewMetadata(
identifier: ServersViewIdentifier(),
title: "Servers",
category: .compute,
renderHandler: { screen, context in
// Render implementation
}
)
ViewRegistry.shared.register(metadata: metadata)
Usage Examples¶
// Get metadata for a view
if let metadata = ViewRegistry.shared.metadata(forId: "servers") {
// Use metadata for rendering
}
// Get views by category
let computeViews = ViewRegistry.shared.metadata(in: .compute)
// Check if view supports multi-select
let supportsMulti = ViewRegistry.shared.supportsMultiSelect(id: "servers")
DataProviderRegistry¶
Purpose¶
Coordinates data fetching across modules with caching, pagination, and phased loading support.
Provider Protocol¶
Each provider implements: - resourceType: Unique identifier for the data type - fetchData: Async data fetching with priority support - clearCache: Cache invalidation - supportsPagination: Whether paginated fetching is supported - needsRefresh: Staleness checking
Phased Data Loading¶
The registry supports three phases for optimal performance: 1. Critical (Phase 1): Essential UI data (servers, networks, flavors) 2. Secondary (Phase 2): Important but not critical (volumes, subnets, keypairs) 3. Expensive (Phase 3): Background operations (ports, routers, quotas)
Registration Process¶
// Module registers its data provider
let provider = ServersDataProvider()
DataProviderRegistry.shared.register(provider, from: "servers")
Usage Examples¶
// Fetch data for a resource type
if let result = await DataProviderRegistry.shared.fetchData(
for: "servers",
priority: .critical,
forceRefresh: false
) {
// Process data
}
// Fetch multiple resources concurrently
let results = await DataProviderRegistry.shared.fetchMultiple(
resourceTypes: ["servers", "networks", "volumes"],
priority: .secondary
)
// Perform phased refresh
await DataProviderRegistry.shared.performPhasedRefresh()
ActionRegistry¶
Purpose¶
Manages keyboard shortcuts and actions across all views, enabling modules to register context-aware actions.
Action Structure¶
- identifier: Unique action ID
- title: Display name
- keybinding: Optional keyboard shortcut
- viewModes: Views where action is available
- handler: Async execution handler
- category: Action category for organization
- requiresConfirmation: Safety flag
Action Categories¶
- General: Common operations
- Lifecycle: Start, stop, restart
- Network: Network-related actions
- Storage: Volume and snapshot operations
- Security: Permission and access control
- Management: Administrative tasks
Registration Process¶
// Register an action
let action = ModuleActionRegistration(
identifier: "server.start",
title: "Start Server",
keybinding: "s",
viewModes: [.servers],
handler: { screen in
// Start server implementation
},
category: .lifecycle
)
ActionRegistry.shared.register(action)
Usage Examples¶
// Execute action by key in current view
let executed = await ActionRegistry.shared.execute(
key: "s",
in: .servers,
screen: screen
)
// Get all actions for a view
let actions = ActionRegistry.shared.actions(for: .servers)
// Get help text for view actions
let helpText = ActionRegistry.shared.helpText(for: .servers)
BatchOperationRegistry¶
Purpose¶
Coordinates bulk operations across modules with dependency resolution and parallel execution.
Provider Protocol¶
Modules implementing BatchOperationProvider must provide:
- supportedBatchOperationTypes: Set of operation types (delete, start, stop)
- executeBatchDelete: Batch deletion implementation
- validateBatchOperation: Pre-flight validation
- deletionPriority: Order for dependency-aware deletion
Deletion Priority System¶
Resources are deleted in priority order to respect dependencies: - Priority 1-3: Independent resources (floating IPs, security groups) - Priority 4-6: Mid-level resources (ports, servers) - Priority 7-9: Base resources (networks, volumes)
Registration Process¶
// Module registers as batch operation provider
extension ServersModule: BatchOperationProvider {
var deletionPriority: Int { 4 }
func executeBatchDelete(
resourceIDs: [String],
client: OSClient
) async -> [IndividualOperationResult] {
// Implementation
}
}
BatchOperationRegistry.shared.register(serversModule)
Usage Examples¶
// Check if module supports batch operations
if BatchOperationRegistry.shared.supportsBatchOperations("servers") {
let provider = BatchOperationRegistry.shared.provider(for: "servers")
// Use provider for batch operations
}
// Get providers in deletion order
let providers = BatchOperationRegistry.shared.allProvidersSortedByDeletionPriority()
FormRegistry¶
Purpose¶
Manages form handlers for resource creation and editing across modules.
Form Handler Structure¶
- viewMode: View where form is used
- formType: Create, edit, or custom
- fields: Form field definitions
- validator: Field validation logic
- submitHandler: Form submission processing
Registration Process¶
// Register a form handler
let formHandler = ModuleFormHandlerRegistration(
viewMode: .servers,
formType: .create,
handler: { context in
// Form handling implementation
}
)
FormRegistry.shared.register(formHandler)
Usage Examples¶
// Get form handler for a view
if let handler = FormRegistry.shared.handler(for: .servers) {
// Use handler for form display
}
DataRefreshRegistry¶
Purpose¶
Coordinates data refresh operations across modules for consistent cache invalidation.
Refresh Handler Structure¶
- identifier: Unique refresh handler ID
- refreshHandler: Async refresh implementation
- priority: Refresh priority
- dependencies: Other handlers that must refresh first
Registration Process¶
// Register refresh handler
let refreshHandler = ModuleDataRefreshRegistration(
identifier: "servers.refresh",
refreshHandler: {
// Refresh implementation
}
)
DataRefreshRegistry.shared.register(refreshHandler)
Usage Examples¶
// Refresh all registered handlers
await DataRefreshRegistry.shared.refreshAll()
// Get specific handler
if let handler = DataRefreshRegistry.shared.handler(for: "servers.refresh") {
try await handler.refreshHandler()
}
ResourceRegistry¶
Purpose¶
Maps command-line navigation commands to view modes and actions, enabling natural language navigation.
Command Categories¶
- Navigation Commands: Navigate to specific views (servers, networks, volumes)
- Action Commands: Perform operations (create, delete, refresh)
- Configuration Commands: System settings
- Discovery Commands: Help and tutorial commands
Command Resolution¶
// Resolve navigation command
if let viewMode = ResourceRegistry.shared.resolve("servers") {
// Navigate to servers view
}
// Resolve action command
if let actionType = ResourceRegistry.shared.resolveAction("delete") {
// Execute delete action
}
// Fuzzy match for typo correction
if let suggestion = ResourceRegistry.shared.fuzzyMatch("srvrs") {
// Suggest "servers"
}
Usage Examples¶
// Get command suggestions
let suggestions = ResourceRegistry.shared.suggestions(for: "ser", limit: 5)
// Get ranked matches for search
let matches = ResourceRegistry.shared.rankedMatches(for: "net", limit: 10)
// Get help text for command
if let help = ResourceRegistry.shared.helpText(for: "servers") {
// Display help
}
Module Registration Pattern¶
Standard Registration Flow¶
When a module is loaded, it follows this registration sequence:
sequenceDiagram
participant M as Module
participant MR as ModuleRegistry
participant VR as ViewRegistry
participant DR as DataProviderRegistry
participant AR as ActionRegistry
participant BR as BatchOperationRegistry
M->>MR: register(module)
MR->>M: configure()
M->>VR: registerViews()
M->>DR: registerDataProviders()
M->>AR: registerActions()
M->>BR: register(if BatchOperationProvider)
MR-->>M: Module ready
Example Module Implementation¶
class ServersModule: OpenStackModule {
let identifier = "servers"
let displayName = "Compute Servers"
let dependencies = ["networks", "images", "flavors"]
func configure() async throws {
// Module-specific configuration
}
func registerViews() -> [ModuleViewRegistration] {
return [
ModuleViewRegistration(
viewMode: .servers,
title: "Servers",
category: .compute,
renderHandler: renderServerList
)
]
}
func registerDataProviders() -> [any DataProvider] {
return [ServersDataProvider()]
}
func registerActions() -> [ModuleActionRegistration] {
return [
ModuleActionRegistration(
identifier: "server.start",
title: "Start Server",
keybinding: "s",
viewModes: [.servers],
handler: startServer
)
]
}
}
Best Practices¶
When to Use Each Registry¶
- ViewRegistry: When adding new views or modifying view behavior
- DataProviderRegistry: When adding new data sources or modifying fetch logic
- ActionRegistry: When adding keyboard shortcuts or context actions
- BatchOperationRegistry: When implementing bulk operations
- FormRegistry: When adding create/edit forms
- DataRefreshRegistry: When coordinating cache invalidation
- ResourceRegistry: When adding navigation commands
Performance Considerations¶
- Lazy Loading: Registries use lazy initialization where appropriate
- Concurrent Access: All registries are thread-safe using @MainActor
- Caching: Command lookups are cached in ResourceRegistry
- Phased Loading: DataProviderRegistry supports prioritized data fetching
- Batch Limits: BatchOperationManager respects system capabilities
Error Handling¶
- Registration Failures: Log errors but don't crash
- Missing Providers: Return nil or empty results gracefully
- Circular Dependencies: Detected during module registration
- Invalid Configuration: Validated before module activation
- Runtime Failures: Isolated to individual operations
Testing Strategies¶
- Clear Methods: All registries provide
clear()for test isolation - Mock Providers: Test with mock implementations
- Dependency Injection: Registries support injection for testing
- Health Checks: Built-in diagnostics for validation
- Registration Status: Methods to verify registration state
Registry Interactions¶
Cross-Registry Dependencies¶
graph LR
MR[ModuleRegistry] --> VR[ViewRegistry]
MR --> DR[DataProviderRegistry]
MR --> AR[ActionRegistry]
MR --> BR[BatchOperationRegistry]
VR --> RR[ResourceRegistry]
AR --> VR
BR --> DR
style MR fill:#f9f,stroke:#333,stroke-width:2px
Data Flow Example¶
- User types
:serverscommand - ResourceRegistry resolves to ViewMode.servers
- ViewRegistry provides view metadata
- DataProviderRegistry fetches server data
- ActionRegistry provides available actions
- TUI renders view with data and actions
Advanced Topics¶
Hot Reload Support¶
The ModuleRegistry supports hot reloading for development:
// Reload a specific module
let result = await ModuleRegistry.shared.reloadModule("servers")
// Reload all modules
let results = await ModuleRegistry.shared.reloadAll()
// Check reload capability
if ModuleRegistry.shared.canReload("servers") {
// Module supports hot reload
}
Registry Diagnostics¶
// ViewRegistry diagnostics
let unregistered = ViewRegistry.shared.unregisteredViews()
ViewRegistry.shared.logRegistrationStatus()
// DataProviderRegistry statistics
let stats = DataProviderRegistry.shared.getProviderStatistics()
// ModuleRegistry health check
let health = await ModuleRegistry.shared.healthCheckAll()
Custom Registry Implementation¶
To create a custom registry:
- Define as @MainActor final class with singleton
- Implement register/unregister methods
- Provide lookup methods
- Add clear() for testing
- Consider thread safety and performance
Example template:
@MainActor
final class CustomRegistry {
static let shared = CustomRegistry()
private var items: [String: CustomType] = [:]
private init() {}
func register(_ item: CustomType, id: String) {
items[id] = item
}
func get(_ id: String) -> CustomType? {
return items[id]
}
func clear() {
items.removeAll()
}
}
Summary¶
The registry pattern is fundamental to Substation's modular architecture. By centralizing component registration while decentralizing implementation, the framework achieves:
- Flexibility: Add/remove modules without core changes
- Maintainability: Clear separation of concerns
- Testability: Isolated components with clear interfaces
- Performance: Optimized lookup and caching strategies
- Extensibility: Easy to add new registries or extend existing ones
Each registry serves a specific purpose in the overall architecture, working together to provide a cohesive, modular system for managing OpenStack resources through a terminal interface.