mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-08-12 06:10:55 +02:00
v2.0.0 (1): Refactor Arcane framework and enhance logging, theme, and auth services (#8)
* [UNTESTED] Fixes notifiers and adds some additional methods. Adds tests. Changes: // ArcaneEnvironment breaking: context.read<ArcaneEnvironment>() -> ArcaneEnvironment.of(context) breaking: context.read<ArcaneEnvironment>().state -> ArcaneEnvironment.of(context).environment; // Feature flag service added: reset() // Logging service added: registerInterface() added: unregisterInterfaces() added: unregisterAllInterfaces() // ArcaneReactiveTheme fixed: currentMode, dark, light now actually emit new values when changed added: getters for lightTheme, darkTheme, and systemTheme TODO: test systemTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Logging service: - added: reset() - added: `skipAutodetection` option for module, method, and metadata - added: unregisterInterface() - Created FileAndLineNumber mixin Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix module/method/fileAndLineNumber calculations in logging service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes the reactive theme service to properly follow the system brightness Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes tests and updates reactive theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes broken tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes bug with following/unfollowing system theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add getters on ArcaneTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added logStream to logger. Updated example code. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Setting a theme style now automatically switches to that theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added examples for feature flags Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Moved platform brightness checking from ArcaneApp to ArcaneThemeSwitcher, where it is more appropriate Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Attempt to correctly address "stable" version in .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Another GH action attempt Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add puro config to git :) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(framework): Overhauls framework core, services, and app integration for 2.0.0 (#5) * Made ArcaneTheme private (_ArcaneTheme) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated changelog Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix updateShouldNotify on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Put service locators in an extension Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Try using a mixin instead Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove 'of' and 'requiredOf' locators Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add service locators. They should be called as: ArcaneService.of<MyService>(context) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed getters on ArcaneService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed serviceInstances => registeredServices and added a removeService method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Broke up arcane service into separate files Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added requiredServiceOfType getter on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated services section of the readme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated example to add a services example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed unused variable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example documentation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Breaking up example into smaller widgets Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove bloc dependency Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes the notifier for feature flags and updates the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed a bug in the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update ServiceProvider to make service instances optional Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary notifyListeners calls Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Organize theme service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix authentication interface and service methods for logout and login. * Fix DebugAuthInterface logout method Added environment key to ArcaneEnvironment constructor Renamed switchEnvironment parameter in ArcaneEnvironment constructor Bumped arcane_helper_utils dependency version * Enhance theme management with StreamBuilder for dynamic updates Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add configuration files and update authentication service error handling - Introduced .vscode/settings.json and .vscode/launch.json for IDE configuration. - Updated DebugAuthInterface and ArcaneAuthenticationService to return const Result.ok() for consistency. - Added ArcaneTheme class for theme management. - Updated pubspec.yaml to change result_monad dependency version. - Modified authentication_service_test to return const Result.ok() in mock setups. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update dependency versions to use 'any' for arcane_helper_utils and arcane_analysis Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary 'const' keyword from Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add 'const' keyword to Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Enhance logging service with new lifecycle capabilities and interceptor support - Updated `LoggingInterface` to remove singleton-style initialization. - Introduced `LoggingInitializable` and `LoggingInitializationMixin` for optional lifecycle management. - Added `LogEvent` and `LogInterceptor` classes for improved logging event handling and interception. - Updated `ArcaneLogger` to support multiple logging interfaces and global interceptors. - Modified `DebugPrint` and `DebugAuthInterface` to align with new logging structure. - Enhanced tests to cover new logging features and interceptor functionality. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add stream-based updates for authentication, feature flags, logging, and theming - Added `statusChanges` and `signedInChanges` streams to `ArcaneAuth` for real-time authentication updates. - Introduced `enabledFeaturesChanges` stream in `ArcaneFeatureFlags` for observing feature updates. - Improved `ArcaneLogger` to maintain `logStream` usability after listener cancellation. - Updated `ArcaneTheme` to provide streams for theme mode and theme data changes. - Enhanced documentation and examples to reflect new stream APIs and usage patterns. - Added regression tests for stream listener cancellation and re-subscription across services. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Refactor ArcaneEnvironment and AuthenticationService for improved environment management and status handling Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add application environment management and tests for authentication status coherence Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat: Enhance service management with ArcaneApp provider composition This commit significantly refactors Arcane's service architecture. ArcaneApp now acts as a StatefulWidget, composing InheritedWidget- based providers for core services (features, environment, theme, auth). Service lookups (e.g., Arcane.features, context.service()) now prioritize provider-registered instances, falling back to singletons. Key changes include: - Renamed ArcaneFeatureFlags to ArcaneFeatureFlagService and ArcaneReactiveTheme to ArcaneThemeService (with typedefs). - Introduced ArcaneEnvironmentService and ArcaneEnvironmentProvider for centralized environment management. - Added BuildContext extensions (e.g., context.featureFlags) for reactive, convenient service access. - Removed .puro.json and its references, streamlining SDK setup. - Updated CHANGELOG.md to reflect a 2.0.0 release, detailing all breaking changes and new features. * chore(example): Add ignore rules for example builds * feat(logging): Introduce log interceptors and origin control Introduces the LogInterceptor class, enabling pre-processing, modification, or suppression of log events before they reach registered interfaces. The LogInterceptorContext provides contextual information such as the originating LoggingInterface. Adds a `skipAutodetection` parameter to Arcane.log, allowing control over the automatic detection of log origin (module, method, file/line). Also makes the LogEvent class extendable to support custom event types and updates existing interceptor callback signatures for consistency. * fix(theme): Update theme mode check to use currentModeOf method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(theme): Improve theme mode detection and toggling `context.isDarkMode` now reflects the app's effective theme brightness, not the raw platform brightness. This ensures `isDarkMode` accurately reports the currently rendered theme. `switchTheme()` now intelligently toggles from the *effective* theme when in `ThemeMode.system` by considering the current app brightness. `followSystemTheme()` is updated to always read the platform brightness directly, decoupling its behavior from the app's overridden theme settings. * feat(example): Improve theme and service integration demo Updates the example application to better demonstrate service and theme integration. The `FavoriteColorService` now actively sets the app's color scheme and can initialize its state from the current theme. Environment display is now reactive, and the UI responds dynamically to service registration and removal. Initial themes are now explicitly seeded. * feat(example): Add interactive persistent metadata toggle Previously, persistent metadata was added unconditionally. This change allows users to dynamically enable or disable the demo persistent metadata via a UI switch, better showcasing the API functionality. * feat(theme): Add assignment-style theme setters Introduces convenience setters for dark and light themes, allowing `Arcane.theme.dark = ...` and `Arcane.theme.light = ...` as an alternative to `setDarkTheme` and `setLightTheme`. This improves developer ergonomics and conciseness. * docs(README): Add Application Environments section to features list Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Adds `ArcaneApp.builder` to provide a provider-aware `BuildContext` for the application root. This deprecates the `ArcaneApp.child` parameter, improving developer experience for accessing Arcane's providers at build time. Updates the `ArcaneThemeSwitcher` to default to `followSystemTheme` when mounted under `ArcaneApp`, ensuring system theme behavior is enabled out of the box. Fixes `setDarkTheme` and `setLightTheme` to only update the rendered theme if their respective modes are currently active. This prevents unintended UI changes when updating the definition of an inactive theme. Updates README and examples to reflect the new `builder` API and clarified theme logic. Adds new tests for these changes. * docs(arcane_app): Clarify ArcaneApp API and provider access documentation Updates the documentation for the `ArcaneApp` constructor to explicitly detail the preferred `builder` API and the deprecated `child` parameter, including migration guidance. Also updates the list of provided services to include `ArcaneFeatureFlagsProvider`. * feat: Overhaul Arcane framework for 2.0.0 release notes Migrate Arcane to a static utility surface, removing the instantiable singleton constructor for simpler and more direct access to services. Introduce new reactive streams for environment, authentication, and theme services, enabling real-time observation of state changes. Enhance `ArcaneApp` integration with the new `builder` callback and `BuildContext` convenience accessors for services and feature flags. Update `ArcaneAuthInterface.logout` signature and revise `src` import paths, requiring consumers to update their implementations and imports. Deprecate `ArcaneApp.child` and `BuildContext.serviceOfType<T>()` in favor of new, more idiomatic APIs. Remove direct `flutter_bloc` dependency and upgrade `result_monad` to `^4.0.0`. * refactor(environment): Rename environment access getter to current This change unifies how the current application environment is accessed across Arcane's services and providers. The `state` and `environment` getters have been replaced with a consistent `current` getter, simplifying API usage and aligning with new patterns. Includes corresponding updates in example and internal code, and documents the migration in the CHANGELOG. * chore: Relax result_monad dependency constraint The `result_monad` dependency was recently upgraded to `^4.0.0`. This update relaxes the constraint to `any` to provide greater flexibility and mitigate potential dependency conflicts for consumers of Arcane, allowing them to use other compatible versions. * refactor(logging): Replace LoggingInitializationMixin with LoggingInitialization and add LoggingFeature annotation support Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Add migration steps for ArcaneThemeService and update themeMode configuration examples Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(workflow): Simplify analyze-and-test workflow by removing unused environment variables and steps Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Update Arcane.log metadata type and add migration steps for structured values Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * test: Add unit tests for authentication and feature flags functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): track user-defined theme overrides in ArcaneThemeService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(service): update service access methods in ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(tests): format login and logout method signatures in MockAuth for improved readability Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(tests): add unit tests for ArcaneThemeService functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(dependencies): update result_monad and arcane_helper_utils constraints Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(auth): add callbacks for login/logout events in DebugAuthInterface fix(logging): ensure proper cancellation of log stream subscriber refactor(arcane): make built-in services list unmodifiable fix(app): update service notifier on widget updates fix(service): improve removeService method to return boolean fix(auth): update BuildContext parameter to unused in setDebug/setNormal fix(logging): correct log level reference in LogInterceptorContext documentation fix(theme): update documentation to reflect ThemeMode instead of ThemeData fix(theme): ensure unawaited cancellation of theme subscriptions chore(dependencies): update arcane_analysis version to ^1.0.4 test: make ArcaneAuthenticationService reset asynchronous in tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(logging): implement global and type-scoped interceptors in ArcaneLogger - Added support for global interceptors that apply to all interfaces. - Introduced type-scoped interceptors for specific interface types. - Updated CHANGELOG with migration steps and new API details. - Adjusted README and tests to reflect changes in interceptor registration. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(theme): clean up theme service tests and improve readability Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(logging): enhance logging service tests with type-scoped interceptors and global interceptor functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(gitignore): add .example/.vscode/ to ignore list Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * chore(readme): add Arcane logo to the README for better branding Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(logging): implement interceptor management with collection-style APIs and scoped matching Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -22,6 +22,7 @@ example/.metadata
|
|||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.example/.vscode/
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
|||||||
+10
-4
@@ -175,22 +175,28 @@ MaterialApp(
|
|||||||
|
|
||||||
- [NEW] Added `logStream` for realtime log subscriptions.
|
- [NEW] Added `logStream` for realtime log subscriptions.
|
||||||
- [NEW] Added explicit `dispose` cleanup for logger stream resources.
|
- [NEW] Added explicit `dispose` cleanup for logger stream resources.
|
||||||
- [BREAKING] `LoggingInterface` no longer includes built-in singleton-style
|
|
||||||
initialization state.
|
|
||||||
- [NEW] Added optional lifecycle capability via `LoggingInitializable` and
|
- [NEW] Added optional lifecycle capability via `LoggingInitializable` and
|
||||||
`LoggingInitialization`.
|
`LoggingInitialization`.
|
||||||
- [NEW] Added optional `feature` tag support via `@LoggingFeature(...)`
|
- [NEW] Added optional `feature` tag support via `@LoggingFeature(...)`
|
||||||
annotation.
|
annotation.
|
||||||
- [CHANGE] `initializeInterfaces()` now initializes only interfaces that
|
|
||||||
implement `LoggingInitializable`; other interfaces are skipped.
|
|
||||||
- [NEW] Added a `skipAutodetection` parameter to `Arcane.log` (defaults to
|
- [NEW] Added a `skipAutodetection` parameter to `Arcane.log` (defaults to
|
||||||
`false`) that, when enabled, skips detection of the `module`, `method`, and
|
`false`) that, when enabled, skips detection of the `module`, `method`, and
|
||||||
file/line number where logs originated from.
|
file/line number where logs originated from.
|
||||||
- [NEW] Added the `LogInterceptor` class which can (optionally) be added to
|
- [NEW] Added the `LogInterceptor` class which can (optionally) be added to
|
||||||
`ArcaneLogger` to pre-process log messages before they are sent to the
|
`ArcaneLogger` to pre-process log messages before they are sent to the
|
||||||
registered `ArcaneLoggingInterface`(s).
|
registered `ArcaneLoggingInterface`(s).
|
||||||
|
- [NEW] Added collection-style interceptor APIs:
|
||||||
|
`Arcane.logger.interceptors.add(...)`,
|
||||||
|
`Arcane.logger.interceptors.remove(...)`, and
|
||||||
|
`Arcane.logger.interceptors.clear()` with an optional `matcher` for
|
||||||
|
explicit type-scoped matching strategies, including subtype-inclusive
|
||||||
|
matching.
|
||||||
- [CHANGE] Updated `Arcane.log` metadata type from `Map<String, String>?` to
|
- [CHANGE] Updated `Arcane.log` metadata type from `Map<String, String>?` to
|
||||||
`Map<String, Object?>?` to support structured metadata values.
|
`Map<String, Object?>?` to support structured metadata values.
|
||||||
|
- [CHANGE] `initializeInterfaces()` now initializes only interfaces that
|
||||||
|
implement `LoggingInitializable`; other interfaces are skipped.
|
||||||
|
- [BREAKING] `LoggingInterface` no longer includes built-in singleton-style
|
||||||
|
initialization state.
|
||||||
|
|
||||||
#### Migration Steps (LoggingInterface)
|
#### Migration Steps (LoggingInterface)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ authentication, feature flags, theming, and more. This framework
|
|||||||
is ideal for building scalable applications that require dynamic configuration
|
is ideal for building scalable applications that require dynamic configuration
|
||||||
and service management.
|
and service management.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://github.com/user-attachments/assets/ab314468-e039-4e6d-8919-5297658c2be3" alt="Arcane logo" width="500">
|
||||||
|
</p>
|
||||||
|
|
||||||
- [Arcane Framework](#arcane-framework)
|
- [Arcane Framework](#arcane-framework)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
@@ -531,14 +535,15 @@ class AnalyticsLogger extends LoggingInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(
|
Arcane.logger.interceptors.add(
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
if (context.interface is AnalyticsLogger && event.level == Level.debug) {
|
if (event.level == Level.debug) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}),
|
}),
|
||||||
|
matcher: (interface) => interface is AnalyticsLogger,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -565,8 +570,8 @@ await Arcane.logger.registerInterface(AuthLogger());
|
|||||||
```
|
```
|
||||||
|
|
||||||
Next, register your logging interface with the Arcane logger service. You can
|
Next, register your logging interface with the Arcane logger service. You can
|
||||||
attach interceptors when registering an interface, or add global interceptors
|
attach interceptors when registering an interface, or manage logger
|
||||||
later at runtime.
|
interceptors via `Arcane.logger.interceptors` later at runtime.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final DebugConsole debugConsole = DebugConsole();
|
final DebugConsole debugConsole = DebugConsole();
|
||||||
@@ -575,7 +580,7 @@ await Arcane.logger.registerInterface(
|
|||||||
debugConsole,
|
debugConsole,
|
||||||
interceptors: [
|
interceptors: [
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
if (context.interface is DebugConsole && event.level == Level.debug) {
|
if (event.level == Level.debug) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +589,7 @@ await Arcane.logger.registerInterface(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(
|
Arcane.logger.interceptors.add(
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
return event.copyWith(
|
return event.copyWith(
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -601,10 +606,24 @@ await Arcane.logger.initializeInterfaces();
|
|||||||
```
|
```
|
||||||
|
|
||||||
Global interceptors are evaluated for each registered interface, and interface
|
Global interceptors are evaluated for each registered interface, and interface
|
||||||
interceptors run immediately after them for that same destination. Every
|
interceptors run immediately after them for that same destination. Use
|
||||||
|
`Arcane.logger.interceptors.add(...)` with no matcher for global behavior, or
|
||||||
|
provide a matcher for scoped behavior. Every
|
||||||
interceptor receives a `LogInterceptorContext` whose `interface` value is the
|
interceptor receives a `LogInterceptorContext` whose `interface` value is the
|
||||||
current destination, which allows a single global interceptor to allow one
|
current destination.
|
||||||
interface to receive an event while dropping it for another.
|
|
||||||
|
If you want subtype-inclusive matching (for example, a base interface plus all
|
||||||
|
derived interfaces), use `Arcane.logger.interceptors.add(...)` with
|
||||||
|
an explicit matcher.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[scoped] ${event.message}");
|
||||||
|
}),
|
||||||
|
matcher: (interface) => interface is AnalyticsLogger,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
Returning `null` from an interceptor drops the event for the current scope.
|
Returning `null` from an interceptor drops the event for the current scope.
|
||||||
Returning a modified `LogEvent` allows you to rewrite the message, metadata,
|
Returning a modified `LogEvent` allows you to rewrite the message, metadata,
|
||||||
@@ -664,10 +683,10 @@ void dispose() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also add and remove global interceptors after startup. Because every
|
You can also add, remove, and clear interceptors after startup. Because every
|
||||||
interceptor receives a `LogInterceptorContext`, a single global interceptor can
|
interceptor receives a `LogInterceptorContext`, global interceptors can still
|
||||||
still make interface-specific decisions by checking `context.interface`. If you
|
make interface-specific decisions when needed by checking `context.interface`.
|
||||||
prefer, you can also define your own interceptor class by implementing
|
If you prefer, you can also define your own interceptor class by implementing
|
||||||
`LogInterceptor` instead of using the callback constructor.
|
`LogInterceptor` instead of using the callback constructor.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
@@ -686,8 +705,8 @@ final LogInterceptor redactSecrets = LogInterceptor((
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(redactSecrets);
|
Arcane.logger.interceptors.add(redactSecrets);
|
||||||
Arcane.logger.unregisterInterceptor(redactSecrets);
|
Arcane.logger.interceptors.remove(redactSecrets);
|
||||||
```
|
```
|
||||||
|
|
||||||
If you prefer a reusable named type, you can also implement `LogInterceptor`
|
If you prefer a reusable named type, you can also implement `LogInterceptor`
|
||||||
@@ -716,8 +735,8 @@ class RedactingLogInterceptor implements LogInterceptor {
|
|||||||
|
|
||||||
final LogInterceptor redactSecrets = RedactingLogInterceptor();
|
final LogInterceptor redactSecrets = RedactingLogInterceptor();
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(redactSecrets);
|
Arcane.logger.interceptors.add(redactSecrets);
|
||||||
Arcane.logger.unregisterInterceptor(redactSecrets);
|
Arcane.logger.interceptors.remove(redactSecrets);
|
||||||
```
|
```
|
||||||
|
|
||||||
Multiple logging interfaces and multiple interceptors can be registered
|
Multiple logging interfaces and multiple interceptors can be registered
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
part of "logging_service.dart";
|
||||||
|
|
||||||
|
final class LoggingInterceptorsService {
|
||||||
|
LoggingInterceptorsService._internal();
|
||||||
|
|
||||||
|
static final LoggingInterceptorsService _instance =
|
||||||
|
LoggingInterceptorsService._internal();
|
||||||
|
|
||||||
|
static LoggingInterceptorsService get I => _instance;
|
||||||
|
|
||||||
|
final List<LogInterceptor> _globalInterceptors = [];
|
||||||
|
final List<_InterfaceScopedInterceptorRegistration>
|
||||||
|
_interfaceScopedInterceptorRegistrations = [];
|
||||||
|
final List<_TypeScopedInterceptorRegistration>
|
||||||
|
_typeScopedInterceptorRegistrations = [];
|
||||||
|
|
||||||
|
/// Registers an interceptor.
|
||||||
|
///
|
||||||
|
/// If [matcher] is omitted, the interceptor is registered globally.
|
||||||
|
void add(
|
||||||
|
LogInterceptor interceptor, {
|
||||||
|
bool Function(LoggingInterface interface)? matcher,
|
||||||
|
}) {
|
||||||
|
if (matcher == null) {
|
||||||
|
_globalInterceptors.add(interceptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typeScopedInterceptorRegistrations.add(
|
||||||
|
_TypeScopedInterceptorRegistration(
|
||||||
|
interceptor: interceptor,
|
||||||
|
matcher: matcher,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers multiple interceptors.
|
||||||
|
void addAll(
|
||||||
|
Iterable<LogInterceptor> interceptors, {
|
||||||
|
bool Function(LoggingInterface interface)? matcher,
|
||||||
|
}) {
|
||||||
|
for (final LogInterceptor interceptor in interceptors) {
|
||||||
|
add(interceptor, matcher: matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes interceptor registrations.
|
||||||
|
///
|
||||||
|
/// If [matcher] is omitted, global registrations are removed.
|
||||||
|
/// If [matcher] is provided, scoped registrations with the same matcher
|
||||||
|
/// identity are removed.
|
||||||
|
void remove(
|
||||||
|
LogInterceptor interceptor, {
|
||||||
|
bool Function(LoggingInterface interface)? matcher,
|
||||||
|
}) {
|
||||||
|
if (matcher == null) {
|
||||||
|
_globalInterceptors.removeWhere(
|
||||||
|
(LogInterceptor current) => identical(current, interceptor),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typeScopedInterceptorRegistrations.removeWhere(
|
||||||
|
(_TypeScopedInterceptorRegistration registration) =>
|
||||||
|
identical(registration.interceptor, interceptor) &&
|
||||||
|
identical(registration.matcher, matcher),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all globally and scoped interceptor registrations.
|
||||||
|
void clear() {
|
||||||
|
_globalInterceptors.clear();
|
||||||
|
_interfaceScopedInterceptorRegistrations.clear();
|
||||||
|
_typeScopedInterceptorRegistrations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears global interceptor registrations.
|
||||||
|
void clearGlobal() => _globalInterceptors.clear();
|
||||||
|
|
||||||
|
void registerForInterface(
|
||||||
|
LoggingInterface interface,
|
||||||
|
Iterable<LogInterceptor> interceptors,
|
||||||
|
) {
|
||||||
|
for (final LogInterceptor interceptor in interceptors) {
|
||||||
|
_interfaceScopedInterceptorRegistrations.add(
|
||||||
|
_InterfaceScopedInterceptorRegistration(
|
||||||
|
interface: interface,
|
||||||
|
interceptor: interceptor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterInterface(LoggingInterface interface) {
|
||||||
|
_interfaceScopedInterceptorRegistrations.removeWhere(
|
||||||
|
(_InterfaceScopedInterceptorRegistration registration) =>
|
||||||
|
identical(registration.interface, interface),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LogInterceptor> resolveForInterface(
|
||||||
|
LoggingInterface interface,
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
..._globalInterceptors,
|
||||||
|
for (final _InterfaceScopedInterceptorRegistration registration
|
||||||
|
in _interfaceScopedInterceptorRegistrations)
|
||||||
|
if (identical(registration.interface, interface))
|
||||||
|
registration.interceptor,
|
||||||
|
for (final _TypeScopedInterceptorRegistration registration
|
||||||
|
in _typeScopedInterceptorRegistrations)
|
||||||
|
if (registration.matches(interface)) registration.interceptor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _LoggingInterfaceRegistration {
|
||||||
|
_LoggingInterfaceRegistration({
|
||||||
|
required this.interface,
|
||||||
|
});
|
||||||
|
|
||||||
|
final LoggingInterface interface;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _InterfaceScopedInterceptorRegistration {
|
||||||
|
const _InterfaceScopedInterceptorRegistration({
|
||||||
|
required this.interface,
|
||||||
|
required this.interceptor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final LoggingInterface interface;
|
||||||
|
final LogInterceptor interceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _TypeScopedInterceptorRegistration {
|
||||||
|
_TypeScopedInterceptorRegistration({
|
||||||
|
required this.interceptor,
|
||||||
|
required bool Function(LoggingInterface interface) matcher,
|
||||||
|
}) : _matcher = matcher;
|
||||||
|
|
||||||
|
final LogInterceptor interceptor;
|
||||||
|
final bool Function(LoggingInterface interface) _matcher;
|
||||||
|
|
||||||
|
bool Function(LoggingInterface interface) get matcher => _matcher;
|
||||||
|
|
||||||
|
bool matches(LoggingInterface interface) => _matcher(interface);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
|||||||
part "log_event.dart";
|
part "log_event.dart";
|
||||||
part "log_interceptor.dart";
|
part "log_interceptor.dart";
|
||||||
part "logging_enums.dart";
|
part "logging_enums.dart";
|
||||||
|
part "logging_interceptors_service.dart";
|
||||||
part "logging_interface.dart";
|
part "logging_interface.dart";
|
||||||
|
|
||||||
/// A singleton class that manages logging to one or more logging interfaces
|
/// A singleton class that manages logging to one or more logging interfaces
|
||||||
@@ -22,8 +23,8 @@ class ArcaneLogger {
|
|||||||
static ArcaneLogger get I => _instance;
|
static ArcaneLogger get I => _instance;
|
||||||
|
|
||||||
final List<_LoggingInterfaceRegistration> _interfaceRegistrations = [];
|
final List<_LoggingInterfaceRegistration> _interfaceRegistrations = [];
|
||||||
|
final LoggingInterceptorsService _interceptorService =
|
||||||
final List<LogInterceptor> _interceptors = [];
|
LoggingInterceptorsService.I;
|
||||||
|
|
||||||
/// A list of registered logging interfaces.
|
/// A list of registered logging interfaces.
|
||||||
List<LoggingInterface> get interfaces => [
|
List<LoggingInterface> get interfaces => [
|
||||||
@@ -32,10 +33,8 @@ class ArcaneLogger {
|
|||||||
registration.interface,
|
registration.interface,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A list of globally registered interceptors.
|
/// Interceptor service used to add, remove, and clear interceptors.
|
||||||
List<LogInterceptor> get interceptors => [
|
LoggingInterceptorsService get interceptors => I._interceptorService;
|
||||||
...I._interceptors,
|
|
||||||
];
|
|
||||||
|
|
||||||
final Map<String, String> _additionalMetadata = {};
|
final Map<String, String> _additionalMetadata = {};
|
||||||
|
|
||||||
@@ -272,16 +271,16 @@ class ArcaneLogger {
|
|||||||
for (final _LoggingInterfaceRegistration registration
|
for (final _LoggingInterfaceRegistration registration
|
||||||
in I._interfaceRegistrations) {
|
in I._interfaceRegistrations) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
|
final List<LogInterceptor> interceptors =
|
||||||
|
I.interceptors.resolveForInterface(registration.interface);
|
||||||
|
|
||||||
final LogEvent? interfaceEvent = _runInterceptors(
|
final LogEvent? interfaceEvent = _runInterceptors(
|
||||||
event.copyWith(
|
event.copyWith(
|
||||||
metadata: event.metadata == null
|
metadata: event.metadata == null
|
||||||
? null
|
? null
|
||||||
: Map<String, Object?>.from(event.metadata!),
|
: Map<String, Object?>.from(event.metadata!),
|
||||||
),
|
),
|
||||||
interceptors: [
|
interceptors: interceptors,
|
||||||
...I._interceptors,
|
|
||||||
...registration.interceptors,
|
|
||||||
],
|
|
||||||
context: LogInterceptorContext(interface: registration.interface),
|
context: LogInterceptorContext(interface: registration.interface),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -316,40 +315,20 @@ class ArcaneLogger {
|
|||||||
}) async {
|
}) async {
|
||||||
if (!initialized) await _init();
|
if (!initialized) await _init();
|
||||||
|
|
||||||
|
I.interceptors.registerForInterface(
|
||||||
|
loggingInterface,
|
||||||
|
interceptors ?? const <LogInterceptor>[],
|
||||||
|
);
|
||||||
|
|
||||||
I._interfaceRegistrations.add(
|
I._interfaceRegistrations.add(
|
||||||
_LoggingInterfaceRegistration(
|
_LoggingInterfaceRegistration(
|
||||||
interface: loggingInterface,
|
interface: loggingInterface,
|
||||||
interceptors: interceptors,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a global [LogInterceptor] to run before interface fan-out.
|
|
||||||
ArcaneLogger registerInterceptor(LogInterceptor interceptor) {
|
|
||||||
I._interceptors.add(interceptor);
|
|
||||||
return I;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a `List` of global [LogInterceptor]s.
|
|
||||||
ArcaneLogger registerInterceptors(List<LogInterceptor> interceptors) {
|
|
||||||
I._interceptors.addAll(interceptors);
|
|
||||||
return I;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unregisters a previously registered global [LogInterceptor].
|
|
||||||
ArcaneLogger unregisterInterceptor(LogInterceptor interceptor) {
|
|
||||||
I._interceptors.remove(interceptor);
|
|
||||||
return I;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes all previously registered global interceptors.
|
|
||||||
ArcaneLogger clearInterceptors() {
|
|
||||||
I._interceptors.clear();
|
|
||||||
return I;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a `List` of [LoggingInterface] with the [ArcaneLogger].
|
/// Registers a `List` of [LoggingInterface] with the [ArcaneLogger].
|
||||||
///
|
///
|
||||||
/// Once registered, logs are eligible to be sent to these interfaces
|
/// Once registered, logs are eligible to be sent to these interfaces
|
||||||
@@ -361,10 +340,14 @@ class ArcaneLogger {
|
|||||||
if (!initialized) await _init();
|
if (!initialized) await _init();
|
||||||
|
|
||||||
for (final LoggingInterface i in interfaces) {
|
for (final LoggingInterface i in interfaces) {
|
||||||
|
I.interceptors.registerForInterface(
|
||||||
|
i,
|
||||||
|
interceptors?[i] ?? const <LogInterceptor>[],
|
||||||
|
);
|
||||||
|
|
||||||
I._interfaceRegistrations.add(
|
I._interfaceRegistrations.add(
|
||||||
_LoggingInterfaceRegistration(
|
_LoggingInterfaceRegistration(
|
||||||
interface: i,
|
interface: i,
|
||||||
interceptors: interceptors?[i],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -379,6 +362,8 @@ class ArcaneLogger {
|
|||||||
) async {
|
) async {
|
||||||
if (!initialized) await _init();
|
if (!initialized) await _init();
|
||||||
|
|
||||||
|
I.interceptors.unregisterInterface(interface);
|
||||||
|
|
||||||
I._interfaceRegistrations.removeWhere(
|
I._interfaceRegistrations.removeWhere(
|
||||||
(_LoggingInterfaceRegistration registration) =>
|
(_LoggingInterfaceRegistration registration) =>
|
||||||
identical(registration.interface, interface),
|
identical(registration.interface, interface),
|
||||||
@@ -395,6 +380,8 @@ class ArcaneLogger {
|
|||||||
if (!initialized) await _init();
|
if (!initialized) await _init();
|
||||||
|
|
||||||
for (final LoggingInterface i in interfaces) {
|
for (final LoggingInterface i in interfaces) {
|
||||||
|
I.interceptors.unregisterInterface(i);
|
||||||
|
|
||||||
I._interfaceRegistrations.removeWhere(
|
I._interfaceRegistrations.removeWhere(
|
||||||
(_LoggingInterfaceRegistration registration) =>
|
(_LoggingInterfaceRegistration registration) =>
|
||||||
identical(registration.interface, i),
|
identical(registration.interface, i),
|
||||||
@@ -408,6 +395,12 @@ class ArcaneLogger {
|
|||||||
/// [ArcaneLogger], if any were previously registered.
|
/// [ArcaneLogger], if any were previously registered.
|
||||||
Future<ArcaneLogger> unregisterAllInterfaces() async {
|
Future<ArcaneLogger> unregisterAllInterfaces() async {
|
||||||
if (!initialized) await _init();
|
if (!initialized) await _init();
|
||||||
|
|
||||||
|
for (final _LoggingInterfaceRegistration registration
|
||||||
|
in I._interfaceRegistrations) {
|
||||||
|
I.interceptors.unregisterInterface(registration.interface);
|
||||||
|
}
|
||||||
|
|
||||||
I._interfaceRegistrations.clear();
|
I._interfaceRegistrations.clear();
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -482,7 +475,7 @@ class ArcaneLogger {
|
|||||||
void reset() {
|
void reset() {
|
||||||
dispose();
|
dispose();
|
||||||
I._interfaceRegistrations.clear();
|
I._interfaceRegistrations.clear();
|
||||||
I._interceptors.clear();
|
I.interceptors.clear();
|
||||||
I._initialized = false;
|
I._initialized = false;
|
||||||
I._additionalMetadata.clear();
|
I._additionalMetadata.clear();
|
||||||
}
|
}
|
||||||
@@ -510,15 +503,3 @@ class ArcaneLogger {
|
|||||||
return currentEvent;
|
return currentEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _LoggingInterfaceRegistration {
|
|
||||||
_LoggingInterfaceRegistration({
|
|
||||||
required this.interface,
|
|
||||||
List<LogInterceptor>? interceptors,
|
|
||||||
}) : interceptors = [
|
|
||||||
...?interceptors,
|
|
||||||
];
|
|
||||||
|
|
||||||
final LoggingInterface interface;
|
|
||||||
final List<LogInterceptor> interceptors;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -60,6 +60,36 @@ class TestPassiveLoggingInterface extends LoggingInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TestAlternativeLoggingInterface extends LoggingInterface {
|
||||||
|
TestAlternativeLoggingInterface(this.name);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<LogEvent> events = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void log(
|
||||||
|
String message, {
|
||||||
|
Map<String, Object?>? metadata,
|
||||||
|
Level? level,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
Object? extra,
|
||||||
|
}) {
|
||||||
|
events.add(
|
||||||
|
LogEvent(
|
||||||
|
message: message,
|
||||||
|
metadata: metadata == null ? null : Map<String, Object?>.from(metadata),
|
||||||
|
level: level,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
extra: extra,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDerivedLoggingInterface extends TestLoggingInterface {
|
||||||
|
TestDerivedLoggingInterface(super.name);
|
||||||
|
}
|
||||||
|
|
||||||
class RedactingLogInterceptor implements LogInterceptor {
|
class RedactingLogInterceptor implements LogInterceptor {
|
||||||
const RedactingLogInterceptor();
|
const RedactingLogInterceptor();
|
||||||
|
|
||||||
@@ -198,7 +228,18 @@ void main() {
|
|||||||
await Arcane.logger.registerInterface(myInterface);
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
Arcane.log("before");
|
Arcane.log("before");
|
||||||
Arcane.logger.registerInterceptor(prefixInterceptor);
|
Arcane.logger.interceptors.add(prefixInterceptor);
|
||||||
|
Arcane.log("after");
|
||||||
|
|
||||||
|
expect(myInterface.events[0].message, "before");
|
||||||
|
expect(myInterface.events[1].message, "[global] after");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("interceptor collection add defaults to global", () async {
|
||||||
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
|
Arcane.log("before");
|
||||||
|
Arcane.logger.interceptors.add(prefixInterceptor);
|
||||||
Arcane.log("after");
|
Arcane.log("after");
|
||||||
|
|
||||||
expect(myInterface.events[0].message, "before");
|
expect(myInterface.events[0].message, "before");
|
||||||
@@ -219,16 +260,90 @@ void main() {
|
|||||||
await Arcane.logger.registerInterface(myInterface);
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
Arcane.log("before");
|
Arcane.log("before");
|
||||||
Arcane.logger.registerInterceptor(dropForPrimary);
|
Arcane.logger.interceptors.add(
|
||||||
|
dropForPrimary,
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
Arcane.log("blocked");
|
Arcane.log("blocked");
|
||||||
Arcane.logger.unregisterInterceptor(dropForPrimary);
|
|
||||||
Arcane.log("after");
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
myInterface.events.map((LogEvent event) => event.message),
|
myInterface.events.map((LogEvent event) => event.message),
|
||||||
["before", "after"],
|
["before"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("type-scoped interceptors only apply to matching interfaces",
|
||||||
|
() async {
|
||||||
|
final TestAlternativeLoggingInterface alternativeInterface =
|
||||||
|
TestAlternativeLoggingInterface("alternative");
|
||||||
|
|
||||||
|
await Arcane.logger.registerInterfaces([
|
||||||
|
myInterface,
|
||||||
|
alternativeInterface,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[typed] ${event.message}");
|
||||||
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.log("typed");
|
||||||
|
|
||||||
|
expect(myInterface.events.last.message, "[typed] typed");
|
||||||
|
expect(alternativeInterface.events.last.message, "typed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matcher-based type-scoped interceptors can include subtypes",
|
||||||
|
() async {
|
||||||
|
final TestDerivedLoggingInterface derivedInterface =
|
||||||
|
TestDerivedLoggingInterface("derived");
|
||||||
|
final TestAlternativeLoggingInterface alternativeInterface =
|
||||||
|
TestAlternativeLoggingInterface("alternative");
|
||||||
|
|
||||||
|
await Arcane.logger.registerInterfaces([
|
||||||
|
myInterface,
|
||||||
|
derivedInterface,
|
||||||
|
alternativeInterface,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[family] ${event.message}");
|
||||||
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.log("typed");
|
||||||
|
|
||||||
|
expect(myInterface.events.last.message, "[family] typed");
|
||||||
|
expect(derivedInterface.events.last.message, "[family] typed");
|
||||||
|
expect(alternativeInterface.events.last.message, "typed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"registering a type-scoped interceptor before matching interface exists stores it for future registrations",
|
||||||
|
() async {
|
||||||
|
final TestLoggingInterface secondary =
|
||||||
|
TestLoggingInterface("secondary");
|
||||||
|
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[future] ${event.message}");
|
||||||
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Arcane.logger.registerInterface(secondary);
|
||||||
|
Arcane.log("hello");
|
||||||
|
|
||||||
|
expect(secondary.events.single.message, "[future] hello");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group("persistent metadata", () {
|
group("persistent metadata", () {
|
||||||
@@ -313,7 +428,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("global interceptors run in registration order", () async {
|
test("global interceptors run in registration order", () async {
|
||||||
Arcane.logger.registerInterceptors([
|
Arcane.logger.interceptors.addAll([
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
expect(context.interface, same(myInterface));
|
expect(context.interface, same(myInterface));
|
||||||
return event.copyWith(message: "${event.message}:first");
|
return event.copyWith(message: "${event.message}:first");
|
||||||
@@ -331,7 +446,7 @@ void main() {
|
|||||||
|
|
||||||
test("global interceptors can drop events for all interfaces", () async {
|
test("global interceptors can drop events for all interfaces", () async {
|
||||||
await Arcane.logger.registerInterface(myInterface);
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
Arcane.logger.registerInterceptor(
|
Arcane.logger.interceptors.add(
|
||||||
LogInterceptor((event, context) => null),
|
LogInterceptor((event, context) => null),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -340,8 +455,17 @@ void main() {
|
|||||||
expect(myInterface.events, isEmpty);
|
expect(myInterface.events, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("interceptor collection remove defaults to global", () async {
|
||||||
|
Arcane.logger.interceptors.add(prefixInterceptor);
|
||||||
|
Arcane.logger.interceptors.remove(prefixInterceptor);
|
||||||
|
|
||||||
|
Arcane.log(logMessage);
|
||||||
|
|
||||||
|
expect(myInterface.events.single.message, logMessage);
|
||||||
|
});
|
||||||
|
|
||||||
test("custom interceptor classes can implement LogInterceptor", () async {
|
test("custom interceptor classes can implement LogInterceptor", () async {
|
||||||
Arcane.logger.registerInterceptor(const RedactingLogInterceptor());
|
Arcane.logger.interceptors.add(const RedactingLogInterceptor());
|
||||||
|
|
||||||
Arcane.log(
|
Arcane.log(
|
||||||
logMessage,
|
logMessage,
|
||||||
@@ -365,7 +489,11 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(allowPrimaryOnly);
|
Arcane.logger.interceptors.add(
|
||||||
|
allowPrimaryOnly,
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
await Arcane.logger.registerInterface(
|
await Arcane.logger.registerInterface(
|
||||||
secondaryInterface,
|
secondaryInterface,
|
||||||
);
|
);
|
||||||
@@ -376,11 +504,39 @@ void main() {
|
|||||||
expect(secondaryInterface.events, isEmpty);
|
expect(secondaryInterface.events, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("interceptor collection remove with matcher removes scoped entries",
|
||||||
|
() async {
|
||||||
|
final TestLoggingInterface secondaryInterface =
|
||||||
|
TestLoggingInterface("secondary");
|
||||||
|
final LogInterceptor scopedInterceptor =
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[scoped] ${event.message}");
|
||||||
|
});
|
||||||
|
bool scopedMatcher(LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface;
|
||||||
|
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
scopedInterceptor,
|
||||||
|
matcher: scopedMatcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Arcane.logger.registerInterface(secondaryInterface);
|
||||||
|
Arcane.logger.interceptors.remove(
|
||||||
|
scopedInterceptor,
|
||||||
|
matcher: scopedMatcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.log(logMessage);
|
||||||
|
|
||||||
|
expect(myInterface.events.single.message, logMessage);
|
||||||
|
expect(secondaryInterface.events.single.message, logMessage);
|
||||||
|
});
|
||||||
|
|
||||||
test("interface interceptors receive the current interface", () async {
|
test("interface interceptors receive the current interface", () async {
|
||||||
final TestLoggingInterface secondaryInterface =
|
final TestLoggingInterface secondaryInterface =
|
||||||
TestLoggingInterface("secondary");
|
TestLoggingInterface("secondary");
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(
|
Arcane.logger.interceptors.add(
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
final TestLoggingInterface currentInterface =
|
final TestLoggingInterface currentInterface =
|
||||||
context.interface! as TestLoggingInterface;
|
context.interface! as TestLoggingInterface;
|
||||||
@@ -391,6 +547,8 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
);
|
);
|
||||||
await Arcane.logger.registerInterface(
|
await Arcane.logger.registerInterface(
|
||||||
secondaryInterface,
|
secondaryInterface,
|
||||||
@@ -410,12 +568,14 @@ void main() {
|
|||||||
final TestLoggingInterface secondaryInterface =
|
final TestLoggingInterface secondaryInterface =
|
||||||
TestLoggingInterface("secondary");
|
TestLoggingInterface("secondary");
|
||||||
|
|
||||||
Arcane.logger.registerInterceptor(
|
Arcane.logger.interceptors.add(
|
||||||
LogInterceptor((event, context) {
|
LogInterceptor((event, context) {
|
||||||
event.metadata?["mutatedBy"] =
|
event.metadata?["mutatedBy"] =
|
||||||
(context.interface as TestLoggingInterface).name;
|
(context.interface as TestLoggingInterface).name;
|
||||||
return event;
|
return event;
|
||||||
}),
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
);
|
);
|
||||||
await Arcane.logger.registerInterface(
|
await Arcane.logger.registerInterface(
|
||||||
secondaryInterface,
|
secondaryInterface,
|
||||||
@@ -457,7 +617,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reset clears global interceptors", () async {
|
test("reset clears global interceptors", () async {
|
||||||
Arcane.logger.registerInterceptor(prefixInterceptor);
|
Arcane.logger.interceptors.add(prefixInterceptor);
|
||||||
Arcane.logger.reset();
|
Arcane.logger.reset();
|
||||||
await Arcane.logger.registerInterface(myInterface);
|
await Arcane.logger.registerInterface(myInterface);
|
||||||
|
|
||||||
@@ -465,6 +625,56 @@ void main() {
|
|||||||
|
|
||||||
expect(myInterface.events.single.message, logMessage);
|
expect(myInterface.events.single.message, logMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("global and type-scoped duplicate registrations are additive",
|
||||||
|
() async {
|
||||||
|
final LogInterceptor duplicateInterceptor = LogInterceptor(
|
||||||
|
(event, context) {
|
||||||
|
final int count = (event.metadata?["count"] as int?) ?? 0;
|
||||||
|
return event.copyWith(
|
||||||
|
metadata: {
|
||||||
|
...?event.metadata,
|
||||||
|
"count": count + 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.logger
|
||||||
|
..interceptors.add(duplicateInterceptor)
|
||||||
|
..interceptors.add(
|
||||||
|
duplicateInterceptor,
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.log(
|
||||||
|
logMessage,
|
||||||
|
metadata: {
|
||||||
|
"count": 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(myInterface.events.single.metadata?["count"], 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"interceptor collection clear removes global and scoped interceptors",
|
||||||
|
() async {
|
||||||
|
Arcane.logger.interceptors.add(prefixInterceptor);
|
||||||
|
Arcane.logger.interceptors.add(
|
||||||
|
LogInterceptor((event, context) {
|
||||||
|
return event.copyWith(message: "[scoped] ${event.message}");
|
||||||
|
}),
|
||||||
|
matcher: (LoggingInterface interface) =>
|
||||||
|
interface is TestLoggingInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
Arcane.logger.interceptors.clear();
|
||||||
|
Arcane.log(logMessage);
|
||||||
|
|
||||||
|
expect(myInterface.events.single.message, logMessage);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ void main() {
|
|||||||
|
|
||||||
test("reset restores defaults", () {
|
test("reset restores defaults", () {
|
||||||
ArcaneThemeService.I.setDarkTheme(
|
ArcaneThemeService.I.setDarkTheme(
|
||||||
ThemeData(primaryColor: Colors.red, brightness: Brightness.dark),);
|
ThemeData(primaryColor: Colors.red, brightness: Brightness.dark),
|
||||||
|
);
|
||||||
ArcaneThemeService.I.setLightTheme(
|
ArcaneThemeService.I.setLightTheme(
|
||||||
ThemeData(primaryColor: Colors.blue, brightness: Brightness.light),);
|
ThemeData(primaryColor: Colors.blue, brightness: Brightness.light),
|
||||||
|
);
|
||||||
ArcaneThemeService.I.reset();
|
ArcaneThemeService.I.reset();
|
||||||
expect(ArcaneThemeService.I.dark, ThemeData.dark());
|
expect(ArcaneThemeService.I.dark, ThemeData.dark());
|
||||||
expect(ArcaneThemeService.I.light, ThemeData.light());
|
expect(ArcaneThemeService.I.light, ThemeData.light());
|
||||||
|
|||||||
Reference in New Issue
Block a user