diff --git a/cherrypick/README.md b/cherrypick/README.md index a4499cb..160ab6b 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -30,7 +30,7 @@ Binding().toProvide(() => "hello world"); // Asynchronous lazy initialization Binding().toProvideAsync(() async => "hello async world"); -/ Asynchronous lazy initialization with dynamic parameters +// Asynchronous lazy initialization with dynamic parameters Binding().toProvideAsyncWithParams((params) async => "hello $params"); // Initialization with dynamic parameters diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index be00592..fe248c5 100644 --- a/cherrypick/lib/src/binding.dart +++ b/cherrypick/lib/src/binding.dart @@ -13,6 +13,8 @@ enum Mode { simple, instance, providerInstance, providerInstanceWithParams } +typedef Provider = T? Function(); + typedef ProviderWithParams = T Function(dynamic params); typedef AsyncProvider = Future Function(); @@ -27,11 +29,13 @@ class Binding { late Type _key; late String _name; T? _instance; - T? Function()? _provider; + Future? _instanceAsync; + Provider? _provider; + ProviderWithParams? _providerWithParams; + AsyncProvider? asyncProvider; AsyncProviderWithParams? asyncProviderWithParams; - ProviderWithParams? _providerWithParams; late bool _isSingleton = false; late bool _isNamed = false; @@ -91,11 +95,22 @@ class Binding { return this; } + /// RU: Инициализация экземляпяра [value]. + /// ENG: Initialization instance [value]. + /// + /// return [Binding] + Binding toInstanceAsync(Future value) { + _mode = Mode.instance; + _instanceAsync = value; + _isSingleton = true; + return this; + } + /// RU: Инициализация экземляпяра  через провайдер [value]. /// ENG: Initialization instance via provider [value]. /// /// return [Binding] - Binding toProvide(T Function() value) { + Binding toProvide(Provider value) { _mode = Mode.providerInstance; _provider = value; return this; @@ -146,6 +161,12 @@ class Binding { /// return [T] T? get instance => _instance; + /// RU: Поиск экземпляра. + /// ENG: Resolve instance. + /// + /// return [T] + Future? get instanceAsync => _instanceAsync; + /// RU: Поиск экземпляра. /// ENG: Resolve instance. /// diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index f0ac55d..cfc9ee1 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -160,11 +160,18 @@ class Scope { if (binding.key == T && ((!binding.isNamed && named == null) || (binding.isNamed && named == binding.name))) { + if (binding.instanceAsync != null) { + return await binding.instanceAsync; + } + if (binding.asyncProvider != null) { return await binding.asyncProvider?.call(); } if (binding.asyncProviderWithParams != null) { + if (params == null) { + throw StateError('Param is null. Maybe you forget pass it'); + } return await binding.asyncProviderWithParams!(params); } } diff --git a/cherrypick/test/src/binding_test.dart b/cherrypick/test/src/binding_test.dart index 9f69647..4d3ce11 100644 --- a/cherrypick/test/src/binding_test.dart +++ b/cherrypick/test/src/binding_test.dart @@ -2,314 +2,270 @@ import 'package:cherrypick/src/binding.dart'; import 'package:test/test.dart'; void main() { - group('Check instance.', () { - group('Without name.', () { - test('Binding resolves null', () { + // --- Instance binding (synchronous) --- + group('Instance Binding (toInstance)', () { + group('Without name', () { + test('Returns null by default', () { final binding = Binding(); expect(binding.instance, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - + test('Sets mode to instance', () { + final binding = Binding().toInstance(5); expect(binding.mode, Mode.instance); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - + test('isSingleton is true', () { + final binding = Binding().toInstance(5); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - - expect(binding.instance, expectedValue); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - expect(binding.instance, expectedValue); + test('Stores value', () { + final binding = Binding().toInstance(5); + expect(binding.instance, 5); }); }); - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue'); + group('With name', () { + test('Returns null by default', () { + final binding = Binding().withName('n'); expect(binding.instance, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('Sets mode to instance', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.mode, Mode.instance); }); - test('Binding check key', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('Sets key', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.key, int); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('isSingleton is true', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - - expect(binding.instance, expectedValue); + test('Stores value', () { + final binding = Binding().withName('n').toInstance(5); + expect(binding.instance, 5); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - - expect(binding.name, 'expectedValue'); + test('Sets name', () { + final binding = Binding().withName('n').toInstance(5); + expect(binding.name, 'n'); }); + }); - test('Binding resolves value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - expect(binding.instance, expectedValue); - }); + test('Multiple toInstance calls change value', () { + final binding = Binding().toInstance(1).toInstance(2); + expect(binding.instance, 2); }); }); - group('Check provide.', () { - group('Without name.', () { - test('Binding resolves null', () { + // --- Instance binding (asynchronous) --- + group('Async Instance Binding (toInstanceAsync)', () { + test('Resolves instanceAsync with expected value', () async { + final binding = Binding().toInstanceAsync(Future.value(42)); + expect(await binding.instanceAsync, 42); + }); + + test('Does not affect instance', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.instance, null); + }); + + test('Sets mode to instance', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.mode, Mode.instance); + }); + + test('isSingleton is true after toInstanceAsync', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.isSingleton, isTrue); + }); + + test('Composes with withName', () async { + final binding = Binding() + .withName('asyncValue') + .toInstanceAsync(Future.value(7)); + expect(binding.isNamed, isTrue); + expect(binding.name, 'asyncValue'); + expect(await binding.instanceAsync, 7); + }); + + test('Keeps value after multiple awaits', () async { + final binding = Binding().toInstanceAsync(Future.value(123)); + final result1 = await binding.instanceAsync; + final result2 = await binding.instanceAsync; + expect(result1, equals(result2)); + }); + }); + + // --- Provider binding (synchronous) --- + group('Provider Binding (toProvide)', () { + group('Without name', () { + test('Returns null by default', () { final binding = Binding(); expect(binding.provider, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - + test('Sets mode to providerInstance', () { + final binding = Binding().toProvide(() => 5); expect(binding.mode, Mode.providerInstance); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - + test('isSingleton is false by default', () { + final binding = Binding().toProvide(() => 5); expect(binding.isSingleton, false); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - - expect(binding.provider, expectedValue); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - expect(binding.provider, expectedValue); + test('Returns provided value', () { + final binding = Binding().toProvide(() => 5); + expect(binding.provider, 5); }); }); - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue'); + group('With name', () { + test('Returns null by default', () { + final binding = Binding().withName('n'); expect(binding.provider, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - + test('Sets mode to providerInstance', () { + final binding = Binding().withName('n').toProvide(() => 5); expect(binding.mode, Mode.providerInstance); }); - test('Binding check key', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - + test('Sets key', () { + final binding = Binding().withName('n').toProvide(() => 5); expect(binding.key, int); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - + test('isSingleton is false by default', () { + final binding = Binding().withName('n').toProvide(() => 5); expect(binding.isSingleton, false); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.provider, expectedValue); + test('Returns provided value', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.provider, 5); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.name, 'expectedValue'); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - expect(binding.provider, expectedValue); + test('Sets name', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.name, 'n'); }); }); }); - group('Check Async provider.', () { - test('Binding resolves value asynchronously', () async { - final expectedValue = 5; - final binding = Binding().toProvideAsync(() async => expectedValue); - - final result = await binding.asyncProvider?.call(); - expect(result, expectedValue); + // --- Async provider binding --- + group('Async Provider Binding', () { + test('Resolves asyncProvider value', () async { + final binding = Binding().toProvideAsync(() async => 5); + expect(await binding.asyncProvider?.call(), 5); }); - test('Binding resolves value asynchronously with params', () async { - final expectedValue = 5; - final binding = Binding().toProvideAsyncWithParams( - (param) async => expectedValue + (param as int)); - - final result = await binding.asyncProviderWithParams?.call(3); - expect(result, expectedValue + 3); + test('Resolves asyncProviderWithParams value', () async { + final binding = Binding() + .toProvideAsyncWithParams((param) async => 5 + (param as int)); + expect(await binding.asyncProviderWithParams?.call(3), 8); }); }); - group('Check singleton provide.', () { - group('Without name.', () { - test('Binding resolves null', () { + // --- Singleton provider binding --- + group('Singleton Provider Binding', () { + group('Without name', () { + test('Returns null if no provider set', () { final binding = Binding().singleton(); expect(binding.provider, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - + test('Sets mode to providerInstance', () { + final binding = Binding().toProvide(() => 5).singleton(); expect(binding.mode, Mode.providerInstance); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - + test('isSingleton is true', () { + final binding = Binding().toProvide(() => 5).singleton(); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - - expect(binding.provider, expectedValue); + test('Returns singleton value', () { + final binding = Binding().toProvide(() => 5).singleton(); + expect(binding.provider, 5); }); - test('Binding resolves value', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - expect(binding.provider, expectedValue); + test('Returns same value each call and provider only called once', () { + int counter = 0; + final binding = Binding().toProvide(() { + counter++; + return counter; + }).singleton(); + + final first = binding.provider; + final second = binding.provider; + expect(first, equals(second)); + expect(counter, 1); }); }); - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue').singleton(); + group('With name', () { + test('Returns null if no provider set', () { + final binding = Binding().withName('n').singleton(); expect(binding.provider, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - + test('Sets mode to providerInstance', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); expect(binding.mode, Mode.providerInstance); }); - test('Binding check key', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - + test('Sets key', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); expect(binding.key, int); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - + test('isSingleton is true', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.provider, expectedValue); + test('Returns singleton value', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.provider, 5); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.name, 'expectedValue'); + test('Sets name', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.name, 'n'); }); + }); - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - expect(binding.provider, expectedValue); - }); + test('Chained withName and singleton preserves mode', () { + final binding = + Binding().toProvide(() => 3).withName("named").singleton(); + expect(binding.mode, Mode.providerInstance); + }); + }); + + // --- WithName / Named binding, isNamed, edge-cases --- + group('Named binding & helpers', () { + test('withName sets isNamed true and stores name', () { + final binding = Binding().withName('foo'); + expect(binding.isNamed, true); + expect(binding.name, 'foo'); + }); + + test('providerWithParams returns null if not set', () { + final binding = Binding(); + expect(binding.providerWithParams(123), null); }); }); } diff --git a/cherrypick/test/src/scope_test.dart b/cherrypick/test/src/scope_test.dart index 267cac8..738ae74 100644 --- a/cherrypick/test/src/scope_test.dart +++ b/cherrypick/test/src/scope_test.dart @@ -3,46 +3,44 @@ import 'package:cherrypick/src/scope.dart'; import 'package:test/test.dart'; void main() { - group('Without parent scope.', () { - test('Parent scope is null.', () { + // -------------------------------------------------------------------------- + group('Scope & Subscope Management', () { + test('Scope has no parent if constructed with null', () { final scope = Scope(null); expect(scope.parentScope, null); }); - test('Open sub scope.', () { + test('Can open and retrieve the same subScope by key', () { final scope = Scope(null); final subScope = scope.openSubScope('subScope'); expect(scope.openSubScope('subScope'), subScope); }); - test("Container throws state error if the value can't be resolved", () { + test('closeSubScope removes subscope so next openSubScope returns new', () { + final scope = Scope(null); + final subScope = scope.openSubScope("child"); + expect(scope.openSubScope("child"), same(subScope)); + scope.closeSubScope("child"); + final newSubScope = scope.openSubScope("child"); + expect(newSubScope, isNot(same(subScope))); + }); + }); + + // -------------------------------------------------------------------------- + group('Dependency Resolution (standard)', () { + test("Throws StateError if value can't be resolved", () { final scope = Scope(null); expect(() => scope.resolve(), throwsA(isA())); }); - test('Container resolves value after adding a dependency', () { + test('Resolves value after adding a dependency', () { final expectedValue = 'test string'; final scope = Scope(null) .installModules([TestModule(value: expectedValue)]); expect(scope.resolve(), expectedValue); }); - }); - group('With parent scope.', () { - /* - test( - "Container bind() throws state error (if it's parent already has a resolver)", - () { - final parentScope = new Scope(null) - .installModules([TestModule(value: "string one")]); - final scope = new Scope(parentScope); - - expect( - () => scope.installModules([TestModule(value: "string two")]), - throwsA(isA())); - }); -*/ - test('Container resolve() returns a value from parent container.', () { + test('Returns a value from parent scope', () { final expectedValue = 5; final parentScope = Scope(null); final scope = Scope(parentScope); @@ -52,8 +50,7 @@ void main() { expect(scope.resolve(), expectedValue); }); - test('Container resolve() returns a several value from parent container.', - () { + test('Returns several values from parent container', () { final expectedIntValue = 5; final expectedStringValue = 'Hello world'; final parentScope = Scope(null).installModules([ @@ -66,15 +63,126 @@ void main() { expect(scope.resolve(), expectedStringValue); }); - test("Container resolve() throws a state error if parent hasn't value too.", - () { + test("Throws StateError if parent hasn't value too", () { final parentScope = Scope(null); final scope = Scope(parentScope); expect(() => scope.resolve(), throwsA(isA())); }); + + test("After dropModules resolves fail", () { + final scope = Scope(null)..installModules([TestModule(value: 5)]); + expect(scope.resolve(), 5); + scope.dropModules(); + expect(() => scope.resolve(), throwsA(isA())); + }); + }); + + // -------------------------------------------------------------------------- + group('Named Dependencies', () { + test('Resolve named binding', () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "first"), + TestModule(value: "second", name: "special") + ]); + expect(scope.resolve(named: "special"), "second"); + expect(scope.resolve(), "first"); + }); + + test('Named binding does not clash with unnamed', () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "foo", name: "bar"), + ]); + expect(() => scope.resolve(), throwsA(isA())); + expect(scope.resolve(named: "bar"), "foo"); + }); + + test("tryResolve returns null for missing named", () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "foo"), + ]); + expect(scope.tryResolve(named: "bar"), isNull); + }); + }); + + // -------------------------------------------------------------------------- + group('Provider with parameters', () { + test('Resolve dependency using providerWithParams', () { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideWithParams((param) => (param as int) * 2); + }), + ]); + expect(scope.resolve(params: 3), 6); + expect(() => scope.resolve(), throwsA(isA())); + }); + }); + + // -------------------------------------------------------------------------- + group('Async Resolution', () { + test('Resolve async instance', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toInstanceAsync(Future.value('async value')); + }), + ]); + expect(await scope.resolveAsync(), "async value"); + }); + + test('Resolve async provider', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideAsync(() async => 7); + }), + ]); + expect(await scope.resolveAsync(), 7); + }); + + test('Resolve async provider with param', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideAsyncWithParams((x) async => (x as int) * 3); + }), + ]); + expect(await scope.resolveAsync(params: 2), 6); + expect(() => scope.resolveAsync(), throwsA(isA())); + }); + + test('tryResolveAsync returns null for missing', () async { + final scope = Scope(null); + final result = await scope.tryResolveAsync(); + expect(result, isNull); + }); + }); + + // -------------------------------------------------------------------------- + group('Optional resolution and error handling', () { + test("tryResolve returns null for missing dependency", () { + final scope = Scope(null); + expect(scope.tryResolve(), isNull); + }); + + // Не реализован: + // test("Container bind() throws state error (if it's parent already has a resolver)", () { + // final parentScope = new Scope(null).installModules([TestModule(value: "string one")]); + // final scope = new Scope(parentScope); + + // expect( + // () => scope.installModules([TestModule(value: "string two")]), + // throwsA(isA())); + // }); }); } +// ---------------------------------------------------------------------------- +// Вспомогательные модули + class TestModule extends Module { final T value; final String? name; @@ -89,3 +197,12 @@ class TestModule extends Module { } } } + +/// Вспомогательный модуль для подстановки builder'а через конструктор +class _InlineModule extends Module { + final void Function(Module, Scope) _builder; + _InlineModule(this._builder); + + @override + void builder(Scope s) => _builder(this, s); +} diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index 02b6015..1b8f1a3 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -28,18 +28,16 @@ packages: cherrypick: dependency: "direct main" description: - name: cherrypick - sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe - url: "https://pub.dev" - source: hosted - version: "2.0.2" + path: "../../cherrypick" + relative: true + source: path + version: "2.1.0" cherrypick_flutter: dependency: "direct main" description: - name: cherrypick_flutter - sha256: ad63ae816b7d1147ffb0a82bcae5a1ea3d51e9d398a79459c619464391a43a79 - url: "https://pub.dev" - source: hosted + path: "../../cherrypick_flutter" + relative: true + source: path version: "1.1.1" clock: dependency: transitive diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index 3fcb01b..880c933 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -153,11 +153,10 @@ packages: cherrypick: dependency: "direct main" description: - name: cherrypick - sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe - url: "https://pub.dev" - source: hosted - version: "2.0.2" + path: "../../cherrypick" + relative: true + source: path + version: "2.1.0" clock: dependency: transitive description: