mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 21:13:35 +00:00
feat(logger): add extensible logging API, usage examples, and bilingual documentation
- Introduce CherryPickLogger interface, PrintLogger and SilentLogger implementations - Add setGlobalLogger() to CherryPick API for custom DI logging - Log key events (scope, module, error) via logger throughout DI lifecycle - Comprehensive comments and code documentation in both English and Russian - Document usage of logging system in quick_start and full_tutorial documentation (EN/RU) - Provide usage examples in docs and code comments - No logging inside GlobalCycleDetectionMixin (design choice: exceptions handled at Scope, not detector/mixin level) and detailed architectural reasoning - Update helper.dart, logger.dart: comments, examples, API doc improvements BREAKING CHANGE: Projects can now inject any logger via CherryPick.setGlobalLogger; default log behavior clarified and docstrings/usage examples enhanced
This commit is contained in:
@@ -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
|
||||
|
||||
- [x] Main Scope and Named Subscopes
|
||||
|
||||
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
|
||||
}
|
||||
@@ -20,3 +20,4 @@ export 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
export 'package:cherrypick/src/helper.dart';
|
||||
export 'package:cherrypick/src/module.dart';
|
||||
export 'package:cherrypick/src/scope.dart';
|
||||
export 'package:cherrypick/src/logger.dart';
|
||||
|
||||
@@ -16,14 +16,51 @@ import 'package:cherrypick/src/binding_resolver.dart';
|
||||
/// RU: Класс Binding<T> настраивает параметры экземпляра.
|
||||
/// ENG: The Binding<T> class configures the settings for the instance.
|
||||
///
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
|
||||
class Binding<T> {
|
||||
late Type _key;
|
||||
String? _name;
|
||||
|
||||
BindingResolver<T>? _resolver;
|
||||
|
||||
Binding() {
|
||||
CherryPickLogger? logger;
|
||||
|
||||
// Deferred logging flags
|
||||
bool _createdLogged = false;
|
||||
bool _namedLogged = false;
|
||||
bool _singletonLogged = false;
|
||||
|
||||
Binding({this.logger}) {
|
||||
_key = T;
|
||||
// Не логируем здесь! Делаем deferred лог после назначения logger
|
||||
}
|
||||
|
||||
void markCreated() {
|
||||
if (!_createdLogged) {
|
||||
logger?.info('Binding<$T> created');
|
||||
_createdLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markNamed() {
|
||||
if (isNamed && !_namedLogged) {
|
||||
logger?.info('Binding<$T> named as [$_name]');
|
||||
_namedLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markSingleton() {
|
||||
if (isSingleton && !_singletonLogged) {
|
||||
logger?.info('Binding<$T> singleton mode enabled');
|
||||
_singletonLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void logAllDeferred() {
|
||||
markCreated();
|
||||
markNamed();
|
||||
markSingleton();
|
||||
}
|
||||
|
||||
/// RU: Метод возвращает тип экземпляра.
|
||||
@@ -58,6 +95,7 @@ class Binding<T> {
|
||||
/// return [Binding]
|
||||
Binding<T> withName(String name) {
|
||||
_name = name;
|
||||
// Не логируем здесь, deferred log via markNamed()
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -67,7 +105,6 @@ class Binding<T> {
|
||||
/// return [Binding]
|
||||
Binding<T> toInstance(Instance<T> value) {
|
||||
_resolver = InstanceResolver<T>(value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -77,7 +114,6 @@ class Binding<T> {
|
||||
/// return [Binding]
|
||||
Binding<T> toProvide(Provider<T> value) {
|
||||
_resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -87,7 +123,6 @@ class Binding<T> {
|
||||
/// return [Binding]
|
||||
Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
|
||||
_resolver = ProviderResolver<T>(value, withParams: true);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -112,15 +147,28 @@ class Binding<T> {
|
||||
/// return [Binding]
|
||||
Binding<T> singleton() {
|
||||
_resolver?.toSingleton();
|
||||
|
||||
// Не логируем здесь, deferred log via markSingleton()
|
||||
return this;
|
||||
}
|
||||
|
||||
T? resolveSync([dynamic params]) {
|
||||
return resolver?.resolveSync(params);
|
||||
final res = resolver?.resolveSync(params);
|
||||
if (res != null) {
|
||||
logger?.info('Binding<$T> resolveSync => object created/resolved.');
|
||||
} else {
|
||||
logger?.warn('Binding<$T> resolveSync => returned null!');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<T>? resolveAsync([dynamic params]) {
|
||||
return resolver?.resolveAsync(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));
|
||||
} else {
|
||||
logger?.warn('Binding<$T> resolveAsync => returned null!');
|
||||
}
|
||||
return future;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
|
||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||
/// ENG: Exception thrown when a circular dependency is detected.
|
||||
@@ -31,24 +32,30 @@ class CircularDependencyException implements Exception {
|
||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||
class CycleDetector {
|
||||
final CherryPickLogger logger;
|
||||
// Стек текущих разрешаемых зависимостей
|
||||
final Set<String> _resolutionStack = HashSet<String>();
|
||||
|
||||
// История разрешения для построения цепочки зависимостей
|
||||
final List<String> _resolutionHistory = [];
|
||||
|
||||
CycleDetector({CherryPickLogger? logger}) : logger = logger ?? const SilentLogger() {
|
||||
// print removed (trace)
|
||||
}
|
||||
|
||||
/// RU: Начинает отслеживание разрешения зависимости.
|
||||
/// ENG: Starts tracking dependency resolution.
|
||||
///
|
||||
/// 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}');
|
||||
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(' -> ')}');
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -63,8 +70,8 @@ class CycleDetector {
|
||||
/// ENG: Finishes tracking dependency resolution.
|
||||
void finishResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
logger.info('CycleDetector: finishResolving $dependencyKey');
|
||||
_resolutionStack.remove(dependencyKey);
|
||||
|
||||
// Удаляем из истории только если это последний элемент
|
||||
if (_resolutionHistory.isNotEmpty &&
|
||||
_resolutionHistory.last == dependencyKey) {
|
||||
@@ -75,6 +82,7 @@ class CycleDetector {
|
||||
/// RU: Очищает все состояние детектора.
|
||||
/// ENG: Clears all detector state.
|
||||
void clear() {
|
||||
logger.info('CycleDetector: clear');
|
||||
_resolutionStack.clear();
|
||||
_resolutionHistory.clear();
|
||||
}
|
||||
@@ -103,16 +111,21 @@ class CycleDetector {
|
||||
mixin CycleDetectionMixin {
|
||||
CycleDetector? _cycleDetector;
|
||||
|
||||
CherryPickLogger? get logger;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей.
|
||||
/// ENG: Enables circular dependency detection.
|
||||
void enableCycleDetection() {
|
||||
_cycleDetector = CycleDetector();
|
||||
// print removed (trace)
|
||||
_cycleDetector = CycleDetector(logger: logger);
|
||||
logger?.info('CycleDetection: cycle detection enabled');
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Disables circular dependency detection.
|
||||
void disableCycleDetection() {
|
||||
_cycleDetector?.clear();
|
||||
logger?.info('CycleDetection: cycle detection disabled');
|
||||
_cycleDetector = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,30 +12,72 @@
|
||||
//
|
||||
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();
|
||||
|
||||
Scope? _rootScope;
|
||||
bool _globalCycleDetectionEnabled = false;
|
||||
bool _globalCrossScopeCycleDetectionEnabled = false;
|
||||
|
||||
class CherryPick {
|
||||
/// Позволяет задать глобальный логгер для всей DI-системы.
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// setGlobalLogger — установка глобального логгера для всей системы CherryPick DI.
|
||||
///
|
||||
/// 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 и ошибки попадут в ваш логгер.
|
||||
/// }
|
||||
/// ```
|
||||
/// ----------------------------------------------------------------------------
|
||||
static void setGlobalLogger(CherryPickLogger logger) {
|
||||
_globalLogger = logger;
|
||||
}
|
||||
|
||||
/// RU: Метод открывает главный [Scope].
|
||||
/// ENG: The method opens the main [Scope].
|
||||
///
|
||||
/// return
|
||||
static Scope openRootScope() {
|
||||
_rootScope ??= Scope(null);
|
||||
|
||||
_rootScope ??= Scope(null, logger: _globalLogger);
|
||||
// Применяем глобальную настройку обнаружения циклических зависимостей
|
||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
}
|
||||
|
||||
// Применяем глобальную настройку обнаружения между скоупами
|
||||
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
|
||||
_rootScope!.enableGlobalCycleDetection();
|
||||
}
|
||||
|
||||
return _rootScope!;
|
||||
}
|
||||
|
||||
|
||||
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,22 @@ import 'package:cherrypick/src/cycle_detector.dart';
|
||||
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';
|
||||
|
||||
Scope openRootScope() => Scope(null);
|
||||
|
||||
CherryPickLogger _globalLogger = const SilentLogger();
|
||||
|
||||
Scope openRootScope({CherryPickLogger? logger}) => Scope(null, logger: logger);
|
||||
|
||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
final Scope? _parentScope;
|
||||
|
||||
CherryPickLogger? _logger;
|
||||
|
||||
@override
|
||||
CherryPickLogger? get logger => _logger;
|
||||
set logger(CherryPickLogger? value) => _logger = value;
|
||||
|
||||
/// RU: Метод возвращает родительский [Scope].
|
||||
///
|
||||
/// ENG: The method returns the parent [Scope].
|
||||
@@ -32,9 +42,11 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
|
||||
final Map<String, Scope> _scopeMap = HashMap();
|
||||
|
||||
Scope(this._parentScope) {
|
||||
Scope(this._parentScope, {CherryPickLogger? logger}) : _logger = logger ?? _globalLogger {
|
||||
// print removed (trace)
|
||||
// Генерируем уникальный ID для скоупа
|
||||
setScopeId(_generateScopeId());
|
||||
_logger?.info('Scope created: id=${scopeId ?? "NO_ID"}, parent=${_parentScope?.scopeId}');
|
||||
}
|
||||
|
||||
final Set<Module> _modulesList = HashSet();
|
||||
@@ -59,8 +71,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// return [Scope]
|
||||
Scope openSubScope(String name) {
|
||||
if (!_scopeMap.containsKey(name)) {
|
||||
final childScope = Scope(this);
|
||||
|
||||
final childScope = Scope(this, logger: logger); // Наследуем логгер вниз по иерархии
|
||||
// print removed (trace)
|
||||
// Наследуем настройки обнаружения циклических зависимостей
|
||||
if (isCycleDetectionEnabled) {
|
||||
childScope.enableCycleDetection();
|
||||
@@ -68,8 +80,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
|
||||
_scopeMap[name] = childScope;
|
||||
logger?.info('SubScope created: $name, id=${childScope.scopeId} (parent=$scopeId)');
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
@@ -86,6 +98,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
logger?.info('SubScope closed: $name, id=${childScope.scopeId} (parent=$scopeId)');
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
@@ -98,7 +111,13 @@ 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');
|
||||
module.builder(this);
|
||||
// После builder: для всех новых биндингов
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.logger = logger;
|
||||
binding.logAllDeferred();
|
||||
}
|
||||
}
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -110,7 +129,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
/// return [Scope]
|
||||
Scope dropModules() {
|
||||
// [AlexeyYuPopkov](https://github.com/AlexeyYuPopkov) Thank you for the [Removed exception "ConcurrentModificationError"](https://github.com/pese-git/cherrypick/pull/2)
|
||||
logger?.info('Modules dropped from scope: $scopeId');
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -130,11 +149,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
T resolve<T>({String? named, dynamic params}) {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
return withGlobalCycleDetection<T>(T, named, () {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
try {
|
||||
return withGlobalCycleDetection<T>(T, named, () {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
logger?.error('Global cycle detection failed during resolve<$T>', e, s);
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
try {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
} catch (e, s) {
|
||||
logger?.error('Failed to resolve<$T>', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +173,10 @@ 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.');
|
||||
return resolved;
|
||||
} else {
|
||||
logger?.error('Failed to resolve<$T> [named=$named] in scope $scopeId.');
|
||||
throw StateError(
|
||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||
}
|
||||
|
||||
60
cherrypick/test/logger_integration_test.dart
Normal file
60
cherrypick/test/logger_integration_test.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
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');
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test('CycleDetector logs cycle detection error', () {
|
||||
final scope = Scope(null, logger: logger);
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
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,7 +1,5 @@
|
||||
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:cherrypick/cherrypick.dart';
|
||||
|
||||
void main() {
|
||||
group('CycleDetector', () {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
});
|
||||
group('CherryPick Cycle Detection Helper Methods', () {
|
||||
setUp(() {
|
||||
// Сбрасываем состояние перед каждым тестом
|
||||
|
||||
@@ -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.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
- **Global DI Scope Access:**
|
||||
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:
|
||||
`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!
|
||||
|
||||
|
||||
@@ -358,6 +358,26 @@ class MyApp extends StatelessWidget {
|
||||
- Вы можете создавать подскоупы, если нужно, например, для экранов или модулей:
|
||||
`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!
|
||||
|
||||
|
||||
@@ -79,6 +79,24 @@ Example:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,24 @@ Scope - это контейнер, который хранит все дерев
|
||||
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 на логгере.
|
||||
|
||||
## Пример приложения
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user