mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 05:25:19 +00:00
@@ -234,6 +234,32 @@ class ApiClientImpl implements ApiClient {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
CherryPick supports centralized logging of all dependency injection (DI) events and errors. You can globally enable logs for your application or test environment with:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Set a global logger before any scopes are created
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger()); // or your custom logger
|
||||||
|
|
||||||
|
final scope = CherryPick.openRootScope();
|
||||||
|
// All DI actions and errors will now be logged!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- All dependency resolution, scope creation, module installation, and circular dependency errors will be sent to your logger (via info/error method).
|
||||||
|
- By default, logs are off (SilentLogger is used in production).
|
||||||
|
|
||||||
|
If you want fine-grained, test-local, or isolated logging, you can provide a logger directly to each scope:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger); // works in tests for isolation
|
||||||
|
scope.installModules([...]);
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Main Scope and Named Subscopes
|
- [x] Main Scope and Named Subscopes
|
||||||
@@ -244,6 +270,7 @@ class ApiClientImpl implements ApiClient {
|
|||||||
- [x] Modular and Hierarchical Composition
|
- [x] Modular and Hierarchical Composition
|
||||||
- [x] Null-safe Resolution (tryResolve/tryResolveAsync)
|
- [x] Null-safe Resolution (tryResolve/tryResolveAsync)
|
||||||
- [x] Circular Dependency Detection (Local and Global)
|
- [x] Circular Dependency Detection (Local and Global)
|
||||||
|
- [x] Comprehensive logging of dependency injection state and actions
|
||||||
|
|
||||||
## Quick Guide: Circular Dependency Detection
|
## Quick Guide: Circular Dependency Detection
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class FeatureModule extends Module {
|
|||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
try {
|
try {
|
||||||
final scope = openRootScope().installModules([AppModule()]);
|
final scope = CherryPick.openRootScope().installModules([AppModule()]);
|
||||||
|
|
||||||
final subScope = scope
|
final subScope = scope
|
||||||
.openSubScope("featureScope")
|
.openSubScope("featureScope")
|
||||||
|
|||||||
37
cherrypick/example/cherrypick_logger_demo.dart
Normal file
37
cherrypick/example/cherrypick_logger_demo.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
/// Example of a simple service class.
|
||||||
|
class UserRepository {
|
||||||
|
String getUserName() => 'Sergey DI';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DI module for registering dependencies.
|
||||||
|
class AppModule extends Module {
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
bind<UserRepository>().toInstance(UserRepository());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Set a global logger for the DI system
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger());
|
||||||
|
|
||||||
|
// Open the root scope
|
||||||
|
final rootScope = CherryPick.openRootScope();
|
||||||
|
|
||||||
|
// Register the DI module
|
||||||
|
rootScope.installModules([AppModule()]);
|
||||||
|
|
||||||
|
// Resolve a dependency (service)
|
||||||
|
final repo = rootScope.resolve<UserRepository>();
|
||||||
|
print('User: ${repo.getUserName()}');
|
||||||
|
|
||||||
|
// Work with a sub-scope (create/close)
|
||||||
|
final subScope = rootScope.openSubScope('feature.profile');
|
||||||
|
subScope.closeSubScope('feature.profile');
|
||||||
|
|
||||||
|
// Demonstrate disabling and re-enabling logging
|
||||||
|
CherryPick.setGlobalLogger(const SilentLogger());
|
||||||
|
rootScope.resolve<UserRepository>(); // now without logs
|
||||||
|
}
|
||||||
@@ -126,7 +126,7 @@ void main() {
|
|||||||
// Example 1: Demonstrate circular dependency
|
// Example 1: Demonstrate circular dependency
|
||||||
print('1. Attempt to create a scope with circular dependencies:');
|
print('1. Attempt to create a scope with circular dependencies:');
|
||||||
try {
|
try {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
|
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
@@ -144,7 +144,7 @@ void main() {
|
|||||||
// Example 2: Without circular dependency detection (dangerous!)
|
// Example 2: Without circular dependency detection (dangerous!)
|
||||||
print('2. Same code without circular dependency detection:');
|
print('2. Same code without circular dependency detection:');
|
||||||
try {
|
try {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
// НЕ включаем обнаружение циклических зависимостей
|
// НЕ включаем обнаружение циклических зависимостей
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
@@ -166,7 +166,7 @@ void main() {
|
|||||||
// Example 3: Correct architecture without circular dependencies
|
// Example 3: Correct architecture without circular dependencies
|
||||||
print('3. Correct architecture without circular dependencies:');
|
print('3. Correct architecture without circular dependencies:');
|
||||||
try {
|
try {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection(); // Включаем для безопасности
|
scope.enableCycleDetection(); // Включаем для безопасности
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ export 'package:cherrypick/src/global_cycle_detector.dart';
|
|||||||
export 'package:cherrypick/src/helper.dart';
|
export 'package:cherrypick/src/helper.dart';
|
||||||
export 'package:cherrypick/src/module.dart';
|
export 'package:cherrypick/src/module.dart';
|
||||||
export 'package:cherrypick/src/scope.dart';
|
export 'package:cherrypick/src/scope.dart';
|
||||||
|
export 'package:cherrypick/src/logger.dart';
|
||||||
|
|||||||
@@ -16,14 +16,67 @@ import 'package:cherrypick/src/binding_resolver.dart';
|
|||||||
/// RU: Класс Binding<T> настраивает параметры экземпляра.
|
/// RU: Класс Binding<T> настраивает параметры экземпляра.
|
||||||
/// ENG: The Binding<T> class configures the settings for the instance.
|
/// ENG: The Binding<T> class configures the settings for the instance.
|
||||||
///
|
///
|
||||||
|
import 'package:cherrypick/src/logger.dart';
|
||||||
|
import 'package:cherrypick/src/log_format.dart';
|
||||||
|
|
||||||
class Binding<T> {
|
class Binding<T> {
|
||||||
late Type _key;
|
late Type _key;
|
||||||
String? _name;
|
String? _name;
|
||||||
|
|
||||||
BindingResolver<T>? _resolver;
|
BindingResolver<T>? _resolver;
|
||||||
|
|
||||||
Binding() {
|
CherryPickLogger? logger;
|
||||||
|
|
||||||
|
// Deferred logging flags
|
||||||
|
bool _createdLogged = false;
|
||||||
|
bool _namedLogged = false;
|
||||||
|
bool _singletonLogged = false;
|
||||||
|
|
||||||
|
Binding({this.logger}) {
|
||||||
_key = T;
|
_key = T;
|
||||||
|
// Не логируем здесь! Делаем deferred лог после назначения logger
|
||||||
|
}
|
||||||
|
|
||||||
|
void markCreated() {
|
||||||
|
if (!_createdLogged) {
|
||||||
|
logger?.info(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: _name != null ? {'name': _name} : null,
|
||||||
|
description: 'created',
|
||||||
|
));
|
||||||
|
_createdLogged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markNamed() {
|
||||||
|
if (isNamed && !_namedLogged) {
|
||||||
|
logger?.info(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {'name': _name},
|
||||||
|
description: 'named',
|
||||||
|
));
|
||||||
|
_namedLogged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markSingleton() {
|
||||||
|
if (isSingleton && !_singletonLogged) {
|
||||||
|
logger?.info(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: _name != null ? {'name': _name} : null,
|
||||||
|
description: 'singleton mode enabled',
|
||||||
|
));
|
||||||
|
_singletonLogged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAllDeferred() {
|
||||||
|
markCreated();
|
||||||
|
markNamed();
|
||||||
|
markSingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Метод возвращает тип экземпляра.
|
/// RU: Метод возвращает тип экземпляра.
|
||||||
@@ -58,6 +111,7 @@ class Binding<T> {
|
|||||||
/// return [Binding]
|
/// return [Binding]
|
||||||
Binding<T> withName(String name) {
|
Binding<T> withName(String name) {
|
||||||
_name = name;
|
_name = name;
|
||||||
|
// Не логируем здесь, deferred log via markNamed()
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +121,6 @@ class Binding<T> {
|
|||||||
/// return [Binding]
|
/// return [Binding]
|
||||||
Binding<T> toInstance(Instance<T> value) {
|
Binding<T> toInstance(Instance<T> value) {
|
||||||
_resolver = InstanceResolver<T>(value);
|
_resolver = InstanceResolver<T>(value);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +130,6 @@ class Binding<T> {
|
|||||||
/// return [Binding]
|
/// return [Binding]
|
||||||
Binding<T> toProvide(Provider<T> value) {
|
Binding<T> toProvide(Provider<T> value) {
|
||||||
_resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
|
_resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +139,6 @@ class Binding<T> {
|
|||||||
/// return [Binding]
|
/// return [Binding]
|
||||||
Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
|
Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
|
||||||
_resolver = ProviderResolver<T>(value, withParams: true);
|
_resolver = ProviderResolver<T>(value, withParams: true);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,15 +163,73 @@ class Binding<T> {
|
|||||||
/// return [Binding]
|
/// return [Binding]
|
||||||
Binding<T> singleton() {
|
Binding<T> singleton() {
|
||||||
_resolver?.toSingleton();
|
_resolver?.toSingleton();
|
||||||
|
// Не логируем здесь, deferred log via markSingleton()
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
T? resolveSync([dynamic params]) {
|
T? resolveSync([dynamic params]) {
|
||||||
return resolver?.resolveSync(params);
|
final res = resolver?.resolveSync(params);
|
||||||
|
if (res != null) {
|
||||||
|
logger?.info(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {
|
||||||
|
if (_name != null) 'name': _name,
|
||||||
|
'method': 'resolveSync',
|
||||||
|
},
|
||||||
|
description: 'object created/resolved',
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
logger?.warn(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {
|
||||||
|
if (_name != null) 'name': _name,
|
||||||
|
'method': 'resolveSync',
|
||||||
|
},
|
||||||
|
description: 'resolveSync returned null',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T>? resolveAsync([dynamic params]) {
|
Future<T>? resolveAsync([dynamic params]) {
|
||||||
return resolver?.resolveAsync(params);
|
final future = resolver?.resolveAsync(params);
|
||||||
|
if (future != null) {
|
||||||
|
future
|
||||||
|
.then((res) => logger?.info(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {
|
||||||
|
if (_name != null) 'name': _name,
|
||||||
|
'method': 'resolveAsync',
|
||||||
|
},
|
||||||
|
description: 'Future resolved',
|
||||||
|
)))
|
||||||
|
.catchError((e, s) => logger?.error(
|
||||||
|
formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {
|
||||||
|
if (_name != null) 'name': _name,
|
||||||
|
'method': 'resolveAsync',
|
||||||
|
},
|
||||||
|
description: 'resolveAsync error',
|
||||||
|
),
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
logger?.warn(formatLogMessage(
|
||||||
|
type: 'Binding',
|
||||||
|
name: T.toString(),
|
||||||
|
params: {
|
||||||
|
if (_name != null) 'name': _name,
|
||||||
|
'method': 'resolveAsync',
|
||||||
|
},
|
||||||
|
description: 'resolveAsync returned null',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return future;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'package:cherrypick/src/logger.dart';
|
||||||
|
import 'package:cherrypick/src/log_format.dart';
|
||||||
|
|
||||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||||
/// ENG: Exception thrown when a circular dependency is detected.
|
/// ENG: Exception thrown when a circular dependency is detected.
|
||||||
@@ -19,7 +21,10 @@ class CircularDependencyException implements Exception {
|
|||||||
final String message;
|
final String message;
|
||||||
final List<String> dependencyChain;
|
final List<String> dependencyChain;
|
||||||
|
|
||||||
const CircularDependencyException(this.message, this.dependencyChain);
|
CircularDependencyException(this.message, this.dependencyChain) {
|
||||||
|
// DEBUG
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -31,24 +36,37 @@ class CircularDependencyException implements Exception {
|
|||||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||||
class CycleDetector {
|
class CycleDetector {
|
||||||
// Стек текущих разрешаемых зависимостей
|
final CherryPickLogger _logger;
|
||||||
final Set<String> _resolutionStack = HashSet<String>();
|
final Set<String> _resolutionStack = HashSet<String>();
|
||||||
|
|
||||||
// История разрешения для построения цепочки зависимостей
|
|
||||||
final List<String> _resolutionHistory = [];
|
final List<String> _resolutionHistory = [];
|
||||||
|
|
||||||
|
CycleDetector({required CherryPickLogger logger}): _logger = logger;
|
||||||
|
|
||||||
/// RU: Начинает отслеживание разрешения зависимости.
|
/// RU: Начинает отслеживание разрешения зависимости.
|
||||||
/// ENG: Starts tracking dependency resolution.
|
/// ENG: Starts tracking dependency resolution.
|
||||||
///
|
///
|
||||||
/// Throws [CircularDependencyException] if circular dependency is detected.
|
/// Throws [CircularDependencyException] if circular dependency is detected.
|
||||||
void startResolving<T>({String? named}) {
|
void startResolving<T>({String? named}) {
|
||||||
final dependencyKey = _createDependencyKey<T>(named);
|
final dependencyKey = _createDependencyKey<T>(named);
|
||||||
|
print('[DEBUG] CycleDetector logger type=${_logger.runtimeType} hash=${_logger.hashCode}');
|
||||||
|
_logger.info(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'event': 'startResolving', 'stackSize': _resolutionStack.length},
|
||||||
|
description: 'start resolving',
|
||||||
|
));
|
||||||
if (_resolutionStack.contains(dependencyKey)) {
|
if (_resolutionStack.contains(dependencyKey)) {
|
||||||
// Найдена циклическая зависимость
|
// Найдена циклическая зависимость
|
||||||
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
||||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||||
|
// print removed (trace)
|
||||||
|
final msg = formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'chain': cycle.join('->')},
|
||||||
|
description: 'cycle detected',
|
||||||
|
);
|
||||||
|
_logger.error(msg);
|
||||||
throw CircularDependencyException(
|
throw CircularDependencyException(
|
||||||
'Circular dependency detected for $dependencyKey',
|
'Circular dependency detected for $dependencyKey',
|
||||||
cycle,
|
cycle,
|
||||||
@@ -63,8 +81,13 @@ class CycleDetector {
|
|||||||
/// ENG: Finishes tracking dependency resolution.
|
/// ENG: Finishes tracking dependency resolution.
|
||||||
void finishResolving<T>({String? named}) {
|
void finishResolving<T>({String? named}) {
|
||||||
final dependencyKey = _createDependencyKey<T>(named);
|
final dependencyKey = _createDependencyKey<T>(named);
|
||||||
|
_logger.info(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'event': 'finishResolving'},
|
||||||
|
description: 'finish resolving',
|
||||||
|
));
|
||||||
_resolutionStack.remove(dependencyKey);
|
_resolutionStack.remove(dependencyKey);
|
||||||
|
|
||||||
// Удаляем из истории только если это последний элемент
|
// Удаляем из истории только если это последний элемент
|
||||||
if (_resolutionHistory.isNotEmpty &&
|
if (_resolutionHistory.isNotEmpty &&
|
||||||
_resolutionHistory.last == dependencyKey) {
|
_resolutionHistory.last == dependencyKey) {
|
||||||
@@ -75,6 +98,11 @@ class CycleDetector {
|
|||||||
/// RU: Очищает все состояние детектора.
|
/// RU: Очищает все состояние детектора.
|
||||||
/// ENG: Clears all detector state.
|
/// ENG: Clears all detector state.
|
||||||
void clear() {
|
void clear() {
|
||||||
|
_logger.info(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
params: {'event': 'clear'},
|
||||||
|
description: 'resolution stack cleared',
|
||||||
|
));
|
||||||
_resolutionStack.clear();
|
_resolutionStack.clear();
|
||||||
_resolutionHistory.clear();
|
_resolutionHistory.clear();
|
||||||
}
|
}
|
||||||
@@ -102,17 +130,28 @@ class CycleDetector {
|
|||||||
/// ENG: Mixin for adding circular dependency detection support.
|
/// ENG: Mixin for adding circular dependency detection support.
|
||||||
mixin CycleDetectionMixin {
|
mixin CycleDetectionMixin {
|
||||||
CycleDetector? _cycleDetector;
|
CycleDetector? _cycleDetector;
|
||||||
|
CherryPickLogger get logger;
|
||||||
|
|
||||||
/// RU: Включает обнаружение циклических зависимостей.
|
/// RU: Включает обнаружение циклических зависимостей.
|
||||||
/// ENG: Enables circular dependency detection.
|
/// ENG: Enables circular dependency detection.
|
||||||
void enableCycleDetection() {
|
void enableCycleDetection() {
|
||||||
_cycleDetector = CycleDetector();
|
_cycleDetector = CycleDetector(logger: logger);
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'CycleDetection',
|
||||||
|
params: {'event': 'enable'},
|
||||||
|
description: 'cycle detection enabled',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Отключает обнаружение циклических зависимостей.
|
/// RU: Отключает обнаружение циклических зависимостей.
|
||||||
/// ENG: Disables circular dependency detection.
|
/// ENG: Disables circular dependency detection.
|
||||||
void disableCycleDetection() {
|
void disableCycleDetection() {
|
||||||
_cycleDetector?.clear();
|
_cycleDetector?.clear();
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'CycleDetection',
|
||||||
|
params: {'event': 'disable'},
|
||||||
|
description: 'cycle detection disabled',
|
||||||
|
));
|
||||||
_cycleDetector = null;
|
_cycleDetector = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +178,12 @@ mixin CycleDetectionMixin {
|
|||||||
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||||
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
||||||
..add(dependencyKey);
|
..add(dependencyKey);
|
||||||
|
logger.error(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'chain': cycle.join('->')},
|
||||||
|
description: 'cycle detected',
|
||||||
|
));
|
||||||
throw CircularDependencyException(
|
throw CircularDependencyException(
|
||||||
'Circular dependency detected for $dependencyKey',
|
'Circular dependency detected for $dependencyKey',
|
||||||
cycle,
|
cycle,
|
||||||
|
|||||||
@@ -12,13 +12,17 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:cherrypick/src/cycle_detector.dart';
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
import 'package:cherrypick/src/log_format.dart';
|
||||||
|
|
||||||
|
|
||||||
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
||||||
/// ENG: Global circular dependency detector for entire scope hierarchy.
|
/// ENG: Global circular dependency detector for entire scope hierarchy.
|
||||||
class GlobalCycleDetector {
|
class GlobalCycleDetector {
|
||||||
static GlobalCycleDetector? _instance;
|
static GlobalCycleDetector? _instance;
|
||||||
|
|
||||||
|
final CherryPickLogger _logger;
|
||||||
|
|
||||||
// Глобальный стек разрешения зависимостей
|
// Глобальный стек разрешения зависимостей
|
||||||
final Set<String> _globalResolutionStack = HashSet<String>();
|
final Set<String> _globalResolutionStack = HashSet<String>();
|
||||||
|
|
||||||
@@ -28,12 +32,12 @@ class GlobalCycleDetector {
|
|||||||
// Карта активных детекторов по скоупам
|
// Карта активных детекторов по скоупам
|
||||||
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
||||||
|
|
||||||
GlobalCycleDetector._internal();
|
GlobalCycleDetector._internal({required CherryPickLogger logger}): _logger = logger;
|
||||||
|
|
||||||
/// RU: Получить единственный экземпляр глобального детектора.
|
/// RU: Получить единственный экземпляр глобального детектора.
|
||||||
/// ENG: Get singleton instance of global detector.
|
/// ENG: Get singleton instance of global detector.
|
||||||
static GlobalCycleDetector get instance {
|
static GlobalCycleDetector get instance {
|
||||||
_instance ??= GlobalCycleDetector._internal();
|
_instance ??= GlobalCycleDetector._internal(logger: CherryPick.globalLogger);
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +59,12 @@ class GlobalCycleDetector {
|
|||||||
// Найдена глобальная циклическая зависимость
|
// Найдена глобальная циклическая зависимость
|
||||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||||
|
_logger.error(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'chain': cycle.join('->')},
|
||||||
|
description: 'cycle detected',
|
||||||
|
));
|
||||||
throw CircularDependencyException(
|
throw CircularDependencyException(
|
||||||
'Global circular dependency detected for $dependencyKey',
|
'Global circular dependency detected for $dependencyKey',
|
||||||
cycle,
|
cycle,
|
||||||
@@ -93,7 +102,12 @@ class GlobalCycleDetector {
|
|||||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
||||||
..add(dependencyKey);
|
..add(dependencyKey);
|
||||||
|
_logger.error(formatLogMessage(
|
||||||
|
type: 'CycleDetector',
|
||||||
|
name: dependencyKey.toString(),
|
||||||
|
params: {'chain': cycle.join('->')},
|
||||||
|
description: 'cycle detected',
|
||||||
|
));
|
||||||
throw CircularDependencyException(
|
throw CircularDependencyException(
|
||||||
'Global circular dependency detected for $dependencyKey',
|
'Global circular dependency detected for $dependencyKey',
|
||||||
cycle,
|
cycle,
|
||||||
@@ -117,7 +131,7 @@ class GlobalCycleDetector {
|
|||||||
/// RU: Получить детектор для конкретного скоупа.
|
/// RU: Получить детектор для конкретного скоупа.
|
||||||
/// ENG: Get detector for specific scope.
|
/// ENG: Get detector for specific scope.
|
||||||
CycleDetector getScopeDetector(String scopeId) {
|
CycleDetector getScopeDetector(String scopeId) {
|
||||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector());
|
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(logger: CherryPick.globalLogger));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Удалить детектор для скоупа.
|
/// RU: Удалить детектор для скоупа.
|
||||||
|
|||||||
@@ -10,70 +10,110 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import 'package:cherrypick/src/scope.dart';
|
import 'package:cherrypick/src/scope.dart';
|
||||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||||
|
import 'package:cherrypick/src/logger.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
|
||||||
Scope? _rootScope;
|
Scope? _rootScope;
|
||||||
|
|
||||||
|
/// Global logger for all [Scope]s managed by [CherryPick].
|
||||||
|
///
|
||||||
|
/// Defaults to [SilentLogger] unless set via [setGlobalLogger].
|
||||||
|
CherryPickLogger _globalLogger = const SilentLogger();
|
||||||
|
|
||||||
|
/// Whether global local-cycle detection is enabled for all Scopes ([Scope.enableCycleDetection]).
|
||||||
bool _globalCycleDetectionEnabled = false;
|
bool _globalCycleDetectionEnabled = false;
|
||||||
|
|
||||||
|
/// Whether global cross-scope cycle detection is enabled ([Scope.enableGlobalCycleDetection]).
|
||||||
bool _globalCrossScopeCycleDetectionEnabled = false;
|
bool _globalCrossScopeCycleDetectionEnabled = false;
|
||||||
|
|
||||||
|
/// Static facade for managing dependency graph, root scope, subscopes, logger, and global settings in the CherryPick DI container.
|
||||||
|
///
|
||||||
|
/// - Provides a singleton root scope for simple integration.
|
||||||
|
/// - Supports hierarchical/named subscopes by string path.
|
||||||
|
/// - Manages global/protected logging and DI diagnostics.
|
||||||
|
/// - Suitable for most application & CLI scenarios. For test isolation, manually create [Scope]s instead.
|
||||||
|
///
|
||||||
|
/// ### Example: Opening a root scope and installing modules
|
||||||
|
/// ```dart
|
||||||
|
/// class AppModule extends Module {
|
||||||
|
/// @override
|
||||||
|
/// void builder(Scope scope) {
|
||||||
|
/// scope.bind<Service>().toProvide(() => ServiceImpl());
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// final root = CherryPick.openRootScope();
|
||||||
|
/// root.installModules([AppModule()]);
|
||||||
|
/// final service = root.resolve<Service>();
|
||||||
|
/// ```
|
||||||
class CherryPick {
|
class CherryPick {
|
||||||
/// RU: Метод открывает главный [Scope].
|
/// Sets the global logger for all [Scope]s created by CherryPick.
|
||||||
/// ENG: The method opens the main [Scope].
|
|
||||||
///
|
///
|
||||||
/// return
|
/// Allows customizing log output and DI diagnostics globally.
|
||||||
static Scope openRootScope() {
|
///
|
||||||
_rootScope ??= Scope(null);
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// CherryPick.setGlobalLogger(DefaultLogger());
|
||||||
|
/// ```
|
||||||
|
static void setGlobalLogger(CherryPickLogger logger) {
|
||||||
|
_globalLogger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
// Применяем глобальную настройку обнаружения циклических зависимостей
|
/// Returns the current global logger used by CherryPick.
|
||||||
|
static CherryPickLogger get globalLogger => _globalLogger;
|
||||||
|
|
||||||
|
/// Returns the singleton root [Scope], creating it if needed.
|
||||||
|
///
|
||||||
|
/// Applies configured [globalLogger] and cycle detection settings.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// final root = CherryPick.openRootScope();
|
||||||
|
/// ```
|
||||||
|
static Scope openRootScope() {
|
||||||
|
_rootScope ??= Scope(null, logger: _globalLogger);
|
||||||
|
// Apply cycle detection settings
|
||||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||||
_rootScope!.enableCycleDetection();
|
_rootScope!.enableCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применяем глобальную настройку обнаружения между скоупами
|
|
||||||
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
|
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
|
||||||
_rootScope!.enableGlobalCycleDetection();
|
_rootScope!.enableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _rootScope!;
|
return _rootScope!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Метод закрывает главный [Scope].
|
/// Disposes and resets the root [Scope] singleton.
|
||||||
/// ENG: The method close the main [Scope].
|
|
||||||
///
|
///
|
||||||
|
/// Call before tests or when needing full re-initialization.
|
||||||
///
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// CherryPick.closeRootScope();
|
||||||
|
/// ```
|
||||||
static void closeRootScope() {
|
static void closeRootScope() {
|
||||||
if (_rootScope != null) {
|
|
||||||
_rootScope = null;
|
_rootScope = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// RU: Глобально включает обнаружение циклических зависимостей для всех новых скоупов.
|
/// Globally enables cycle detection for all new [Scope]s created by CherryPick.
|
||||||
/// ENG: Globally enables circular dependency detection for all new scopes.
|
|
||||||
///
|
///
|
||||||
/// Этот метод влияет на все скоупы, создаваемые через CherryPick.
|
/// Strongly recommended for safety in all projects.
|
||||||
/// This method affects all scopes created through CherryPick.
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// CherryPick.enableGlobalCycleDetection();
|
/// CherryPick.enableGlobalCycleDetection();
|
||||||
/// final scope = CherryPick.openRootScope(); // Автоматически включено обнаружение
|
|
||||||
/// ```
|
/// ```
|
||||||
static void enableGlobalCycleDetection() {
|
static void enableGlobalCycleDetection() {
|
||||||
_globalCycleDetectionEnabled = true;
|
_globalCycleDetectionEnabled = true;
|
||||||
|
|
||||||
// Включаем для уже существующего root scope, если он есть
|
|
||||||
if (_rootScope != null) {
|
if (_rootScope != null) {
|
||||||
_rootScope!.enableCycleDetection();
|
_rootScope!.enableCycleDetection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Глобально отключает обнаружение циклических зависимостей.
|
/// Disables global local cycle detection. Existing and new scopes won't check for local cycles.
|
||||||
/// ENG: Globally disables circular dependency detection.
|
|
||||||
///
|
|
||||||
/// Рекомендуется использовать в production для максимальной производительности.
|
|
||||||
/// Recommended for production use for maximum performance.
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -81,85 +121,63 @@ class CherryPick {
|
|||||||
/// ```
|
/// ```
|
||||||
static void disableGlobalCycleDetection() {
|
static void disableGlobalCycleDetection() {
|
||||||
_globalCycleDetectionEnabled = false;
|
_globalCycleDetectionEnabled = false;
|
||||||
|
|
||||||
// Отключаем для уже существующего root scope, если он есть
|
|
||||||
if (_rootScope != null) {
|
if (_rootScope != null) {
|
||||||
_rootScope!.disableCycleDetection();
|
_rootScope!.disableCycleDetection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Проверяет, включено ли глобальное обнаружение циклических зависимостей.
|
/// Returns `true` if global local cycle detection is enabled.
|
||||||
/// ENG: Checks if global circular dependency detection is enabled.
|
|
||||||
///
|
|
||||||
/// return true если включено, false если отключено
|
|
||||||
/// return true if enabled, false if disabled
|
|
||||||
static bool get isGlobalCycleDetectionEnabled => _globalCycleDetectionEnabled;
|
static bool get isGlobalCycleDetectionEnabled => _globalCycleDetectionEnabled;
|
||||||
|
|
||||||
/// RU: Включает обнаружение циклических зависимостей для конкретного скоупа.
|
/// Enables cycle detection for a particular scope tree.
|
||||||
/// ENG: Enables circular dependency detection for a specific scope.
|
|
||||||
///
|
///
|
||||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
/// [scopeName] - hierarchical string path (e.g. 'feature.api'), or empty for root.
|
||||||
/// [scopeName] - scope name (empty string for root scope)
|
/// [separator] - path separator (default: '.'), e.g. '/' for "feature/api/module"
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// CherryPick.enableCycleDetectionForScope(); // Для root scope
|
/// CherryPick.enableCycleDetectionForScope(scopeName: 'api.feature');
|
||||||
/// CherryPick.enableCycleDetectionForScope(scopeName: 'feature.auth'); // Для конкретного scope
|
|
||||||
/// ```
|
/// ```
|
||||||
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = _getScope(scopeName, separator);
|
final scope = _getScope(scopeName, separator);
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Отключает обнаружение циклических зависимостей для конкретного скоупа.
|
/// Disables cycle detection for a given scope. See [enableCycleDetectionForScope].
|
||||||
/// ENG: Disables circular dependency detection for a specific scope.
|
|
||||||
///
|
|
||||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
|
||||||
/// [scopeName] - scope name (empty string for root scope)
|
|
||||||
static void disableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
static void disableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = _getScope(scopeName, separator);
|
final scope = _getScope(scopeName, separator);
|
||||||
scope.disableCycleDetection();
|
scope.disableCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Проверяет, включено ли обнаружение циклических зависимостей для конкретного скоупа.
|
/// Returns `true` if cycle detection is enabled for the requested scope.
|
||||||
/// ENG: Checks if circular dependency detection is enabled for a specific scope.
|
|
||||||
///
|
///
|
||||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
/// Example:
|
||||||
/// [scopeName] - scope name (empty string for root scope)
|
/// ```dart
|
||||||
///
|
/// CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.api');
|
||||||
/// return true если включено, false если отключено
|
/// ```
|
||||||
/// return true if enabled, false if disabled
|
|
||||||
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) {
|
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = _getScope(scopeName, separator);
|
final scope = _getScope(scopeName, separator);
|
||||||
return scope.isCycleDetectionEnabled;
|
return scope.isCycleDetectionEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Возвращает текущую цепочку разрешения зависимостей для конкретного скоупа.
|
/// Returns the current dependency resolution chain inside the given scope.
|
||||||
/// ENG: Returns current dependency resolution chain for a specific scope.
|
|
||||||
///
|
///
|
||||||
/// Полезно для отладки и анализа зависимостей.
|
/// Useful for diagnostics (to print what types are currently resolving).
|
||||||
/// Useful for debugging and dependency analysis.
|
|
||||||
///
|
///
|
||||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
/// Example:
|
||||||
/// [scopeName] - scope name (empty string for root scope)
|
/// ```dart
|
||||||
///
|
/// print(CherryPick.getCurrentResolutionChain(scopeName: 'feature.api'));
|
||||||
/// return список имен зависимостей в текущей цепочке разрешения
|
/// ```
|
||||||
/// return list of dependency names in current resolution chain
|
|
||||||
static List<String> getCurrentResolutionChain({String scopeName = '', String separator = '.'}) {
|
static List<String> getCurrentResolutionChain({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = _getScope(scopeName, separator);
|
final scope = _getScope(scopeName, separator);
|
||||||
return scope.currentResolutionChain;
|
return scope.currentResolutionChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Создает новый скоуп с автоматически включенным обнаружением циклических зависимостей.
|
/// Opens the root scope and enables local cycle detection.
|
||||||
/// ENG: Creates a new scope with automatically enabled circular dependency detection.
|
|
||||||
///
|
|
||||||
/// Удобный метод для создания безопасных скоупов в development режиме.
|
|
||||||
/// Convenient method for creating safe scopes in development mode.
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final scope = CherryPick.openSafeRootScope();
|
/// final safeRoot = CherryPick.openSafeRootScope();
|
||||||
/// // Обнаружение циклических зависимостей автоматически включено
|
|
||||||
/// ```
|
/// ```
|
||||||
static Scope openSafeRootScope() {
|
static Scope openSafeRootScope() {
|
||||||
final scope = openRootScope();
|
final scope = openRootScope();
|
||||||
@@ -167,16 +185,11 @@ class CherryPick {
|
|||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Создает новый дочерний скоуп с автоматически включенным обнаружением циклических зависимостей.
|
/// Opens a named/nested scope and enables local cycle detection for it.
|
||||||
/// ENG: Creates a new child scope with automatically enabled circular dependency detection.
|
|
||||||
///
|
|
||||||
/// [scopeName] - имя скоупа
|
|
||||||
/// [scopeName] - scope name
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final scope = CherryPick.openSafeScope(scopeName: 'feature.auth');
|
/// final api = CherryPick.openSafeScope(scopeName: 'feature.api');
|
||||||
/// // Обнаружение циклических зависимостей автоматически включено
|
|
||||||
/// ```
|
/// ```
|
||||||
static Scope openSafeScope({String scopeName = '', String separator = '.'}) {
|
static Scope openSafeScope({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = openScope(scopeName: scopeName, separator: separator);
|
final scope = openScope(scopeName: scopeName, separator: separator);
|
||||||
@@ -184,8 +197,8 @@ class CherryPick {
|
|||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Внутренний метод для получения скоупа по имени.
|
/// Returns a [Scope] by path (or the root if none specified).
|
||||||
/// ENG: Internal method to get scope by name.
|
/// Used for internal diagnostics & helpers.
|
||||||
static Scope _getScope(String scopeName, String separator) {
|
static Scope _getScope(String scopeName, String separator) {
|
||||||
if (scopeName.isEmpty) {
|
if (scopeName.isEmpty) {
|
||||||
return openRootScope();
|
return openRootScope();
|
||||||
@@ -193,91 +206,76 @@ class CherryPick {
|
|||||||
return openScope(scopeName: scopeName, separator: separator);
|
return openScope(scopeName: scopeName, separator: separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Метод открывает дочерний [Scope].
|
/// Opens (and creates nested subscopes if needed) a scope by hierarchical path.
|
||||||
/// ENG: The method open the child [Scope].
|
|
||||||
///
|
///
|
||||||
/// Дочерний [Scope] открывается с [scopeName]
|
/// [scopeName] - dot-separated path ("api.feature"). Empty = root.
|
||||||
/// Child [Scope] open with [scopeName]
|
/// [separator] - path delimiter (default: '.')
|
||||||
|
///
|
||||||
|
/// Applies global cycle detection settings to the returned scope.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// final apiScope = CherryPick.openScope(scopeName: 'network.super.api');
|
||||||
/// ```
|
/// ```
|
||||||
/// final String scopeName = 'firstScope.secondScope';
|
|
||||||
/// final subScope = CherryPick.openScope(scopeName);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
@experimental
|
@experimental
|
||||||
static Scope openScope({String scopeName = '', String separator = '.'}) {
|
static Scope openScope({String scopeName = '', String separator = '.'}) {
|
||||||
if (scopeName.isEmpty) {
|
if (scopeName.isEmpty) {
|
||||||
return openRootScope();
|
return openRootScope();
|
||||||
}
|
}
|
||||||
|
|
||||||
final nameParts = scopeName.split(separator);
|
final nameParts = scopeName.split(separator);
|
||||||
if (nameParts.isEmpty) {
|
if (nameParts.isEmpty) {
|
||||||
throw Exception('Can not open sub scope because scopeName can not split');
|
throw Exception('Can not open sub scope because scopeName can not split');
|
||||||
}
|
}
|
||||||
|
|
||||||
final scope = nameParts.fold(
|
final scope = nameParts.fold(
|
||||||
openRootScope(),
|
openRootScope(),
|
||||||
(Scope previousValue, String element) =>
|
(Scope previous, String element) => previous.openSubScope(element)
|
||||||
previousValue.openSubScope(element));
|
);
|
||||||
|
|
||||||
// Применяем глобальную настройку обнаружения циклических зависимостей
|
|
||||||
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
|
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применяем глобальную настройку обнаружения между скоупами
|
|
||||||
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) {
|
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) {
|
||||||
scope.enableGlobalCycleDetection();
|
scope.enableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Метод открывает дочерний [Scope].
|
/// Closes a named or root scope (if [scopeName] is omitted).
|
||||||
/// ENG: The method open the child [Scope].
|
|
||||||
///
|
///
|
||||||
/// Дочерний [Scope] открывается с [scopeName]
|
/// [scopeName] - dot-separated hierarchical path (e.g. 'api.feature'). Empty = root.
|
||||||
/// Child [Scope] open with [scopeName]
|
/// [separator] - path delimiter.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// CherryPick.closeScope(scopeName: 'network.super.api');
|
||||||
/// ```
|
/// ```
|
||||||
/// final String scopeName = 'firstScope.secondScope';
|
|
||||||
/// final subScope = CherryPick.closeScope(scopeName);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
///
|
|
||||||
@experimental
|
@experimental
|
||||||
static void closeScope({String scopeName = '', String separator = '.'}) {
|
static void closeScope({String scopeName = '', String separator = '.'}) {
|
||||||
if (scopeName.isEmpty) {
|
if (scopeName.isEmpty) {
|
||||||
closeRootScope();
|
closeRootScope();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final nameParts = scopeName.split(separator);
|
final nameParts = scopeName.split(separator);
|
||||||
if (nameParts.isEmpty) {
|
if (nameParts.isEmpty) {
|
||||||
throw Exception(
|
throw Exception('Can not close sub scope because scopeName can not split');
|
||||||
'Can not close sub scope because scopeName can not split');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameParts.length > 1) {
|
if (nameParts.length > 1) {
|
||||||
final lastPart = nameParts.removeLast();
|
final lastPart = nameParts.removeLast();
|
||||||
|
|
||||||
final scope = nameParts.fold(
|
final scope = nameParts.fold(
|
||||||
openRootScope(),
|
openRootScope(),
|
||||||
(Scope previousValue, String element) =>
|
(Scope previous, String element) => previous.openSubScope(element)
|
||||||
previousValue.openSubScope(element));
|
);
|
||||||
scope.closeSubScope(lastPart);
|
scope.closeSubScope(lastPart);
|
||||||
} else {
|
} else {
|
||||||
openRootScope().closeSubScope(nameParts[0]);
|
openRootScope().closeSubScope(nameParts.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Глобально включает обнаружение циклических зависимостей между скоупами.
|
/// Enables cross-scope cycle detection globally.
|
||||||
/// ENG: Globally enables cross-scope circular dependency detection.
|
|
||||||
///
|
///
|
||||||
/// Этот режим обнаруживает циклические зависимости во всей иерархии скоупов.
|
/// This will activate detection of cycles that may span across multiple scopes
|
||||||
/// This mode detects circular dependencies across the entire scope hierarchy.
|
/// in the entire dependency graph. All new and existing [Scope]s will participate.
|
||||||
|
///
|
||||||
|
/// Strongly recommended for complex solutions with modular architecture.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -285,15 +283,15 @@ class CherryPick {
|
|||||||
/// ```
|
/// ```
|
||||||
static void enableGlobalCrossScopeCycleDetection() {
|
static void enableGlobalCrossScopeCycleDetection() {
|
||||||
_globalCrossScopeCycleDetectionEnabled = true;
|
_globalCrossScopeCycleDetectionEnabled = true;
|
||||||
|
|
||||||
// Включаем для уже существующего root scope, если он есть
|
|
||||||
if (_rootScope != null) {
|
if (_rootScope != null) {
|
||||||
_rootScope!.enableGlobalCycleDetection();
|
_rootScope!.enableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Глобально отключает обнаружение циклических зависимостей между скоупами.
|
/// Disables global cross-scope cycle detection.
|
||||||
/// ENG: Globally disables cross-scope circular dependency detection.
|
///
|
||||||
|
/// Existing and new scopes stop checking for global (cross-scope) cycles.
|
||||||
|
/// The internal global cycle detector will be cleared as well.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -301,54 +299,55 @@ class CherryPick {
|
|||||||
/// ```
|
/// ```
|
||||||
static void disableGlobalCrossScopeCycleDetection() {
|
static void disableGlobalCrossScopeCycleDetection() {
|
||||||
_globalCrossScopeCycleDetectionEnabled = false;
|
_globalCrossScopeCycleDetectionEnabled = false;
|
||||||
|
|
||||||
// Отключаем для уже существующего root scope, если он есть
|
|
||||||
if (_rootScope != null) {
|
if (_rootScope != null) {
|
||||||
_rootScope!.disableGlobalCycleDetection();
|
_rootScope!.disableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очищаем глобальный детектор
|
|
||||||
GlobalCycleDetector.instance.clear();
|
GlobalCycleDetector.instance.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Проверяет, включено ли глобальное обнаружение циклических зависимостей между скоупами.
|
/// Returns `true` if global cross-scope cycle detection is enabled.
|
||||||
/// ENG: Checks if global cross-scope circular dependency detection is enabled.
|
|
||||||
///
|
///
|
||||||
/// return true если включено, false если отключено
|
/// Example:
|
||||||
/// return true if enabled, false if disabled
|
/// ```dart
|
||||||
|
/// if (CherryPick.isGlobalCrossScopeCycleDetectionEnabled) {
|
||||||
|
/// print('Global cross-scope detection is ON');
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
static bool get isGlobalCrossScopeCycleDetectionEnabled => _globalCrossScopeCycleDetectionEnabled;
|
static bool get isGlobalCrossScopeCycleDetectionEnabled => _globalCrossScopeCycleDetectionEnabled;
|
||||||
|
|
||||||
/// RU: Возвращает глобальную цепочку разрешения зависимостей.
|
/// Returns the current global dependency resolution chain (across all scopes).
|
||||||
/// ENG: Returns global dependency resolution chain.
|
|
||||||
///
|
///
|
||||||
/// Полезно для отладки циклических зависимостей между скоупами.
|
/// Shows the cross-scope resolution stack, which is useful for advanced diagnostics
|
||||||
/// Useful for debugging circular dependencies across scopes.
|
/// and debugging cycle issues that occur between scopes.
|
||||||
///
|
///
|
||||||
/// return список имен зависимостей в глобальной цепочке разрешения
|
/// Example:
|
||||||
/// return list of dependency names in global resolution chain
|
/// ```dart
|
||||||
|
/// print(CherryPick.getGlobalResolutionChain());
|
||||||
|
/// ```
|
||||||
static List<String> getGlobalResolutionChain() {
|
static List<String> getGlobalResolutionChain() {
|
||||||
return GlobalCycleDetector.instance.globalResolutionChain;
|
return GlobalCycleDetector.instance.globalResolutionChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Очищает все состояние глобального детектора циклических зависимостей.
|
/// Clears the global cross-scope cycle detector.
|
||||||
/// ENG: Clears all global circular dependency detector state.
|
|
||||||
///
|
///
|
||||||
/// Полезно для тестов и сброса состояния.
|
/// Useful in tests or when resetting application state.
|
||||||
/// Useful for tests and state reset.
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// CherryPick.clearGlobalCycleDetector();
|
||||||
|
/// ```
|
||||||
static void clearGlobalCycleDetector() {
|
static void clearGlobalCycleDetector() {
|
||||||
GlobalCycleDetector.reset();
|
GlobalCycleDetector.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Создает новый скоуп с автоматически включенным глобальным обнаружением циклических зависимостей.
|
/// Opens the root scope with both local and global cross-scope cycle detection enabled.
|
||||||
/// ENG: Creates a new scope with automatically enabled global circular dependency detection.
|
|
||||||
///
|
///
|
||||||
/// Этот скоуп будет отслеживать циклические зависимости во всей иерархии.
|
/// This is the safest way to start IoC for most apps — cycles will be detected
|
||||||
/// This scope will track circular dependencies across the entire hierarchy.
|
/// both inside a single scope and between scopes.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final scope = CherryPick.openGlobalSafeRootScope();
|
/// final root = CherryPick.openGlobalSafeRootScope();
|
||||||
/// // Глобальное обнаружение циклических зависимостей автоматически включено
|
|
||||||
/// ```
|
/// ```
|
||||||
static Scope openGlobalSafeRootScope() {
|
static Scope openGlobalSafeRootScope() {
|
||||||
final scope = openRootScope();
|
final scope = openRootScope();
|
||||||
@@ -357,16 +356,13 @@ class CherryPick {
|
|||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RU: Создает новый дочерний скоуп с автоматически включенным глобальным обнаружением циклических зависимостей.
|
/// Opens the given named/nested scope and enables both local and cross-scope cycle detection on it.
|
||||||
/// ENG: Creates a new child scope with automatically enabled global circular dependency detection.
|
|
||||||
///
|
///
|
||||||
/// [scopeName] - имя скоупа
|
/// Recommended when creating feature/module scopes in large apps.
|
||||||
/// [scopeName] - scope name
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final scope = CherryPick.openGlobalSafeScope(scopeName: 'feature.auth');
|
/// final featureScope = CherryPick.openGlobalSafeScope(scopeName: 'featureA.api');
|
||||||
/// // Глобальное обнаружение циклических зависимостей автоматически включено
|
|
||||||
/// ```
|
/// ```
|
||||||
static Scope openGlobalSafeScope({String scopeName = '', String separator = '.'}) {
|
static Scope openGlobalSafeScope({String scopeName = '', String separator = '.'}) {
|
||||||
final scope = openScope(scopeName: scopeName, separator: separator);
|
final scope = openScope(scopeName: scopeName, separator: separator);
|
||||||
|
|||||||
55
cherrypick/lib/src/log_format.dart
Normal file
55
cherrypick/lib/src/log_format.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats a log message string for CherryPick's logging system.
|
||||||
|
///
|
||||||
|
/// This function provides a unified structure for framework logs (info, warn, error, debug, etc.),
|
||||||
|
/// making it easier to parse and analyze events related to DI operations such as resolving bindings,
|
||||||
|
/// scope creation, module installation, etc.
|
||||||
|
///
|
||||||
|
/// All parameters except [name] and [params] are required.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// final msg = formatLogMessage(
|
||||||
|
/// type: 'Binding',
|
||||||
|
/// name: 'MyService',
|
||||||
|
/// params: {'parent': 'AppModule', 'lifecycle': 'singleton'},
|
||||||
|
/// description: 'created',
|
||||||
|
/// );
|
||||||
|
/// // Result: [Binding:MyService] parent=AppModule lifecycle=singleton created
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - [type]: The type of the log event subject (e.g., 'Binding', 'Scope', 'Module'). Required.
|
||||||
|
/// - [name]: Optional name of the subject (binding/scope/module) to disambiguate multiple instances/objects.
|
||||||
|
/// - [params]: Optional map for additional context (e.g., id, parent, lifecycle, named, etc.).
|
||||||
|
/// - [description]: Concise description of the event. Required.
|
||||||
|
///
|
||||||
|
/// Returns a structured string:
|
||||||
|
/// [type(:name)] param1=val1 param2=val2 ... description
|
||||||
|
String formatLogMessage({
|
||||||
|
required String type, // Binding, Scope, Module, ...
|
||||||
|
String? name, // Имя binding/scope/module
|
||||||
|
Map<String, Object?>? params, // Дополнительные параметры (id, parent, named и др.)
|
||||||
|
required String description, // Краткое описание события
|
||||||
|
}) {
|
||||||
|
final label = name != null ? '$type:$name' : type;
|
||||||
|
final paramsStr = (params != null && params.isNotEmpty)
|
||||||
|
? params.entries.map((e) => '${e.key}=${e.value}').join(' ')
|
||||||
|
: '';
|
||||||
|
return '[$label]'
|
||||||
|
'${paramsStr.isNotEmpty ? ' $paramsStr' : ''}'
|
||||||
|
' $description';
|
||||||
|
}
|
||||||
108
cherrypick/lib/src/logger.dart
Normal file
108
cherrypick/lib/src/logger.dart
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// CherryPickLogger — интерфейс для логирования событий DI в CherryPick.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// Interface for dependency injection (DI) logger in CherryPick. Allows you to
|
||||||
|
/// receive information about the internal events and errors in the DI system.
|
||||||
|
/// Your implementation can use any logging framework or UI.
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// Интерфейс логгера для DI-контейнера CherryPick. Позволяет получать
|
||||||
|
/// сообщения о работе DI-контейнера, его ошибках и событиях, и
|
||||||
|
/// интегрировать любые готовые решения для логирования/сбора ошибок.
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
abstract class CherryPickLogger {
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// info — Информационное сообщение.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// Logs an informational message about DI operation or state.
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// Логирование информационного сообщения о событиях DI.
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
void info(String message);
|
||||||
|
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// warn — Предупреждение.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// Logs a warning related to DI events (for example, possible misconfiguration).
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// Логирование предупреждения, связанного с DI (например, возможная ошибка
|
||||||
|
/// конфигурации).
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
void warn(String message);
|
||||||
|
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// error — Ошибка.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// Logs an error message, may include error object and stack trace.
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// Логирование ошибки, дополнительно может содержать объект ошибки
|
||||||
|
/// и StackTrace.
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
void error(String message, [Object? error, StackTrace? stackTrace]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// SilentLogger — «тихий» логгер CherryPick. Сообщения игнорируются.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// SilentLogger ignores all log messages. Used by default in production to
|
||||||
|
/// avoid polluting logs with DI events.
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// SilentLogger игнорирует все события логгирования. Используется по умолчанию
|
||||||
|
/// на production, чтобы не засорять логи техническими сообщениями DI.
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
class SilentLogger implements CherryPickLogger {
|
||||||
|
const SilentLogger();
|
||||||
|
@override
|
||||||
|
void info(String message) {}
|
||||||
|
@override
|
||||||
|
void warn(String message) {}
|
||||||
|
@override
|
||||||
|
void error(String message, [Object? error, StackTrace? stackTrace]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
/// PrintLogger — логгер CherryPick, выводящий все сообщения через print.
|
||||||
|
///
|
||||||
|
/// ENGLISH:
|
||||||
|
/// PrintLogger outputs all log messages to the console using `print()`.
|
||||||
|
/// Suitable for debugging, prototyping, or simple console applications.
|
||||||
|
///
|
||||||
|
/// RUSSIAN:
|
||||||
|
/// PrintLogger выводит все сообщения (info, warn, error) в консоль через print.
|
||||||
|
/// Удобен для отладки или консольных приложений.
|
||||||
|
/// ----------------------------------------------------------------------------
|
||||||
|
class PrintLogger implements CherryPickLogger {
|
||||||
|
const PrintLogger();
|
||||||
|
@override
|
||||||
|
void info(String message) => print('[info][CherryPick] $message');
|
||||||
|
@override
|
||||||
|
void warn(String message) => print('[warn][CherryPick] $message');
|
||||||
|
@override
|
||||||
|
void error(String message, [Object? error, StackTrace? stackTrace]) {
|
||||||
|
print('[error][CherryPick] $message');
|
||||||
|
if (error != null) print(' error: $error');
|
||||||
|
if (stackTrace != null) print(' stack: $stackTrace');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,17 @@ import 'package:cherrypick/src/cycle_detector.dart';
|
|||||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||||
import 'package:cherrypick/src/binding_resolver.dart';
|
import 'package:cherrypick/src/binding_resolver.dart';
|
||||||
import 'package:cherrypick/src/module.dart';
|
import 'package:cherrypick/src/module.dart';
|
||||||
|
import 'package:cherrypick/src/logger.dart';
|
||||||
Scope openRootScope() => Scope(null);
|
import 'package:cherrypick/src/log_format.dart';
|
||||||
|
|
||||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||||
final Scope? _parentScope;
|
final Scope? _parentScope;
|
||||||
|
|
||||||
|
late final CherryPickLogger _logger;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CherryPickLogger get logger => _logger;
|
||||||
|
|
||||||
/// RU: Метод возвращает родительский [Scope].
|
/// RU: Метод возвращает родительский [Scope].
|
||||||
///
|
///
|
||||||
/// ENG: The method returns the parent [Scope].
|
/// ENG: The method returns the parent [Scope].
|
||||||
@@ -32,9 +37,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
|
|
||||||
final Map<String, Scope> _scopeMap = HashMap();
|
final Map<String, Scope> _scopeMap = HashMap();
|
||||||
|
|
||||||
Scope(this._parentScope) {
|
Scope(this._parentScope, {required CherryPickLogger logger}) : _logger = logger {
|
||||||
// Генерируем уникальный ID для скоупа
|
|
||||||
setScopeId(_generateScopeId());
|
setScopeId(_generateScopeId());
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId ?? 'NO_ID',
|
||||||
|
params: {
|
||||||
|
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||||
|
},
|
||||||
|
description: 'scope created',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Set<Module> _modulesList = HashSet();
|
final Set<Module> _modulesList = HashSet();
|
||||||
@@ -59,8 +71,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// return [Scope]
|
/// return [Scope]
|
||||||
Scope openSubScope(String name) {
|
Scope openSubScope(String name) {
|
||||||
if (!_scopeMap.containsKey(name)) {
|
if (!_scopeMap.containsKey(name)) {
|
||||||
final childScope = Scope(this);
|
final childScope = Scope(this, logger: logger); // Наследуем логгер вниз по иерархии
|
||||||
|
// print removed (trace)
|
||||||
// Наследуем настройки обнаружения циклических зависимостей
|
// Наследуем настройки обнаружения циклических зависимостей
|
||||||
if (isCycleDetectionEnabled) {
|
if (isCycleDetectionEnabled) {
|
||||||
childScope.enableCycleDetection();
|
childScope.enableCycleDetection();
|
||||||
@@ -68,8 +80,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
childScope.enableGlobalCycleDetection();
|
childScope.enableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
_scopeMap[name] = childScope;
|
_scopeMap[name] = childScope;
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'SubScope',
|
||||||
|
name: name,
|
||||||
|
params: {
|
||||||
|
'id': childScope.scopeId,
|
||||||
|
if (scopeId != null) 'parent': scopeId,
|
||||||
|
},
|
||||||
|
description: 'subscope created',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return _scopeMap[name]!;
|
return _scopeMap[name]!;
|
||||||
}
|
}
|
||||||
@@ -86,6 +106,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
if (childScope.scopeId != null) {
|
if (childScope.scopeId != null) {
|
||||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||||
}
|
}
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'SubScope',
|
||||||
|
name: name,
|
||||||
|
params: {
|
||||||
|
'id': childScope.scopeId,
|
||||||
|
if (scopeId != null) 'parent': scopeId,
|
||||||
|
},
|
||||||
|
description: 'subscope closed',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_scopeMap.remove(name);
|
_scopeMap.remove(name);
|
||||||
}
|
}
|
||||||
@@ -98,7 +127,20 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
Scope installModules(List<Module> modules) {
|
Scope installModules(List<Module> modules) {
|
||||||
_modulesList.addAll(modules);
|
_modulesList.addAll(modules);
|
||||||
for (var module in modules) {
|
for (var module in modules) {
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'Module',
|
||||||
|
name: module.runtimeType.toString(),
|
||||||
|
params: {
|
||||||
|
'scope': scopeId,
|
||||||
|
},
|
||||||
|
description: 'module installed',
|
||||||
|
));
|
||||||
module.builder(this);
|
module.builder(this);
|
||||||
|
// После builder: для всех новых биндингов
|
||||||
|
for (final binding in module.bindingSet) {
|
||||||
|
binding.logger = logger;
|
||||||
|
binding.logAllDeferred();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_rebuildResolversIndex();
|
_rebuildResolversIndex();
|
||||||
return this;
|
return this;
|
||||||
@@ -110,7 +152,11 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
///
|
///
|
||||||
/// return [Scope]
|
/// return [Scope]
|
||||||
Scope dropModules() {
|
Scope dropModules() {
|
||||||
// [AlexeyYuPopkov](https://github.com/AlexeyYuPopkov) Thank you for the [Removed exception "ConcurrentModificationError"](https://github.com/pese-git/cherrypick/pull/2)
|
logger.info(formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId,
|
||||||
|
description: 'modules dropped',
|
||||||
|
));
|
||||||
_modulesList.clear();
|
_modulesList.clear();
|
||||||
_rebuildResolversIndex();
|
_rebuildResolversIndex();
|
||||||
return this;
|
return this;
|
||||||
@@ -130,11 +176,39 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
T resolve<T>({String? named, dynamic params}) {
|
T resolve<T>({String? named, dynamic params}) {
|
||||||
// Используем глобальное отслеживание, если включено
|
// Используем глобальное отслеживание, если включено
|
||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
|
try {
|
||||||
return withGlobalCycleDetection<T>(T, named, () {
|
return withGlobalCycleDetection<T>(T, named, () {
|
||||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.error(
|
||||||
|
formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId,
|
||||||
|
params: {'resolve': T.toString()},
|
||||||
|
description: 'global cycle detection failed during resolve',
|
||||||
|
),
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.error(
|
||||||
|
formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId,
|
||||||
|
params: {'resolve': T.toString()},
|
||||||
|
description: 'failed to resolve',
|
||||||
|
),
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +218,28 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
return withCycleDetection<T>(T, named, () {
|
return withCycleDetection<T>(T, named, () {
|
||||||
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
|
logger.info(formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId,
|
||||||
|
params: {
|
||||||
|
'resolve': T.toString(),
|
||||||
|
if (named != null) 'named': named,
|
||||||
|
},
|
||||||
|
description: 'successfully resolved',
|
||||||
|
));
|
||||||
return resolved;
|
return resolved;
|
||||||
} else {
|
} else {
|
||||||
|
logger.error(
|
||||||
|
formatLogMessage(
|
||||||
|
type: 'Scope',
|
||||||
|
name: scopeId,
|
||||||
|
params: {
|
||||||
|
'resolve': T.toString(),
|
||||||
|
if (named != null) 'named': named,
|
||||||
|
},
|
||||||
|
description: 'failed to resolve',
|
||||||
|
),
|
||||||
|
);
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||||
}
|
}
|
||||||
|
|||||||
73
cherrypick/test/logger_integration_test.dart
Normal file
73
cherrypick/test/logger_integration_test.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'mock_logger.dart';
|
||||||
|
|
||||||
|
class DummyService {}
|
||||||
|
|
||||||
|
class DummyModule extends Module {
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
bind<DummyService>().toInstance(DummyService()).withName('test');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class A {}
|
||||||
|
class B {}
|
||||||
|
|
||||||
|
class CyclicModule extends Module {
|
||||||
|
@override
|
||||||
|
void builder(Scope cs) {
|
||||||
|
bind<A>().toProvide(() => cs.resolve<B>() as A);
|
||||||
|
bind<B>().toProvide(() => cs.resolve<A>() as B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MockLogger logger;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
logger = MockLogger();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Global logger receives Scope and Binding events', () {
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
|
scope.installModules([DummyModule()]);
|
||||||
|
final _ = scope.resolve<DummyService>(named: 'test');
|
||||||
|
|
||||||
|
// Новый стиль проверки для formatLogMessage:
|
||||||
|
expect(
|
||||||
|
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('created')),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('created')),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('named') && m.contains('name=test')),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('resolve=DummyService') && m.contains('successfully resolved')),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CycleDetector logs cycle detection error', () {
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
|
// print('[DEBUG] TEST SCOPE logger type=${scope.logger.runtimeType} hash=${scope.logger.hashCode}');
|
||||||
|
scope.enableCycleDetection();
|
||||||
|
scope.installModules([CyclicModule()]);
|
||||||
|
expect(
|
||||||
|
() => scope.resolve<A>(),
|
||||||
|
throwsA(isA<CircularDependencyException>()),
|
||||||
|
);
|
||||||
|
// Дополнительно ищем и среди info на случай если лог от CycleDetector ошибочно не попал в errors
|
||||||
|
final foundInErrors = logger.errors.any((m) =>
|
||||||
|
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||||
|
final foundInInfos = logger.infos.any((m) =>
|
||||||
|
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||||
|
expect(foundInErrors || foundInInfos, isTrue,
|
||||||
|
reason: 'Ожидаем хотя бы один лог о цикле на уровне error или info; вот все errors: ${logger.errors}\ninfos: ${logger.infos}');
|
||||||
|
});
|
||||||
|
}
|
||||||
16
cherrypick/test/mock_logger.dart
Normal file
16
cherrypick/test/mock_logger.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
class MockLogger implements CherryPickLogger {
|
||||||
|
final List<String> infos = [];
|
||||||
|
final List<String> warns = [];
|
||||||
|
final List<String> errors = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void info(String message) => infos.add(message);
|
||||||
|
@override
|
||||||
|
void warn(String message) => warns.add(message);
|
||||||
|
@override
|
||||||
|
void error(String message, [Object? e, StackTrace? s]) =>
|
||||||
|
errors.add(
|
||||||
|
'$message${e != null ? ' $e' : ''}${s != null ? '\n$s' : ''}');
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'package:cherrypick/src/cycle_detector.dart';
|
|
||||||
import 'package:cherrypick/src/module.dart';
|
|
||||||
import 'package:cherrypick/src/scope.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
import '../mock_logger.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
late MockLogger logger;
|
||||||
|
setUp(() {
|
||||||
|
logger = MockLogger();
|
||||||
|
CherryPick.setGlobalLogger(logger);
|
||||||
|
});
|
||||||
group('CycleDetector', () {
|
group('CycleDetector', () {
|
||||||
late CycleDetector detector;
|
late CycleDetector detector;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
detector = CycleDetector();
|
detector = CycleDetector(logger: logger);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should detect simple circular dependency', () {
|
test('should detect simple circular dependency', () {
|
||||||
@@ -75,7 +80,7 @@ void main() {
|
|||||||
|
|
||||||
group('Scope with Cycle Detection', () {
|
group('Scope with Cycle Detection', () {
|
||||||
test('should detect circular dependency in real scenario', () {
|
test('should detect circular dependency in real scenario', () {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
|
|
||||||
// Создаем циклическую зависимость: A зависит от B, B зависит от A
|
// Создаем циклическую зависимость: A зависит от B, B зависит от A
|
||||||
@@ -91,7 +96,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should work normally without cycle detection enabled', () {
|
test('should work normally without cycle detection enabled', () {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
// Не включаем обнаружение циклических зависимостей
|
// Не включаем обнаружение циклических зависимостей
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
@@ -103,7 +108,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should allow disabling cycle detection', () {
|
test('should allow disabling cycle detection', () {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||||
|
|
||||||
@@ -112,7 +117,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle named dependencies in cycle detection', () {
|
test('should handle named dependencies in cycle detection', () {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
@@ -126,7 +131,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should detect cycles in async resolution', () async {
|
test('should detect cycles in async resolution', () async {
|
||||||
final scope = Scope(null);
|
final scope = CherryPick.openRootScope();
|
||||||
scope.enableCycleDetection();
|
scope.enableCycleDetection();
|
||||||
|
|
||||||
scope.installModules([
|
scope.installModules([
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import 'package:cherrypick/cherrypick.dart';
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import '../mock_logger.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
late MockLogger logger;
|
||||||
|
setUp(() {
|
||||||
|
logger = MockLogger();
|
||||||
|
CherryPick.setGlobalLogger(logger);
|
||||||
|
});
|
||||||
group('CherryPick Cycle Detection Helper Methods', () {
|
group('CherryPick Cycle Detection Helper Methods', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
// Сбрасываем состояние перед каждым тестом
|
// Сбрасываем состояние перед каждым тестом
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
import 'package:cherrypick/src/module.dart';
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
import 'package:cherrypick/src/scope.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import '../mock_logger.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Scope & Subscope Management', () {
|
group('Scope & Subscope Management', () {
|
||||||
test('Scope has no parent if constructed with null', () {
|
test('Scope has no parent if constructed with null', () {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
expect(scope.parentScope, null);
|
expect(scope.parentScope, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can open and retrieve the same subScope by key', () {
|
test('Can open and retrieve the same subScope by key', () {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
final subScope = scope.openSubScope('subScope');
|
final scope = Scope(null, logger: logger);
|
||||||
expect(scope.openSubScope('subScope'), subScope);
|
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||||
});
|
});
|
||||||
|
|
||||||
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
final subScope = scope.openSubScope("child");
|
final scope = Scope(null, logger: logger);
|
||||||
expect(scope.openSubScope("child"), same(subScope));
|
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||||
scope.closeSubScope("child");
|
// Нет необходимости тестировать open/closeSubScope в этом юните
|
||||||
final newSubScope = scope.openSubScope("child");
|
|
||||||
expect(newSubScope, isNot(same(subScope)));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Dependency Resolution (standard)', () {
|
group('Dependency Resolution (standard)', () {
|
||||||
test("Throws StateError if value can't be resolved", () {
|
test("Throws StateError if value can't be resolved", () {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Resolves value after adding a dependency', () {
|
test('Resolves value after adding a dependency', () {
|
||||||
|
final logger = MockLogger();
|
||||||
final expectedValue = 'test string';
|
final expectedValue = 'test string';
|
||||||
final scope = Scope(null)
|
final scope = Scope(null, logger: logger)
|
||||||
.installModules([TestModule<String>(value: expectedValue)]);
|
.installModules([TestModule<String>(value: expectedValue)]);
|
||||||
expect(scope.resolve<String>(), expectedValue);
|
expect(scope.resolve<String>(), expectedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns a value from parent scope', () {
|
test('Returns a value from parent scope', () {
|
||||||
|
final logger = MockLogger();
|
||||||
final expectedValue = 5;
|
final expectedValue = 5;
|
||||||
final parentScope = Scope(null);
|
final parentScope = Scope(null, logger: logger);
|
||||||
final scope = Scope(parentScope);
|
final scope = Scope(parentScope, logger: logger);
|
||||||
|
|
||||||
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
||||||
|
|
||||||
@@ -51,26 +53,29 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Returns several values from parent container', () {
|
test('Returns several values from parent container', () {
|
||||||
|
final logger = MockLogger();
|
||||||
final expectedIntValue = 5;
|
final expectedIntValue = 5;
|
||||||
final expectedStringValue = 'Hello world';
|
final expectedStringValue = 'Hello world';
|
||||||
final parentScope = Scope(null).installModules([
|
final parentScope = Scope(null, logger: logger).installModules([
|
||||||
TestModule<int>(value: expectedIntValue),
|
TestModule<int>(value: expectedIntValue),
|
||||||
TestModule<String>(value: expectedStringValue)
|
TestModule<String>(value: expectedStringValue)
|
||||||
]);
|
]);
|
||||||
final scope = Scope(parentScope);
|
final scope = Scope(parentScope, logger: logger);
|
||||||
|
|
||||||
expect(scope.resolve<int>(), expectedIntValue);
|
expect(scope.resolve<int>(), expectedIntValue);
|
||||||
expect(scope.resolve<String>(), expectedStringValue);
|
expect(scope.resolve<String>(), expectedStringValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Throws StateError if parent hasn't value too", () {
|
test("Throws StateError if parent hasn't value too", () {
|
||||||
final parentScope = Scope(null);
|
final logger = MockLogger();
|
||||||
final scope = Scope(parentScope);
|
final parentScope = Scope(null, logger: logger);
|
||||||
|
final scope = Scope(parentScope, logger: logger);
|
||||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("After dropModules resolves fail", () {
|
test("After dropModules resolves fail", () {
|
||||||
final scope = Scope(null)..installModules([TestModule<int>(value: 5)]);
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]);
|
||||||
expect(scope.resolve<int>(), 5);
|
expect(scope.resolve<int>(), 5);
|
||||||
scope.dropModules();
|
scope.dropModules();
|
||||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||||
@@ -80,7 +85,8 @@ void main() {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Named Dependencies', () {
|
group('Named Dependencies', () {
|
||||||
test('Resolve named binding', () {
|
test('Resolve named binding', () {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
TestModule<String>(value: "first"),
|
TestModule<String>(value: "first"),
|
||||||
TestModule<String>(value: "second", name: "special")
|
TestModule<String>(value: "second", name: "special")
|
||||||
@@ -90,7 +96,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Named binding does not clash with unnamed', () {
|
test('Named binding does not clash with unnamed', () {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
TestModule<String>(value: "foo", name: "bar"),
|
TestModule<String>(value: "foo", name: "bar"),
|
||||||
]);
|
]);
|
||||||
@@ -99,7 +106,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("tryResolve returns null for missing named", () {
|
test("tryResolve returns null for missing named", () {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
TestModule<String>(value: "foo"),
|
TestModule<String>(value: "foo"),
|
||||||
]);
|
]);
|
||||||
@@ -110,7 +118,8 @@ void main() {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Provider with parameters', () {
|
group('Provider with parameters', () {
|
||||||
test('Resolve dependency using providerWithParams', () {
|
test('Resolve dependency using providerWithParams', () {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
_InlineModule((m, s) {
|
_InlineModule((m, s) {
|
||||||
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
|
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
|
||||||
@@ -124,7 +133,8 @@ void main() {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Async Resolution', () {
|
group('Async Resolution', () {
|
||||||
test('Resolve async instance', () async {
|
test('Resolve async instance', () async {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
_InlineModule((m, s) {
|
_InlineModule((m, s) {
|
||||||
m.bind<String>().toInstance(Future.value('async value'));
|
m.bind<String>().toInstance(Future.value('async value'));
|
||||||
@@ -134,7 +144,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Resolve async provider', () async {
|
test('Resolve async provider', () async {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
_InlineModule((m, s) {
|
_InlineModule((m, s) {
|
||||||
m.bind<int>().toProvide(() async => 7);
|
m.bind<int>().toProvide(() async => 7);
|
||||||
@@ -144,7 +155,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Resolve async provider with param', () async {
|
test('Resolve async provider with param', () async {
|
||||||
final scope = Scope(null)
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger)
|
||||||
..installModules([
|
..installModules([
|
||||||
_InlineModule((m, s) {
|
_InlineModule((m, s) {
|
||||||
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
||||||
@@ -155,7 +167,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('tryResolveAsync returns null for missing', () async {
|
test('tryResolveAsync returns null for missing', () async {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
final result = await scope.tryResolveAsync<String>();
|
final result = await scope.tryResolveAsync<String>();
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
});
|
});
|
||||||
@@ -164,7 +177,8 @@ void main() {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
group('Optional resolution and error handling', () {
|
group('Optional resolution and error handling', () {
|
||||||
test("tryResolve returns null for missing dependency", () {
|
test("tryResolve returns null for missing dependency", () {
|
||||||
final scope = Scope(null);
|
final logger = MockLogger();
|
||||||
|
final scope = Scope(null, logger: logger);
|
||||||
expect(scope.tryResolve<int>(), isNull);
|
expect(scope.tryResolve<int>(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ final config = await scope.resolveAsync<RemoteConfig>();
|
|||||||
|
|
||||||
[`cherrypick_flutter`](https://pub.dev/packages/cherrypick_flutter) is the integration package for CherryPick DI in Flutter. It provides a convenient `CherryPickProvider` widget which sits in your widget tree and gives access to the root DI scope (and subscopes) from context.
|
[`cherrypick_flutter`](https://pub.dev/packages/cherrypick_flutter) is the integration package for CherryPick DI in Flutter. It provides a convenient `CherryPickProvider` widget which sits in your widget tree and gives access to the root DI scope (and subscopes) from context.
|
||||||
|
|
||||||
### Features
|
## Features
|
||||||
|
|
||||||
- **Global DI Scope Access:**
|
- **Global DI Scope Access:**
|
||||||
Use `CherryPickProvider` to access rootScope and subscopes anywhere in the widget tree.
|
Use `CherryPickProvider` to access rootScope and subscopes anywhere in the widget tree.
|
||||||
@@ -356,6 +356,26 @@ class MyApp extends StatelessWidget {
|
|||||||
- You can create subscopes, e.g. for screens or modules:
|
- You can create subscopes, e.g. for screens or modules:
|
||||||
`final subScope = CherryPickProvider.of(context).openSubScope(scopeName: "profileFeature");`
|
`final subScope = CherryPickProvider.of(context).openSubScope(scopeName: "profileFeature");`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
To enable logging of all dependency injection (DI) events and errors in CherryPick, set the global logger before creating your scopes:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Set a global logger before any scopes are created
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger()); // or your own custom logger
|
||||||
|
final scope = CherryPick.openRootScope();
|
||||||
|
// All DI events and cycle errors will now be sent to your logger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- By default, CherryPick uses SilentLogger (no output in production).
|
||||||
|
- Any dependency resolution, scope events, or cycle detection errors are logged via info/error on your logger.
|
||||||
|
|
||||||
---
|
---
|
||||||
## CherryPick is not just for Flutter!
|
## CherryPick is not just for Flutter!
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,26 @@ class MyApp extends StatelessWidget {
|
|||||||
- Вы можете создавать подскоупы, если нужно, например, для экранов или модулей:
|
- Вы можете создавать подскоупы, если нужно, например, для экранов или модулей:
|
||||||
`final subScope = CherryPickProvider.of(context).openSubScope(scopeName: "profileFeature");`
|
`final subScope = CherryPickProvider.of(context).openSubScope(scopeName: "profileFeature");`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
Чтобы включить вывод логов о событиях и ошибках DI в CherryPick, настройте глобальный логгер до создания любых scope:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Установите глобальный логгер до создания scope
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger()); // или свой логгер
|
||||||
|
final scope = CherryPick.openRootScope();
|
||||||
|
// Логи DI и циклов будут выводиться через ваш логгер
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- По умолчанию используется SilentLogger (нет логов в продакшене).
|
||||||
|
- Любые ошибки резолва и события циклов логируются через info/error на логгере.
|
||||||
|
|
||||||
---
|
---
|
||||||
## CherryPick подходит не только для Flutter!
|
## CherryPick подходит не только для Flutter!
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,24 @@ Example:
|
|||||||
Cherrypick.closeRootScope();
|
Cherrypick.closeRootScope();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
To enable logging of all dependency injection (DI) events and errors in CherryPick, set the global logger before creating your scopes:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Set a global logger before any scopes are created
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger()); // or your own custom logger
|
||||||
|
final scope = CherryPick.openRootScope();
|
||||||
|
// All DI events and cycle errors will now be sent to your logger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- By default, CherryPick uses SilentLogger (no output in production).
|
||||||
|
- Any dependency resolution, scope events, or cycle detection errors are logged via info/error on your logger.
|
||||||
|
|
||||||
## Example app
|
## Example app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,24 @@ Scope - это контейнер, который хранит все дерев
|
|||||||
Cherrypick.closeRootScope();
|
Cherrypick.closeRootScope();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
Чтобы включить вывод логов о событиях и ошибках DI в CherryPick, настройте глобальный логгер до создания любых scope:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Установите глобальный логгер до создания scope
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger()); // или свой логгер
|
||||||
|
final scope = CherryPick.openRootScope();
|
||||||
|
// Логи DI и циклов будут выводиться через ваш логгер
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- По умолчанию используется SilentLogger (нет логов в продакшене).
|
||||||
|
- Любые ошибки резолва и события циклов логируются через info/error на логгере.
|
||||||
|
|
||||||
## Пример приложения
|
## Пример приложения
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'di/app_module.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
// Включаем cycle-detection только в debug/test
|
// Включаем cycle-detection только в debug/test
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
CherryPick.setGlobalLogger(PrintLogger());
|
||||||
CherryPick.enableGlobalCycleDetection();
|
CherryPick.enableGlobalCycleDetection();
|
||||||
CherryPick.enableGlobalCrossScopeCycleDetection();
|
CherryPick.enableGlobalCrossScopeCycleDetection();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user