mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 05:25:19 +00:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,5 +18,7 @@ pubspec_overrides.yaml
|
||||
melos_cherrypick.iml
|
||||
melos_cherrypick_workspace.iml
|
||||
melos_cherrypick_flutter.iml
|
||||
melos_benchmark_di.iml
|
||||
melos_talker_cherrypick_logger.iml
|
||||
|
||||
coverage
|
||||
@@ -47,7 +47,7 @@ packages:
|
||||
path: "../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.5"
|
||||
version: "3.0.0-dev.7"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -422,30 +422,68 @@ final dialogManager = dialogScope.resolve<DialogManager>();
|
||||
|
||||
### 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:
|
||||
CherryPick lets you log all dependency injection (DI) events and errors using a flexible observer mechanism.
|
||||
|
||||
#### Custom Observers
|
||||
|
||||
You can pass any implementation of `CherryPickObserver` to your root scope or any sub-scope.
|
||||
This allows centralized and extensible logging, which you can direct to print, files, visualization frameworks, external loggers, or systems like [Talker](https://pub.dev/packages/talker).
|
||||
|
||||
##### Example: Printing All Events
|
||||
|
||||
```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!
|
||||
// Use the built-in PrintCherryPickObserver for console logs
|
||||
final observer = PrintCherryPickObserver();
|
||||
final scope = CherryPick.openRootScope(observer: observer);
|
||||
// All DI actions and errors will now be printed!
|
||||
}
|
||||
```
|
||||
- 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:
|
||||
##### Example: Advanced Logging with Talker
|
||||
|
||||
For richer logging, analytics, or UI overlays, use an advanced observer such as [talker_cherrypick_logger](../talker_cherrypick_logger):
|
||||
|
||||
```dart
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger); // works in tests for isolation
|
||||
scope.installModules([...]);
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final observer = TalkerCherryPickObserver(talker);
|
||||
CherryPick.openRootScope(observer: observer);
|
||||
// All container events go to the Talker log system!
|
||||
}
|
||||
```
|
||||
|
||||
#### Default Behavior
|
||||
|
||||
- By default, logging is silent (`SilentCherryPickObserver`) for production, with no output unless you supply an observer.
|
||||
- You can configure observers **per scope** for isolated, test-specific, or feature-specific logging.
|
||||
|
||||
#### Observer Capabilities
|
||||
|
||||
Events you can observe and log:
|
||||
- Dependency registration
|
||||
- Instance requests, creations, disposals
|
||||
- Module installs/removals
|
||||
- Scope opening/closing
|
||||
- Cache hits/misses
|
||||
- Cycle detection
|
||||
- Diagnostics, warnings, errors
|
||||
|
||||
Just implement or extend `CherryPickObserver` and direct messages anywhere you want!
|
||||
|
||||
#### When to Use
|
||||
|
||||
- Enable verbose logging and debugging in development or test builds.
|
||||
- Route logs to your main log system or analytics.
|
||||
- Hook into DI lifecycle for profiling or monitoring.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Circular Dependency Detection
|
||||
|
||||
@@ -15,7 +15,7 @@ class AppModule extends Module {
|
||||
|
||||
void main() {
|
||||
// Set a global logger for the DI system
|
||||
CherryPick.setGlobalLogger(PrintLogger());
|
||||
CherryPick.setGlobalObserver(PrintCherryPickObserver());
|
||||
|
||||
// Open the root scope
|
||||
final rootScope = CherryPick.openRootScope();
|
||||
@@ -32,6 +32,6 @@ void main() {
|
||||
subScope.closeSubScope('feature.profile');
|
||||
|
||||
// Demonstrate disabling and re-enabling logging
|
||||
CherryPick.setGlobalLogger(const SilentLogger());
|
||||
CherryPick.setGlobalObserver(SilentCherryPickObserver());
|
||||
rootScope.resolve<UserRepository>(); // now without logs
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ 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';
|
||||
export 'package:cherrypick/src/disposable.dart';
|
||||
export 'package:cherrypick/src/observer.dart';
|
||||
|
||||
@@ -16,8 +16,7 @@ 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';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
|
||||
class Binding<T> {
|
||||
late Type _key;
|
||||
@@ -25,50 +24,54 @@ class Binding<T> {
|
||||
|
||||
BindingResolver<T>? _resolver;
|
||||
|
||||
CherryPickLogger? logger;
|
||||
CherryPickObserver? observer;
|
||||
|
||||
// Deferred logging flags
|
||||
bool _createdLogged = false;
|
||||
bool _namedLogged = false;
|
||||
bool _singletonLogged = false;
|
||||
|
||||
Binding({this.logger}) {
|
||||
Binding({this.observer}) {
|
||||
_key = T;
|
||||
// Не логируем здесь! Делаем deferred лог после назначения logger
|
||||
// Deferred уведомения observer, не логировать здесь напрямую
|
||||
}
|
||||
|
||||
void markCreated() {
|
||||
if (!_createdLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: _name != null ? {'name': _name} : null,
|
||||
description: 'created',
|
||||
));
|
||||
observer?.onBindingRegistered(
|
||||
runtimeType.toString(),
|
||||
T,
|
||||
);
|
||||
_createdLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markNamed() {
|
||||
if (isNamed && !_namedLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {'name': _name},
|
||||
description: 'named',
|
||||
));
|
||||
observer?.onDiagnostic(
|
||||
'Binding named: ${T.toString()} name: $_name',
|
||||
details: {
|
||||
'type': 'Binding',
|
||||
'name': T.toString(),
|
||||
'nameParam': _name,
|
||||
'description': 'named',
|
||||
},
|
||||
);
|
||||
_namedLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markSingleton() {
|
||||
if (isSingleton && !_singletonLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: _name != null ? {'name': _name} : null,
|
||||
description: 'singleton mode enabled',
|
||||
));
|
||||
observer?.onDiagnostic(
|
||||
'Binding singleton: ${T.toString()}${_name != null ? ' name: $_name' : ''}',
|
||||
details: {
|
||||
'type': 'Binding',
|
||||
'name': T.toString(),
|
||||
if (_name != null) 'name': _name,
|
||||
'description': 'singleton mode enabled',
|
||||
},
|
||||
);
|
||||
_singletonLogged = true;
|
||||
}
|
||||
}
|
||||
@@ -170,25 +173,23 @@ class Binding<T> {
|
||||
T? resolveSync([dynamic params]) {
|
||||
final res = resolver?.resolveSync(params);
|
||||
if (res != null) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onDiagnostic(
|
||||
'Binding resolved instance: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
'description': 'object created/resolved',
|
||||
},
|
||||
description: 'object created/resolved',
|
||||
));
|
||||
);
|
||||
} else {
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onWarning(
|
||||
'resolveSync returned null: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
'description': 'resolveSync returned null',
|
||||
},
|
||||
description: 'resolveSync returned null',
|
||||
));
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -197,38 +198,28 @@ class Binding<T> {
|
||||
final future = resolver?.resolveAsync(params);
|
||||
if (future != null) {
|
||||
future
|
||||
.then((res) => logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
.then((res) => observer?.onDiagnostic(
|
||||
'Future resolved for: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
'description': 'Future resolved',
|
||||
},
|
||||
description: 'Future resolved',
|
||||
)))
|
||||
.catchError((e, s) => logger?.error(
|
||||
formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
},
|
||||
description: 'resolveAsync error',
|
||||
),
|
||||
))
|
||||
.catchError((e, s) => observer?.onError(
|
||||
'resolveAsync error: ${T.toString()}',
|
||||
e,
|
||||
s,
|
||||
));
|
||||
} else {
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onWarning(
|
||||
'resolveAsync returned null: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
'description': 'resolveAsync returned null',
|
||||
},
|
||||
description: 'resolveAsync returned null',
|
||||
));
|
||||
);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
//
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
|
||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||
/// ENG: Exception thrown when a circular dependency is detected.
|
||||
@@ -36,11 +35,11 @@ class CircularDependencyException implements Exception {
|
||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||
class CycleDetector {
|
||||
final CherryPickLogger _logger;
|
||||
final CherryPickObserver _observer;
|
||||
final Set<String> _resolutionStack = HashSet<String>();
|
||||
final List<String> _resolutionHistory = [];
|
||||
|
||||
CycleDetector({required CherryPickLogger logger}): _logger = logger;
|
||||
CycleDetector({required CherryPickObserver observer}) : _observer = observer;
|
||||
|
||||
/// RU: Начинает отслеживание разрешения зависимости.
|
||||
/// ENG: Starts tracking dependency resolution.
|
||||
@@ -48,25 +47,24 @@ class CycleDetector {
|
||||
/// Throws [CircularDependencyException] if circular dependency is detected.
|
||||
void startResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
print('[DEBUG] CycleDetector logger type=${_logger.runtimeType} hash=${_logger.hashCode}');
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'event': 'startResolving', 'stackSize': _resolutionStack.length},
|
||||
description: 'start resolving',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector startResolving: $dependencyKey',
|
||||
details: {
|
||||
'event': 'startResolving',
|
||||
'stackSize': _resolutionStack.length,
|
||||
},
|
||||
);
|
||||
if (_resolutionStack.contains(dependencyKey)) {
|
||||
// Найдена циклическая зависимость
|
||||
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
// print removed (trace)
|
||||
final msg = formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
_observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
_logger.error(msg);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -81,12 +79,10 @@ class CycleDetector {
|
||||
/// ENG: Finishes tracking dependency resolution.
|
||||
void finishResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'event': 'finishResolving'},
|
||||
description: 'finish resolving',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector finishResolving: $dependencyKey',
|
||||
details: {'event': 'finishResolving'},
|
||||
);
|
||||
_resolutionStack.remove(dependencyKey);
|
||||
// Удаляем из истории только если это последний элемент
|
||||
if (_resolutionHistory.isNotEmpty &&
|
||||
@@ -98,11 +94,13 @@ class CycleDetector {
|
||||
/// RU: Очищает все состояние детектора.
|
||||
/// ENG: Clears all detector state.
|
||||
void clear() {
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
params: {'event': 'clear'},
|
||||
description: 'resolution stack cleared',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector clear',
|
||||
details: {
|
||||
'event': 'clear',
|
||||
'description': 'resolution stack cleared',
|
||||
},
|
||||
);
|
||||
_resolutionStack.clear();
|
||||
_resolutionHistory.clear();
|
||||
}
|
||||
@@ -130,28 +128,32 @@ class CycleDetector {
|
||||
/// ENG: Mixin for adding circular dependency detection support.
|
||||
mixin CycleDetectionMixin {
|
||||
CycleDetector? _cycleDetector;
|
||||
CherryPickLogger get logger;
|
||||
CherryPickObserver get observer;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей.
|
||||
/// ENG: Enables circular dependency detection.
|
||||
void enableCycleDetection() {
|
||||
_cycleDetector = CycleDetector(logger: logger);
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'enable'},
|
||||
description: 'cycle detection enabled',
|
||||
));
|
||||
_cycleDetector = CycleDetector(observer: observer);
|
||||
observer.onDiagnostic(
|
||||
'CycleDetection enabled',
|
||||
details: {
|
||||
'event': 'enable',
|
||||
'description': 'cycle detection enabled',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Disables circular dependency detection.
|
||||
void disableCycleDetection() {
|
||||
_cycleDetector?.clear();
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'disable'},
|
||||
description: 'cycle detection disabled',
|
||||
));
|
||||
observer.onDiagnostic(
|
||||
'CycleDetection disabled',
|
||||
details: {
|
||||
'event': 'disable',
|
||||
'description': 'cycle detection disabled',
|
||||
},
|
||||
);
|
||||
_cycleDetector = null;
|
||||
}
|
||||
|
||||
@@ -178,12 +180,14 @@ mixin CycleDetectionMixin {
|
||||
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
|
||||
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
||||
@@ -21,7 +20,7 @@ import 'package:cherrypick/src/log_format.dart';
|
||||
class GlobalCycleDetector {
|
||||
static GlobalCycleDetector? _instance;
|
||||
|
||||
final CherryPickLogger _logger;
|
||||
final CherryPickObserver _observer;
|
||||
|
||||
// Глобальный стек разрешения зависимостей
|
||||
final Set<String> _globalResolutionStack = HashSet<String>();
|
||||
@@ -32,12 +31,12 @@ class GlobalCycleDetector {
|
||||
// Карта активных детекторов по скоупам
|
||||
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
||||
|
||||
GlobalCycleDetector._internal({required CherryPickLogger logger}): _logger = logger;
|
||||
GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer;
|
||||
|
||||
/// RU: Получить единственный экземпляр глобального детектора.
|
||||
/// ENG: Get singleton instance of global detector.
|
||||
static GlobalCycleDetector get instance {
|
||||
_instance ??= GlobalCycleDetector._internal(logger: CherryPick.globalLogger);
|
||||
_instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@@ -59,12 +58,15 @@ class GlobalCycleDetector {
|
||||
// Найдена глобальная циклическая зависимость
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
_logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -102,12 +104,15 @@ class GlobalCycleDetector {
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
_logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -131,7 +136,7 @@ class GlobalCycleDetector {
|
||||
/// RU: Получить детектор для конкретного скоупа.
|
||||
/// ENG: Get detector for specific scope.
|
||||
CycleDetector getScopeDetector(String scopeId) {
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(logger: CherryPick.globalLogger));
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
|
||||
}
|
||||
|
||||
/// RU: Удалить детектор для скоупа.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import 'package:cherrypick/src/scope.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';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Scope? _rootScope;
|
||||
/// Global logger for all [Scope]s managed by [CherryPick].
|
||||
///
|
||||
/// 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]).
|
||||
bool _globalCycleDetectionEnabled = false;
|
||||
@@ -59,12 +59,12 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// CherryPick.setGlobalLogger(DefaultLogger());
|
||||
/// ```
|
||||
static void setGlobalLogger(CherryPickLogger logger) {
|
||||
_globalLogger = logger;
|
||||
static void setGlobalObserver(CherryPickObserver observer) {
|
||||
_globalObserver = observer;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -75,7 +75,7 @@ class CherryPick {
|
||||
/// final root = CherryPick.openRootScope();
|
||||
/// ```
|
||||
static Scope openRootScope() {
|
||||
_rootScope ??= Scope(null, logger: _globalLogger);
|
||||
_rootScope ??= Scope(null, observer: _globalObserver);
|
||||
// Apply cycle detection settings
|
||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
|
||||
@@ -1,55 +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.
|
||||
//
|
||||
|
||||
|
||||
/// Formats a log message string for CherryPick's logging system.
|
||||
///
|
||||
/// This function provides a unified structure for framework logs (info, warn, error, debug, etc.),
|
||||
/// making it easier to parse and analyze events related to DI operations such as resolving bindings,
|
||||
/// scope creation, module installation, etc.
|
||||
///
|
||||
/// All parameters except [name] and [params] are required.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final msg = formatLogMessage(
|
||||
/// type: 'Binding',
|
||||
/// name: 'MyService',
|
||||
/// params: {'parent': 'AppModule', 'lifecycle': 'singleton'},
|
||||
/// description: 'created',
|
||||
/// );
|
||||
/// // Result: [Binding:MyService] parent=AppModule lifecycle=singleton created
|
||||
/// ```
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [type]: The type of the log event subject (e.g., 'Binding', 'Scope', 'Module'). Required.
|
||||
/// - [name]: Optional name of the subject (binding/scope/module) to disambiguate multiple instances/objects.
|
||||
/// - [params]: Optional map for additional context (e.g., id, parent, lifecycle, named, etc.).
|
||||
/// - [description]: Concise description of the event. Required.
|
||||
///
|
||||
/// Returns a structured string:
|
||||
/// [type(:name)] param1=val1 param2=val2 ... description
|
||||
String formatLogMessage({
|
||||
required String type, // Binding, Scope, Module, ...
|
||||
String? name, // Имя binding/scope/module
|
||||
Map<String, Object?>? params, // Дополнительные параметры (id, parent, named и др.)
|
||||
required String description, // Краткое описание события
|
||||
}) {
|
||||
final label = name != null ? '$type:$name' : type;
|
||||
final paramsStr = (params != null && params.isNotEmpty)
|
||||
? params.entries.map((e) => '${e.key}=${e.value}').join(' ')
|
||||
: '';
|
||||
return '[$label]'
|
||||
'${paramsStr.isNotEmpty ? ' $paramsStr' : ''}'
|
||||
' $description';
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
236
cherrypick/lib/src/observer.dart
Normal file
236
cherrypick/lib/src/observer.dart
Normal file
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
/// An abstract Observer for CherryPick DI container events.
|
||||
///
|
||||
/// Extend this class to react to and log various events inside the CherryPick Dependency Injection container.
|
||||
/// Allows monitoring of registration, creation, disposal, module changes, cache hits/misses, cycles, and
|
||||
/// errors/warnings for improved diagnostics and debugging.
|
||||
///
|
||||
/// All methods have detailed event information, including name, type, scope, and other arguments.
|
||||
///
|
||||
/// Example: Logging and debugging container events
|
||||
/// ```dart
|
||||
/// final CherryPickObserver observer = PrintCherryPickObserver();
|
||||
/// // Pass observer to CherryPick during setup
|
||||
/// CherryPick.openRootScope(observer: observer);
|
||||
/// ```
|
||||
abstract class CherryPickObserver {
|
||||
// === Registration and instance lifecycle ===
|
||||
/// Called when a binding is registered within the container (new dependency mapping).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onBindingRegistered('MyService', MyService, scopeName: 'root');
|
||||
/// ```
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName});
|
||||
|
||||
/// Called when an instance is requested (before it is created or retrieved from cache).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onInstanceRequested('MyService', MyService, scopeName: 'root');
|
||||
/// ```
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName});
|
||||
|
||||
/// Called when a new instance is successfully created.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onInstanceCreated('MyService', MyService, instance, scopeName: 'root');
|
||||
/// ```
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName});
|
||||
|
||||
/// Called when an instance is disposed (removed from cache and/or finalized).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onInstanceDisposed('MyService', MyService, instance, scopeName: 'root');
|
||||
/// ```
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName});
|
||||
|
||||
// === Module events ===
|
||||
/// Called when modules are installed into the container.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onModulesInstalled(['NetworkModule', 'RepositoryModule'], scopeName: 'root');
|
||||
/// ```
|
||||
void onModulesInstalled(List<String> moduleNames, {String? scopeName});
|
||||
|
||||
/// Called when modules are removed from the container.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onModulesRemoved(['RepositoryModule'], scopeName: 'root');
|
||||
/// ```
|
||||
void onModulesRemoved(List<String> moduleNames, {String? scopeName});
|
||||
|
||||
// === Scope lifecycle ===
|
||||
/// Called when a new DI scope is opened (for example, starting a new feature or screen).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onScopeOpened('user-session');
|
||||
/// ```
|
||||
void onScopeOpened(String name);
|
||||
|
||||
/// Called when an existing DI scope is closed.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onScopeClosed('user-session');
|
||||
/// ```
|
||||
void onScopeClosed(String name);
|
||||
|
||||
// === Cycle detection ===
|
||||
/// Called if a dependency cycle is detected during resolution.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onCycleDetected(['A', 'B', 'C', 'A'], scopeName: 'root');
|
||||
/// ```
|
||||
void onCycleDetected(List<String> chain, {String? scopeName});
|
||||
|
||||
// === Cache events ===
|
||||
/// Called when an instance is found in the cache.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onCacheHit('MyService', MyService, scopeName: 'root');
|
||||
/// ```
|
||||
void onCacheHit(String name, Type type, {String? scopeName});
|
||||
|
||||
/// Called when an instance is not found in the cache and should be created.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onCacheMiss('MyService', MyService, scopeName: 'root');
|
||||
/// ```
|
||||
void onCacheMiss(String name, Type type, {String? scopeName});
|
||||
|
||||
// === Diagnostic ===
|
||||
/// Used for custom diagnostic and debug messages.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onDiagnostic('Cache cleared', details: detailsObj);
|
||||
/// ```
|
||||
void onDiagnostic(String message, {Object? details});
|
||||
|
||||
// === Warnings & errors ===
|
||||
/// Called on non-fatal, recoverable DI container warnings.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onWarning('Binding override', details: {...});
|
||||
/// ```
|
||||
void onWarning(String message, {Object? details});
|
||||
|
||||
/// Called on error (typically exceptions thrown during resolution, instantiation, or disposal).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// observer.onError('Failed to resolve dependency', errorObj, stackTraceObj);
|
||||
/// ```
|
||||
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: ignores all events
|
||||
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) {}
|
||||
}
|
||||
@@ -18,16 +18,16 @@ import 'package:cherrypick/src/disposable.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';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
// import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
final Scope? _parentScope;
|
||||
|
||||
late final CherryPickLogger _logger;
|
||||
late final CherryPickObserver _observer;
|
||||
|
||||
@override
|
||||
CherryPickLogger get logger => _logger;
|
||||
CherryPickObserver get observer => _observer;
|
||||
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
@@ -41,16 +41,18 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
|
||||
final Map<String, Scope> _scopeMap = HashMap();
|
||||
|
||||
Scope(this._parentScope, {required CherryPickLogger logger}) : _logger = logger {
|
||||
Scope(this._parentScope, {required CherryPickObserver observer}) : _observer = observer {
|
||||
setScopeId(_generateScopeId());
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId ?? 'NO_ID',
|
||||
params: {
|
||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||
observer.onDiagnostic(
|
||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId ?? 'NO_ID',
|
||||
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||
'description': 'scope created',
|
||||
},
|
||||
description: 'scope created',
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
final Set<Module> _modulesList = HashSet();
|
||||
@@ -75,7 +77,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// return [Scope]
|
||||
Scope openSubScope(String name) {
|
||||
if (!_scopeMap.containsKey(name)) {
|
||||
final childScope = Scope(this, logger: logger); // Наследуем логгер вниз по иерархии
|
||||
final childScope = Scope(this, observer: observer); // Наследуем observer вниз по иерархии
|
||||
// print removed (trace)
|
||||
// Наследуем настройки обнаружения циклических зависимостей
|
||||
if (isCycleDetectionEnabled) {
|
||||
@@ -85,15 +87,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
_scopeMap[name] = childScope;
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'SubScope created: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope created',
|
||||
},
|
||||
description: 'subscope created',
|
||||
));
|
||||
);
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
@@ -111,15 +114,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
observer.onScopeClosed(childScope.scopeId ?? name);
|
||||
observer.onDiagnostic(
|
||||
'SubScope closed: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope closed',
|
||||
},
|
||||
description: 'subscope closed',
|
||||
));
|
||||
);
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
@@ -131,19 +136,26 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// return [Scope]
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
if (modules.isNotEmpty) {
|
||||
observer.onModulesInstalled(
|
||||
modules.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
for (var module in modules) {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Module',
|
||||
name: module.runtimeType.toString(),
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'Module installed: ${module.runtimeType}',
|
||||
details: {
|
||||
'type': 'Module',
|
||||
'name': module.runtimeType.toString(),
|
||||
'scope': scopeId,
|
||||
'description': 'module installed',
|
||||
},
|
||||
description: 'module installed',
|
||||
));
|
||||
);
|
||||
module.builder(this);
|
||||
// После builder: для всех новых биндингов
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.logger = logger;
|
||||
binding.observer = observer;
|
||||
binding.logAllDeferred();
|
||||
}
|
||||
}
|
||||
@@ -157,11 +169,20 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
/// return [Scope]
|
||||
Scope dropModules() {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
description: 'modules dropped',
|
||||
));
|
||||
if (_modulesList.isNotEmpty) {
|
||||
observer.onModulesRemoved(
|
||||
_modulesList.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
observer.onDiagnostic(
|
||||
'Modules dropped for scope: $scopeId',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'description': 'modules dropped',
|
||||
},
|
||||
);
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -179,6 +200,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// return - returns an object of type [T] or [StateError]
|
||||
///
|
||||
T resolve<T>({String? named, dynamic params}) {
|
||||
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
@@ -187,13 +209,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'global cycle detection failed during resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Global cycle detection failed during resolve: $T',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
@@ -203,13 +220,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
try {
|
||||
result = _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
} catch (e, s) {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Failed to resolve: $T',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
@@ -226,27 +238,23 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return withCycleDetection<T>(T, named, () {
|
||||
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
|
||||
observer.onDiagnostic(
|
||||
'Successfully resolved: $T',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
'description': 'successfully resolved',
|
||||
},
|
||||
description: 'successfully resolved',
|
||||
));
|
||||
);
|
||||
return resolved;
|
||||
} else {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Failed to resolve: $T',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw StateError(
|
||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||
@@ -324,8 +332,24 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return withCycleDetection<Future<T>>(T, named, () async {
|
||||
var resolved = await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
|
||||
observer.onDiagnostic(
|
||||
'Successfully async resolved: $T',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
'description': 'successfully resolved (async)',
|
||||
},
|
||||
);
|
||||
return resolved;
|
||||
} else {
|
||||
observer.onError(
|
||||
'Failed to async resolve: $T',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw StateError(
|
||||
'Can\'t resolve async dependency `$T`. Maybe you forget register it?');
|
||||
}
|
||||
|
||||
@@ -23,38 +23,27 @@ class CyclicModule extends Module {
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
observer = MockObserver();
|
||||
});
|
||||
|
||||
test('Global logger receives Scope and Binding events', () {
|
||||
final scope = Scope(null, logger: logger);
|
||||
test('Global logger receives Binding events', () {
|
||||
final scope = Scope(null, observer: observer);
|
||||
scope.installModules([DummyModule()]);
|
||||
final _ = scope.resolve<DummyService>(named: 'test');
|
||||
|
||||
// Новый стиль проверки для formatLogMessage:
|
||||
// Проверяем, что биндинг DummyService зарегистрирован
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('created')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('created')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('named') && m.contains('name=test')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('resolve=DummyService') && m.contains('successfully resolved')),
|
||||
observer.bindings.any((m) => m.contains('DummyService')),
|
||||
isTrue,
|
||||
);
|
||||
// Можно добавить проверки diagnostics, если Scope что-то пишет туда
|
||||
});
|
||||
|
||||
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}');
|
||||
scope.enableCycleDetection();
|
||||
scope.installModules([CyclicModule()]);
|
||||
@@ -62,12 +51,11 @@ void main() {
|
||||
() => scope.resolve<A>(),
|
||||
throwsA(isA<CircularDependencyException>()),
|
||||
);
|
||||
// Дополнительно ищем и среди info на случай если лог от CycleDetector ошибочно не попал в errors
|
||||
final foundInErrors = logger.errors.any((m) =>
|
||||
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||
final foundInInfos = logger.infos.any((m) =>
|
||||
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||
expect(foundInErrors || foundInInfos, isTrue,
|
||||
reason: 'Ожидаем хотя бы один лог о цикле на уровне error или info; вот все errors: ${logger.errors}\ninfos: ${logger.infos}');
|
||||
// Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
|
||||
final foundInErrors = observer.errors.any((m) => m.contains('cycle detected'));
|
||||
final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected'));
|
||||
final foundCycleNotified = observer.cycles.isNotEmpty;
|
||||
expect(foundInErrors || foundInDiagnostics || foundCycleNotified, isTrue,
|
||||
reason: 'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,48 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
class MockLogger implements CherryPickLogger {
|
||||
final List<String> infos = [];
|
||||
final List<String> warns = [];
|
||||
class MockObserver implements CherryPickObserver {
|
||||
final List<String> diagnostics = [];
|
||||
final List<String> warnings = [];
|
||||
final List<String> errors = [];
|
||||
final List<List<String>> cycles = [];
|
||||
final List<String> bindings = [];
|
||||
|
||||
@override
|
||||
void info(String message) => infos.add(message);
|
||||
void onDiagnostic(String message, {Object? details}) =>
|
||||
diagnostics.add(message);
|
||||
|
||||
@override
|
||||
void warn(String message) => warns.add(message);
|
||||
void onWarning(String message, {Object? details}) => warnings.add(message);
|
||||
|
||||
@override
|
||||
void error(String message, [Object? e, StackTrace? s]) =>
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) =>
|
||||
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}) {}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import 'package:cherrypick/cherrypick.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
observer = MockObserver();
|
||||
CherryPick.setGlobalObserver(observer);
|
||||
});
|
||||
group('CycleDetector', () {
|
||||
late CycleDetector detector;
|
||||
|
||||
setUp(() {
|
||||
detector = CycleDetector(logger: logger);
|
||||
detector = CycleDetector(observer: observer);
|
||||
});
|
||||
|
||||
test('should detect simple circular dependency', () {
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
observer = MockObserver();
|
||||
CherryPick.setGlobalObserver(observer);
|
||||
});
|
||||
group('CherryPick Cycle Detection Helper Methods', () {
|
||||
setUp(() {
|
||||
|
||||
@@ -110,18 +110,18 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Scope & Subscope Management', () {
|
||||
test('Scope has no parent if constructed with null', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(scope.parentScope, null);
|
||||
});
|
||||
test('Can open and retrieve the same subScope by key', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
|
||||
});
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
final subScope = scope.openSubScope("child");
|
||||
expect(scope.openSubScope("child"), same(subScope));
|
||||
await scope.closeSubScope("child");
|
||||
@@ -130,9 +130,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
|
||||
// Нет необходимости тестировать open/closeSubScope в этом юните
|
||||
});
|
||||
});
|
||||
@@ -140,48 +140,48 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Dependency Resolution (standard)', () {
|
||||
test("Throws StateError if value can't be resolved", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test('Resolves value after adding a dependency', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedValue = 'test string';
|
||||
final scope = Scope(null, logger: logger)
|
||||
final scope = Scope(null, observer: observer)
|
||||
.installModules([TestModule<String>(value: expectedValue)]);
|
||||
expect(scope.resolve<String>(), expectedValue);
|
||||
});
|
||||
test('Returns a value from parent scope', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedValue = 5;
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final parentScope = Scope(null, observer: observer);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
|
||||
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
||||
expect(scope.resolve<int>(), expectedValue);
|
||||
});
|
||||
test('Returns several values from parent container', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedIntValue = 5;
|
||||
final expectedStringValue = 'Hello world';
|
||||
final parentScope = Scope(null, logger: logger).installModules([
|
||||
final parentScope = Scope(null, observer: observer).installModules([
|
||||
TestModule<int>(value: expectedIntValue),
|
||||
TestModule<String>(value: expectedStringValue)
|
||||
]);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
|
||||
expect(scope.resolve<int>(), expectedIntValue);
|
||||
expect(scope.resolve<String>(), expectedStringValue);
|
||||
});
|
||||
test("Throws StateError if parent hasn't value too", () {
|
||||
final logger = MockLogger();
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final parentScope = Scope(null, observer: observer);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test("After dropModules resolves fail", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)..installModules([TestModule<int>(value: 5)]);
|
||||
expect(scope.resolve<int>(), 5);
|
||||
scope.dropModules();
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
@@ -191,8 +191,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Named Dependencies', () {
|
||||
test('Resolve named binding', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "first"),
|
||||
TestModule<String>(value: "second", name: "special")
|
||||
@@ -201,8 +201,8 @@ void main() {
|
||||
expect(scope.resolve<String>(), "first");
|
||||
});
|
||||
test('Named binding does not clash with unnamed', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo", name: "bar"),
|
||||
]);
|
||||
@@ -210,8 +210,8 @@ void main() {
|
||||
expect(scope.resolve<String>(named: "bar"), "foo");
|
||||
});
|
||||
test("tryResolve returns null for missing named", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo"),
|
||||
]);
|
||||
@@ -222,8 +222,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Provider with parameters', () {
|
||||
test('Resolve dependency using providerWithParams', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
|
||||
@@ -237,8 +237,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Async Resolution', () {
|
||||
test('Resolve async instance', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<String>().toInstance(Future.value('async value'));
|
||||
@@ -247,8 +247,8 @@ void main() {
|
||||
expect(await scope.resolveAsync<String>(), "async value");
|
||||
});
|
||||
test('Resolve async provider', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvide(() async => 7);
|
||||
@@ -257,8 +257,8 @@ void main() {
|
||||
expect(await scope.resolveAsync<int>(), 7);
|
||||
});
|
||||
test('Resolve async provider with param', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
||||
@@ -268,8 +268,8 @@ void main() {
|
||||
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test('tryResolveAsync returns null for missing', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
final result = await scope.tryResolveAsync<String>();
|
||||
expect(result, isNull);
|
||||
});
|
||||
@@ -278,8 +278,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Optional resolution and error handling', () {
|
||||
test("tryResolve returns null for missing dependency", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(scope.tryResolve<int>(), isNull);
|
||||
});
|
||||
});
|
||||
@@ -295,7 +295,7 @@ void main() {
|
||||
expect(t.disposed, isTrue);
|
||||
});
|
||||
test('scope.disposeAsync calls dispose on all unique disposables', () async {
|
||||
final scope = Scope(null, logger: MockLogger());
|
||||
final scope = Scope(null, observer: MockObserver());
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t1 = scope.resolve<TestDisposable>();
|
||||
final t2 = scope.resolve<AnotherDisposable>();
|
||||
|
||||
@@ -127,28 +127,28 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.5"
|
||||
version: "3.0.0-dev.7"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
cherrypick_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.3-dev.5"
|
||||
version: "1.1.3-dev.7"
|
||||
cherrypick_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
|
||||
import 'domain/repository/post_repository.dart';
|
||||
import 'presentation/bloc/post_bloc.dart';
|
||||
@@ -9,26 +11,38 @@ import 'router/app_router.dart';
|
||||
|
||||
part 'app.inject.cherrypick.g.dart';
|
||||
|
||||
class TalkerProvider extends InheritedWidget {
|
||||
final Talker talker;
|
||||
const TalkerProvider({required this.talker, required super.child, super.key});
|
||||
static Talker of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
|
||||
@override
|
||||
bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
class MyApp extends StatelessWidget with _$MyApp {
|
||||
final _appRouter = AppRouter();
|
||||
final Talker talker;
|
||||
|
||||
@named('repo')
|
||||
@inject()
|
||||
late final PostRepository repository;
|
||||
|
||||
MyApp({super.key}) {
|
||||
MyApp({super.key, required this.talker}) {
|
||||
_inject(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => PostBloc(repository),
|
||||
child: MaterialApp.router(
|
||||
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||
routerDelegate: _appRouter.delegate(),
|
||||
theme: ThemeData.light(),
|
||||
return TalkerProvider(
|
||||
talker: talker,
|
||||
child: BlocProvider(
|
||||
create: (_) => PostBloc(repository),
|
||||
child: MaterialApp.router(
|
||||
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||
routerDelegate: _appRouter.delegate(),
|
||||
theme: ThemeData.light(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
import '../data/network/json_placeholder_api.dart';
|
||||
import '../data/post_repository_impl.dart';
|
||||
import '../domain/repository/post_repository.dart';
|
||||
@@ -9,6 +12,18 @@ part 'app_module.module.cherrypick.g.dart';
|
||||
|
||||
@module()
|
||||
abstract class AppModule extends Module {
|
||||
@provide()
|
||||
@singleton()
|
||||
TalkerDioLoggerSettings talkerDioLoggerSettings() => TalkerDioLoggerSettings(
|
||||
printRequestHeaders: true,
|
||||
printResponseHeaders: true,
|
||||
printResponseMessage: true,
|
||||
);
|
||||
|
||||
@provide()
|
||||
@singleton()
|
||||
TalkerDioLogger talkerDioLogger(Talker talker, TalkerDioLoggerSettings settings) => TalkerDioLogger(talker: talker, settings: settings);
|
||||
|
||||
@instance()
|
||||
int timeout() => 1000;
|
||||
|
||||
@@ -35,8 +50,8 @@ abstract class AppModule extends Module {
|
||||
@provide()
|
||||
@singleton()
|
||||
@named('dio')
|
||||
Dio dio(@named('baseUrl') String baseUrl) =>
|
||||
Dio(BaseOptions(baseUrl: baseUrl));
|
||||
Dio dio(@named('baseUrl') String baseUrl, TalkerDioLogger logger) =>
|
||||
Dio(BaseOptions(baseUrl: baseUrl))..interceptors.add(logger);
|
||||
|
||||
@provide()
|
||||
@singleton()
|
||||
|
||||
13
examples/postly/lib/di/core_module.dart
Normal file
13
examples/postly/lib/di/core_module.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
class CoreModule extends Module {
|
||||
final Talker _talker;
|
||||
|
||||
CoreModule({required Talker talker}) : _talker = talker;
|
||||
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<Talker>().toProvide(() => _talker).singleton();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,30 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:postly/app.dart';
|
||||
import 'package:postly/di/core_module.dart';
|
||||
import 'package:talker_bloc_logger/talker_bloc_logger_observer.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
import 'di/app_module.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final talkerLogger = TalkerCherryPickObserver(talker);
|
||||
|
||||
|
||||
Bloc.observer = TalkerBlocObserver(talker: talker);
|
||||
|
||||
CherryPick.setGlobalObserver(talkerLogger);
|
||||
// Включаем cycle-detection только в debug/test
|
||||
if (kDebugMode) {
|
||||
CherryPick.setGlobalLogger(PrintLogger());
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
CherryPick.enableGlobalCrossScopeCycleDetection();
|
||||
}
|
||||
|
||||
// Используем safe root scope для гарантии защиты
|
||||
CherryPick.openRootScope().installModules([$AppModule()]);
|
||||
runApp(MyApp());
|
||||
CherryPick.openRootScope().installModules([CoreModule(talker: talker), $AppModule()]);
|
||||
|
||||
runApp(MyApp(talker: talker,));
|
||||
}
|
||||
|
||||
15
examples/postly/lib/presentation/pages/logs_page.dart
Normal file
15
examples/postly/lib/presentation/pages/logs_page.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
import '../../app.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LogsPage extends StatelessWidget {
|
||||
const LogsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final talker = TalkerProvider.of(context);
|
||||
return TalkerScreen(talker: talker);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,18 @@ class PostsPage extends StatelessWidget {
|
||||
create: (context) =>
|
||||
context.read<PostBloc>()..add(const PostEvent.fetchAll()),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('Posts')),
|
||||
appBar: AppBar(
|
||||
title: const Text('Posts'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
tooltip: 'Open logs',
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).push(const LogsRoute());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<PostBloc, PostState>(
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
import 'app_router.gr.dart';
|
||||
|
||||
@AutoRouterConfig()
|
||||
@@ -8,5 +7,6 @@ class AppRouter extends RootStackRouter {
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(page: PostsRoute.page, initial: true),
|
||||
AutoRoute(page: PostDetailsRoute.page),
|
||||
AutoRoute(page: LogsRoute.page),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.4.5"
|
||||
ansi_styles:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansi_styles
|
||||
sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2+1"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -42,7 +58,7 @@ packages:
|
||||
source: hosted
|
||||
version: "9.3.0+1"
|
||||
auto_route_generator:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46
|
||||
@@ -98,7 +114,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
@@ -137,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -151,21 +175,37 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.5"
|
||||
version: "3.0.0-dev.7"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
cherrypick_generator:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
cli_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_launcher
|
||||
sha256: "67d89e0a1c07b103d1253f6b953a43d3f502ee36805c8cfc21196282c9ddf177"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -190,6 +230,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
conventional_commit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: conventional_commit
|
||||
sha256: fad254feb6fb8eace2be18855176b0a4b97e0d50e416ff0fe590d5ba83735d34
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -198,6 +246,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -254,6 +310,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -284,7 +348,7 @@ packages:
|
||||
source: hosted
|
||||
version: "9.1.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
@@ -292,12 +356,17 @@ packages:
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||
@@ -336,6 +405,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
group_button:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: group_button
|
||||
sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -360,6 +437,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -385,7 +470,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||
@@ -448,6 +533,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
melos:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: melos
|
||||
sha256: "3f3ab3f902843d1e5a1b1a4dd39a4aca8ba1056f2d32fd8995210fa2843f646f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -464,6 +557,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mustache_template:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mustache_template
|
||||
sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -488,6 +589,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -496,6 +645,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -504,6 +669,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
prompts:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: prompts
|
||||
sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -528,6 +709,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pub_updater:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_updater
|
||||
sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -545,13 +734,29 @@ packages:
|
||||
source: hosted
|
||||
version: "4.4.2"
|
||||
retrofit_generator:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: retrofit_generator
|
||||
sha256: "65d28d3a7b4db485f1c73fee8ee32f552ef23ee4ecb68ba491f39d80b73bdcbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.0"
|
||||
share_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -597,6 +802,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -629,6 +842,53 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
talker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker
|
||||
sha256: "028a753874d98df39f210cb74f0ee09a0a95e28f8bc2dc975c3c328e24fde23d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3"
|
||||
talker_bloc_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_bloc_logger
|
||||
sha256: cf1e3b1d70f9a47e061288f0d230ba0e04a0f6394629d5df1c7b0933b236e397
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3"
|
||||
talker_cherrypick_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../talker_cherrypick_logger"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
talker_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_dio_logger
|
||||
sha256: dcf784f1841e248c270ef741f8a07ca9cf562c6424ee43fc6e598c4eb7f18238
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3"
|
||||
talker_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_flutter
|
||||
sha256: "2cfee6661277d415a895b6258ecb0bf80d7b564e91ea7e769fc6d0f970a01c09"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3"
|
||||
talker_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker_logger
|
||||
sha256: "778ec673f1b71a6516e5576ae8d90ea23bbbcf9f405a97cc30e8ccdc33e26d27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -661,6 +921,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -709,6 +1009,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -725,6 +1041,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
yaml_edit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml_edit
|
||||
sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.27.0"
|
||||
|
||||
@@ -24,9 +24,12 @@ dependencies:
|
||||
flutter_bloc: ^9.1.1
|
||||
auto_route: ^9.3.0+1
|
||||
|
||||
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
dev_dependencies:
|
||||
talker_flutter: ^4.9.3
|
||||
talker_cherrypick_logger:
|
||||
path: ../../talker_cherrypick_logger
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -40,7 +43,11 @@ dev_dependencies:
|
||||
freezed: ^2.5.8
|
||||
json_serializable: ^6.9.0
|
||||
auto_route_generator: ^9.0.0
|
||||
talker_dio_logger: ^4.9.3
|
||||
talker_bloc_logger: ^4.9.3
|
||||
|
||||
flutter:
|
||||
|
||||
uses-material-design: true
|
||||
dev_dependencies:
|
||||
melos: ^6.3.2
|
||||
|
||||
@@ -8,6 +8,7 @@ packages:
|
||||
- cherrypick_flutter
|
||||
- cherrypick_annotations
|
||||
- cherrypick_generator
|
||||
- talker_cherrypick_logger
|
||||
- examples/client_app
|
||||
- examples/postly
|
||||
|
||||
|
||||
7
talker_cherrypick_logger/.gitignore
vendored
Normal file
7
talker_cherrypick_logger/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
|
||||
# Avoid committing pubspec.lock for library packages; see
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
pubspec.lock
|
||||
3
talker_cherrypick_logger/CHANGELOG.md
Normal file
3
talker_cherrypick_logger/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
201
talker_cherrypick_logger/LICENSE
Normal file
201
talker_cherrypick_logger/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
122
talker_cherrypick_logger/README.md
Normal file
122
talker_cherrypick_logger/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# talker_cherrypick_logger
|
||||
|
||||
An integration package that allows you to log [CherryPick](https://github.com/pese-dot-work/cherrypick) Dependency Injection (DI) container events using the [Talker](https://pub.dev/packages/talker) logging system.
|
||||
All CherryPick lifecycle events, instance creations, cache operations, module activities, cycles, and errors are routed directly to your Talker logger for easy debugging and advanced diagnostics.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic DI container logging:**
|
||||
All core CherryPick events (instance creation/disposal, cache hits/misses, module install/removal, scopes, cycles, errors) are logged through Talker.
|
||||
- **Flexible log levels:**
|
||||
Each event uses the appropriate Talker log level (`info`, `warning`, `verbose`, `handle` for errors).
|
||||
- **Works with any Talker setup:**
|
||||
No extra dependencies required except Talker and CherryPick.
|
||||
- **Improves debugging and DI transparency** in both development and production.
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
### 1. Add dependencies
|
||||
|
||||
In your `pubspec.yaml`:
|
||||
```yaml
|
||||
dependencies:
|
||||
cherrypick: ^latest
|
||||
talker: ^latest
|
||||
talker_cherrypick_logger:
|
||||
git:
|
||||
url: https://github.com/pese-dot-work/cherrypick.git
|
||||
path: talker_cherrypick_logger
|
||||
```
|
||||
|
||||
### 2. Import the package
|
||||
```dart
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic integration
|
||||
|
||||
1. **Create a Talker instance** (optionally customize Talker as you wish):
|
||||
```dart
|
||||
final talker = Talker();
|
||||
```
|
||||
|
||||
2. **Create the observer and pass it to CherryPick:**
|
||||
```dart
|
||||
final observer = TalkerCherryPickObserver(talker);
|
||||
|
||||
// On DI setup, pass observer when opening (or re-opening) root or any custom scope
|
||||
CherryPick.openRootScope(observer: observer);
|
||||
```
|
||||
|
||||
3. **Now all DI events appear in your Talker logs!**
|
||||
|
||||
#### Example log output
|
||||
|
||||
- `[binding][CherryPick] MyService — MyServiceImpl (scope: root)`
|
||||
- `[create][CherryPick] MyService — MyServiceImpl => Instance(...) (scope: root)`
|
||||
- `[cache hit][CherryPick] MyService — MyServiceImpl (scope: root)`
|
||||
- `[cycle][CherryPick] Detected: A -> B -> C -> A (scope: root)`
|
||||
- `[error][CherryPick] Failed to resolve dependency`
|
||||
- `[diagnostic][CherryPick] Cache cleared`
|
||||
|
||||
#### How it works
|
||||
|
||||
`TalkerCherryPickObserver` implements `CherryPickObserver` and routes all methods/events to Talker:
|
||||
- Regular events: `.info()`
|
||||
- DI Warnings and cycles: `.warning()`
|
||||
- Diagnostics: `.verbose()`
|
||||
- Errors: `.handle()` (so they are visible in Talker error console, with stack trace)
|
||||
|
||||
---
|
||||
|
||||
## Extended example
|
||||
|
||||
```dart
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final observer = TalkerCherryPickObserver(talker);
|
||||
|
||||
// Optionally: customize Talker output or filtering
|
||||
// talker.settings.logLevel = TalkerLogLevel.debug;
|
||||
|
||||
CherryPick.openRootScope(observer: observer);
|
||||
|
||||
// ...setup your DI modules as usual
|
||||
// All container events will appear in Talker logs for easy debugging!
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional information
|
||||
|
||||
- This package is especially useful for debugging large or layered projects using CherryPick.
|
||||
- For advanced Talker configurations (UI, outputs to remote, filtering), see the [Talker documentation](https://pub.dev/packages/talker).
|
||||
- This package does **not** interfere with DI graph construction or your app's behavior — it's purely diagnostic.
|
||||
- For questions or issues, open an issue on the main [cherrypick repository](https://github.com/pese-dot-work/cherrypick).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to contribute improvements or report bugs via pull requests or issues!
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
30
talker_cherrypick_logger/analysis_options.yaml
Normal file
30
talker_cherrypick_logger/analysis_options.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final logger = TalkerCherryPickObserver(talker);
|
||||
|
||||
logger.onDiagnostic('Hello from CherryPickLogger!');
|
||||
logger.onWarning('Something might be wrong...');
|
||||
logger.onError('Oops! An error occurred', Exception('Test error'), null);
|
||||
|
||||
// Вывод всех логов
|
||||
print('\nВсе сообщения логирования через Talker:');
|
||||
for (final log in talker.history) {
|
||||
print(log); // Пример, либо log.toString(), либо log.message
|
||||
}
|
||||
}
|
||||
141
talker_cherrypick_logger/lib/src/talker_cherrypick_observer.dart
Normal file
141
talker_cherrypick_logger/lib/src/talker_cherrypick_observer.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
|
||||
/// An implementation of [CherryPickObserver] that logs all DI container events
|
||||
/// through the [Talker] logging system.
|
||||
///
|
||||
/// This observer allows you to automatically route all important events from the
|
||||
/// CherryPick DI container (such as instance creation, cache hits, errors, module install,
|
||||
/// scope lifecycle events, and more) directly to your Talker logger. It is useful for
|
||||
/// debugging, monitoring, and analytics.
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```dart
|
||||
/// import 'package:talker/talker.dart';
|
||||
/// import 'package:cherrypick/cherrypick.dart';
|
||||
/// import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
///
|
||||
/// final talker = Talker();
|
||||
/// final observer = TalkerCherryPickObserver(talker);
|
||||
///
|
||||
/// // Pass the observer to your CherryPick root scope (or any scope)
|
||||
/// CherryPick.openRootScope(observer: observer);
|
||||
///
|
||||
/// // Now all DI container events will be logged with Talker
|
||||
/// ```
|
||||
///
|
||||
/// ## Logged event examples
|
||||
/// - "[binding][CherryPick] MyService — MyServiceImpl (scope: root)"
|
||||
/// - "[create][CherryPick] MyService — MyServiceImpl => Instance(...) (scope: root)"
|
||||
/// - "[cache hit][CherryPick] MyService — MyServiceImpl (scope: root)"
|
||||
/// - "[cycle][CherryPick] Detected: A -> B -> C -> A (scope: root)"
|
||||
///
|
||||
/// ## Log levels mapping
|
||||
/// - `info`: regular events (registered, resolved, created, disposed, modules, scopes, cache hits/misses)
|
||||
/// - `warning`: cycles, cherry pick warnings
|
||||
/// - `verbose`: diagnostics
|
||||
/// - `handle`: errors (includes error object/stack)
|
||||
class TalkerCherryPickObserver implements CherryPickObserver {
|
||||
/// The target [Talker] instance to send logs to.
|
||||
final Talker talker;
|
||||
|
||||
/// Creates a [TalkerCherryPickObserver] that routes CherryPick DI events into the given [Talker] logger.
|
||||
TalkerCherryPickObserver(this.talker);
|
||||
|
||||
/// Called when a binding (dependency mapping) is registered in the DI container.
|
||||
@override
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[binding][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when an instance is requested (before creation or retrieval).
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[request][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when a new instance is created.
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {
|
||||
talker.info('[create][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when an instance is disposed.
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {
|
||||
talker.info('[dispose][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when modules are installed.
|
||||
@override
|
||||
void onModulesInstalled(List<String> modules, {String? scopeName}) {
|
||||
talker.info('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when modules are removed.
|
||||
@override
|
||||
void onModulesRemoved(List<String> modules, {String? scopeName}) {
|
||||
talker.info('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when a DI scope is opened.
|
||||
@override
|
||||
void onScopeOpened(String name) {
|
||||
talker.info('[scope opened][CherryPick] $name');
|
||||
}
|
||||
|
||||
/// Called when a DI scope is closed.
|
||||
@override
|
||||
void onScopeClosed(String name) {
|
||||
talker.info('[scope closed][CherryPick] $name');
|
||||
}
|
||||
|
||||
/// Called if the DI container detects a cycle in the dependency graph.
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) {
|
||||
talker.warning('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
|
||||
}
|
||||
|
||||
/// Called when an instance is found in the cache.
|
||||
@override
|
||||
void onCacheHit(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[cache hit][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when an instance is NOT found in the cache and will be created.
|
||||
@override
|
||||
void onCacheMiss(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[cache miss][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called for generic diagnostic/debug events.
|
||||
@override
|
||||
void onDiagnostic(String message, {Object? details}) {
|
||||
talker.verbose('[diagnostic][CherryPick] $message ${details ?? ''}');
|
||||
}
|
||||
|
||||
/// Called for non-fatal DI container warnings.
|
||||
@override
|
||||
void onWarning(String message, {Object? details}) {
|
||||
talker.warning('[warn][CherryPick] $message ${details ?? ''}');
|
||||
}
|
||||
|
||||
/// Called for error events with optional stack trace.
|
||||
@override
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) {
|
||||
talker.handle(error ?? '[CherryPick] $message', stackTrace, '[error][CherryPick] $message');
|
||||
}
|
||||
}
|
||||
18
talker_cherrypick_logger/lib/talker_cherrypick_logger.dart
Normal file
18
talker_cherrypick_logger/lib/talker_cherrypick_logger.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
library;
|
||||
|
||||
export 'src/talker_cherrypick_observer.dart';
|
||||
|
||||
// TODO: Export any libraries intended for clients of this package.
|
||||
18
talker_cherrypick_logger/pubspec.yaml
Normal file
18
talker_cherrypick_logger/pubspec.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: talker_cherrypick_logger
|
||||
description: A starting point for Dart libraries or applications.
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
# repository: https://github.com/my_org/my_repo
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
talker: ^4.9.3
|
||||
cherrypick: ^3.0.0-dev.7
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^5.0.0
|
||||
test: ^1.24.0
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
group('TalkerCherryPickObserver', () {
|
||||
late Talker talker;
|
||||
late TalkerCherryPickObserver observer;
|
||||
|
||||
setUp(() {
|
||||
talker = Talker();
|
||||
observer = TalkerCherryPickObserver(talker);
|
||||
});
|
||||
|
||||
test('onInstanceRequested logs info', () {
|
||||
observer.onInstanceRequested('A', String, scopeName: 'test');
|
||||
final log = talker.history.last;
|
||||
expect(log.message, contains('[request][CherryPick] A — String (scope: test)'));
|
||||
});
|
||||
|
||||
test('onCycleDetected logs warning', () {
|
||||
observer.onCycleDetected(['A', 'B'], scopeName: 's');
|
||||
final log = talker.history.last;
|
||||
expect(log.message, contains('[cycle][CherryPick] Detected'));
|
||||
//expect(log.level, TalkerLogLevel.warning);
|
||||
});
|
||||
|
||||
test('onError calls handle', () {
|
||||
final error = Exception('fail');
|
||||
final stack = StackTrace.current;
|
||||
observer.onError('Oops', error, stack);
|
||||
final log = talker.history.last;
|
||||
expect(log.message, contains('[error][CherryPick] Oops'));
|
||||
expect(log.exception, error);
|
||||
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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user