diff --git a/.github/workflows/parent-pipeline.yml b/.github/workflows/parent-pipeline.yml index 27553ee..8dcd6bb 100644 --- a/.github/workflows/parent-pipeline.yml +++ b/.github/workflows/parent-pipeline.yml @@ -48,6 +48,7 @@ jobs: # branch_name: ${{ needs.determine-workflow.outputs.branch_name }} # event_name: ${{ needs.determine-workflow.outputs.event_name }} + contributor-workflow: name: Trigger Contributor Workflow needs: determine-workflow diff --git a/.github/workflows/validation-workflow.yml b/.github/workflows/validation-workflow.yml index dc18b29..9e00e7b 100644 --- a/.github/workflows/validation-workflow.yml +++ b/.github/workflows/validation-workflow.yml @@ -100,7 +100,7 @@ jobs: echo "$PUBSPEC_FILES" # Define the minimum required Dart SDK version - MINIMUM_VERSION="3.7.2" + MINIMUM_VERSION="3.9.2" ALL_VALID="true" # Iterate over each pubspec.yaml file diff --git a/CHANGELOG.md b/CHANGELOG.md index c0cd273..c4c63a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,4 +110,4 @@ ### ✨ New Features -* release v.0.0.1-pre+1 ([#25](https://github.com/open-feature/dart-server-sdk/issues/25)) ([83b6438](https://github.com/open-feature/dart-server-sdk/commit/83b643864d7d6e100cfc337e2abf05eadd8f241e)) +* release v.0.0.1-pre+1 ([#25](https://github.com/open-feature/dart-server-sdk/issues/25)) ([83b6438](https://github.com/open-feature/dart-server-sdk/commit/83b643864d7d6e100cfc337e2abf05eadd8f241e)) \ No newline at end of file diff --git a/README.md b/README.md index 5482b74..0718639 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ ### Requirements -Dart language version: [3.7.2](https://dart.dev/get-dart/archive) +Dart language version: [3.9.2](https://dart.dev/get-dart/archive) > [!NOTE] > The OpenFeature DartServer SDK only supports the latest currently maintained Dart language versions. diff --git a/lib/client.dart b/lib/client.dart index a7316a5..5ad7398 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -112,14 +112,7 @@ class FeatureClient { (_metrics.errorCounts[result.errorCode!.name] ?? 0) + 1; } - // Handle errors from the provider - if (result.errorCode != null) { - _logger.warning( - 'Flag evaluation error for $flagKey: ${result.errorMessage}', - ); - _metrics.errorCounts[result.errorCode!.name] = - (_metrics.errorCounts[result.errorCode!.name] ?? 0) + 1; - } + _metrics.responseTimes.add(DateTime.now().difference(startTime)); return result.value; @@ -157,6 +150,7 @@ class FeatureClient { /// Evaluate string flag + Future getStringFlag( String flagKey, { EvaluationContext? context, @@ -170,6 +164,7 @@ class FeatureClient { /// Evaluate integer flag + Future getIntegerFlag( String flagKey, { EvaluationContext? context, @@ -182,6 +177,7 @@ class FeatureClient { ); + /// Evaluate double flag Future getDoubleFlag( String flagKey, { diff --git a/lib/feature_provider.dart b/lib/feature_provider.dart index a2ade61..a8af2a4 100644 --- a/lib/feature_provider.dart +++ b/lib/feature_provider.dart @@ -206,6 +206,11 @@ abstract class CachedFeatureProvider implements FeatureProvider { ProviderMetadata get metadata => _metadata; @override + + ProviderMetadata get metadata => _metadata; + + @override + String get name => _metadata.name; /// Set provider state @@ -474,6 +479,7 @@ class InMemoryProvider extends CachedFeatureProvider { setState(ProviderState.CONNECTING); + try { // Simulate initialization work await Future.delayed(Duration(milliseconds: 10)); @@ -517,7 +523,7 @@ class InMemoryProvider extends CachedFeatureProvider { void _checkState() { if (state != ProviderState.READY) { throw ProviderException( - +t 'Provider not in READY state: ${state.name}', code: ErrorCode.PROVIDER_NOT_READY, details: {'currentState': state.name}, diff --git a/lib/open_feature_api.dart b/lib/open_feature_api.dart index fdd55b2..2dda4ab 100644 --- a/lib/open_feature_api.dart +++ b/lib/open_feature_api.dart @@ -140,6 +140,207 @@ class _ImmediateReadyProvider implements FeatureProvider { } } +/// Default provider that's immediately ready - completely independent +class _ImmediateReadyProvider implements FeatureProvider { + @override + String get name => 'InMemoryProvider'; + + @override + ProviderState get state => ProviderState.READY; + + @override + ProviderConfig get config => const ProviderConfig(); + + @override + ProviderMetadata get metadata => + const ProviderMetadata(name: 'InMemoryProvider'); + + @override + Future initialize([Map? config]) async {} + + @override + Future connect() async {} + + @override + Future shutdown() async {} + + @override + Future> getBooleanFlag( + String flagKey, + bool defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getStringFlag( + String flagKey, + String defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getIntegerFlag( + String flagKey, + int defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getDoubleFlag( + String flagKey, + double defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future>> getObjectFlag( + String flagKey, + Map defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + +} + +/// Default provider that's immediately ready - completely independent +class _ImmediateReadyProvider implements FeatureProvider { + @override + String get name => 'InMemoryProvider'; + + @override + ProviderState get state => ProviderState.READY; + + @override + ProviderConfig get config => const ProviderConfig(); + + @override + ProviderMetadata get metadata => + const ProviderMetadata(name: 'InMemoryProvider'); + + @override + Future initialize([Map? config]) async {} + + @override + Future connect() async {} + + @override + Future shutdown() async {} + + @override + Future> getBooleanFlag( + String flagKey, + bool defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getStringFlag( + String flagKey, + String defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getIntegerFlag( + String flagKey, + int defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future> getDoubleFlag( + String flagKey, + double defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } + + @override + Future>> getObjectFlag( + String flagKey, + Map defaultValue, { + Map? context, + }) async { + return FlagEvaluationResult.error( + flagKey, + defaultValue, + ErrorCode.FLAG_NOT_FOUND, + 'Flag not found', + evaluatorId: name, + ); + } +} + class OpenFeatureAPI { static final Logger _logger = Logger('OpenFeatureAPI'); static OpenFeatureAPI? _instance; @@ -217,7 +418,6 @@ class OpenFeatureAPI { Future setProvider(FeatureProvider provider) async { _logger.info('Setting provider: ${provider.name}'); - try { // Only initialize if provider is NOT_READY if (provider.state == ProviderState.NOT_READY) { @@ -297,7 +497,6 @@ class OpenFeatureAPI { _runBeforeEvaluationHooks(flagKey, context); - final result = await _provider.getBooleanFlag( flagKey, false, diff --git a/pubspec.yaml b/pubspec.yaml index 63258c4..09020cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,21 +16,21 @@ version: 0.0.10 homepage: https://github.com/open-feature/dart-server-sdk environment: - sdk: ^3.8.1 + sdk: ^3.9.2 dependencies: -# Core dependencies - collection: ^1.19.1 # For efficient list and map operations - logging: ^1.3.0 # For structured logging - meta: ^1.16.0 # For annotations like @required + # Core dependencies + collection: ^1.19.1 # For efficient list and map operations + logging: ^1.3.0 # For structured logging + meta: ^1.17.0 # For annotations like @required dev_dependencies: -# For testing - coverage: ^1.14.0 # For Code Coverage Reports - git_hooks: ^1.0.2 # Provides hooks for locla ddevelopment - lints: ^6.0.0 # Recommended lints for maintaining code quality - mockito: ^5.4.6 # For mocking objects in unit tests - test: ^1.26.2 # Dart's core testing framework + # For testing + coverage: ^1.15.0 # For Code Coverage Reports + git_hooks: ^1.0.2 # Provides hooks for locla ddevelopment + lints: ^6.0.0 # Recommended lints for maintaining code quality + mockito: ^5.5.0 # For mocking objects in unit tests + test: ^1.26.3 # Dart's core testing framework # Optional Local Hooks for development. @@ -38,4 +38,4 @@ git_hooks: hooks: pre-commit: ../local_dev_tools/validate_commit.dart commit-msg: ../local_dev_tools/validate_commit_msg.dart - pre-push: ../local_dev_tools/validate_branch.dart \ No newline at end of file + pre-push: ../local_dev_tools/validate_branch.dart diff --git a/test/client_test.dart b/test/client_test.dart index 62d3c53..8e4de6a 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -183,13 +183,12 @@ void main() { }); test('converts to JSON correctly', () { - final metrics = - ClientMetrics() - ..flagEvaluations = 10 + final metrics = ClientMetrics() + ..flagEvaluations = 10 + ..responseTimes.add(Duration(milliseconds: 100)) + ..errorCounts['TestError'] = 1; - ..responseTimes.add(Duration(milliseconds: 100)) - ..errorCounts['TestError'] = 1; final json = metrics.toJson(); @@ -249,7 +248,6 @@ void main() { }); - test('evaluates string flags', () async { final result = await client.getStringFlag('string-flag'); expect(result, equals('hello')); diff --git a/test/feature_provider_test.dart b/test/feature_provider_test.dart index 0b86f31..16d28e5 100644 --- a/test/feature_provider_test.dart +++ b/test/feature_provider_test.dart @@ -234,6 +234,70 @@ void main() { }); }); + + + group('caching behavior', () { + test('caches successful evaluations', () async { + // First evaluation + final result1 = await provider.getBooleanFlag('bool-flag', false); + expect(result1.reason, equals('STATIC')); + + // Second evaluation should be cached + final result2 = await provider.getBooleanFlag('bool-flag', false); + expect(result2.reason, equals('CACHED')); + expect(result2.value, equals(result1.value)); + }); + + test('does not cache error results', () async { + // First evaluation (error) + final result1 = await provider.getBooleanFlag('missing-flag', false); + expect(result1.errorCode, equals(ErrorCode.FLAG_NOT_FOUND)); + + // Second evaluation should still be an error, not cached + final result2 = await provider.getBooleanFlag('missing-flag', false); + expect(result2.errorCode, equals(ErrorCode.FLAG_NOT_FOUND)); + expect(result2.reason, equals('ERROR')); + }); + + test('clears cache on shutdown', () async { + // Cache a value + await provider.getBooleanFlag('bool-flag', false); + + // Shutdown clears cache + await provider.shutdown(); + + // Reinitialize + provider = InMemoryProvider(testFlags); + await provider.initialize(); + + // Should not be cached + final result = await provider.getBooleanFlag('bool-flag', false); + expect(result.reason, equals('STATIC')); + }); + + test('respects cache configuration', () async { + final noCacheProvider = InMemoryProvider( + testFlags, + ProviderConfig(enableCache: false), + ); + await noCacheProvider.initialize(); + + // First evaluation + final result1 = await noCacheProvider.getBooleanFlag( + 'bool-flag', + false, + ); + expect(result1.reason, equals('STATIC')); + + // Second evaluation should not be cached + final result2 = await noCacheProvider.getBooleanFlag( + 'bool-flag', + false, + ); + expect(result2.reason, equals('STATIC')); + }); + }); + }); }); diff --git a/test/open_feature_api_test.dart b/test/open_feature_api_test.dart index 1bc54a6..b762bbf 100644 --- a/test/open_feature_api_test.dart +++ b/test/open_feature_api_test.dart @@ -98,7 +98,9 @@ class IsolatedDefaultProvider implements FeatureProvider { String get name => 'InMemoryProvider'; @override - ProviderState get state => ProviderState.READY; + + ProviderState get state => _state; + @override ProviderConfig get config => ProviderConfig(); @@ -107,13 +109,24 @@ class IsolatedDefaultProvider implements FeatureProvider { ProviderMetadata get metadata => ProviderMetadata(name: 'InMemoryProvider'); @override - Future initialize([Map? config]) async {} + + ProviderMetadata get metadata => ProviderMetadata(name: 'InMemoryProvider'); @override - Future connect() async {} + Future initialize([Map? config]) async { + _state = ProviderState.READY; + } @override - Future shutdown() async {} + Future connect() async { + _state = ProviderState.READY; + } + + @override + Future shutdown() async { + _state = ProviderState.SHUTDOWN; + } + @override Future> getBooleanFlag( @@ -369,6 +382,19 @@ void main() { expect(true, isTrue); }); + + test('emits error events for flag evaluation issues', () async { + final provider = IsolatedTestProvider({}, ProviderState.NOT_READY); + await api.setProvider(provider); + + api.bindClientToProvider('test-client', 'TestProvider'); + await api.evaluateBooleanFlag('missing-flag', 'test-client'); + + // No events in this simplified version + expect(true, isTrue); + }); + + test('handles evaluation errors gracefully', () async { final provider = IsolatedTestProvider({'string-flag': 'not-boolean'}); @@ -401,6 +427,18 @@ void main() { await api.setProvider(provider); expect(api.provider.metadata.name, equals('TestProvider')); }); + + test('initializes default provider', () { + expect(api.provider, isNotNull); + expect(api.provider.name, equals('InMemoryProvider')); + expect(api.provider.state, equals(ProviderState.READY)); + }); + + test('provider metadata is accessible', () async { + final provider = IsolatedTestProvider({'test': true}); + await api.setProvider(provider); + expect(api.provider.metadata.name, equals('TestProvider')); + }); }); }