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

Creating a Provider

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

  1. Type Safety: Use the typed provider pattern for better type safety
  2. Base Classes: Create base classes for related resource types
  3. Read-Only Resources: Use ReadOnly mixin or ReadOnlyResource for external resources
  4. Idempotency: Operations should be idempotent
  5. Error Messages: Provide clear, actionable error messages
  6. Retry Logic: Implement retry logic for transient failures
  7. Rate Limiting: Respect API rate limits
  8. Connection Pooling: Reuse connections when possible
  9. Logging: Log all operations for debugging
  10. Validation: Validate resource properties before API calls