refactor(core,logger)migrate to CherryPickObserver API and drop CherryPickLogger

BREAKING CHANGE:
- Removed the deprecated CherryPickLogger interface from cherrypick
- Logger/adapters (e.g., talker_cherrypick_logger) must now implement CherryPickObserver
- talker_cherrypick_logger: replace TalkerCherryPickLogger with TalkerCherryPickObserver
- All usages, docs, tests migrated to observer API
- Improved test mocks and integration tests for observer pattern
- Removed obsolete files: cherrypick/src/logger.dart, talker_cherrypick_logger/src/talker_cherrypick_logger.dart
- Updated README and example usages for new CherryPickObserver model

This refactor introduces a unified observer pattern (CherryPickObserver) to handle all DI lifecycle events, replacing the limited info/warn/error logger pattern.
All external logging adapters and integrations must migrate to use CherryPickObserver.
This commit is contained in:
Sergey Penkovsky
2025-08-11 18:01:21 +03:00
parent 4dc9e269cd
commit efed72cc39
24 changed files with 507 additions and 436 deletions

View File

@@ -15,7 +15,7 @@ class AppModule extends Module {
void main() { void main() {
// Set a global logger for the DI system // Set a global logger for the DI system
CherryPick.setGlobalLogger(PrintLogger()); CherryPick.setGlobalObserver(PrintCherryPickObserver());
// Open the root scope // Open the root scope
final rootScope = CherryPick.openRootScope(); final rootScope = CherryPick.openRootScope();
@@ -32,6 +32,6 @@ void main() {
subScope.closeSubScope('feature.profile'); subScope.closeSubScope('feature.profile');
// Demonstrate disabling and re-enabling logging // Demonstrate disabling and re-enabling logging
CherryPick.setGlobalLogger(const SilentLogger()); CherryPick.setGlobalObserver(SilentCherryPickObserver());
rootScope.resolve<UserRepository>(); // now without logs rootScope.resolve<UserRepository>(); // now without logs
} }

View File

@@ -20,5 +20,5 @@ 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';
export 'package:cherrypick/src/disposable.dart'; export 'package:cherrypick/src/disposable.dart';
export 'package:cherrypick/src/observer.dart';

View File

@@ -16,8 +16,7 @@ import 'package:cherrypick/src/binding_resolver.dart';
/// RU: Класс Binding&lt;T&gt; настраивает параметры экземпляра. /// RU: Класс Binding&lt;T&gt; настраивает параметры экземпляра.
/// ENG: The Binding&lt;T&gt; class configures the settings for the instance. /// ENG: The Binding&lt;T&gt; class configures the settings for the instance.
/// ///
import 'package:cherrypick/src/logger.dart'; import 'package:cherrypick/src/observer.dart';
import 'package:cherrypick/src/log_format.dart';
class Binding<T> { class Binding<T> {
late Type _key; late Type _key;
@@ -25,50 +24,54 @@ class Binding<T> {
BindingResolver<T>? _resolver; BindingResolver<T>? _resolver;
CherryPickLogger? logger; CherryPickObserver? observer;
// Deferred logging flags // Deferred logging flags
bool _createdLogged = false; bool _createdLogged = false;
bool _namedLogged = false; bool _namedLogged = false;
bool _singletonLogged = false; bool _singletonLogged = false;
Binding({this.logger}) { Binding({this.observer}) {
_key = T; _key = T;
// Не логируем здесь! Делаем deferred лог после назначения logger // Deferred уведомения observer, не логировать здесь напрямую
} }
void markCreated() { void markCreated() {
if (!_createdLogged) { if (!_createdLogged) {
logger?.info(formatLogMessage( observer?.onBindingRegistered(
type: 'Binding', runtimeType.toString(),
name: T.toString(), T,
params: _name != null ? {'name': _name} : null, );
description: 'created',
));
_createdLogged = true; _createdLogged = true;
} }
} }
void markNamed() { void markNamed() {
if (isNamed && !_namedLogged) { if (isNamed && !_namedLogged) {
logger?.info(formatLogMessage( observer?.onDiagnostic(
type: 'Binding', 'Binding named: ${T.toString()} name: $_name',
name: T.toString(), details: {
params: {'name': _name}, 'type': 'Binding',
description: 'named', 'name': T.toString(),
)); 'nameParam': _name,
'description': 'named',
},
);
_namedLogged = true; _namedLogged = true;
} }
} }
void markSingleton() { void markSingleton() {
if (isSingleton && !_singletonLogged) { if (isSingleton && !_singletonLogged) {
logger?.info(formatLogMessage( observer?.onDiagnostic(
type: 'Binding', 'Binding singleton: ${T.toString()}${_name != null ? ' name: $_name' : ''}',
name: T.toString(), details: {
params: _name != null ? {'name': _name} : null, 'type': 'Binding',
description: 'singleton mode enabled', 'name': T.toString(),
)); if (_name != null) 'name': _name,
'description': 'singleton mode enabled',
},
);
_singletonLogged = true; _singletonLogged = true;
} }
} }
@@ -170,25 +173,23 @@ class Binding<T> {
T? resolveSync([dynamic params]) { T? resolveSync([dynamic params]) {
final res = resolver?.resolveSync(params); final res = resolver?.resolveSync(params);
if (res != null) { if (res != null) {
logger?.info(formatLogMessage( observer?.onDiagnostic(
type: 'Binding', 'Binding resolved instance: ${T.toString()}',
name: T.toString(), details: {
params: {
if (_name != null) 'name': _name, if (_name != null) 'name': _name,
'method': 'resolveSync', 'method': 'resolveSync',
'description': 'object created/resolved',
}, },
description: 'object created/resolved', );
));
} else { } else {
logger?.warn(formatLogMessage( observer?.onWarning(
type: 'Binding', 'resolveSync returned null: ${T.toString()}',
name: T.toString(), details: {
params: {
if (_name != null) 'name': _name, if (_name != null) 'name': _name,
'method': 'resolveSync', 'method': 'resolveSync',
'description': 'resolveSync returned null',
}, },
description: 'resolveSync returned null', );
));
} }
return res; return res;
} }
@@ -197,38 +198,28 @@ class Binding<T> {
final future = resolver?.resolveAsync(params); final future = resolver?.resolveAsync(params);
if (future != null) { if (future != null) {
future future
.then((res) => logger?.info(formatLogMessage( .then((res) => observer?.onDiagnostic(
type: 'Binding', 'Future resolved for: ${T.toString()}',
name: T.toString(), details: {
params: {
if (_name != null) 'name': _name, if (_name != null) 'name': _name,
'method': 'resolveAsync', 'method': 'resolveAsync',
'description': 'Future resolved',
}, },
description: 'Future resolved', ))
))) .catchError((e, s) => observer?.onError(
.catchError((e, s) => logger?.error( 'resolveAsync error: ${T.toString()}',
formatLogMessage(
type: 'Binding',
name: T.toString(),
params: {
if (_name != null) 'name': _name,
'method': 'resolveAsync',
},
description: 'resolveAsync error',
),
e, e,
s, s,
)); ));
} else { } else {
logger?.warn(formatLogMessage( observer?.onWarning(
type: 'Binding', 'resolveAsync returned null: ${T.toString()}',
name: T.toString(), details: {
params: {
if (_name != null) 'name': _name, if (_name != null) 'name': _name,
'method': 'resolveAsync', 'method': 'resolveAsync',
'description': 'resolveAsync returned null',
}, },
description: 'resolveAsync returned null', );
));
} }
return future; return future;
} }

View File

@@ -12,8 +12,7 @@
// //
import 'dart:collection'; import 'dart:collection';
import 'package:cherrypick/src/logger.dart'; import 'package:cherrypick/src/observer.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.
@@ -36,11 +35,11 @@ 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 CherryPickObserver _observer;
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; CycleDetector({required CherryPickObserver observer}) : _observer = observer;
/// RU: Начинает отслеживание разрешения зависимости. /// RU: Начинает отслеживание разрешения зависимости.
/// ENG: Starts tracking dependency resolution. /// ENG: Starts tracking dependency resolution.
@@ -48,25 +47,24 @@ class CycleDetector {
/// 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}'); _observer.onDiagnostic(
_logger.info(formatLogMessage( 'CycleDetector startResolving: $dependencyKey',
type: 'CycleDetector', details: {
name: dependencyKey.toString(), 'event': 'startResolving',
params: {'event': 'startResolving', 'stackSize': _resolutionStack.length}, '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) _observer.onCycleDetected(
final msg = formatLogMessage( cycle,
type: 'CycleDetector', );
name: dependencyKey.toString(), _observer.onError(
params: {'chain': cycle.join('->')}, 'Cycle detected for $dependencyKey',
description: 'cycle detected', null,
null,
); );
_logger.error(msg);
throw CircularDependencyException( throw CircularDependencyException(
'Circular dependency detected for $dependencyKey', 'Circular dependency detected for $dependencyKey',
cycle, cycle,
@@ -81,12 +79,10 @@ 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( _observer.onDiagnostic(
type: 'CycleDetector', 'CycleDetector finishResolving: $dependencyKey',
name: dependencyKey.toString(), details: {'event': 'finishResolving'},
params: {'event': 'finishResolving'}, );
description: 'finish resolving',
));
_resolutionStack.remove(dependencyKey); _resolutionStack.remove(dependencyKey);
// Удаляем из истории только если это последний элемент // Удаляем из истории только если это последний элемент
if (_resolutionHistory.isNotEmpty && if (_resolutionHistory.isNotEmpty &&
@@ -98,11 +94,13 @@ class CycleDetector {
/// RU: Очищает все состояние детектора. /// RU: Очищает все состояние детектора.
/// ENG: Clears all detector state. /// ENG: Clears all detector state.
void clear() { void clear() {
_logger.info(formatLogMessage( _observer.onDiagnostic(
type: 'CycleDetector', 'CycleDetector clear',
params: {'event': 'clear'}, details: {
description: 'resolution stack cleared', 'event': 'clear',
)); 'description': 'resolution stack cleared',
},
);
_resolutionStack.clear(); _resolutionStack.clear();
_resolutionHistory.clear(); _resolutionHistory.clear();
} }
@@ -130,28 +128,32 @@ 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; CherryPickObserver get observer;
/// RU: Включает обнаружение циклических зависимостей. /// RU: Включает обнаружение циклических зависимостей.
/// ENG: Enables circular dependency detection. /// ENG: Enables circular dependency detection.
void enableCycleDetection() { void enableCycleDetection() {
_cycleDetector = CycleDetector(logger: logger); _cycleDetector = CycleDetector(observer: observer);
logger.info(formatLogMessage( observer.onDiagnostic(
type: 'CycleDetection', 'CycleDetection enabled',
params: {'event': 'enable'}, details: {
description: 'cycle detection enabled', '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( observer.onDiagnostic(
type: 'CycleDetection', 'CycleDetection disabled',
params: {'event': 'disable'}, details: {
description: 'cycle detection disabled', 'event': 'disable',
)); 'description': 'cycle detection disabled',
},
);
_cycleDetector = null; _cycleDetector = null;
} }
@@ -178,12 +180,14 @@ 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( observer.onCycleDetected(
type: 'CycleDetector', cycle,
name: dependencyKey.toString(), );
params: {'chain': cycle.join('->')}, observer.onError(
description: 'cycle detected', 'Cycle detected for $dependencyKey',
)); null,
null,
);
throw CircularDependencyException( throw CircularDependencyException(
'Circular dependency detected for $dependencyKey', 'Circular dependency detected for $dependencyKey',
cycle, cycle,

View File

@@ -13,7 +13,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:cherrypick/cherrypick.dart'; import 'package:cherrypick/cherrypick.dart';
import 'package:cherrypick/src/log_format.dart';
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов. /// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
@@ -21,7 +20,7 @@ import 'package:cherrypick/src/log_format.dart';
class GlobalCycleDetector { class GlobalCycleDetector {
static GlobalCycleDetector? _instance; static GlobalCycleDetector? _instance;
final CherryPickLogger _logger; final CherryPickObserver _observer;
// Глобальный стек разрешения зависимостей // Глобальный стек разрешения зависимостей
final Set<String> _globalResolutionStack = HashSet<String>(); final Set<String> _globalResolutionStack = HashSet<String>();
@@ -32,12 +31,12 @@ class GlobalCycleDetector {
// Карта активных детекторов по скоупам // Карта активных детекторов по скоупам
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>(); final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
GlobalCycleDetector._internal({required CherryPickLogger logger}): _logger = logger; GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer;
/// 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(logger: CherryPick.globalLogger); _instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
return _instance!; return _instance!;
} }
@@ -59,12 +58,15 @@ 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( _observer.onCycleDetected(
type: 'CycleDetector', cycle,
name: dependencyKey.toString(), scopeName: scopeId,
params: {'chain': cycle.join('->')}, );
description: 'cycle detected', _observer.onError(
)); 'Global circular dependency detected for $dependencyKey',
null,
null,
);
throw CircularDependencyException( throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey', 'Global circular dependency detected for $dependencyKey',
cycle, cycle,
@@ -102,12 +104,15 @@ 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( _observer.onCycleDetected(
type: 'CycleDetector', cycle,
name: dependencyKey.toString(), scopeName: scopeId,
params: {'chain': cycle.join('->')}, );
description: 'cycle detected', _observer.onError(
)); 'Global circular dependency detected for $dependencyKey',
null,
null,
);
throw CircularDependencyException( throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey', 'Global circular dependency detected for $dependencyKey',
cycle, cycle,
@@ -131,7 +136,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(logger: CherryPick.globalLogger)); return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
} }
/// RU: Удалить детектор для скоупа. /// RU: Удалить детектор для скоупа.

View File

@@ -13,7 +13,7 @@
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:cherrypick/src/observer.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@@ -22,7 +22,7 @@ Scope? _rootScope;
/// Global logger for all [Scope]s managed by [CherryPick]. /// Global logger for all [Scope]s managed by [CherryPick].
/// ///
/// Defaults to [SilentLogger] unless set via [setGlobalLogger]. /// Defaults to [SilentLogger] unless set via [setGlobalLogger].
CherryPickLogger _globalLogger = const SilentLogger(); CherryPickObserver _globalObserver = SilentCherryPickObserver();
/// Whether global local-cycle detection is enabled for all Scopes ([Scope.enableCycleDetection]). /// Whether global local-cycle detection is enabled for all Scopes ([Scope.enableCycleDetection]).
bool _globalCycleDetectionEnabled = false; bool _globalCycleDetectionEnabled = false;
@@ -59,12 +59,12 @@ class CherryPick {
/// ```dart /// ```dart
/// CherryPick.setGlobalLogger(DefaultLogger()); /// CherryPick.setGlobalLogger(DefaultLogger());
/// ``` /// ```
static void setGlobalLogger(CherryPickLogger logger) { static void setGlobalObserver(CherryPickObserver observer) {
_globalLogger = logger; _globalObserver = observer;
} }
/// Returns the current global logger used by CherryPick. /// Returns the current global logger used by CherryPick.
static CherryPickLogger get globalLogger => _globalLogger; static CherryPickObserver get globalObserver => _globalObserver;
/// Returns the singleton root [Scope], creating it if needed. /// Returns the singleton root [Scope], creating it if needed.
/// ///
@@ -75,7 +75,7 @@ class CherryPick {
/// final root = CherryPick.openRootScope(); /// final root = CherryPick.openRootScope();
/// ``` /// ```
static Scope openRootScope() { static Scope openRootScope() {
_rootScope ??= Scope(null, logger: _globalLogger); _rootScope ??= Scope(null, observer: _globalObserver);
// Apply cycle detection settings // Apply cycle detection settings
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) { if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
_rootScope!.enableCycleDetection(); _rootScope!.enableCycleDetection();

View File

@@ -1,108 +0,0 @@
//
// 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
// https://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');
}
}

View File

@@ -0,0 +1,119 @@
/// Observer for DI container (CherryPick): lifecycle, cache, modules, errors, etc.
abstract class CherryPickObserver {
// === Registration and instance lifecycle ===
void onBindingRegistered(String name, Type type, {String? scopeName});
void onInstanceRequested(String name, Type type, {String? scopeName});
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName});
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName});
// === Module events ===
void onModulesInstalled(List<String> moduleNames, {String? scopeName});
void onModulesRemoved(List<String> moduleNames, {String? scopeName});
// === Scope lifecycle ===
void onScopeOpened(String name);
void onScopeClosed(String name);
// === Cycle detection ===
void onCycleDetected(List<String> chain, {String? scopeName});
// === Cache events ===
void onCacheHit(String name, Type type, {String? scopeName});
void onCacheMiss(String name, Type type, {String? scopeName});
// === Диагностика ===
void onDiagnostic(String message, {Object? details});
// === Warnings & errors ===
void onWarning(String message, {Object? details});
void onError(String message, Object? error, StackTrace? stackTrace);
}
/// Diagnostic/Debug observer that prints all events
class PrintCherryPickObserver implements CherryPickObserver {
@override
void onBindingRegistered(String name, Type type, {String? scopeName}) =>
print('[binding][CherryPick] $name$type (scope: $scopeName)');
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) =>
print('[request][CherryPick] $name$type (scope: $scopeName)');
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) =>
print('[create][CherryPick] $name$type => $instance (scope: $scopeName)');
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) =>
print('[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) =>
print('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
@override
void onModulesRemoved(List<String> modules, {String? scopeName}) =>
print('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
@override
void onScopeOpened(String name) => print('[scope opened][CherryPick] $name');
@override
void onScopeClosed(String name) => print('[scope closed][CherryPick] $name');
@override
void onCycleDetected(List<String> chain, {String? scopeName}) =>
print('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
@override
void onCacheHit(String name, Type type, {String? scopeName}) =>
print('[cache hit][CherryPick] $name$type (scope: $scopeName)');
@override
void onCacheMiss(String name, Type type, {String? scopeName}) =>
print('[cache miss][CherryPick] $name$type (scope: $scopeName)');
@override
void onDiagnostic(String message, {Object? details}) =>
print('[diagnostic][CherryPick] $message ${details ?? ''}');
@override
void onWarning(String message, {Object? details}) =>
print('[warn][CherryPick] $message ${details ?? ''}');
@override
void onError(String message, Object? error, StackTrace? stackTrace) {
print('[error][CherryPick] $message');
if (error != null) print(' error: $error');
if (stackTrace != null) print(' stack: $stackTrace');
}
}
/// Silent observer: игнорирует все события
class SilentCherryPickObserver implements CherryPickObserver {
@override
void onBindingRegistered(String name, Type type, {String? scopeName}) {}
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) {}
@override
void onModulesRemoved(List<String> modules, {String? scopeName}) {}
@override
void onScopeOpened(String name) {}
@override
void onScopeClosed(String name) {}
@override
void onCycleDetected(List<String> chain, {String? scopeName}) {}
@override
void onCacheHit(String name, Type type, {String? scopeName}) {}
@override
void onCacheMiss(String name, Type type, {String? scopeName}) {}
@override
void onDiagnostic(String message, {Object? details}) {}
@override
void onWarning(String message, {Object? details}) {}
@override
void onError(String message, Object? error, StackTrace? stackTrace) {}
}

View File

@@ -18,16 +18,16 @@ import 'package:cherrypick/src/disposable.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'; import 'package:cherrypick/src/observer.dart';
import 'package:cherrypick/src/log_format.dart'; // 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; late final CherryPickObserver _observer;
@override @override
CherryPickLogger get logger => _logger; CherryPickObserver get observer => _observer;
/// COLLECTS all resolved instances that implement [Disposable]. /// COLLECTS all resolved instances that implement [Disposable].
final Set<Disposable> _disposables = HashSet(); final Set<Disposable> _disposables = HashSet();
@@ -41,16 +41,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
final Map<String, Scope> _scopeMap = HashMap(); final Map<String, Scope> _scopeMap = HashMap();
Scope(this._parentScope, {required CherryPickLogger logger}) : _logger = logger { Scope(this._parentScope, {required CherryPickObserver observer}) : _observer = observer {
setScopeId(_generateScopeId()); setScopeId(_generateScopeId());
logger.info(formatLogMessage( observer.onDiagnostic(
type: 'Scope', 'Scope created: ${scopeId ?? 'NO_ID'}',
name: scopeId ?? 'NO_ID', details: {
params: { 'type': 'Scope',
'name': scopeId ?? 'NO_ID',
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId, if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
'description': 'scope created',
}, },
description: 'scope created', );
));
} }
final Set<Module> _modulesList = HashSet(); final Set<Module> _modulesList = HashSet();
@@ -75,7 +76,7 @@ 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, logger: logger); // Наследуем логгер вниз по иерархии final childScope = Scope(this, observer: observer); // Наследуем observer вниз по иерархии
// print removed (trace) // print removed (trace)
// Наследуем настройки обнаружения циклических зависимостей // Наследуем настройки обнаружения циклических зависимостей
if (isCycleDetectionEnabled) { if (isCycleDetectionEnabled) {
@@ -85,15 +86,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
childScope.enableGlobalCycleDetection(); childScope.enableGlobalCycleDetection();
} }
_scopeMap[name] = childScope; _scopeMap[name] = childScope;
logger.info(formatLogMessage( observer.onDiagnostic(
type: 'SubScope', 'SubScope created: $name',
name: name, details: {
params: { 'type': 'SubScope',
'name': name,
'id': childScope.scopeId, 'id': childScope.scopeId,
if (scopeId != null) 'parent': scopeId, if (scopeId != null) 'parent': scopeId,
'description': 'subscope created',
}, },
description: 'subscope created', );
));
} }
return _scopeMap[name]!; return _scopeMap[name]!;
} }
@@ -111,15 +113,16 @@ 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( observer.onDiagnostic(
type: 'SubScope', 'SubScope closed: $name',
name: name, details: {
params: { 'type': 'SubScope',
'name': name,
'id': childScope.scopeId, 'id': childScope.scopeId,
if (scopeId != null) 'parent': scopeId, if (scopeId != null) 'parent': scopeId,
'description': 'subscope closed',
}, },
description: 'subscope closed', );
));
} }
_scopeMap.remove(name); _scopeMap.remove(name);
} }
@@ -132,18 +135,19 @@ 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( observer.onDiagnostic(
type: 'Module', 'Module installed: ${module.runtimeType}',
name: module.runtimeType.toString(), details: {
params: { 'type': 'Module',
'name': module.runtimeType.toString(),
'scope': scopeId, 'scope': scopeId,
'description': 'module installed',
}, },
description: 'module installed', );
));
module.builder(this); module.builder(this);
// После builder: для всех новых биндингов // После builder: для всех новых биндингов
for (final binding in module.bindingSet) { for (final binding in module.bindingSet) {
binding.logger = logger; binding.observer = observer;
binding.logAllDeferred(); binding.logAllDeferred();
} }
} }
@@ -157,11 +161,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// ///
/// return [Scope] /// return [Scope]
Scope dropModules() { Scope dropModules() {
logger.info(formatLogMessage( observer.onDiagnostic(
type: 'Scope', 'Modules dropped for scope: $scopeId',
name: scopeId, details: {
description: 'modules dropped', 'type': 'Scope',
)); 'name': scopeId,
'description': 'modules dropped',
},
);
_modulesList.clear(); _modulesList.clear();
_rebuildResolversIndex(); _rebuildResolversIndex();
return this; return this;
@@ -187,13 +194,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
return _resolveWithLocalDetection<T>(named: named, params: params); return _resolveWithLocalDetection<T>(named: named, params: params);
}); });
} catch (e, s) { } catch (e, s) {
logger.error( observer.onError(
formatLogMessage( 'Global cycle detection failed during resolve: $T',
type: 'Scope',
name: scopeId,
params: {'resolve': T.toString()},
description: 'global cycle detection failed during resolve',
),
e, e,
s, s,
); );
@@ -203,13 +205,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
try { try {
result = _resolveWithLocalDetection<T>(named: named, params: params); result = _resolveWithLocalDetection<T>(named: named, params: params);
} catch (e, s) { } catch (e, s) {
logger.error( observer.onError(
formatLogMessage( 'Failed to resolve: $T',
type: 'Scope',
name: scopeId,
params: {'resolve': T.toString()},
description: 'failed to resolve',
),
e, e,
s, s,
); );
@@ -226,27 +223,22 @@ 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( observer.onDiagnostic(
type: 'Scope', 'Successfully resolved: $T',
name: scopeId, details: {
params: { 'type': 'Scope',
'name': scopeId,
'resolve': T.toString(), 'resolve': T.toString(),
if (named != null) 'named': named, if (named != null) 'named': named,
'description': 'successfully resolved',
}, },
description: 'successfully resolved', );
));
return resolved; return resolved;
} else { } else {
logger.error( observer.onError(
formatLogMessage( 'Failed to resolve: $T',
type: 'Scope', null,
name: scopeId, null,
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?');

View File

@@ -23,38 +23,27 @@ class CyclicModule extends Module {
} }
void main() { void main() {
late MockLogger logger; late MockObserver observer;
setUp(() { setUp(() {
logger = MockLogger(); observer = MockObserver();
}); });
test('Global logger receives Scope and Binding events', () { test('Global logger receives Binding events', () {
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
scope.installModules([DummyModule()]); scope.installModules([DummyModule()]);
final _ = scope.resolve<DummyService>(named: 'test'); final _ = scope.resolve<DummyService>(named: 'test');
// Новый стиль проверки для formatLogMessage: // Проверяем, что биндинг DummyService зарегистрирован
expect( expect(
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('created')), observer.bindings.any((m) => m.contains('DummyService')),
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, isTrue,
); );
// Можно добавить проверки diagnostics, если Scope что-то пишет туда
}); });
test('CycleDetector logs cycle detection error', () { test('CycleDetector logs cycle detection error', () {
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
// print('[DEBUG] TEST SCOPE logger type=${scope.logger.runtimeType} hash=${scope.logger.hashCode}'); // print('[DEBUG] TEST SCOPE logger type=${scope.logger.runtimeType} hash=${scope.logger.hashCode}');
scope.enableCycleDetection(); scope.enableCycleDetection();
scope.installModules([CyclicModule()]); scope.installModules([CyclicModule()]);
@@ -62,12 +51,11 @@ void main() {
() => scope.resolve<A>(), () => scope.resolve<A>(),
throwsA(isA<CircularDependencyException>()), throwsA(isA<CircularDependencyException>()),
); );
// Дополнительно ищем и среди info на случай если лог от CycleDetector ошибочно не попал в errors // Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
final foundInErrors = logger.errors.any((m) => final foundInErrors = observer.errors.any((m) => m.contains('cycle detected'));
m.startsWith('[CycleDetector:') && m.contains('cycle detected')); final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected'));
final foundInInfos = logger.infos.any((m) => final foundCycleNotified = observer.cycles.isNotEmpty;
m.startsWith('[CycleDetector:') && m.contains('cycle detected')); expect(foundInErrors || foundInDiagnostics || foundCycleNotified, isTrue,
expect(foundInErrors || foundInInfos, isTrue, reason: 'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
reason: 'Ожидаем хотя бы один лог о цикле на уровне error или info; вот все errors: ${logger.errors}\ninfos: ${logger.infos}');
}); });
} }

View File

@@ -1,16 +1,48 @@
import 'package:cherrypick/cherrypick.dart'; import 'package:cherrypick/cherrypick.dart';
class MockLogger implements CherryPickLogger { class MockObserver implements CherryPickObserver {
final List<String> infos = []; final List<String> diagnostics = [];
final List<String> warns = []; final List<String> warnings = [];
final List<String> errors = []; final List<String> errors = [];
final List<List<String>> cycles = [];
final List<String> bindings = [];
@override @override
void info(String message) => infos.add(message); void onDiagnostic(String message, {Object? details}) =>
diagnostics.add(message);
@override @override
void warn(String message) => warns.add(message); void onWarning(String message, {Object? details}) => warnings.add(message);
@override @override
void error(String message, [Object? e, StackTrace? s]) => void onError(String message, Object? error, StackTrace? stackTrace) =>
errors.add( errors.add(
'$message${e != null ? ' $e' : ''}${s != null ? '\n$s' : ''}'); '$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
@override
void onCycleDetected(List<String> chain, {String? scopeName}) =>
cycles.add(chain);
@override
void onBindingRegistered(String name, Type type, {String? scopeName}) =>
bindings.add('$name $type');
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
@override
void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {}
@override
void onModulesRemoved(List<String> moduleNames, {String? scopeName}) {}
@override
void onScopeOpened(String name) {}
@override
void onScopeClosed(String name) {}
@override
void onCacheHit(String name, Type type, {String? scopeName}) {}
@override
void onCacheMiss(String name, Type type, {String? scopeName}) {}
} }

View File

@@ -4,16 +4,16 @@ import 'package:cherrypick/cherrypick.dart';
import '../mock_logger.dart'; import '../mock_logger.dart';
void main() { void main() {
late MockLogger logger; late MockObserver observer;
setUp(() { setUp(() {
logger = MockLogger(); observer = MockObserver();
CherryPick.setGlobalLogger(logger); CherryPick.setGlobalObserver(observer);
}); });
group('CycleDetector', () { group('CycleDetector', () {
late CycleDetector detector; late CycleDetector detector;
setUp(() { setUp(() {
detector = CycleDetector(logger: logger); detector = CycleDetector(observer: observer);
}); });
test('should detect simple circular dependency', () { test('should detect simple circular dependency', () {

View File

@@ -3,10 +3,10 @@ import 'package:test/test.dart';
import '../mock_logger.dart'; import '../mock_logger.dart';
void main() { void main() {
late MockLogger logger; late MockObserver observer;
setUp(() { setUp(() {
logger = MockLogger(); observer = MockObserver();
CherryPick.setGlobalLogger(logger); CherryPick.setGlobalObserver(observer);
}); });
group('CherryPick Cycle Detection Helper Methods', () { group('CherryPick Cycle Detection Helper Methods', () {
setUp(() { setUp(() {

View File

@@ -110,14 +110,14 @@ 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 logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
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 logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
expect(Scope(scope, logger: logger), isNotNull); // эквивалент expect(Scope(scope, observer: observer), isNotNull); // эквивалент
}); });
test('closeSubScope removes subscope so next openSubScope returns new', () async { test('closeSubScope removes subscope so next openSubScope returns new', () async {
final logger = MockLogger(); final logger = MockLogger();
@@ -130,9 +130,9 @@ void main() {
}); });
test('closeSubScope removes subscope so next openSubScope returns new', () { test('closeSubScope removes subscope so next openSubScope returns new', () {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
expect(Scope(scope, logger: logger), isNotNull); // эквивалент expect(Scope(scope, observer: observer), isNotNull); // эквивалент
// Нет необходимости тестировать open/closeSubScope в этом юните // Нет необходимости тестировать open/closeSubScope в этом юните
}); });
}); });
@@ -140,48 +140,48 @@ void main() {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
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 logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
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 observer = MockObserver();
final expectedValue = 'test string'; final expectedValue = 'test string';
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
.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 observer = MockObserver();
final expectedValue = 5; final expectedValue = 5;
final parentScope = Scope(null, logger: logger); final parentScope = Scope(null, observer: observer);
final scope = Scope(parentScope, logger: logger); final scope = Scope(parentScope, observer: observer);
parentScope.installModules([TestModule<int>(value: expectedValue)]); parentScope.installModules([TestModule<int>(value: expectedValue)]);
expect(scope.resolve<int>(), expectedValue); expect(scope.resolve<int>(), expectedValue);
}); });
test('Returns several values from parent container', () { test('Returns several values from parent container', () {
final logger = MockLogger(); final observer = MockObserver();
final expectedIntValue = 5; final expectedIntValue = 5;
final expectedStringValue = 'Hello world'; final expectedStringValue = 'Hello world';
final parentScope = Scope(null, logger: logger).installModules([ final parentScope = Scope(null, observer: observer).installModules([
TestModule<int>(value: expectedIntValue), TestModule<int>(value: expectedIntValue),
TestModule<String>(value: expectedStringValue) TestModule<String>(value: expectedStringValue)
]); ]);
final scope = Scope(parentScope, logger: logger); final scope = Scope(parentScope, observer: observer);
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 logger = MockLogger(); final observer = MockObserver();
final parentScope = Scope(null, logger: logger); final parentScope = Scope(null, observer: observer);
final scope = Scope(parentScope, logger: logger); final scope = Scope(parentScope, observer: observer);
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 logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]); final scope = Scope(null, observer: observer)..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>()));
@@ -191,8 +191,8 @@ void main() {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
group('Named Dependencies', () { group('Named Dependencies', () {
test('Resolve named binding', () { test('Resolve named binding', () {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..installModules([ ..installModules([
TestModule<String>(value: "first"), TestModule<String>(value: "first"),
TestModule<String>(value: "second", name: "special") TestModule<String>(value: "second", name: "special")
@@ -201,8 +201,8 @@ void main() {
expect(scope.resolve<String>(), "first"); expect(scope.resolve<String>(), "first");
}); });
test('Named binding does not clash with unnamed', () { test('Named binding does not clash with unnamed', () {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..installModules([ ..installModules([
TestModule<String>(value: "foo", name: "bar"), TestModule<String>(value: "foo", name: "bar"),
]); ]);
@@ -210,8 +210,8 @@ void main() {
expect(scope.resolve<String>(named: "bar"), "foo"); expect(scope.resolve<String>(named: "bar"), "foo");
}); });
test("tryResolve returns null for missing named", () { test("tryResolve returns null for missing named", () {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..installModules([ ..installModules([
TestModule<String>(value: "foo"), TestModule<String>(value: "foo"),
]); ]);
@@ -222,8 +222,8 @@ void main() {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
group('Provider with parameters', () { group('Provider with parameters', () {
test('Resolve dependency using providerWithParams', () { test('Resolve dependency using providerWithParams', () {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..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);
@@ -237,8 +237,8 @@ void main() {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
group('Async Resolution', () { group('Async Resolution', () {
test('Resolve async instance', () async { test('Resolve async instance', () async {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..installModules([ ..installModules([
_InlineModule((m, s) { _InlineModule((m, s) {
m.bind<String>().toInstance(Future.value('async value')); m.bind<String>().toInstance(Future.value('async value'));
@@ -247,8 +247,8 @@ void main() {
expect(await scope.resolveAsync<String>(), "async value"); expect(await scope.resolveAsync<String>(), "async value");
}); });
test('Resolve async provider', () async { test('Resolve async provider', () async {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..installModules([ ..installModules([
_InlineModule((m, s) { _InlineModule((m, s) {
m.bind<int>().toProvide(() async => 7); m.bind<int>().toProvide(() async => 7);
@@ -257,8 +257,8 @@ void main() {
expect(await scope.resolveAsync<int>(), 7); expect(await scope.resolveAsync<int>(), 7);
}); });
test('Resolve async provider with param', () async { test('Resolve async provider with param', () async {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger) final scope = Scope(null, observer: observer)
..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);
@@ -268,8 +268,8 @@ void main() {
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>())); expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
}); });
test('tryResolveAsync returns null for missing', () async { test('tryResolveAsync returns null for missing', () async {
final logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
final result = await scope.tryResolveAsync<String>(); final result = await scope.tryResolveAsync<String>();
expect(result, isNull); expect(result, isNull);
}); });
@@ -278,8 +278,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 logger = MockLogger(); final observer = MockObserver();
final scope = Scope(null, logger: logger); final scope = Scope(null, observer: observer);
expect(scope.tryResolve<int>(), isNull); expect(scope.tryResolve<int>(), isNull);
}); });
}); });

View File

@@ -2,7 +2,8 @@ import 'package:cherrypick/cherrypick.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick_annotations/cherrypick_annotations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:talker/talker.dart'; import 'package:talker_flutter/talker_flutter.dart';
import 'domain/repository/post_repository.dart'; import 'domain/repository/post_repository.dart';
import 'presentation/bloc/post_bloc.dart'; import 'presentation/bloc/post_bloc.dart';
@@ -12,7 +13,7 @@ part 'app.inject.cherrypick.g.dart';
class TalkerProvider extends InheritedWidget { class TalkerProvider extends InheritedWidget {
final Talker talker; final Talker talker;
const TalkerProvider({required this.talker, required Widget child, Key? key}) : super(key: key, child: child); const TalkerProvider({required this.talker, required super.child, super.key});
static Talker of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker; static Talker of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
@override @override
bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker; bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker;

View File

@@ -11,12 +11,12 @@ import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
void main() { void main() {
final talker = Talker(); final talker = Talker();
final talkerLogger = TalkerCherryPickLogger(talker); final talkerLogger = TalkerCherryPickObserver(talker);
Bloc.observer = TalkerBlocObserver(talker: talker); Bloc.observer = TalkerBlocObserver(talker: talker);
CherryPick.setGlobalLogger(talkerLogger); CherryPick.setGlobalObserver(talkerLogger);
// Включаем cycle-detection только в debug/test // Включаем cycle-detection только в debug/test
if (kDebugMode) { if (kDebugMode) {
CherryPick.enableGlobalCycleDetection(); CherryPick.enableGlobalCycleDetection();

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:postly/app.dart';
import '../../router/app_router.gr.dart'; import '../../router/app_router.gr.dart';
import '../bloc/post_bloc.dart'; import '../bloc/post_bloc.dart';

View File

@@ -1,5 +1,4 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import '../presentation/pages/logs_page.dart';
import 'app_router.gr.dart'; import 'app_router.gr.dart';
@AutoRouterConfig() @AutoRouterConfig()

View File

@@ -3,11 +3,11 @@ import 'package:talker/talker.dart';
void main() { void main() {
final talker = Talker(); final talker = Talker();
final logger = TalkerCherryPickLogger(talker); final logger = TalkerCherryPickObserver(talker);
logger.info('Hello from CherryPickLogger!'); logger.onDiagnostic('Hello from CherryPickLogger!');
logger.warn('Something might be wrong...'); logger.onWarning('Something might be wrong...');
logger.error('Oops! An error occurred', Exception('Test error')); logger.onError('Oops! An error occurred', Exception('Test error'), null);
// Вывод всех логов // Вывод всех логов
print('\nВсе сообщения логирования через Talker:'); print('\nВсе сообщения логирования через Talker:');

View File

@@ -1,24 +0,0 @@
import 'package:cherrypick/cherrypick.dart';
import 'package:talker/talker.dart';
/// Реализация CherryPickLogger для логирования через Talker
class TalkerCherryPickLogger implements CherryPickLogger {
final Talker talker;
TalkerCherryPickLogger(this.talker);
@override
void info(String message) => talker.info('[CherryPick] $message');
@override
void warn(String message) => talker.warning('[CherryPick] $message');
@override
void error(String message, [Object? error, StackTrace? stackTrace]) {
talker.handle(
error ?? '[CherryPick] $message',
stackTrace,
'[CherryPick] $message',
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:cherrypick/cherrypick.dart';
import 'package:talker/talker.dart';
/// CherryPickObserver-адаптер для логирования событий CherryPick через Talker
class TalkerCherryPickObserver implements CherryPickObserver {
final Talker talker;
TalkerCherryPickObserver(this.talker);
@override
void onBindingRegistered(String name, Type type, {String? scopeName}) {
talker.info('[binding][CherryPick] $name$type (scope: $scopeName)');
}
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) {
talker.info('[request][CherryPick] $name$type (scope: $scopeName)');
}
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {
talker.info('[create][CherryPick] $name$type => $instance (scope: $scopeName)');
}
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {
talker.info('[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
}
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) {
talker.info('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
}
@override
void onModulesRemoved(List<String> modules, {String? scopeName}) {
talker.info('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
}
@override
void onScopeOpened(String name) {
talker.info('[scope opened][CherryPick] $name');
}
@override
void onScopeClosed(String name) {
talker.info('[scope closed][CherryPick] $name');
}
@override
void onCycleDetected(List<String> chain, {String? scopeName}) {
talker.warning('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
}
@override
void onCacheHit(String name, Type type, {String? scopeName}) {
talker.info('[cache hit][CherryPick] $name$type (scope: $scopeName)');
}
@override
void onCacheMiss(String name, Type type, {String? scopeName}) {
talker.info('[cache miss][CherryPick] $name$type (scope: $scopeName)');
}
@override
void onDiagnostic(String message, {Object? details}) {
talker.verbose('[diagnostic][CherryPick] $message ${details ?? ''}');
}
@override
void onWarning(String message, {Object? details}) {
talker.warning('[warn][CherryPick] $message ${details ?? ''}');
}
@override
void onError(String message, Object? error, StackTrace? stackTrace) {
talker.handle(error ?? '[CherryPick] $message', stackTrace, '[error][CherryPick] $message');
}
}

View File

@@ -3,6 +3,6 @@
/// More dartdocs go here. /// More dartdocs go here.
library; library;
export 'src/talker_cherrypick_logger.dart'; export 'src/talker_cherrypick_observer.dart';
// TODO: Export any libraries intended for clients of this package. // TODO: Export any libraries intended for clients of this package.

View File

@@ -1,6 +1,7 @@
name: talker_cherrypick_logger name: talker_cherrypick_logger
description: A starting point for Dart libraries or applications. description: A starting point for Dart libraries or applications.
version: 1.0.0 version: 1.0.0
publish_to: none
# repository: https://github.com/my_org/my_repo # repository: https://github.com/my_org/my_repo
environment: environment:

View File

@@ -3,38 +3,44 @@ import 'package:talker/talker.dart';
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart'; import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
void main() { void main() {
group('TalkerCherryPickLogger', () { group('TalkerCherryPickObserver', () {
late Talker talker; late Talker talker;
late TalkerCherryPickLogger logger; late TalkerCherryPickObserver observer;
setUp(() { setUp(() {
talker = Talker(); talker = Talker();
logger = TalkerCherryPickLogger(talker); observer = TalkerCherryPickObserver(talker);
}); });
test('logs info messages correctly', () { test('onInstanceRequested logs info', () {
logger.info('Test info'); observer.onInstanceRequested('A', String, scopeName: 'test');
final log = talker.history.last; final log = talker.history.last;
expect(log.message, contains('[CherryPick] Test info')); expect(log.message, contains('[request][CherryPick] A — String (scope: test)'));
//xpect(log.level, TalkerLogLevel.info);
}); });
test('logs warning messages correctly', () { test('onCycleDetected logs warning', () {
logger.warn('Danger!'); observer.onCycleDetected(['A', 'B'], scopeName: 's');
final log = talker.history.last; final log = talker.history.last;
expect(log.message, contains('[CherryPick] Danger!')); expect(log.message, contains('[cycle][CherryPick] Detected'));
//expect(log.level, TalkerLogLevel.warning); //expect(log.level, TalkerLogLevel.warning);
}); });
test('logs error messages correctly', () { test('onError calls handle', () {
final error = Exception('some error'); final error = Exception('fail');
final stack = StackTrace.current; final stack = StackTrace.current;
logger.error('ERR', error, stack); observer.onError('Oops', error, stack);
final log = talker.history.last; final log = talker.history.last;
//expect(log.level, TalkerLogLevel.error); expect(log.message, contains('[error][CherryPick] Oops'));
expect(log.message, contains('[CherryPick] ERR'));
expect(log.exception, error); expect(log.exception, error);
expect(log.stackTrace, stack); expect(log.stackTrace, stack);
}); });
test('onDiagnostic logs verbose', () {
observer.onDiagnostic('hello', details: 123);
final log = talker.history.last;
//expect(log.level, TalkerLogLevel.verbose);
expect(log.message, contains('hello'));
expect(log.message, contains('123'));
});
}); });
} }