diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index 502f8d3..cde465d 100644 --- a/cherrypick/lib/src/binding.dart +++ b/cherrypick/lib/src/binding.dart @@ -11,6 +11,8 @@ // limitations under the License. // +import 'dart:async'; + import 'package:cherrypick/src/binding_resolver.dart'; /// {@template binding_docs} @@ -69,6 +71,8 @@ class Binding { CherryPickObserver? observer; + bool get _isSilentObserver => observer is SilentCherryPickObserver; + // Deferred logging flags bool _createdLogged = false; bool _namedLogged = false; @@ -191,10 +195,9 @@ class Binding { /// } /// ``` /// This restriction only applies to [toInstance] bindings. - // ignore: deprecated_member_use_from_same_package /// With [toProvide]/[toProvideAsync] you may freely use `scope.resolve()` in the builder or provider function. - Binding toInstance(Instance value) { - _resolver = InstanceResolver(value); + Binding toInstance(FutureOr value) { + _resolver = InstanceResolver.create(value); return this; } @@ -205,8 +208,8 @@ class Binding { /// bind().toProvide(() => ApiService()); /// bind().toProvide(() async => await openDb()); /// ``` - Binding toProvide(Provider value) { - _resolver = ProviderResolver((_) => value.call(), withParams: false); + Binding toProvide(FutureOr Function() value) { + _resolver = ProviderResolver.create(value); return this; } @@ -216,24 +219,32 @@ class Binding { /// ```dart /// bind().toProvideWithParams((params) => User(name: params["name"])); /// ``` - Binding toProvideWithParams(ProviderWithParams value) { - _resolver = ProviderResolver(value, withParams: true); + Binding toProvideWithParams(FutureOr Function(dynamic) value) { + _resolver = ProviderResolver.createWithParams(value); return this; } @Deprecated('Use toInstance instead of toInstanceAsync') - Binding toInstanceAsync(Instance value) { + Binding toInstanceAsync(FutureOr value) { return toInstance(value); } - @Deprecated('Use toProvide instead of toProvideAsync') - Binding toProvideAsync(Provider value) { - return toProvide(value); + /// Asynchronous variant of [toProvide] for providers that return [Future]. + /// + /// Prefer this over [toProvide] when the provider is async, so the resolver + /// is type-safe and avoids runtime detection overhead. + Binding toProvideAsync(AsyncProvider value) { + _resolver = ProviderResolver.async(value); + return this; } - @Deprecated('Use toProvideWithParams instead of toProvideAsyncWithParams') - Binding toProvideAsyncWithParams(ProviderWithParams value) { - return toProvideWithParams(value); + /// Asynchronous variant of [toProvideWithParams] for providers that return [Future]. + /// + /// Prefer this over [toProvideWithParams] when the provider is async, so the resolver + /// is type-safe and avoids runtime detection overhead. + Binding toProvideAsyncWithParams(AsyncProviderWithParams value) { + _resolver = ProviderResolver.asyncWithParams(value); + return this; } /// Marks this binding as singleton (will only create and cache one instance per scope). @@ -281,6 +292,10 @@ class Binding { /// ``` T? resolveSync([dynamic params]) { final res = resolver?.resolveSync(params); + if (_isSilentObserver) { + return res; + } + if (res != null) { observer?.onDiagnostic( 'Binding resolved instance: ${T.toString()}', @@ -313,6 +328,10 @@ class Binding { /// ``` Future? resolveAsync([dynamic params]) { final future = resolver?.resolveAsync(params); + if (_isSilentObserver) { + return future; + } + if (future != null) { future .then((res) => observer?.onDiagnostic( diff --git a/cherrypick/lib/src/binding_resolver.dart b/cherrypick/lib/src/binding_resolver.dart index 0689933..7fadab2 100644 --- a/cherrypick/lib/src/binding_resolver.dart +++ b/cherrypick/lib/src/binding_resolver.dart @@ -13,86 +13,37 @@ import 'dart:async'; -/// Represents a direct instance or an async instance ([T] or [Future]). -/// Used for both direct and async bindings. -/// -/// Example: -/// ```dart -/// Instance sync = "hello"; -/// Instance async = Future.value(MyApi()); -/// ``` -typedef Instance = FutureOr; +/// Synchronous factory: `T Function()`. +typedef Provider = T Function(); -/// Provider function type for synchronous or asynchronous, parameterless creation of [T]. -/// Can return [T] or [Future]. -/// -/// Example: -/// ```dart -/// Provider provider = () => MyService(); -/// Provider asyncProvider = () async => await Api.connect(); -/// ``` -typedef Provider = FutureOr Function(); +/// Parameterized synchronous factory: `T Function(dynamic)`. +typedef ProviderWithParams = T Function(dynamic); -/// Provider function type that accepts a dynamic parameter, for factory/parametrized injection. -/// Returns [T] or [Future]. -/// -/// Example: -/// ```dart -/// ProviderWithParams provider = (params) => User(params["name"]); -/// ``` -typedef ProviderWithParams = FutureOr Function(dynamic); +/// Asynchronous factory: `Future Function()`. +typedef AsyncProvider = Future Function(); -/// Abstract interface for dependency resolvers used by [Binding]. -/// Defines how to resolve instances of type [T]. -/// -/// You usually don't use this directly; it's used internally for advanced/low-level DI. +/// Parameterized asynchronous factory: `Future Function(dynamic)`. +typedef AsyncProviderWithParams = Future Function(dynamic); + +/// Internal interface for resolvers managed by [Binding]. abstract class BindingResolver { - /// Synchronously resolves the dependency, optionally taking parameters (for factory cases). - /// Throws if implementation does not support sync resolution. T? resolveSync([dynamic params]); - - /// Asynchronously resolves the dependency, optionally taking parameters (for factory cases). - /// If instance is already a [Future], returns it directly. Future? resolveAsync([dynamic params]); - - /// Marks this resolver as singleton: instance(s) will be cached and reused inside the scope. void toSingleton(); - - /// Returns true if this resolver is marked as singleton. bool get isSingleton; } -/// Concrete resolver for direct instance ([T] or [Future]). No provider is called. -/// -/// Used for [Binding.toInstance]. -/// Supports both sync and async resolution; sync will throw if underlying instance is [Future]. -/// Examples: -/// ```dart -/// var resolver = InstanceResolver("hello"); -/// resolver.resolveSync(); // == "hello" -/// var asyncResolver = InstanceResolver(Future.value(7)); -/// asyncResolver.resolveAsync(); // Future -/// ``` -class InstanceResolver implements BindingResolver { - final Instance _instance; +/// Resolver for a pre-built synchronous instance. +class SyncInstanceResolver implements BindingResolver { + final T _instance; - /// Wraps the given instance (sync or async) in a resolver. - InstanceResolver(this._instance); + SyncInstanceResolver(this._instance); @override - T resolveSync([_]) { - if (_instance is T) return _instance; - throw StateError( - 'Instance $_instance is Future; ' - 'use resolveAsync() instead', - ); - } + T resolveSync([_]) => _instance; @override - Future resolveAsync([_]) { - if (_instance is Future) return _instance; - return Future.value(_instance); - } + Future resolveAsync([_]) => Future.value(_instance); @override void toSingleton() {} @@ -101,63 +52,31 @@ class InstanceResolver implements BindingResolver { bool get isSingleton => true; } -/// Resolver for provider functions (sync/async/factory), with optional singleton caching. -/// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton]. -/// -/// Examples: -/// ```dart -/// // No param, sync: -/// var r = ProviderResolver((_) => 5, withParams: false); -/// r.resolveSync(); // == 5 -/// // With param: -/// var rp = ProviderResolver((p) => p * 2, withParams: true); -/// rp.resolveSync(2); // == 4 -/// // Singleton: -/// r.toSingleton(); -/// // Async: -/// var ra = ProviderResolver((_) async => await Future.value(10), withParams: false); -/// await ra.resolveAsync(); // == 10 -/// ``` -class ProviderResolver implements BindingResolver { - final ProviderWithParams _provider; - final bool _withParams; +/// Resolver for a pre-built async instance ([Future]). +class AsyncInstanceResolver implements BindingResolver { + final Future _instance; - FutureOr? _cache; + AsyncInstanceResolver(this._instance); + + @override + T resolveSync([_]) { + throw StateError('Instance is a Future; use resolveAsync() instead'); + } + + @override + Future resolveAsync([_]) => _instance; + + @override + void toSingleton() {} + + @override + bool get isSingleton => true; +} + +/// Base class for provider-based resolvers with singleton flag support. +abstract class _BaseProviderResolver implements BindingResolver { bool _singleton = false; - /// Creates a resolver from [provider], optionally accepting dynamic params. - ProviderResolver( - ProviderWithParams provider, { - required bool withParams, - }) : _provider = provider, - _withParams = withParams; - - @override - T resolveSync([dynamic params]) { - _checkParams(params); - final result = _cache ?? _provider(params); - if (result is T) { - if (_singleton) { - _cache ??= result; - } - return result; - } - throw StateError( - 'Provider [$_provider] return Future<$T>. Use resolveAsync() instead.', - ); - } - - @override - Future resolveAsync([dynamic params]) { - _checkParams(params); - final result = _cache ?? _provider(params); - final target = result is Future ? result : Future.value(result); - if (_singleton) { - _cache ??= target; - } - return target; - } - @override void toSingleton() { _singleton = true; @@ -165,13 +84,281 @@ class ProviderResolver implements BindingResolver { @override bool get isSingleton => _singleton; +} - /// Throws if params required but not supplied. - void _checkParams(dynamic params) { - if (_withParams && params == null) { - throw StateError( - '[$T] Params is null. Maybe you forgot to pass it?', - ); +/// Resolves [Binding.toProvide] with a sync `T Function()` provider. +class SyncProviderResolver extends _BaseProviderResolver { + final Provider _provider; + Object? _cached; + bool _isCached = false; + + SyncProviderResolver(this._provider); + + @override + T resolveSync([_]) { + if (_singleton && _isCached) { + return _cached as T; } + final result = _provider(); + if (_singleton) { + _cached = result; + _isCached = true; + } + return result; + } + + @override + Future resolveAsync([_]) => Future.value(resolveSync()); +} + +/// Resolves [Binding.toProvideWithParams] with a sync `T Function(dynamic)` provider. +class SyncProviderWithParamsResolver extends _BaseProviderResolver { + final ProviderWithParams _provider; + Object? _cached; + bool _isCached = false; + + SyncProviderWithParamsResolver(this._provider); + + @override + T resolveSync([dynamic params]) { + if (_singleton && _isCached) { + return _cached as T; + } + if (params == null) { + throw StateError('[$T] Params is null. Maybe you forgot to pass it?'); + } + final result = _provider(params); + if (_singleton) { + _cached = result; + _isCached = true; + } + return result; + } + + @override + Future resolveAsync([dynamic params]) => + Future.value(resolveSync(params)); +} + +/// Resolves [Binding.toProvide] with an async `Future Function()` provider. +class AsyncProviderResolver extends _BaseProviderResolver { + final AsyncProvider _provider; + Future? _cache; + bool _isCached = false; + + AsyncProviderResolver(this._provider); + + @override + T resolveSync([_]) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + + @override + Future resolveAsync([_]) { + if (_singleton && _isCached) { + return _cache!; + } + final result = _provider(); + if (_singleton) { + _cache = result; + _isCached = true; + } + return result; + } +} + +/// Resolves [Binding.toProvideWithParams] with an async `Future Function(dynamic)` provider. +class AsyncProviderWithParamsResolver extends _BaseProviderResolver { + final AsyncProviderWithParams _provider; + Future? _cache; + bool _isCached = false; + + AsyncProviderWithParamsResolver(this._provider); + + @override + T resolveSync([_]) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + + @override + Future resolveAsync([dynamic params]) { + if (_singleton && _isCached) { + return _cache!; + } + if (params == null) { + throw StateError('[$T] Params is null. Maybe you forgot to pass it?'); + } + final result = _provider(params); + if (_singleton) { + _cache = result; + _isCached = true; + } + return result; + } +} + +/// Fallback resolver for `FutureOr Function()` providers whose static type +/// is not known at compile time. Detects sync vs async at resolve time. +class FutureOrProviderResolver extends _BaseProviderResolver { + final FutureOr Function() _provider; + Object? _cached; + bool _isCached = false; + + FutureOrProviderResolver(this._provider); + + @override + T resolveSync([_]) { + if (_singleton && _isCached) { + final cached = _cached; + if (cached is Future) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + return cached as T; + } + final result = _provider(); + if (result is Future) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + if (_singleton) { + _cached = result; + _isCached = true; + } + return result; + } + + @override + Future resolveAsync([_]) { + if (_singleton && _isCached) { + final cached = _cached; + if (cached is Future) return cached; + return Future.value(cached as T); + } + final result = _provider(); + if (_singleton) { + _cached = result; + _isCached = true; + } + if (result is Future) return result; + return Future.value(result); + } +} + +/// Fallback resolver for `FutureOr Function(dynamic)` providers with params +/// whose static type is not known at compile time. +class FutureOrProviderWithParamsResolver extends _BaseProviderResolver { + final FutureOr Function(dynamic) _provider; + Object? _cached; + bool _isCached = false; + + FutureOrProviderWithParamsResolver(this._provider); + + @override + T resolveSync([dynamic params]) { + if (_singleton && _isCached) { + final cached = _cached; + if (cached is Future) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + return cached as T; + } + if (params == null) { + throw StateError('[$T] Params is null. Maybe you forgot to pass it?'); + } + final result = _provider(params); + if (result is Future) { + throw StateError( + 'Provider returns Future<$T>. Use resolveAsync() instead.', + ); + } + if (_singleton) { + _cached = result; + _isCached = true; + } + return result; + } + + @override + Future resolveAsync([dynamic params]) { + if (_singleton && _isCached) { + final cached = _cached; + if (cached is Future) return cached; + return Future.value(cached as T); + } + if (params == null) { + throw StateError('[$T] Params is null. Maybe you forgot to pass it?'); + } + final result = _provider(params); + if (_singleton) { + _cached = result; + _isCached = true; + } + if (result is Future) return result; + return Future.value(result); + } +} + +/// Factory for creating instance resolvers (sync or async). +class InstanceResolver { + static BindingResolver create(FutureOr instance) { + if (instance is Future) { + return AsyncInstanceResolver(instance); + } + return SyncInstanceResolver(instance); + } +} + +/// Factory for creating the correct provider resolver based on the +/// provider's static return type, avoiding runtime checks in fast paths. +class ProviderResolver { + static BindingResolver create(FutureOr Function() provider) { + if (provider is T Function()) { + return SyncProviderResolver(provider); + } + if (provider is Future Function()) { + return AsyncProviderResolver(provider); + } + return FutureOrProviderResolver(provider); + } + + static BindingResolver createWithParams( + FutureOr Function(dynamic) provider) { + if (provider is T Function(dynamic)) { + return SyncProviderWithParamsResolver(provider); + } + if (provider is Future Function(dynamic)) { + return AsyncProviderWithParamsResolver(provider); + } + return FutureOrProviderWithParamsResolver(provider); + } + + /// Explicit sync resolver without parameters. + static BindingResolver sync(Provider provider) { + return SyncProviderResolver(provider); + } + + /// Explicit sync resolver with parameters. + static BindingResolver syncWithParams(ProviderWithParams provider) { + return SyncProviderWithParamsResolver(provider); + } + + /// Explicit async resolver without parameters. + static BindingResolver async(AsyncProvider provider) { + return AsyncProviderResolver(provider); + } + + /// Explicit async resolver with parameters. + static BindingResolver asyncWithParams( + AsyncProviderWithParams provider) { + return AsyncProviderWithParamsResolver(provider); } } diff --git a/cherrypick/test/src/binding_test.dart b/cherrypick/test/src/binding_test.dart index c61cc89..61756ac 100644 --- a/cherrypick/test/src/binding_test.dart +++ b/cherrypick/test/src/binding_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cherrypick/cherrypick.dart'; import 'package:test/test.dart'; @@ -12,7 +14,8 @@ void main() { test('Sets mode to instance', () { final binding = Binding().toInstance(5); - expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); }); test('isSingleton is true', () { @@ -34,7 +37,8 @@ void main() { test('Sets mode to instance', () { final binding = Binding().withName('n').toInstance(5); - expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); }); test('Sets key', () { @@ -73,7 +77,8 @@ void main() { test('Sets mode to instance', () { final binding = Binding().toInstance(Future.value(5)); - expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); }); test('isSingleton is true after toInstanceAsync', () { @@ -107,7 +112,8 @@ void main() { test('Sets mode to providerInstance', () { final binding = Binding().toProvide(() => 5); - expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); + expect(binding.resolveSync(), 5); }); test('isSingleton is false by default', () { @@ -129,7 +135,8 @@ void main() { test('Sets mode to providerInstance', () { final binding = Binding().withName('n').toProvide(() => 5); - expect(binding.resolver, isA>()); + expect(binding.resolver, isA>()); + expect(binding.resolveSync(), 5); }); test('Sets key', () { @@ -166,6 +173,43 @@ void main() { .toProvideWithParams((param) async => 5 + (param as int)); expect(await binding.resolveAsync(3), 8); }); + + test('Resolves toProvideAsync value', () async { + final binding = Binding().toProvideAsync(() async => 5); + expect(await binding.resolveAsync(), 5); + }); + + test('toProvideAsync singleton caches instance', () async { + int counter = 0; + final binding = Binding().toProvideAsync(() async { + counter++; + return counter; + }).singleton(); + + final first = await binding.resolveAsync(); + final second = await binding.resolveAsync(); + expect(first, equals(second)); + expect(counter, 1); + }); + + test('Resolves toProvideAsyncWithParams value', () async { + final binding = Binding() + .toProvideAsyncWithParams((param) async => 5 + (param as int)); + expect(await binding.resolveAsync(3), 8); + }); + + test('toProvideAsyncWithParams singleton caches instance', () async { + int counter = 0; + final binding = Binding().toProvideAsyncWithParams((param) async { + counter++; + return counter + (param as int); + }).singleton(); + + final first = await binding.resolveAsync(10); + final second = await binding.resolveAsync(20); + expect(first, equals(second)); + expect(counter, 1); + }); }); // --- Singleton provider binding --- @@ -232,6 +276,103 @@ void main() { }); }); + // --- FutureOr provider resolver (lazy detection, no eager side-effects) --- + group('FutureOr Provider Resolver', () { + test('Does not call provider at binding creation', () { + int calls = 0; + FutureOr provider() { + calls++; + return 42; + } + + final binding = Binding().toProvide(provider); + expect(calls, 0); + expect(binding.resolveSync(), 42); + expect(calls, 1); + }); + + test('Sync FutureOr provider resolves via resolveSync', () { + final binding = Binding().toProvide(() => 7); + expect(binding.resolveSync(), 7); + }); + + test('Async FutureOr provider throws on resolveSync', () { + final binding = + Binding().toProvide(() async => 7); + expect(() => binding.resolveSync(), throwsStateError); + }); + + test('Async FutureOr provider resolves via resolveAsync', () async { + final binding = + Binding().toProvide(() async => 7); + expect(await binding.resolveAsync(), 7); + }); + + test('FutureOr provider with params does not call provider at creation', + () { + int calls = 0; + FutureOr provider(dynamic p) { + calls++; + return (p as int) + 1; + } + + final binding = Binding().toProvideWithParams(provider); + expect(calls, 0); + expect(binding.resolveSync(5), 6); + expect(calls, 1); + }); + + test('Async FutureOr provider with params throws on resolveSync', () { + final binding = Binding() + .toProvideWithParams((p) async => (p as int) + 1); + expect(() => binding.resolveSync(5), throwsStateError); + }); + + test('Async FutureOr provider with params resolves via resolveAsync', + () async { + final binding = Binding() + .toProvideWithParams((p) async => (p as int) + 1); + expect(await binding.resolveAsync(5), 6); + }); + + test('FutureOr singleton caches sync result', () { + int calls = 0; + final binding = Binding().toProvide(() { + calls++; + return 99; + }).singleton(); + + expect(binding.resolveSync(), 99); + expect(binding.resolveSync(), 99); + expect(calls, 1); + }); + + test('FutureOr singleton caches async result', () async { + int calls = 0; + final binding = Binding().toProvide(() async { + calls++; + return 99; + }).singleton(); + + expect(await binding.resolveAsync(), 99); + expect(await binding.resolveAsync(), 99); + expect(calls, 1); + }); + }); + + // --- InstanceResolver factory --- + group('InstanceResolver.create', () { + test('Returns SyncInstanceResolver for sync value', () { + final binding = Binding().toInstance(5); + expect(binding.resolver, isA>()); + }); + + test('Returns AsyncInstanceResolver for Future value', () { + final binding = Binding().toInstance(Future.value(5)); + expect(binding.resolver, isA>()); + }); + }); + // --- WithName / Named binding, isNamed, edge-cases --- group('Named binding & helpers', () { test('withName sets isNamed true and stores name', () {