Provider Development Guide
Providers are plugins that enable DRFT to manage resources on different platforms. This guide explains how to create custom providers using the typed provider pattern.
Provider Interface
All providers must implement the Provider interface, which uses generics for type safety:
abstract class Provider<ResourceType extends Resource> {
String get name;
String get version;
Future<void> configure(Map<String, dynamic> config);
Future<ResourceState> createResource(ResourceType resource);
Future<ResourceState> readResource(ResourceType resource);
Future<ResourceState> updateResource(
ResourceState current,
ResourceType desired,
);
Future<void> deleteResource(ResourceState state);
// Automatically implemented - checks if resource is ResourceType
bool canHandle(Resource resource) {
return resource is ResourceType;
}
Future<void> initialize();
Future<void> dispose();
}
Typed Provider Pattern
The Provider interface is generic, allowing you to specify the type of resources your provider handles. This provides:
- Type Safety: Methods receive the correct resource type automatically
- Automatic Type Checking:
canHandle()is implemented automatically - No Casting: No need to cast resources in your methods
- Better IDE Support: Full autocomplete and type checking
Single Resource Type
For providers that handle a single resource type:
import 'package:drft/drft.dart';
class MyAppProvider extends Provider<MyApp> {
MyAppProvider() : super(
name: 'myapp',
version: '1.0.0',
);
@override
Future<ResourceState> createResource(MyApp resource) async {
// No casting needed - resource is already MyApp
return await _createMyApp(resource);
}
@override
Future<ResourceState> readResource(MyApp resource) async {
// No casting needed
return await _readMyApp(resource);
}
// canHandle() is automatically implemented
// It checks if resource is MyApp
}
Multiple Resource Types via Base Class
For providers that handle multiple related resource types, create a base class:
/// Base class for all MyCloud resources
abstract class MyCloudResource extends Resource<ResourceState> {
const MyCloudResource({
required super.id,
super.dependencies = const [],
});
}
/// Virtual Machine resource
class VirtualMachine extends MyCloudResource {
final String name;
final String image;
final String size;
const VirtualMachine({
required String id,
required this.name,
required this.image,
required this.size,
super.dependencies = const [],
}) : super(id: id);
}
/// Database resource
class Database extends MyCloudResource {
final String name;
final String engine;
final int storageGB;
const Database({
required String id,
required this.name,
required this.engine,
required this.storageGB,
super.dependencies = const [],
}) : super(id: id);
}
/// Provider that handles all MyCloud resources
class MyCloudProvider extends Provider<MyCloudResource> {
MyCloudProvider({
required String region,
String? apiKey,
}) : _region = region,
_apiKey = apiKey,
super(
name: 'mycloud',
version: '1.0.0',
);
final String _region;
final String? _apiKey;
MyCloudClient? _client;
@override
Future<void> configure(Map<String, dynamic> config) async {
_client = MyCloudClient(
region: config['region'] ?? _region,
apiKey: config['apiKey'] ?? _apiKey,
);
}
@override
Future<void> initialize() async {
await _client?.connect();
}
@override
Future<void> dispose() async {
await _client?.disconnect();
}
// canHandle() is automatically implemented
// It checks if resource is MyCloudResource
@override
Future<ResourceState> createResource(MyCloudResource resource) async {
// Type checking still needed for specific types
if (resource is VirtualMachine) {
return await _createVirtualMachine(resource);
} else if (resource is Database) {
return await _createDatabase(resource);
}
throw ProviderNotFoundException(
'Resource type ${resource.runtimeType} not supported',
);
}
Future<ResourceState> _createVirtualMachine(VirtualMachine vm) async {
final response = await _client!.createVM(
name: vm.name,
image: vm.image,
size: vm.size,
region: _region,
);
return VirtualMachineState(
resource: VirtualMachine(
id: vm.id,
name: vm.name,
image: vm.image,
size: vm.size,
),
vmId: response.id,
ipAddress: response.ipAddress,
);
}
Future<ResourceState> _createDatabase(Database db) async {
// Implementation
}
@override
Future<ResourceState> readResource(MyCloudResource resource) async {
if (resource is VirtualMachine) {
return await _readVirtualMachine(resource);
} else if (resource is Database) {
return await _readDatabase(resource);
}
throw ProviderNotFoundException(
'Resource type ${resource.runtimeType} not supported',
);
}
@override
Future<ResourceState> updateResource(
ResourceState current,
MyCloudResource desired,
) async {
// Implementation
}
@override
Future<void> deleteResource(ResourceState state) async {
// Implementation
}
}
Example: Firebase Provider
The Firebase provider demonstrates the typed provider pattern with multiple resource types:
/// Base class for Firebase resources
abstract class FirebaseResource extends Resource<ResourceState> {
const FirebaseResource({
required super.id,
super.dependencies = const [],
});
}
/// Firebase App resource
class FirebaseApp extends FirebaseResource {
final String projectId;
final FirebaseAppPlatform platform;
final String displayName;
// ...
}
/// Firebase Project resource (read-only)
class FirebaseProject extends FirebaseResource with ReadOnly {
final String projectId;
final String displayName;
// ...
}
/// Firebase Provider
class FirebaseProvider extends Provider<FirebaseResource> {
FirebaseProvider({Credential? credentials})
: _credentials = credentials,
super(name: 'firebase', version: '0.1.0');
// canHandle() automatically checks if resource is FirebaseResource
@override
Future<ResourceState> createResource(FirebaseResource resource) async {
if (resource is FirebaseProject) {
throw UnsupportedError('Firebase projects cannot be created');
} else if (resource is FirebaseApp) {
return await _createFirebaseApp(resource);
}
throw ProviderNotFoundException(
'Resource type ${resource.runtimeType} not supported',
);
}
// Other methods...
}
Read-Only Resources
Some resources cannot be created, updated, or deleted. Mark them with the ReadOnly mixin:
/// Resource that exists externally and cannot be modified
class ExternalProject extends MyCloudResource with ReadOnly {
final String projectId;
final String displayName;
const ExternalProject({
required String id,
required this.projectId,
required this.displayName,
super.dependencies = const [],
}) : super(id: id);
}
The framework automatically skips create, update, and delete operations for resources with the ReadOnly mixin. They can still be read and used as dependencies.
Alternatively, extend ReadOnlyResource:
class ExternalProject extends ReadOnlyResource<ExternalProjectState> {
// ...
}
Custom canHandle() Logic
You can override canHandle() for custom logic:
class ConditionalProvider extends Provider<MyResource> {
@override
bool canHandle(Resource resource) {
if (resource is! MyResource) return false;
// Additional custom logic
return resource.shouldBeHandledByThisProvider;
}
}
Error Handling
Providers should handle errors gracefully:
@override
Future<ResourceState> createResource(MyCloudResource resource) async {
try {
// ... create resource
} on MyCloudApiException catch (e) {
throw DrftException(
'Failed to create resource: ${e.message}',
originalError: e,
);
} on TimeoutException catch (e) {
throw DrftException(
'Timeout creating resource',
originalError: e,
);
}
}
Testing Providers
Use mock clients for testing:
class MockMyCloudClient implements MyCloudClient {
final Map<String, dynamic> _vms = {};
@override
Future<CreateVMResponse> createVM({
required String name,
required String image,
required String size,
required String region,
}) async {
final id = 'vm-${DateTime.now().millisecondsSinceEpoch}';
_vms[id] = {
'id': id,
'name': name,
'image': image,
'size': size,
'ip': '10.0.0.1',
};
return CreateVMResponse(id: id, ipAddress: '10.0.0.1');
}
// Other methods...
}
Best Practices
- Type Safety: Use the typed provider pattern for better type safety
- Base Classes: Create base classes for related resource types
- Read-Only Resources: Use
ReadOnlymixin orReadOnlyResourcefor external resources - Idempotency: Operations should be idempotent
- Error Messages: Provide clear, actionable error messages
- Retry Logic: Implement retry logic for transient failures
- Rate Limiting: Respect API rate limits
- Connection Pooling: Reuse connections when possible
- Logging: Log all operations for debugging
- Validation: Validate resource properties before API calls