Skip to content

Commit d974871

Browse files
fix: register correctly composed providers and export values
1 parent 360bcad commit d974871

11 files changed

Lines changed: 374 additions & 65 deletions

File tree

packages/serinus/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
# Changelog
22

3+
## 2.1.9
4+
5+
**Released on:** 04-05-2026
6+
7+
### Fixes
8+
9+
- Fix a bug that caused the composed providers to not be correctly registered and finalized.
10+
311
## 2.1.8
412

13+
**Released on:** 07-04-2026
14+
15+
### Fixes
16+
517
- Re-add logs for Composed Modules that were falsely logged as internal modules in the previous release. This fix ensures that composed modules are correctly identified and logged as composed modules, providing better visibility and debugging capabilities for developers working with composed modules in Serinus applications.
618
- Prevent empty modules to be registered.
19+
20+
### Features
21+
722
- Add `useService` and `canUseService` methods to the Application class to allow for service retrival in the main function. This allows for better integration with the Serinus CLI and other tools that may need to access services from the application context.
823

924
## 2.1.7

packages/serinus/lib/src/containers/modules/provider_registry.dart

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -267,23 +267,25 @@ class ProviderRegistry {
267267
Type dependency,
268268
ScopeManager scopeManager,
269269
) {
270+
final providerDependency = _providerDependencyType(dependency);
271+
final valueDependencyToken = _valueDependencyToken(dependency);
270272
final localProviders = getProvidersForScope(scope);
271273
final localValues = getValuesForScope(scope);
272274

273-
if (localProviders.containsKey(dependency)) {
275+
if (providerDependency != null &&
276+
localProviders.containsKey(providerDependency)) {
274277
return (
275-
provider: localProviders[dependency],
278+
provider: localProviders[providerDependency],
276279
valueToken: null,
277280
value: null,
278281
);
279282
}
280283

281-
final localValueToken = ValueToken(dependency, null);
282-
if (localValues.containsKey(localValueToken)) {
284+
if (localValues.containsKey(valueDependencyToken)) {
283285
return (
284286
provider: null,
285-
valueToken: localValueToken,
286-
value: localValues[localValueToken],
287+
valueToken: valueDependencyToken,
288+
value: localValues[valueDependencyToken],
287289
);
288290
}
289291

@@ -300,27 +302,21 @@ class ProviderRegistry {
300302
final importedProviders = getProvidersForScope(importedScope);
301303
final importedValues = getValuesForScope(importedScope);
302304

303-
if (importedScope.exports.contains(dependency) &&
304-
importedProviders.containsKey(dependency)) {
305+
if (providerDependency != null &&
306+
_exportsDependency(importedScope.exports, dependency) &&
307+
importedProviders.containsKey(providerDependency)) {
305308
importProviderMatches.add((
306309
scope: importedScope,
307-
provider: importedProviders[dependency]!,
310+
provider: importedProviders[providerDependency]!,
308311
));
309312
}
310313

311-
final valueToken = ValueToken(dependency, null);
312-
final exportsValue = importedScope.exports.any((exportType) {
313-
if (exportType is Export) {
314-
return exportType.toValueToken() == valueToken;
315-
}
316-
return exportType == dependency;
317-
});
318-
319-
if (exportsValue && importedValues.containsKey(valueToken)) {
314+
if (_exportsValue(importedScope.exports, valueDependencyToken) &&
315+
importedValues.containsKey(valueDependencyToken)) {
320316
importValueMatches.add((
321317
scope: importedScope,
322-
token: valueToken,
323-
value: importedValues[valueToken],
318+
token: valueDependencyToken,
319+
value: importedValues[valueDependencyToken],
324320
));
325321
}
326322
}
@@ -356,34 +352,75 @@ class ProviderRegistry {
356352
);
357353
}
358354

359-
final globalProviderMatches = _globalProviders
360-
.where((provider) => provider.runtimeType == dependency)
361-
.toList();
362-
if (globalProviderMatches.length > 1) {
363-
throw InitializationError(
364-
'Ambiguous global dependency resolution for $dependency.',
365-
);
366-
}
367-
if (globalProviderMatches.isNotEmpty) {
368-
return (
369-
provider: globalProviderMatches.first,
370-
valueToken: null,
371-
value: null,
372-
);
355+
if (providerDependency != null) {
356+
final globalProviderMatches = _globalProviders
357+
.where((provider) => provider.runtimeType == providerDependency)
358+
.toList();
359+
if (globalProviderMatches.length > 1) {
360+
throw InitializationError(
361+
'Ambiguous global dependency resolution for $dependency.',
362+
);
363+
}
364+
if (globalProviderMatches.isNotEmpty) {
365+
return (
366+
provider: globalProviderMatches.first,
367+
valueToken: null,
368+
value: null,
369+
);
370+
}
373371
}
374372

375-
final globalValueToken = ValueToken(dependency, null);
376-
if (_globalValueProviders.containsKey(globalValueToken)) {
373+
if (_globalValueProviders.containsKey(valueDependencyToken)) {
377374
return (
378375
provider: null,
379-
valueToken: globalValueToken,
380-
value: _globalValueProviders[globalValueToken],
376+
valueToken: valueDependencyToken,
377+
value: _globalValueProviders[valueDependencyToken],
381378
);
382379
}
383380

384381
return null;
385382
}
386383

384+
Type? _providerDependencyType(Type dependency) {
385+
if (dependency case Export(:final exportedType, :final name)) {
386+
if (name != null) {
387+
return null;
388+
}
389+
return exportedType;
390+
}
391+
return dependency;
392+
}
393+
394+
ValueToken _valueDependencyToken(Type dependency) {
395+
if (dependency case Export()) {
396+
return dependency.toValueToken();
397+
}
398+
return ValueToken(dependency, null);
399+
}
400+
401+
bool _exportsDependency(Set<Type> exports, Type dependency) {
402+
final providerDependency = _providerDependencyType(dependency);
403+
if (providerDependency == null) {
404+
return false;
405+
}
406+
return exports.any((exportType) {
407+
if (exportType is Export) {
408+
return exportType.name == null &&
409+
exportType.exportedType == providerDependency;
410+
}
411+
return exportType == providerDependency;
412+
});
413+
}
414+
415+
bool _exportsValue(Set<Type> exports, ValueToken valueToken) {
416+
return exports.any((exportType) {
417+
if (exportType is Export) {
418+
return exportType.toValueToken() == valueToken;
419+
}
420+
return exportType == valueToken.type && valueToken.name == null;
421+
});
422+
}
423+
387424
/// Returns dependency types that cannot be resolved for a scope.
388425
List<Type> getMissingDependenciesForScope(
389426
ModuleScope scope,

packages/serinus/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: Serinus is a framework written in Dart
44
documentation: https://serinus.app
55
homepage: https://serinus.app
66
repository: https://github.com/francescovallone/serinus
7-
version: 2.1.8
7+
version: 2.1.9
88
funding:
99
- https://github.com/sponsors/francescovallone
1010
topics:

packages/serinus/test/containers/value_provider_test.dart

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ class TestProviderWithValue extends Provider {
3838
String getValue() => value;
3939
}
4040

41+
class ImportedRepository {
42+
final String source;
43+
44+
ImportedRepository(this.source);
45+
}
46+
47+
class TestProviderWithImportedRepository extends Provider {
48+
final ImportedRepository repository;
49+
50+
TestProviderWithImportedRepository(this.repository);
51+
}
52+
4153
class SimpleModuleWithValueProvider extends Module {
4254
SimpleModuleWithValueProvider()
4355
: super(
@@ -104,6 +116,102 @@ class ModuleWithComposedProviderUsingValue extends Module {
104116
);
105117
}
106118

119+
class ModuleWithExportedRepositoryValue extends Module {
120+
ModuleWithExportedRepositoryValue()
121+
: super(
122+
controllers: [],
123+
providers: [
124+
Provider.forValue(
125+
ImportedRepository('imported'),
126+
asType: ImportedRepository,
127+
),
128+
],
129+
exports: [ImportedRepository],
130+
);
131+
}
132+
133+
class ModuleWithImportedRepositoryDependency extends Module {
134+
ModuleWithImportedRepositoryDependency()
135+
: super(
136+
imports: [ModuleWithExportedRepositoryValue()],
137+
controllers: [],
138+
providers: [
139+
Provider.composed<TestProviderWithImportedRepository>((
140+
CompositionContext ctx,
141+
) async {
142+
return TestProviderWithImportedRepository(
143+
ctx.use<ImportedRepository>(),
144+
);
145+
}, inject: [ImportedRepository]),
146+
],
147+
exports: [],
148+
);
149+
}
150+
151+
class ModuleWithImportedExportDependency extends Module {
152+
ModuleWithImportedExportDependency()
153+
: super(
154+
imports: [ModuleWithExportedRepositoryValue()],
155+
controllers: [],
156+
providers: [
157+
Provider.composed<TestProviderWithImportedRepository>((
158+
CompositionContext ctx,
159+
) async {
160+
return TestProviderWithImportedRepository(
161+
ctx.use<ImportedRepository>(),
162+
);
163+
}, inject: [const Export(ImportedRepository)]),
164+
],
165+
exports: [],
166+
);
167+
}
168+
169+
class ModuleWithNamedRepositoryValue extends Module {
170+
ModuleWithNamedRepositoryValue()
171+
: super(
172+
controllers: [],
173+
providers: [
174+
Provider.forValue(
175+
ImportedRepository('secondary'),
176+
asType: ImportedRepository,
177+
name: 'secondary',
178+
),
179+
],
180+
exports: [const Export(ImportedRepository, name: 'secondary')],
181+
);
182+
}
183+
184+
class SecondaryRepositoryBranch extends Module {
185+
SecondaryRepositoryBranch()
186+
: super(
187+
imports: [ModuleWithNamedRepositoryValue()],
188+
controllers: [],
189+
providers: [],
190+
exports: [],
191+
);
192+
}
193+
194+
class ModuleWithDefaultAndSecondaryRepositoryImports extends Module {
195+
ModuleWithDefaultAndSecondaryRepositoryImports()
196+
: super(
197+
imports: [
198+
ModuleWithExportedRepositoryValue(),
199+
SecondaryRepositoryBranch(),
200+
],
201+
controllers: [],
202+
providers: [
203+
Provider.composed<TestProviderWithImportedRepository>((
204+
CompositionContext ctx,
205+
) async {
206+
return TestProviderWithImportedRepository(
207+
ctx.use<ImportedRepository>(),
208+
);
209+
}, inject: [ImportedRepository]),
210+
],
211+
exports: [],
212+
);
213+
}
214+
107215
void main() {
108216
group('ValueProvider', () {
109217
late ApplicationConfig config;
@@ -216,6 +324,63 @@ void main() {
216324
expect(provider.getValue(), 'Base Value');
217325
});
218326

327+
test(
328+
'ComposedProvider can use imported exported ValueProvider by raw type',
329+
() async {
330+
final container = ModulesContainer(config);
331+
final module = ModuleWithImportedRepositoryDependency();
332+
await container.registerModules(module);
333+
await container.finalize(module);
334+
335+
final scope = container.getScope(InjectionToken.fromModule(module));
336+
final provider =
337+
scope.unifiedProviders.firstWhere(
338+
(p) => p is TestProviderWithImportedRepository,
339+
)
340+
as TestProviderWithImportedRepository;
341+
342+
expect(provider.repository.source, 'imported');
343+
},
344+
);
345+
346+
test(
347+
'ComposedProvider can use imported exported ValueProvider by Export token',
348+
() async {
349+
final container = ModulesContainer(config);
350+
final module = ModuleWithImportedExportDependency();
351+
await container.registerModules(module);
352+
await container.finalize(module);
353+
354+
final scope = container.getScope(InjectionToken.fromModule(module));
355+
final provider =
356+
scope.unifiedProviders.firstWhere(
357+
(p) => p is TestProviderWithImportedRepository,
358+
)
359+
as TestProviderWithImportedRepository;
360+
361+
expect(provider.repository.source, 'imported');
362+
},
363+
);
364+
365+
test(
366+
'ComposedProvider uses the default exported value when a named sibling exists',
367+
() async {
368+
final container = ModulesContainer(config);
369+
final module = ModuleWithDefaultAndSecondaryRepositoryImports();
370+
await container.registerModules(module);
371+
await container.finalize(module);
372+
373+
final scope = container.getScope(InjectionToken.fromModule(module));
374+
final provider =
375+
scope.unifiedProviders.firstWhere(
376+
(p) => p is TestProviderWithImportedRepository,
377+
)
378+
as TestProviderWithImportedRepository;
379+
380+
expect(provider.repository.source, 'imported');
381+
},
382+
);
383+
219384
test('context.canUse<T>() returns true for ValueProvider types', () async {
220385
final container = ModulesContainer(config);
221386
final module = SimpleModuleWithValueProvider();

0 commit comments

Comments
 (0)