mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 13:47:24 +00:00
feat(core): refactor root scope API, improve logger injection, helpers, and tests
- BREAKING CHANGE: introduce CherryPick.openRootScope - add logger injection to Scope - refactor helper and scope logic - improve internal logging - enhance and update tests - add log_format.dart module
This commit is contained in:
@@ -47,7 +47,7 @@ class FeatureModule extends Module {
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
final scope = openRootScope().installModules([AppModule()]);
|
||||
final scope = CherryPick.openRootScope().installModules([AppModule()]);
|
||||
|
||||
final subScope = scope
|
||||
.openSubScope("featureScope")
|
||||
|
||||
@@ -126,7 +126,7 @@ void main() {
|
||||
// Example 1: Demonstrate circular dependency
|
||||
print('1. Attempt to create a scope with circular dependencies:');
|
||||
try {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
|
||||
|
||||
scope.installModules([
|
||||
@@ -144,7 +144,7 @@ void main() {
|
||||
// Example 2: Without circular dependency detection (dangerous!)
|
||||
print('2. Same code without circular dependency detection:');
|
||||
try {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
// НЕ включаем обнаружение циклических зависимостей
|
||||
|
||||
scope.installModules([
|
||||
@@ -166,7 +166,7 @@ void main() {
|
||||
// Example 3: Correct architecture without circular dependencies
|
||||
print('3. Correct architecture without circular dependencies:');
|
||||
try {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection(); // Включаем для безопасности
|
||||
|
||||
scope.installModules([
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:cherrypick/src/binding_resolver.dart';
|
||||
/// 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> {
|
||||
late Type _key;
|
||||
@@ -38,21 +39,36 @@ class Binding<T> {
|
||||
|
||||
void markCreated() {
|
||||
if (!_createdLogged) {
|
||||
logger?.info('Binding<$T> created');
|
||||
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('Binding<$T> named as [$_name]');
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {'name': _name},
|
||||
description: 'named',
|
||||
));
|
||||
_namedLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markSingleton() {
|
||||
if (isSingleton && !_singletonLogged) {
|
||||
logger?.info('Binding<$T> singleton mode enabled');
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: _name != null ? {'name': _name} : null,
|
||||
description: 'singleton mode enabled',
|
||||
));
|
||||
_singletonLogged = true;
|
||||
}
|
||||
}
|
||||
@@ -154,9 +170,25 @@ class Binding<T> {
|
||||
T? resolveSync([dynamic params]) {
|
||||
final res = resolver?.resolveSync(params);
|
||||
if (res != null) {
|
||||
logger?.info('Binding<$T> resolveSync => object created/resolved.');
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
},
|
||||
description: 'object created/resolved',
|
||||
));
|
||||
} else {
|
||||
logger?.warn('Binding<$T> resolveSync => returned null!');
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
},
|
||||
description: 'resolveSync returned null',
|
||||
));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -164,10 +196,39 @@ class Binding<T> {
|
||||
Future<T>? resolveAsync([dynamic params]) {
|
||||
final future = resolver?.resolveAsync(params);
|
||||
if (future != null) {
|
||||
future.then((res) => logger?.info('Binding<$T> resolveAsync => Future resolved'))
|
||||
.catchError((e, s) => logger?.error('Binding<$T> resolveAsync error', e, s));
|
||||
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('Binding<$T> resolveAsync => returned null!');
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
},
|
||||
description: 'resolveAsync returned null',
|
||||
));
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||
/// ENG: Exception thrown when a circular dependency is detected.
|
||||
@@ -20,7 +21,10 @@ class CircularDependencyException implements Exception {
|
||||
final String message;
|
||||
final List<String> dependencyChain;
|
||||
|
||||
const CircularDependencyException(this.message, this.dependencyChain);
|
||||
CircularDependencyException(this.message, this.dependencyChain) {
|
||||
// DEBUG
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -32,16 +36,11 @@ class CircularDependencyException implements Exception {
|
||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||
class CycleDetector {
|
||||
final CherryPickLogger logger;
|
||||
// Стек текущих разрешаемых зависимостей
|
||||
final CherryPickLogger _logger;
|
||||
final Set<String> _resolutionStack = HashSet<String>();
|
||||
|
||||
// История разрешения для построения цепочки зависимостей
|
||||
final List<String> _resolutionHistory = [];
|
||||
|
||||
CycleDetector({CherryPickLogger? logger}) : logger = logger ?? const SilentLogger() {
|
||||
// print removed (trace)
|
||||
}
|
||||
CycleDetector({required CherryPickLogger logger}): _logger = logger;
|
||||
|
||||
/// RU: Начинает отслеживание разрешения зависимости.
|
||||
/// ENG: Starts tracking dependency resolution.
|
||||
@@ -49,13 +48,25 @@ class CycleDetector {
|
||||
/// Throws [CircularDependencyException] if circular dependency is detected.
|
||||
void startResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
logger.info('CycleDetector: startResolving $dependencyKey stackSize=${_resolutionStack.length}');
|
||||
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)) {
|
||||
// Найдена циклическая зависимость
|
||||
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
// print removed (trace)
|
||||
logger.error('CycleDetector: CYCLE DETECTED! $dependencyKey chain: ${cycle.join(' -> ')}');
|
||||
final msg = formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
);
|
||||
_logger.error(msg);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -70,7 +81,12 @@ class CycleDetector {
|
||||
/// ENG: Finishes tracking dependency resolution.
|
||||
void finishResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
logger.info('CycleDetector: finishResolving $dependencyKey');
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'event': 'finishResolving'},
|
||||
description: 'finish resolving',
|
||||
));
|
||||
_resolutionStack.remove(dependencyKey);
|
||||
// Удаляем из истории только если это последний элемент
|
||||
if (_resolutionHistory.isNotEmpty &&
|
||||
@@ -82,7 +98,11 @@ class CycleDetector {
|
||||
/// RU: Очищает все состояние детектора.
|
||||
/// ENG: Clears all detector state.
|
||||
void clear() {
|
||||
logger.info('CycleDetector: clear');
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
params: {'event': 'clear'},
|
||||
description: 'resolution stack cleared',
|
||||
));
|
||||
_resolutionStack.clear();
|
||||
_resolutionHistory.clear();
|
||||
}
|
||||
@@ -110,22 +130,28 @@ class CycleDetector {
|
||||
/// ENG: Mixin for adding circular dependency detection support.
|
||||
mixin CycleDetectionMixin {
|
||||
CycleDetector? _cycleDetector;
|
||||
|
||||
CherryPickLogger? get logger;
|
||||
CherryPickLogger get logger;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей.
|
||||
/// ENG: Enables circular dependency detection.
|
||||
void enableCycleDetection() {
|
||||
// print removed (trace)
|
||||
_cycleDetector = CycleDetector(logger: logger);
|
||||
logger?.info('CycleDetection: cycle detection enabled');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'enable'},
|
||||
description: 'cycle detection enabled',
|
||||
));
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Disables circular dependency detection.
|
||||
void disableCycleDetection() {
|
||||
_cycleDetector?.clear();
|
||||
logger?.info('CycleDetection: cycle detection disabled');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'disable'},
|
||||
description: 'cycle detection disabled',
|
||||
));
|
||||
_cycleDetector = null;
|
||||
}
|
||||
|
||||
@@ -152,7 +178,12 @@ mixin CycleDetectionMixin {
|
||||
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
|
||||
logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
|
||||
@@ -12,13 +12,19 @@
|
||||
//
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick/src/cycle_detector.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
|
||||
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
||||
/// ENG: Global circular dependency detector for entire scope hierarchy.
|
||||
class GlobalCycleDetector {
|
||||
static GlobalCycleDetector? _instance;
|
||||
|
||||
final CherryPickLogger _logger;
|
||||
|
||||
// Глобальный стек разрешения зависимостей
|
||||
final Set<String> _globalResolutionStack = HashSet<String>();
|
||||
|
||||
@@ -28,12 +34,12 @@ class GlobalCycleDetector {
|
||||
// Карта активных детекторов по скоупам
|
||||
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
||||
|
||||
GlobalCycleDetector._internal();
|
||||
GlobalCycleDetector._internal({required CherryPickLogger logger}): _logger = logger;
|
||||
|
||||
/// RU: Получить единственный экземпляр глобального детектора.
|
||||
/// ENG: Get singleton instance of global detector.
|
||||
static GlobalCycleDetector get instance {
|
||||
_instance ??= GlobalCycleDetector._internal();
|
||||
_instance ??= GlobalCycleDetector._internal(logger: CherryPick.globalLogger);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@@ -55,7 +61,12 @@ class GlobalCycleDetector {
|
||||
// Найдена глобальная циклическая зависимость
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(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(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -93,7 +104,12 @@ class GlobalCycleDetector {
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(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(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -117,7 +133,7 @@ class GlobalCycleDetector {
|
||||
/// RU: Получить детектор для конкретного скоупа.
|
||||
/// ENG: Get detector for specific scope.
|
||||
CycleDetector getScopeDetector(String scopeId) {
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector());
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(logger: CherryPick.globalLogger));
|
||||
}
|
||||
|
||||
/// RU: Удалить детектор для скоупа.
|
||||
|
||||
@@ -10,224 +10,129 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:cherrypick/src/scope.dart';
|
||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
CherryPickLogger? _globalLogger = const SilentLogger();
|
||||
/// Global logger for all [Scope]s managed by [CherryPick].
|
||||
///
|
||||
/// Defaults to [SilentLogger] unless set via [setGlobalLogger].
|
||||
CherryPickLogger _globalLogger = const SilentLogger();
|
||||
|
||||
Scope? _rootScope;
|
||||
/// Whether global local-cycle detection is enabled for all Scopes ([Scope.enableCycleDetection]).
|
||||
bool _globalCycleDetectionEnabled = false;
|
||||
|
||||
/// Whether global cross-scope cycle detection is enabled ([Scope.enableGlobalCycleDetection]).
|
||||
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.
|
||||
class CherryPick {
|
||||
/// Позволяет задать глобальный логгер для всей DI-системы.
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// setGlobalLogger — установка глобального логгера для всей системы CherryPick DI.
|
||||
static Scope? _rootScope;
|
||||
|
||||
/// Sets the global logger for all subsequent [Scope]s created by [CherryPick].
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// Sets the global logger for all CherryPick DI containers and scopes.
|
||||
/// All dependency resolution, scope lifecycle, and error events will use
|
||||
/// this logger instance for info/warn/error output.
|
||||
/// Can be used to connect a custom logger (e.g. to external monitoring or UI).
|
||||
///
|
||||
/// Usage example:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick/cherrypick.dart';
|
||||
///
|
||||
/// void main() {
|
||||
/// CherryPick.setGlobalLogger(PrintLogger()); // Or your custom logger
|
||||
/// final rootScope = CherryPick.openRootScope();
|
||||
/// // DI logs and errors will now go to your logger
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// Устанавливает глобальный логгер для всей DI-системы CherryPick.
|
||||
/// Все операции разрешения зависимостей, жизненного цикла скоупов и ошибки
|
||||
/// будут регистрироваться через этот логгер (info/warn/error).
|
||||
/// Можно подключить свою реализацию для интеграции со сторонними системами.
|
||||
///
|
||||
/// Пример использования:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick/cherrypick.dart';
|
||||
///
|
||||
/// void main() {
|
||||
/// CherryPick.setGlobalLogger(PrintLogger()); // Или ваш собственный логгер
|
||||
/// final rootScope = CherryPick.openRootScope();
|
||||
/// // Все события DI и ошибки попадут в ваш логгер.
|
||||
/// }
|
||||
/// ```
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// [logger] The logger implementation to use (see [SilentLogger], [DefaultLogger], etc).
|
||||
static void setGlobalLogger(CherryPickLogger logger) {
|
||||
_globalLogger = logger;
|
||||
}
|
||||
|
||||
/// RU: Метод открывает главный [Scope].
|
||||
/// ENG: The method opens the main [Scope].
|
||||
/// Returns the current global logger used by [CherryPick].
|
||||
static CherryPickLogger get globalLogger => _globalLogger;
|
||||
|
||||
/// Returns the singleton root [Scope], creating it if needed.
|
||||
///
|
||||
/// return
|
||||
/// Uses the current [globalLogger], and applies global cycle detection flags if enabled.
|
||||
/// Call [closeRootScope] to dispose and reset the singleton.
|
||||
static Scope openRootScope() {
|
||||
_rootScope ??= Scope(null, logger: _globalLogger);
|
||||
// Применяем глобальную настройку обнаружения циклических зависимостей
|
||||
// Apply cycle detection settings
|
||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
}
|
||||
// Применяем глобальную настройку обнаружения между скоупами
|
||||
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
|
||||
_rootScope!.enableGlobalCycleDetection();
|
||||
}
|
||||
return _rootScope!;
|
||||
}
|
||||
|
||||
/// RU: Метод закрывает главный [Scope].
|
||||
/// ENG: The method close the main [Scope].
|
||||
///
|
||||
///
|
||||
/// Disposes and resets the root [Scope] singleton.
|
||||
/// The next [openRootScope] call will create a new root [Scope].
|
||||
static void closeRootScope() {
|
||||
if (_rootScope != null) {
|
||||
_rootScope = null;
|
||||
}
|
||||
_rootScope = null;
|
||||
}
|
||||
|
||||
/// RU: Глобально включает обнаружение циклических зависимостей для всех новых скоупов.
|
||||
/// ENG: Globally enables circular dependency detection for all new scopes.
|
||||
/// Globally enables local cycle detection on all [Scope]s created by [CherryPick].
|
||||
///
|
||||
/// Этот метод влияет на все скоупы, создаваемые через CherryPick.
|
||||
/// This method affects all scopes created through CherryPick.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// CherryPick.enableGlobalCycleDetection();
|
||||
/// final scope = CherryPick.openRootScope(); // Автоматически включено обнаружение
|
||||
/// ```
|
||||
/// Also calls [Scope.enableCycleDetection] on the rootScope (if already created).
|
||||
static void enableGlobalCycleDetection() {
|
||||
_globalCycleDetectionEnabled = true;
|
||||
|
||||
// Включаем для уже существующего root scope, если он есть
|
||||
if (_rootScope != null) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Глобально отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Globally disables circular dependency detection.
|
||||
///
|
||||
/// Рекомендуется использовать в production для максимальной производительности.
|
||||
/// Recommended for production use for maximum performance.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// CherryPick.disableGlobalCycleDetection();
|
||||
/// ```
|
||||
/// Disables global local cycle detection.
|
||||
static void disableGlobalCycleDetection() {
|
||||
_globalCycleDetectionEnabled = false;
|
||||
|
||||
// Отключаем для уже существующего root scope, если он есть
|
||||
if (_rootScope != null) {
|
||||
_rootScope!.disableCycleDetection();
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Проверяет, включено ли глобальное обнаружение циклических зависимостей.
|
||||
/// ENG: Checks if global circular dependency detection is enabled.
|
||||
///
|
||||
/// return true если включено, false если отключено
|
||||
/// return true if enabled, false if disabled
|
||||
/// Returns whether global local cycle detection is enabled via [enableGlobalCycleDetection].
|
||||
static bool get isGlobalCycleDetectionEnabled => _globalCycleDetectionEnabled;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей для конкретного скоупа.
|
||||
/// ENG: Enables circular dependency detection for a specific scope.
|
||||
/// Enables cycle detection for a specific (possibly nested) [Scope].
|
||||
///
|
||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
||||
/// [scopeName] - scope name (empty string for root scope)
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// CherryPick.enableCycleDetectionForScope(); // Для root scope
|
||||
/// CherryPick.enableCycleDetectionForScope(scopeName: 'feature.auth'); // Для конкретного scope
|
||||
/// ```
|
||||
/// [scopeName] Hierarchical path string ("outer.inner.deeper"),
|
||||
/// or empty for root. [separator] custom path delimiter (defaults to '.').
|
||||
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
scope.enableCycleDetection();
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей для конкретного скоупа.
|
||||
/// ENG: Disables circular dependency detection for a specific scope.
|
||||
///
|
||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
||||
/// [scopeName] - scope name (empty string for root scope)
|
||||
/// Disables cycle detection for a specific Scope.
|
||||
static void disableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
scope.disableCycleDetection();
|
||||
}
|
||||
|
||||
/// RU: Проверяет, включено ли обнаружение циклических зависимостей для конкретного скоупа.
|
||||
/// ENG: Checks if circular dependency detection is enabled for a specific scope.
|
||||
///
|
||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
||||
/// [scopeName] - scope name (empty string for root scope)
|
||||
///
|
||||
/// return true если включено, false если отключено
|
||||
/// return true if enabled, false if disabled
|
||||
/// Returns true if cycle detection is enabled for the requested scope.
|
||||
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
return scope.isCycleDetectionEnabled;
|
||||
}
|
||||
|
||||
/// RU: Возвращает текущую цепочку разрешения зависимостей для конкретного скоупа.
|
||||
/// ENG: Returns current dependency resolution chain for a specific scope.
|
||||
///
|
||||
/// Полезно для отладки и анализа зависимостей.
|
||||
/// Useful for debugging and dependency analysis.
|
||||
///
|
||||
/// [scopeName] - имя скоупа (пустая строка для root scope)
|
||||
/// [scopeName] - scope name (empty string for root scope)
|
||||
///
|
||||
/// return список имен зависимостей в текущей цепочке разрешения
|
||||
/// return list of dependency names in current resolution chain
|
||||
/// Returns the current dependency resolution chain inside the given scope.
|
||||
/// Useful for diagnostics and runtime debugging.
|
||||
static List<String> getCurrentResolutionChain({String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
return scope.currentResolutionChain;
|
||||
}
|
||||
|
||||
/// RU: Создает новый скоуп с автоматически включенным обнаружением циклических зависимостей.
|
||||
/// ENG: Creates a new scope with automatically enabled circular dependency detection.
|
||||
///
|
||||
/// Удобный метод для создания безопасных скоупов в development режиме.
|
||||
/// Convenient method for creating safe scopes in development mode.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final scope = CherryPick.openSafeRootScope();
|
||||
/// // Обнаружение циклических зависимостей автоматически включено
|
||||
/// ```
|
||||
/// Opens [openRootScope] and enables local cycle detection on it.
|
||||
static Scope openSafeRootScope() {
|
||||
final scope = openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// RU: Создает новый дочерний скоуп с автоматически включенным обнаружением циклических зависимостей.
|
||||
/// ENG: Creates a new child scope with automatically enabled circular dependency detection.
|
||||
///
|
||||
/// [scopeName] - имя скоупа
|
||||
/// [scopeName] - scope name
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final scope = CherryPick.openSafeScope(scopeName: 'feature.auth');
|
||||
/// // Обнаружение циклических зависимостей автоматически включено
|
||||
/// ```
|
||||
/// Opens a scope (by hierarchical name) with local cycle detection enabled.
|
||||
static Scope openSafeScope({String scopeName = '', String separator = '.'}) {
|
||||
final scope = openScope(scopeName: scopeName, separator: separator);
|
||||
scope.enableCycleDetection();
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// RU: Внутренний метод для получения скоупа по имени.
|
||||
/// ENG: Internal method to get scope by name.
|
||||
/// Returns a [Scope] by path (or the rootScope if none specified).
|
||||
/// Used internally for diagnostics and utility operations.
|
||||
static Scope _getScope(String scopeName, String separator) {
|
||||
if (scopeName.isEmpty) {
|
||||
return openRootScope();
|
||||
@@ -235,163 +140,90 @@ class CherryPick {
|
||||
return openScope(scopeName: scopeName, separator: separator);
|
||||
}
|
||||
|
||||
/// RU: Метод открывает дочерний [Scope].
|
||||
/// ENG: The method open the child [Scope].
|
||||
///
|
||||
/// Дочерний [Scope] открывается с [scopeName]
|
||||
/// Child [Scope] open with [scopeName]
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// final String scopeName = 'firstScope.secondScope';
|
||||
/// final subScope = CherryPick.openScope(scopeName);
|
||||
/// ```
|
||||
///
|
||||
/// Opens (and creates nested subscopes if needed) a scope by name/path.
|
||||
///
|
||||
/// - [scopeName]: Hierarchical dot-separated path (e.g. 'outer.inner.sub'). Empty string is root.
|
||||
/// - [separator]: Use a custom string separator (default ".").
|
||||
/// - Always applies global cycle detection settings.
|
||||
@experimental
|
||||
static Scope openScope({String scopeName = '', String separator = '.'}) {
|
||||
if (scopeName.isEmpty) {
|
||||
return openRootScope();
|
||||
}
|
||||
|
||||
final nameParts = scopeName.split(separator);
|
||||
if (nameParts.isEmpty) {
|
||||
throw Exception('Can not open sub scope because scopeName can not split');
|
||||
}
|
||||
|
||||
final scope = nameParts.fold(
|
||||
openRootScope(),
|
||||
(Scope previousValue, String element) =>
|
||||
previousValue.openSubScope(element));
|
||||
|
||||
// Применяем глобальную настройку обнаружения циклических зависимостей
|
||||
openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element)
|
||||
);
|
||||
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
|
||||
scope.enableCycleDetection();
|
||||
}
|
||||
|
||||
// Применяем глобальную настройку обнаружения между скоупами
|
||||
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) {
|
||||
scope.enableGlobalCycleDetection();
|
||||
}
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// RU: Метод открывает дочерний [Scope].
|
||||
/// ENG: The method open the child [Scope].
|
||||
///
|
||||
/// Дочерний [Scope] открывается с [scopeName]
|
||||
/// Child [Scope] open with [scopeName]
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// final String scopeName = 'firstScope.secondScope';
|
||||
/// final subScope = CherryPick.closeScope(scopeName);
|
||||
/// ```
|
||||
///
|
||||
/// Closes a named or root scope (if [scopeName] omitted).
|
||||
///
|
||||
/// - [scopeName]: Hierarchical dot-separated path (e.g. 'outer.inner.sub'). Empty string is root.
|
||||
/// - [separator]: Custom separator for path.
|
||||
@experimental
|
||||
static void closeScope({String scopeName = '', String separator = '.'}) {
|
||||
if (scopeName.isEmpty) {
|
||||
closeRootScope();
|
||||
return;
|
||||
}
|
||||
|
||||
final nameParts = scopeName.split(separator);
|
||||
if (nameParts.isEmpty) {
|
||||
throw Exception(
|
||||
'Can not close sub scope because scopeName can not split');
|
||||
throw Exception('Can not close sub scope because scopeName can not split');
|
||||
}
|
||||
|
||||
if (nameParts.length > 1) {
|
||||
final lastPart = nameParts.removeLast();
|
||||
|
||||
final scope = nameParts.fold(
|
||||
openRootScope(),
|
||||
(Scope previousValue, String element) =>
|
||||
previousValue.openSubScope(element));
|
||||
openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element)
|
||||
);
|
||||
scope.closeSubScope(lastPart);
|
||||
} else {
|
||||
openRootScope().closeSubScope(nameParts[0]);
|
||||
openRootScope().closeSubScope(nameParts.first);
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Глобально включает обнаружение циклических зависимостей между скоупами.
|
||||
/// ENG: Globally enables cross-scope circular dependency detection.
|
||||
///
|
||||
/// Этот режим обнаруживает циклические зависимости во всей иерархии скоупов.
|
||||
/// This mode detects circular dependencies across the entire scope hierarchy.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// CherryPick.enableGlobalCrossScopeCycleDetection();
|
||||
/// ```
|
||||
/// Enables cross-scope cycle detection globally. All new and current [Scope]s get this feature.
|
||||
static void enableGlobalCrossScopeCycleDetection() {
|
||||
_globalCrossScopeCycleDetectionEnabled = true;
|
||||
|
||||
// Включаем для уже существующего root scope, если он есть
|
||||
if (_rootScope != null) {
|
||||
_rootScope!.enableGlobalCycleDetection();
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Глобально отключает обнаружение циклических зависимостей между скоупами.
|
||||
/// ENG: Globally disables cross-scope circular dependency detection.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// CherryPick.disableGlobalCrossScopeCycleDetection();
|
||||
/// ```
|
||||
/// Disables cross-scope cycle detection globally, and clears the detector.
|
||||
static void disableGlobalCrossScopeCycleDetection() {
|
||||
_globalCrossScopeCycleDetectionEnabled = false;
|
||||
|
||||
// Отключаем для уже существующего root scope, если он есть
|
||||
if (_rootScope != null) {
|
||||
_rootScope!.disableGlobalCycleDetection();
|
||||
}
|
||||
|
||||
// Очищаем глобальный детектор
|
||||
GlobalCycleDetector.instance.clear();
|
||||
}
|
||||
|
||||
/// RU: Проверяет, включено ли глобальное обнаружение циклических зависимостей между скоупами.
|
||||
/// ENG: Checks if global cross-scope circular dependency detection is enabled.
|
||||
///
|
||||
/// return true если включено, false если отключено
|
||||
/// return true if enabled, false if disabled
|
||||
/// Returns whether global cross-scope detection is enabled.
|
||||
static bool get isGlobalCrossScopeCycleDetectionEnabled => _globalCrossScopeCycleDetectionEnabled;
|
||||
|
||||
/// RU: Возвращает глобальную цепочку разрешения зависимостей.
|
||||
/// ENG: Returns global dependency resolution chain.
|
||||
///
|
||||
/// Полезно для отладки циклических зависимостей между скоупами.
|
||||
/// Useful for debugging circular dependencies across scopes.
|
||||
///
|
||||
/// return список имен зависимостей в глобальной цепочке разрешения
|
||||
/// return list of dependency names in global resolution chain
|
||||
/// Returns the global dependency resolution chain (for diagnostics/cross-scope cycle detection).
|
||||
static List<String> getGlobalResolutionChain() {
|
||||
return GlobalCycleDetector.instance.globalResolutionChain;
|
||||
}
|
||||
|
||||
/// RU: Очищает все состояние глобального детектора циклических зависимостей.
|
||||
/// ENG: Clears all global circular dependency detector state.
|
||||
///
|
||||
/// Полезно для тестов и сброса состояния.
|
||||
/// Useful for tests and state reset.
|
||||
/// Clears the global cross-scope detector, useful for tests and resets.
|
||||
static void clearGlobalCycleDetector() {
|
||||
GlobalCycleDetector.reset();
|
||||
}
|
||||
|
||||
/// RU: Создает новый скоуп с автоматически включенным глобальным обнаружением циклических зависимостей.
|
||||
/// ENG: Creates a new scope with automatically enabled global circular dependency detection.
|
||||
///
|
||||
/// Этот скоуп будет отслеживать циклические зависимости во всей иерархии.
|
||||
/// This scope will track circular dependencies across the entire hierarchy.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final scope = CherryPick.openGlobalSafeRootScope();
|
||||
/// // Глобальное обнаружение циклических зависимостей автоматически включено
|
||||
/// ```
|
||||
/// Opens [openRootScope], then enables local and cross-scope cycle detection.
|
||||
static Scope openGlobalSafeRootScope() {
|
||||
final scope = openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
@@ -399,17 +231,7 @@ class CherryPick {
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// RU: Создает новый дочерний скоуп с автоматически включенным глобальным обнаружением циклических зависимостей.
|
||||
/// ENG: Creates a new child scope with automatically enabled global circular dependency detection.
|
||||
///
|
||||
/// [scopeName] - имя скоупа
|
||||
/// [scopeName] - scope name
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final scope = CherryPick.openGlobalSafeScope(scopeName: 'feature.auth');
|
||||
/// // Глобальное обнаружение циклических зависимостей автоматически включено
|
||||
/// ```
|
||||
/// Opens [openScope] and enables both local and cross-scope cycle detection on the result.
|
||||
static Scope openGlobalSafeScope({String scopeName = '', String separator = '.'}) {
|
||||
final scope = openScope(scopeName: scopeName, separator: separator);
|
||||
scope.enableCycleDetection();
|
||||
|
||||
34
cherrypick/lib/src/log_format.dart
Normal file
34
cherrypick/lib/src/log_format.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
|
||||
/// Ёдиный форматтер лог-сообщений CherryPick.
|
||||
///
|
||||
/// Используйте для формирования сообщений всех уровней (info, warn, error)
|
||||
/// Например:
|
||||
/// log.info(formatLogMessage(type:'Binding', name:..., params:{...}, description:'created'));
|
||||
|
||||
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';
|
||||
}
|
||||
@@ -18,20 +18,15 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/binding_resolver.dart';
|
||||
import 'package:cherrypick/src/module.dart';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
|
||||
|
||||
CherryPickLogger _globalLogger = const SilentLogger();
|
||||
|
||||
Scope openRootScope({CherryPickLogger? logger}) => Scope(null, logger: logger);
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
final Scope? _parentScope;
|
||||
|
||||
CherryPickLogger? _logger;
|
||||
late final CherryPickLogger _logger;
|
||||
|
||||
@override
|
||||
CherryPickLogger? get logger => _logger;
|
||||
set logger(CherryPickLogger? value) => _logger = value;
|
||||
CherryPickLogger get logger => _logger;
|
||||
|
||||
/// RU: Метод возвращает родительский [Scope].
|
||||
///
|
||||
@@ -42,11 +37,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
|
||||
final Map<String, Scope> _scopeMap = HashMap();
|
||||
|
||||
Scope(this._parentScope, {CherryPickLogger? logger}) : _logger = logger ?? _globalLogger {
|
||||
// print removed (trace)
|
||||
// Генерируем уникальный ID для скоупа
|
||||
Scope(this._parentScope, {required CherryPickLogger logger}) : _logger = logger {
|
||||
setScopeId(_generateScopeId());
|
||||
_logger?.info('Scope created: id=${scopeId ?? "NO_ID"}, parent=${_parentScope?.scopeId}');
|
||||
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();
|
||||
@@ -81,7 +81,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
_scopeMap[name] = childScope;
|
||||
logger?.info('SubScope created: $name, id=${childScope.scopeId} (parent=$scopeId)');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
},
|
||||
description: 'subscope created',
|
||||
));
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
@@ -98,7 +106,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
logger?.info('SubScope closed: $name, id=${childScope.scopeId} (parent=$scopeId)');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
},
|
||||
description: 'subscope closed',
|
||||
));
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
@@ -111,7 +127,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
for (var module in modules) {
|
||||
logger?.info('Installing module: ${module.runtimeType} in scope $scopeId');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Module',
|
||||
name: module.runtimeType.toString(),
|
||||
params: {
|
||||
'scope': scopeId,
|
||||
},
|
||||
description: 'module installed',
|
||||
));
|
||||
module.builder(this);
|
||||
// После builder: для всех новых биндингов
|
||||
for (final binding in module.bindingSet) {
|
||||
@@ -129,7 +152,11 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
/// return [Scope]
|
||||
Scope dropModules() {
|
||||
logger?.info('Modules dropped from scope: $scopeId');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
description: 'modules dropped',
|
||||
));
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -154,14 +181,32 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
logger?.error('Global cycle detection failed during resolve<$T>', e, s);
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'global cycle detection failed during resolve',
|
||||
),
|
||||
e,
|
||||
s,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
} catch (e, s) {
|
||||
logger?.error('Failed to resolve<$T>', e, s);
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
e,
|
||||
s,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -173,10 +218,28 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return withCycleDetection<T>(T, named, () {
|
||||
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
logger?.info('Resolve<$T> [named=$named]: successfully resolved in scope $scopeId.');
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
},
|
||||
description: 'successfully resolved',
|
||||
));
|
||||
return resolved;
|
||||
} else {
|
||||
logger?.error('Failed to resolve<$T> [named=$named] in scope $scopeId.');
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
);
|
||||
throw StateError(
|
||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||
}
|
||||
|
||||
@@ -34,27 +34,40 @@ void main() {
|
||||
scope.installModules([DummyModule()]);
|
||||
final _ = scope.resolve<DummyService>(named: 'test');
|
||||
|
||||
expect(logger.infos.any((m) => m.contains('Scope created')), isTrue);
|
||||
expect(logger.infos.any((m) => m.contains('Binding<DummyService> created')), isTrue);
|
||||
expect(logger.infos.any((m) =>
|
||||
m.contains('Binding<DummyService> named as [test]') || m.contains('named as [test]')), isTrue);
|
||||
expect(logger.infos.any((m) =>
|
||||
m.contains('Resolve<DummyService> [named=test]: successfully resolved') ||
|
||||
m.contains('Resolve<DummyService> [named=test]: successfully resolved in scope')), isTrue);
|
||||
// Новый стиль проверки для 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>()),
|
||||
);
|
||||
expect(
|
||||
logger.errors.any((m) =>
|
||||
m.contains('CYCLE DETECTED!') || m.contains('Circular dependency detected')),
|
||||
isTrue,
|
||||
);
|
||||
// Дополнительно ищем и среди 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}');
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
});
|
||||
group('CycleDetector', () {
|
||||
late CycleDetector detector;
|
||||
|
||||
setUp(() {
|
||||
detector = CycleDetector();
|
||||
detector = CycleDetector(logger: logger);
|
||||
});
|
||||
|
||||
test('should detect simple circular dependency', () {
|
||||
@@ -73,7 +80,7 @@ void main() {
|
||||
|
||||
group('Scope with Cycle Detection', () {
|
||||
test('should detect circular dependency in real scenario', () {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
|
||||
// Создаем циклическую зависимость: A зависит от B, B зависит от A
|
||||
@@ -89,7 +96,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should work normally without cycle detection enabled', () {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
// Не включаем обнаружение циклических зависимостей
|
||||
|
||||
scope.installModules([
|
||||
@@ -101,7 +108,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should allow disabling cycle detection', () {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
|
||||
@@ -110,7 +117,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should handle named dependencies in cycle detection', () {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
|
||||
scope.installModules([
|
||||
@@ -124,7 +131,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should detect cycles in async resolution', () async {
|
||||
final scope = Scope(null);
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
|
||||
scope.installModules([
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
import 'package:cherrypick/src/module.dart';
|
||||
import 'package:cherrypick/src/scope.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Scope & Subscope Management', () {
|
||||
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);
|
||||
});
|
||||
|
||||
test('Can open and retrieve the same subScope by key', () {
|
||||
final scope = Scope(null);
|
||||
final subScope = scope.openSubScope('subScope');
|
||||
expect(scope.openSubScope('subScope'), subScope);
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
});
|
||||
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
||||
final scope = Scope(null);
|
||||
final subScope = scope.openSubScope("child");
|
||||
expect(scope.openSubScope("child"), same(subScope));
|
||||
scope.closeSubScope("child");
|
||||
final newSubScope = scope.openSubScope("child");
|
||||
expect(newSubScope, isNot(same(subScope)));
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
// Нет необходимости тестировать open/closeSubScope в этом юните
|
||||
});
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
group('Dependency Resolution (standard)', () {
|
||||
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>()));
|
||||
});
|
||||
|
||||
test('Resolves value after adding a dependency', () {
|
||||
final logger = MockLogger();
|
||||
final expectedValue = 'test string';
|
||||
final scope = Scope(null)
|
||||
final scope = Scope(null, logger: logger)
|
||||
.installModules([TestModule<String>(value: expectedValue)]);
|
||||
expect(scope.resolve<String>(), expectedValue);
|
||||
});
|
||||
|
||||
test('Returns a value from parent scope', () {
|
||||
final logger = MockLogger();
|
||||
final expectedValue = 5;
|
||||
final parentScope = Scope(null);
|
||||
final scope = Scope(parentScope);
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
|
||||
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
||||
|
||||
@@ -51,26 +53,29 @@ void main() {
|
||||
});
|
||||
|
||||
test('Returns several values from parent container', () {
|
||||
final logger = MockLogger();
|
||||
final expectedIntValue = 5;
|
||||
final expectedStringValue = 'Hello world';
|
||||
final parentScope = Scope(null).installModules([
|
||||
final parentScope = Scope(null, logger: logger).installModules([
|
||||
TestModule<int>(value: expectedIntValue),
|
||||
TestModule<String>(value: expectedStringValue)
|
||||
]);
|
||||
final scope = Scope(parentScope);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
|
||||
expect(scope.resolve<int>(), expectedIntValue);
|
||||
expect(scope.resolve<String>(), expectedStringValue);
|
||||
});
|
||||
|
||||
test("Throws StateError if parent hasn't value too", () {
|
||||
final parentScope = Scope(null);
|
||||
final scope = Scope(parentScope);
|
||||
final logger = MockLogger();
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
|
||||
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);
|
||||
scope.dropModules();
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
@@ -80,7 +85,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Named Dependencies', () {
|
||||
test('Resolve named binding', () {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
TestModule<String>(value: "first"),
|
||||
TestModule<String>(value: "second", name: "special")
|
||||
@@ -90,7 +96,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('Named binding does not clash with unnamed', () {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo", name: "bar"),
|
||||
]);
|
||||
@@ -99,7 +106,8 @@ void main() {
|
||||
});
|
||||
|
||||
test("tryResolve returns null for missing named", () {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo"),
|
||||
]);
|
||||
@@ -110,7 +118,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Provider with parameters', () {
|
||||
test('Resolve dependency using providerWithParams', () {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
|
||||
@@ -124,7 +133,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Async Resolution', () {
|
||||
test('Resolve async instance', () async {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<String>().toInstance(Future.value('async value'));
|
||||
@@ -134,7 +144,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('Resolve async provider', () async {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvide(() async => 7);
|
||||
@@ -144,7 +155,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('Resolve async provider with param', () async {
|
||||
final scope = Scope(null)
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
||||
@@ -155,7 +167,8 @@ void main() {
|
||||
});
|
||||
|
||||
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>();
|
||||
expect(result, isNull);
|
||||
});
|
||||
@@ -164,7 +177,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Optional resolution and error handling', () {
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user