mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 13:47:24 +00:00
Compare commits
18 Commits
cherrypick
...
cherrypick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4c5fd922e | ||
|
|
8870b8ce54 | ||
|
|
298cb65ac8 | ||
|
|
1b9db31c13 | ||
|
|
ca3cd2c8fd | ||
|
|
c91e15319b | ||
|
|
99e662124f | ||
|
|
03f54981f3 | ||
|
|
349efe6ba6 | ||
|
|
c2f0e027b6 | ||
|
|
f85036d20f | ||
|
|
db4d128d04 | ||
|
|
2c4e2ed251 | ||
|
|
7b4642f407 | ||
|
|
7d45d00d6a | ||
|
|
884df50a34 | ||
|
|
5710af2f9b | ||
|
|
9312ef46ea |
145
CHANGELOG.md
145
CHANGELOG.md
@@ -3,6 +3,151 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 2025-08-15
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`cherrypick` - `v3.0.0-dev.10`](#cherrypick---v300-dev10)
|
||||
- [`cherrypick_annotations` - `v1.1.2-dev.1`](#cherrypick_annotations---v112-dev1)
|
||||
- [`cherrypick_flutter` - `v1.1.3-dev.10`](#cherrypick_flutter---v113-dev10)
|
||||
- [`cherrypick_generator` - `v2.0.0-dev.1`](#cherrypick_generator---v200-dev1)
|
||||
- [`talker_cherrypick_logger` - `v1.1.0-dev.5`](#talker_cherrypick_logger---v110-dev5)
|
||||
|
||||
---
|
||||
|
||||
#### `cherrypick` - `v3.0.0-dev.10`
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
#### `cherrypick_annotations` - `v1.1.2-dev.1`
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
#### `cherrypick_flutter` - `v1.1.3-dev.10`
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
#### `cherrypick_generator` - `v2.0.0-dev.1`
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
#### `talker_cherrypick_logger` - `v1.1.0-dev.5`
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
|
||||
## 2025-08-13
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`talker_cherrypick_logger` - `v1.1.0-dev.4`](#talker_cherrypick_logger---v110-dev4)
|
||||
|
||||
---
|
||||
|
||||
#### `talker_cherrypick_logger` - `v1.1.0-dev.4`
|
||||
|
||||
- **DOCS**(readme): update install instructions to use pub.dev as default method and remove obsolete git example.
|
||||
|
||||
|
||||
## 2025-08-13
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`talker_cherrypick_logger` - `v1.1.0-dev.3`](#talker_cherrypick_logger---v110-dev3)
|
||||
|
||||
---
|
||||
|
||||
#### `talker_cherrypick_logger` - `v1.1.0-dev.3`
|
||||
|
||||
|
||||
## 2025-08-13
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`talker_cherrypick_logger` - `v1.1.0-dev.2`](#talker_cherrypick_logger---v110-dev2)
|
||||
|
||||
---
|
||||
|
||||
#### `talker_cherrypick_logger` - `v1.1.0-dev.2`
|
||||
|
||||
- Bump "talker_cherrypick_logger" to `1.1.0-dev.2`.
|
||||
|
||||
|
||||
## 2025-08-13
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- [`cherrypick_generator` - `v2.0.0-dev.0`](#cherrypick_generator---v200-dev0)
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`cherrypick` - `v3.0.0-dev.9`](#cherrypick---v300-dev9)
|
||||
- [`cherrypick_annotations` - `v1.1.2-dev.0`](#cherrypick_annotations---v112-dev0)
|
||||
- [`cherrypick_flutter` - `v1.1.3-dev.9`](#cherrypick_flutter---v113-dev9)
|
||||
- [`talker_cherrypick_logger` - `v1.1.0-dev.0`](#talker_cherrypick_logger---v110-dev0)
|
||||
|
||||
---
|
||||
|
||||
#### `cherrypick_generator` - `v2.0.0-dev.0`
|
||||
|
||||
- **BREAKING** **DOCS**(generator): improve and unify English documentation and examples for all DI source files.
|
||||
|
||||
#### `cherrypick` - `v3.0.0-dev.9`
|
||||
|
||||
- **DOCS**(readme): add talker_cherrypick_logger to Additional Modules section.
|
||||
- **DOCS**(api): improve all DI core code documentation with English dartdoc and examples.
|
||||
|
||||
#### `cherrypick_annotations` - `v1.1.2-dev.0`
|
||||
|
||||
- **DOCS**(annotations): unify and improve English DartDoc for all DI annotations.
|
||||
|
||||
#### `cherrypick_flutter` - `v1.1.3-dev.9`
|
||||
|
||||
- **DOCS**(provider): add detailed English API documentation for CherryPickProvider Flutter integration.
|
||||
|
||||
#### `talker_cherrypick_logger` - `v1.1.0-dev.0`
|
||||
|
||||
- **FEAT**(logging): add talker_dio_logger and talker_bloc_logger integration, improve cherrypick logger structure, add UI log screen for DI and network/bloc debug.
|
||||
- **DOCS**: add full English documentation and usage guide to README.md.
|
||||
- **DOCS**: add detailed English documentation and usage examples for TalkerCherryPickObserver.
|
||||
|
||||
|
||||
## 2025-08-12
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -73,7 +73,8 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
|
||||
_childDi!.resolve<UniversalService>();
|
||||
break;
|
||||
case UniversalScenario.asyncChain:
|
||||
throw UnsupportedError('asyncChain supported only in UniversalChainAsyncBenchmark');
|
||||
throw UnsupportedError(
|
||||
'asyncChain supported only in UniversalChainAsyncBenchmark');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,11 @@ class BenchmarkCliRunner {
|
||||
if (config.di == 'getit') {
|
||||
final di = GetItAdapter();
|
||||
if (scenario == UniversalScenario.asyncChain) {
|
||||
final benchAsync = UniversalChainAsyncBenchmark<GetIt>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode,
|
||||
final benchAsync = UniversalChainAsyncBenchmark<GetIt>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runAsync(
|
||||
benchmark: benchAsync,
|
||||
@@ -45,8 +48,12 @@ class BenchmarkCliRunner {
|
||||
repeats: config.repeats,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<GetIt>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
|
||||
final benchSync = UniversalChainBenchmark<GetIt>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
scenario: scenario,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runSync(
|
||||
benchmark: benchSync,
|
||||
@@ -57,8 +64,12 @@ class BenchmarkCliRunner {
|
||||
} else if (config.di == 'riverpod') {
|
||||
final di = RiverpodAdapter();
|
||||
if (scenario == UniversalScenario.asyncChain) {
|
||||
final benchAsync = UniversalChainAsyncBenchmark<Map<String, rp.ProviderBase<Object?>>>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode,
|
||||
final benchAsync = UniversalChainAsyncBenchmark<
|
||||
Map<String, rp.ProviderBase<Object?>>>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runAsync(
|
||||
benchmark: benchAsync,
|
||||
@@ -66,8 +77,13 @@ class BenchmarkCliRunner {
|
||||
repeats: config.repeats,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<Map<String, rp.ProviderBase<Object?>>>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
|
||||
final benchSync = UniversalChainBenchmark<
|
||||
Map<String, rp.ProviderBase<Object?>>>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
scenario: scenario,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runSync(
|
||||
benchmark: benchSync,
|
||||
@@ -78,8 +94,11 @@ class BenchmarkCliRunner {
|
||||
} else {
|
||||
final di = CherrypickDIAdapter();
|
||||
if (scenario == UniversalScenario.asyncChain) {
|
||||
final benchAsync = UniversalChainAsyncBenchmark<Scope>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode,
|
||||
final benchAsync = UniversalChainAsyncBenchmark<Scope>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runAsync(
|
||||
benchmark: benchAsync,
|
||||
@@ -87,8 +106,12 @@ class BenchmarkCliRunner {
|
||||
repeats: config.repeats,
|
||||
);
|
||||
} else {
|
||||
final benchSync = UniversalChainBenchmark<Scope>(di,
|
||||
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
|
||||
final benchSync = UniversalChainBenchmark<Scope>(
|
||||
di,
|
||||
chainCount: c,
|
||||
nestingDepth: d,
|
||||
mode: mode,
|
||||
scenario: scenario,
|
||||
);
|
||||
benchResult = await BenchmarkRunner.runSync(
|
||||
benchmark: benchSync,
|
||||
@@ -103,7 +126,11 @@ class BenchmarkCliRunner {
|
||||
var median = timings[timings.length ~/ 2];
|
||||
var minVal = timings.first;
|
||||
var maxVal = timings.last;
|
||||
var stddev = timings.isEmpty ? 0 : sqrt(timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) / timings.length);
|
||||
var stddev = timings.isEmpty
|
||||
? 0
|
||||
: sqrt(
|
||||
timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) /
|
||||
timings.length);
|
||||
results.add({
|
||||
'benchmark': 'Universal_$bench',
|
||||
'chainCount': c,
|
||||
@@ -128,6 +155,7 @@ class BenchmarkCliRunner {
|
||||
'json': JsonReport(),
|
||||
'markdown': MarkdownReport(),
|
||||
};
|
||||
print(reportGenerators[config.format]?.render(results) ?? PrettyReport().render(results));
|
||||
print(reportGenerators[config.format]?.render(results) ??
|
||||
PrettyReport().render(results));
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,19 @@ import 'package:benchmark_di/scenarios/universal_scenario.dart';
|
||||
enum UniversalBenchmark {
|
||||
/// Simple singleton registration benchmark
|
||||
registerSingleton,
|
||||
|
||||
/// Chain of singleton dependencies
|
||||
chainSingleton,
|
||||
|
||||
/// Chain using factories
|
||||
chainFactory,
|
||||
|
||||
/// Async chain resolution
|
||||
chainAsync,
|
||||
|
||||
/// Named registration benchmark
|
||||
named,
|
||||
|
||||
/// Override/child-scope benchmark
|
||||
override,
|
||||
}
|
||||
@@ -65,23 +70,32 @@ T parseEnum<T>(String value, List<T> values, T defaultValue) {
|
||||
}
|
||||
|
||||
/// Parses comma-separated integer list from [s].
|
||||
List<int> parseIntList(String s) =>
|
||||
s.split(',').map((e) => int.tryParse(e.trim()) ?? 0).where((x) => x > 0).toList();
|
||||
List<int> parseIntList(String s) => s
|
||||
.split(',')
|
||||
.map((e) => int.tryParse(e.trim()) ?? 0)
|
||||
.where((x) => x > 0)
|
||||
.toList();
|
||||
|
||||
/// CLI config describing what and how to benchmark.
|
||||
class BenchmarkCliConfig {
|
||||
/// Benchmarks enabled to run (scenarios).
|
||||
final List<UniversalBenchmark> benchesToRun;
|
||||
|
||||
/// List of chain counts (parallel, per test).
|
||||
final List<int> chainCounts;
|
||||
|
||||
/// List of nesting depths (max chain length, per test).
|
||||
final List<int> nestDepths;
|
||||
|
||||
/// How many times to repeat each trial.
|
||||
final int repeats;
|
||||
|
||||
/// How many times to warm-up before measuring.
|
||||
final int warmups;
|
||||
|
||||
/// Output report format.
|
||||
final String format;
|
||||
|
||||
/// Name of DI implementation ("cherrypick" or "getit")
|
||||
final String di;
|
||||
BenchmarkCliConfig({
|
||||
@@ -105,7 +119,9 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
||||
..addOption('repeat', abbr: 'r', defaultsTo: '2')
|
||||
..addOption('warmup', abbr: 'w', defaultsTo: '1')
|
||||
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
|
||||
..addOption('di', defaultsTo: 'cherrypick', help: 'DI implementation: cherrypick, getit or riverpod')
|
||||
..addOption('di',
|
||||
defaultsTo: 'cherrypick',
|
||||
help: 'DI implementation: cherrypick, getit or riverpod')
|
||||
..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');
|
||||
final result = parser.parse(args);
|
||||
if (result['help'] == true) {
|
||||
|
||||
@@ -5,20 +5,32 @@ class CsvReport extends ReportGenerator {
|
||||
/// List of all keys/columns to include in the CSV output.
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
|
||||
'min_us','max_us','trials','timings_us','memory_diff_kb','delta_peak_kb','peak_rss_kb'
|
||||
'benchmark',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
'median_us',
|
||||
'stddev_us',
|
||||
'min_us',
|
||||
'max_us',
|
||||
'trials',
|
||||
'timings_us',
|
||||
'memory_diff_kb',
|
||||
'delta_peak_kb',
|
||||
'peak_rss_kb'
|
||||
];
|
||||
|
||||
/// Renders rows as a CSV table string.
|
||||
@override
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
final header = keys.join(',');
|
||||
final lines = rows.map((r) =>
|
||||
keys.map((k) {
|
||||
final v = r[k];
|
||||
if (v is List) return '"${v.join(';')}"';
|
||||
return (v ?? '').toString();
|
||||
}).join(',')
|
||||
).toList();
|
||||
final lines = rows
|
||||
.map((r) => keys.map((k) {
|
||||
final v = r[k];
|
||||
if (v is List) return '"${v.join(';')}"';
|
||||
return (v ?? '').toString();
|
||||
}).join(','))
|
||||
.toList();
|
||||
return ([header] + lines).join('\n');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ class JsonReport extends ReportGenerator {
|
||||
/// No specific keys; outputs all fields in raw map.
|
||||
@override
|
||||
List<String> get keys => [];
|
||||
|
||||
/// Renders all result rows as a pretty-printed JSON array.
|
||||
@override
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
|
||||
@@ -7,25 +7,46 @@ class MarkdownReport extends ReportGenerator {
|
||||
/// List of columns (keys) to show in the Markdown table.
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
|
||||
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb'
|
||||
'benchmark',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
'median_us',
|
||||
'stddev_us',
|
||||
'min_us',
|
||||
'max_us',
|
||||
'trials',
|
||||
'memory_diff_kb',
|
||||
'delta_peak_kb',
|
||||
'peak_rss_kb'
|
||||
];
|
||||
|
||||
/// Friendly display names for each benchmark type.
|
||||
static const nameMap = {
|
||||
'Universal_UniversalBenchmark.registerSingleton':'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton':'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory':'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync':'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named':'Named',
|
||||
'Universal_UniversalBenchmark.override':'Override',
|
||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named': 'Named',
|
||||
'Universal_UniversalBenchmark.override': 'Override',
|
||||
};
|
||||
|
||||
/// Renders all results as a formatted Markdown table with aligned columns and a legend.
|
||||
@override
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
final headers = [
|
||||
'Benchmark', 'Chain Count', 'Depth', 'Mean (us)', 'Median', 'Stddev', 'Min', 'Max', 'N', 'ΔRSS(KB)', 'ΔPeak(KB)', 'PeakRSS(KB)'
|
||||
'Benchmark',
|
||||
'Chain Count',
|
||||
'Depth',
|
||||
'Mean (us)',
|
||||
'Median',
|
||||
'Stddev',
|
||||
'Min',
|
||||
'Max',
|
||||
'N',
|
||||
'ΔRSS(KB)',
|
||||
'ΔPeak(KB)',
|
||||
'PeakRSS(KB)'
|
||||
];
|
||||
final dataRows = rows.map((r) {
|
||||
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
||||
@@ -73,6 +94,6 @@ class MarkdownReport extends ReportGenerator {
|
||||
> `PeakRSS(KB)` – Max observed RSS memory (KB)
|
||||
''';
|
||||
|
||||
return '$legend\n\n${([headerLine, divider] + lines).join('\n')}' ;
|
||||
return '$legend\n\n${([headerLine, divider] + lines).join('\n')}';
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,46 @@ class PrettyReport extends ReportGenerator {
|
||||
/// List of columns to output in the pretty report.
|
||||
@override
|
||||
final List<String> keys = [
|
||||
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
|
||||
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb'
|
||||
'benchmark',
|
||||
'chainCount',
|
||||
'nestingDepth',
|
||||
'mean_us',
|
||||
'median_us',
|
||||
'stddev_us',
|
||||
'min_us',
|
||||
'max_us',
|
||||
'trials',
|
||||
'memory_diff_kb',
|
||||
'delta_peak_kb',
|
||||
'peak_rss_kb'
|
||||
];
|
||||
|
||||
/// Mappings from internal benchmark IDs to display names.
|
||||
static const nameMap = {
|
||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named': 'Named',
|
||||
'Universal_UniversalBenchmark.override': 'Override',
|
||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||
'Universal_UniversalBenchmark.named': 'Named',
|
||||
'Universal_UniversalBenchmark.override': 'Override',
|
||||
};
|
||||
|
||||
/// Renders the results as a header + tab-separated value table.
|
||||
@override
|
||||
String render(List<Map<String, dynamic>> rows) {
|
||||
final headers = [
|
||||
'Benchmark', 'Chain Count', 'Depth', 'Mean (us)', 'Median', 'Stddev', 'Min', 'Max', 'N', 'ΔRSS(KB)', 'ΔPeak(KB)', 'PeakRSS(KB)'
|
||||
'Benchmark',
|
||||
'Chain Count',
|
||||
'Depth',
|
||||
'Mean (us)',
|
||||
'Median',
|
||||
'Stddev',
|
||||
'Min',
|
||||
'Max',
|
||||
'N',
|
||||
'ΔRSS(KB)',
|
||||
'ΔPeak(KB)',
|
||||
'PeakRSS(KB)'
|
||||
];
|
||||
final header = headers.join('\t');
|
||||
final lines = rows.map((r) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
abstract class ReportGenerator {
|
||||
/// Renders the given [results] as a formatted string (table, markdown, csv, etc).
|
||||
String render(List<Map<String, dynamic>> results);
|
||||
|
||||
/// List of output columns/keys included in the export (or [] for auto/all).
|
||||
List<String> get keys;
|
||||
}
|
||||
@@ -7,10 +7,13 @@ import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
|
||||
class BenchmarkResult {
|
||||
/// List of timings for each run (in microseconds).
|
||||
final List<num> timings;
|
||||
|
||||
/// Difference in memory (RSS, in KB) after running.
|
||||
final int memoryDiffKb;
|
||||
|
||||
/// Difference between peak RSS and initial RSS (in KB).
|
||||
final int deltaPeakKb;
|
||||
|
||||
/// Peak RSS memory observed (in KB).
|
||||
final int peakRssKb;
|
||||
BenchmarkResult({
|
||||
@@ -19,6 +22,7 @@ class BenchmarkResult {
|
||||
required this.deltaPeakKb,
|
||||
required this.peakRssKb,
|
||||
});
|
||||
|
||||
/// Computes a BenchmarkResult instance from run timings and memory data.
|
||||
factory BenchmarkResult.collect({
|
||||
required List<num> timings,
|
||||
@@ -64,7 +68,8 @@ class BenchmarkRunner {
|
||||
rssValues.add(ProcessInfo.currentRss);
|
||||
benchmark.teardown();
|
||||
}
|
||||
return BenchmarkResult.collect(timings: timings, rssValues: rssValues, memBefore: memBefore);
|
||||
return BenchmarkResult.collect(
|
||||
timings: timings, rssValues: rssValues, memBefore: memBefore);
|
||||
}
|
||||
|
||||
/// Runs an asynchronous benchmark ([UniversalChainAsyncBenchmark]) for a given number of [warmups] and [repeats].
|
||||
@@ -91,6 +96,7 @@ class BenchmarkRunner {
|
||||
rssValues.add(ProcessInfo.currentRss);
|
||||
await benchmark.teardown();
|
||||
}
|
||||
return BenchmarkResult.collect(timings: timings, rssValues: rssValues, memBefore: memBefore);
|
||||
return BenchmarkResult.collect(
|
||||
timings: timings, rssValues: rssValues, memBefore: memBefore);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'di_adapter.dart';
|
||||
|
||||
|
||||
/// Test module that generates a chain of service bindings for benchmarking.
|
||||
///
|
||||
/// Configurable by chain count, nesting depth, binding mode, and scenario
|
||||
@@ -12,10 +11,13 @@ import 'di_adapter.dart';
|
||||
class UniversalChainModule extends Module {
|
||||
/// Number of chains to create.
|
||||
final int chainCount;
|
||||
|
||||
/// Depth of each chain.
|
||||
final int nestingDepth;
|
||||
|
||||
/// How modules are registered (factory/singleton/async).
|
||||
final UniversalBindingMode bindingMode;
|
||||
|
||||
/// Which di scenario to generate (chained, named, etc).
|
||||
final UniversalScenario scenario;
|
||||
|
||||
@@ -38,17 +40,18 @@ class UniversalChainModule extends Module {
|
||||
final prevDepName = '${chain}_${level - 1}';
|
||||
final depName = '${chain}_$level';
|
||||
bind<UniversalService>()
|
||||
.toProvideAsync(() async {
|
||||
final prev = level > 1
|
||||
? await currentScope.resolveAsync<UniversalService>(named: prevDepName)
|
||||
: null;
|
||||
return UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: prev,
|
||||
);
|
||||
})
|
||||
.withName(depName)
|
||||
.singleton();
|
||||
.toProvideAsync(() async {
|
||||
final prev = level > 1
|
||||
? await currentScope.resolveAsync<UniversalService>(
|
||||
named: prevDepName)
|
||||
: null;
|
||||
return UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: prev,
|
||||
);
|
||||
})
|
||||
.withName(depName)
|
||||
.singleton();
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -58,13 +61,18 @@ class UniversalChainModule extends Module {
|
||||
case UniversalScenario.register:
|
||||
// Simple singleton registration.
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(value: 'reg', dependency: null))
|
||||
.toProvide(
|
||||
() => UniversalServiceImpl(value: 'reg', dependency: null))
|
||||
.singleton();
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
// Named factory registration for two distinct objects.
|
||||
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl1')).withName('impl1');
|
||||
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl2')).withName('impl2');
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(value: 'impl1'))
|
||||
.withName('impl1');
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(value: 'impl2'))
|
||||
.withName('impl2');
|
||||
break;
|
||||
case UniversalScenario.chain:
|
||||
// Chain of nested services, with dependency on previous level by name.
|
||||
@@ -79,7 +87,8 @@ class UniversalChainModule extends Module {
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName),
|
||||
dependency: currentScope.tryResolve<UniversalService>(
|
||||
named: prevDepName),
|
||||
))
|
||||
.withName(depName)
|
||||
.singleton();
|
||||
@@ -88,7 +97,8 @@ class UniversalChainModule extends Module {
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName),
|
||||
dependency: currentScope.tryResolve<UniversalService>(
|
||||
named: prevDepName),
|
||||
))
|
||||
.withName(depName);
|
||||
break;
|
||||
@@ -96,7 +106,9 @@ class UniversalChainModule extends Module {
|
||||
bind<UniversalService>()
|
||||
.toProvideAsync(() async => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: await currentScope.resolveAsync<UniversalService>(named: prevDepName),
|
||||
dependency:
|
||||
await currentScope.resolveAsync<UniversalService>(
|
||||
named: prevDepName),
|
||||
))
|
||||
.withName(depName)
|
||||
.singleton();
|
||||
@@ -107,14 +119,16 @@ class UniversalChainModule extends Module {
|
||||
// Регистрация алиаса без имени (на последний элемент цепочки)
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
|
||||
.toProvide(
|
||||
() => currentScope.resolve<UniversalService>(named: depName))
|
||||
.singleton();
|
||||
break;
|
||||
case UniversalScenario.override:
|
||||
// handled at benchmark level, но алиас нужен прямо в этом scope!
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
bind<UniversalService>()
|
||||
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
|
||||
.toProvide(
|
||||
() => currentScope.resolve<UniversalService>(named: depName))
|
||||
.singleton();
|
||||
break;
|
||||
case UniversalScenario.asyncChain:
|
||||
@@ -124,7 +138,6 @@ class UniversalChainModule extends Module {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CherrypickDIAdapter extends DIAdapter<Scope> {
|
||||
Scope? _scope;
|
||||
final bool _isSubScope;
|
||||
@@ -158,7 +171,8 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
|
||||
]);
|
||||
};
|
||||
}
|
||||
throw UnsupportedError('Scenario $scenario not supported by CherrypickDIAdapter');
|
||||
throw UnsupportedError(
|
||||
'Scenario $scenario not supported by CherrypickDIAdapter');
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:benchmark_di/scenarios/universal_binding_mode.dart';
|
||||
|
||||
/// Универсальная абстракция для DI-адаптера с унифицированной функцией регистрации.
|
||||
/// Теперь для каждого адаптера задаём строгий generic тип контейнера.
|
||||
typedef Registration<TContainer> = void Function(TContainer);
|
||||
|
||||
@@ -80,9 +80,11 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
getIt.registerSingletonAsync<UniversalService>(
|
||||
() async {
|
||||
final prev = level > 1
|
||||
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
|
||||
? await getIt.getAsync<UniversalService>(
|
||||
instanceName: prevDepName)
|
||||
: null;
|
||||
return UniversalServiceImpl(value: depName, dependency: prev);
|
||||
return UniversalServiceImpl(
|
||||
value: depName, dependency: prev);
|
||||
},
|
||||
instanceName: depName,
|
||||
);
|
||||
@@ -90,11 +92,16 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
}
|
||||
break;
|
||||
case UniversalScenario.register:
|
||||
getIt.registerSingleton<UniversalService>(UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
getIt.registerSingleton<UniversalService>(
|
||||
UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl1'), instanceName: 'impl1');
|
||||
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl2'), instanceName: 'impl2');
|
||||
getIt.registerFactory<UniversalService>(
|
||||
() => UniversalServiceImpl(value: 'impl1'),
|
||||
instanceName: 'impl1');
|
||||
getIt.registerFactory<UniversalService>(
|
||||
() => UniversalServiceImpl(value: 'impl2'),
|
||||
instanceName: 'impl2');
|
||||
break;
|
||||
case UniversalScenario.chain:
|
||||
for (int chain = 1; chain <= chainCount; chain++) {
|
||||
@@ -107,8 +114,8 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? getIt<UniversalService>(instanceName: prevDepName)
|
||||
: null,
|
||||
? getIt<UniversalService>(instanceName: prevDepName)
|
||||
: null,
|
||||
),
|
||||
instanceName: depName,
|
||||
);
|
||||
@@ -129,8 +136,9 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
() async => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
|
||||
: null,
|
||||
? await getIt.getAsync<UniversalService>(
|
||||
instanceName: prevDepName)
|
||||
: null,
|
||||
),
|
||||
instanceName: depName,
|
||||
);
|
||||
@@ -143,7 +151,8 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
||||
// handled at benchmark level
|
||||
break;
|
||||
}
|
||||
if (scenario == UniversalScenario.chain || scenario == UniversalScenario.override) {
|
||||
if (scenario == UniversalScenario.chain ||
|
||||
scenario == UniversalScenario.override) {
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
getIt.registerSingleton<UniversalService>(
|
||||
getIt<UniversalService>(instanceName: depName),
|
||||
|
||||
@@ -20,7 +20,9 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
|
||||
_parent = parent;
|
||||
|
||||
@override
|
||||
void setupDependencies(void Function(Map<String, rp.ProviderBase<Object?>> container) registration) {
|
||||
void setupDependencies(
|
||||
void Function(Map<String, rp.ProviderBase<Object?>> container)
|
||||
registration) {
|
||||
_container ??= _parent == null
|
||||
? rp.ProviderContainer()
|
||||
: rp.ProviderContainer(parent: _parent);
|
||||
@@ -76,7 +78,8 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
|
||||
}
|
||||
|
||||
@override
|
||||
Registration<Map<String, rp.ProviderBase<Object?>>> universalRegistration<S extends Enum>({
|
||||
Registration<Map<String, rp.ProviderBase<Object?>>>
|
||||
universalRegistration<S extends Enum>({
|
||||
required S scenario,
|
||||
required int chainCount,
|
||||
required int nestingDepth,
|
||||
@@ -86,25 +89,34 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
|
||||
return (providers) {
|
||||
switch (scenario) {
|
||||
case UniversalScenario.register:
|
||||
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
providers['UniversalService'] = rp.Provider<UniversalService>(
|
||||
(ref) => UniversalServiceImpl(value: 'reg', dependency: null));
|
||||
break;
|
||||
case UniversalScenario.named:
|
||||
providers['impl1'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl1'));
|
||||
providers['impl2'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl2'));
|
||||
providers['impl1'] = rp.Provider<UniversalService>(
|
||||
(ref) => UniversalServiceImpl(value: 'impl1'));
|
||||
providers['impl2'] = rp.Provider<UniversalService>(
|
||||
(ref) => UniversalServiceImpl(value: 'impl2'));
|
||||
break;
|
||||
case UniversalScenario.chain:
|
||||
for (int chain = 1; chain <= chainCount; chain++) {
|
||||
for (int level = 1; level <= nestingDepth; level++) {
|
||||
final prevDepName = '${chain}_${level - 1}';
|
||||
final depName = '${chain}_$level';
|
||||
providers[depName] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1 ? ref.watch(providers[prevDepName] as rp.ProviderBase<UniversalService>) : null,
|
||||
));
|
||||
providers[depName] =
|
||||
rp.Provider<UniversalService>((ref) => UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? ref.watch(providers[prevDepName]
|
||||
as rp.ProviderBase<UniversalService>)
|
||||
: null,
|
||||
));
|
||||
}
|
||||
}
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => ref.watch(providers[depName] as rp.ProviderBase<UniversalService>));
|
||||
providers['UniversalService'] = rp.Provider<UniversalService>(
|
||||
(ref) => ref.watch(
|
||||
providers[depName] as rp.ProviderBase<UniversalService>));
|
||||
break;
|
||||
case UniversalScenario.override:
|
||||
// handled at benchmark level
|
||||
@@ -114,24 +126,31 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
|
||||
for (int level = 1; level <= nestingDepth; level++) {
|
||||
final prevDepName = '${chain}_${level - 1}';
|
||||
final depName = '${chain}_$level';
|
||||
providers[depName] = rp.FutureProvider<UniversalService>((ref) async {
|
||||
providers[depName] =
|
||||
rp.FutureProvider<UniversalService>((ref) async {
|
||||
return UniversalServiceImpl(
|
||||
value: depName,
|
||||
dependency: level > 1
|
||||
? await ref.watch((providers[prevDepName] as rp.FutureProvider<UniversalService>).future) as UniversalService?
|
||||
? await ref.watch((providers[prevDepName]
|
||||
as rp.FutureProvider<UniversalService>)
|
||||
.future) as UniversalService?
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
final depName = '${chainCount}_$nestingDepth';
|
||||
providers['UniversalService'] = rp.FutureProvider<UniversalService>((ref) async {
|
||||
return await ref.watch((providers[depName] as rp.FutureProvider<UniversalService>).future);
|
||||
providers['UniversalService'] =
|
||||
rp.FutureProvider<UniversalService>((ref) async {
|
||||
return await ref.watch(
|
||||
(providers[depName] as rp.FutureProvider<UniversalService>)
|
||||
.future);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
throw UnsupportedError('Scenario $scenario not supported by RiverpodAdapter');
|
||||
throw UnsupportedError(
|
||||
'Scenario $scenario not supported by RiverpodAdapter');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
enum UniversalScenario {
|
||||
/// Single registration.
|
||||
register,
|
||||
|
||||
/// Chain of dependencies.
|
||||
chain,
|
||||
|
||||
/// Named registrations.
|
||||
named,
|
||||
|
||||
/// Child-scope override scenario.
|
||||
override,
|
||||
|
||||
/// Asynchronous chain scenario.
|
||||
asyncChain,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/// Base interface for any universal service in the benchmarks.
|
||||
///
|
||||
/// Represents an object in the dependency chain with an identifiable value
|
||||
@@ -6,6 +5,7 @@
|
||||
abstract class UniversalService {
|
||||
/// String ID for this service instance (e.g. chain/level info).
|
||||
final String value;
|
||||
|
||||
/// Optional reference to dependency service in the chain.
|
||||
final UniversalService? dependency;
|
||||
UniversalService({required this.value, this.dependency});
|
||||
|
||||
@@ -47,7 +47,7 @@ packages:
|
||||
path: "../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.7"
|
||||
version: "3.0.0-dev.9"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
## 3.0.0-dev.10
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
## 3.0.0-dev.9
|
||||
|
||||
- **DOCS**(readme): add talker_cherrypick_logger to Additional Modules section.
|
||||
- **DOCS**(api): improve all DI core code documentation with English dartdoc and examples.
|
||||
|
||||
## 3.0.0-dev.8
|
||||
|
||||
- **REFACTOR**(tests): replace MockLogger with MockObserver in scope tests to align with updated observer API.
|
||||
|
||||
@@ -707,11 +707,12 @@ Yes! Even if none of your services currently implement `Disposable`, always use
|
||||
|
||||
CherryPick provides a set of official add-on modules for advanced use cases and specific platforms:
|
||||
|
||||
| Module name | Description | Documentation |
|
||||
|-------------|-------------|---------------|
|
||||
| [**cherrypick_annotations**](https://pub.dev/packages/cherrypick_annotations) | Dart annotations for concise DI definitions and code generation. | [README](../cherrypick_annotations/README.md) |
|
||||
| [**cherrypick_generator**](https://pub.dev/packages/cherrypick_generator) | Code generator to produce DI bindings based on annotations. | [README](../cherrypick_generator/README.md) |
|
||||
| [**cherrypick_flutter**](https://pub.dev/packages/cherrypick_flutter) | Flutter integration: DI provider widgets and helpers for Flutter. | [README](../cherrypick_flutter/README.md) |
|
||||
| Module name | Description |
|
||||
|-------------|-------------|
|
||||
| [**cherrypick_annotations**](https://pub.dev/packages/cherrypick_annotations) | Dart annotations for concise DI definitions and code generation. |
|
||||
| [**cherrypick_generator**](https://pub.dev/packages/cherrypick_generator) | Code generator to produce DI bindings based on annotations. |
|
||||
| [**cherrypick_flutter**](https://pub.dev/packages/cherrypick_flutter) | Flutter integration: DI provider widgets and helpers for Flutter. |
|
||||
| [**talker_cherrypick_logger**](https://pub.dev/packages/talker_cherrypick_logger) | Advanced logger for CherryPick DI events and state. Provides seamless integration with [Talker](https://pub.dev/packages/talker) logger, enabling central and visual tracking of DI events, errors, and diagnostics in both UI and console. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,19 +47,19 @@ class FeatureModule extends Module {
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
final scope = CherryPick.openRootScope().installModules([AppModule()]);
|
||||
final scope = CherryPick.openRootScope().installModules([AppModule()]);
|
||||
|
||||
final subScope = scope
|
||||
.openSubScope("featureScope")
|
||||
.installModules([FeatureModule(isMock: true)]);
|
||||
|
||||
// Asynchronous instance resolution
|
||||
final dataBloc = await subScope.resolveAsync<DataBloc>();
|
||||
dataBloc.data.listen(
|
||||
(d) => print('Received data: $d'),
|
||||
onError: (e) => print('Error: $e'),
|
||||
onDone: () => print('DONE'),
|
||||
);
|
||||
// Asynchronous instance resolution
|
||||
final dataBloc = await subScope.resolveAsync<DataBloc>();
|
||||
dataBloc.data.listen(
|
||||
(d) => print('Received data: $d'),
|
||||
onError: (e) => print('Error: $e'),
|
||||
onDone: () => print('DONE'),
|
||||
);
|
||||
|
||||
await dataBloc.fetchData();
|
||||
} catch (e) {
|
||||
|
||||
@@ -36,18 +36,16 @@ class DatabaseModule extends Module {
|
||||
class ApiModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<ApiService>().toProvide(() => ApiService(
|
||||
currentScope.resolve<DatabaseService>()
|
||||
));
|
||||
bind<ApiService>()
|
||||
.toProvide(() => ApiService(currentScope.resolve<DatabaseService>()));
|
||||
}
|
||||
}
|
||||
|
||||
class UserModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<UserService>().toProvide(() => UserService(
|
||||
currentScope.resolve<ApiService>()
|
||||
));
|
||||
bind<UserService>()
|
||||
.toProvide(() => UserService(currentScope.resolve<ApiService>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,18 +63,16 @@ class CircularServiceB {
|
||||
class CircularModuleA extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<CircularServiceA>().toProvide(() => CircularServiceA(
|
||||
currentScope.resolve<CircularServiceB>()
|
||||
));
|
||||
bind<CircularServiceA>().toProvide(
|
||||
() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
|
||||
}
|
||||
}
|
||||
|
||||
class CircularModuleB extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<CircularServiceB>().toProvide(() => CircularServiceB(
|
||||
currentScope.resolve<CircularServiceA>()
|
||||
));
|
||||
bind<CircularServiceB>().toProvide(
|
||||
() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +83,13 @@ void main() {
|
||||
print('1. Globally enable cycle detection:');
|
||||
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
print('✅ Global cycle detection enabled: ${CherryPick.isGlobalCycleDetectionEnabled}');
|
||||
print(
|
||||
'✅ Global cycle detection enabled: ${CherryPick.isGlobalCycleDetectionEnabled}');
|
||||
|
||||
// All new scopes will automatically have cycle detection enabled
|
||||
final globalScope = CherryPick.openRootScope();
|
||||
print('✅ Root scope has cycle detection enabled: ${globalScope.isCycleDetectionEnabled}');
|
||||
print(
|
||||
'✅ Root scope has cycle detection enabled: ${globalScope.isCycleDetectionEnabled}');
|
||||
|
||||
// Install modules without circular dependencies
|
||||
globalScope.installModules([
|
||||
@@ -112,7 +110,8 @@ void main() {
|
||||
|
||||
// Создаем безопасный скоуп (с автоматически включенным обнаружением)
|
||||
final safeScope = CherryPick.openSafeRootScope();
|
||||
print('✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}');
|
||||
print(
|
||||
'✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}');
|
||||
|
||||
safeScope.installModules([
|
||||
DatabaseModule(),
|
||||
@@ -153,30 +152,37 @@ void main() {
|
||||
// Создаем скоуп без обнаружения
|
||||
// ignore: unused_local_variable
|
||||
final specificScope = CherryPick.openRootScope();
|
||||
print(' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
|
||||
print(
|
||||
' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
|
||||
|
||||
// Включаем обнаружение для конкретного скоупа
|
||||
CherryPick.enableCycleDetectionForScope();
|
||||
print('✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
|
||||
print(
|
||||
'✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
|
||||
|
||||
// Создаем дочерний скоуп
|
||||
// ignore: unused_local_variable
|
||||
final featureScope = CherryPick.openScope(scopeName: 'feature.auth');
|
||||
print(' Detection in feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
|
||||
print(
|
||||
' Detection in feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
|
||||
|
||||
// Включаем обнаружение для дочернего скоупа
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: 'feature.auth');
|
||||
print('✅ Detection enabled for feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
|
||||
print(
|
||||
'✅ Detection enabled for feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
|
||||
print('');
|
||||
|
||||
// Example 5: Creating safe child scopes
|
||||
print('5. Creating safe child scopes:');
|
||||
|
||||
final safeFeatureScope = CherryPick.openSafeScope(scopeName: 'feature.payments');
|
||||
print('✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}');
|
||||
final safeFeatureScope =
|
||||
CherryPick.openSafeScope(scopeName: 'feature.payments');
|
||||
print(
|
||||
'✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}');
|
||||
|
||||
// You can create a complex hierarchy of scopes
|
||||
final complexScope = CherryPick.openSafeScope(scopeName: 'app.feature.auth.login');
|
||||
final complexScope =
|
||||
CherryPick.openSafeScope(scopeName: 'app.feature.auth.login');
|
||||
print('✅ Complex scope created: ${complexScope.isCycleDetectionEnabled}');
|
||||
print('');
|
||||
|
||||
@@ -209,7 +215,8 @@ void main() {
|
||||
print('');
|
||||
|
||||
print('🚀 Production mode:');
|
||||
print(' CherryPick.disableGlobalCycleDetection(); // Disable for performance');
|
||||
print(
|
||||
' CherryPick.disableGlobalCycleDetection(); // Disable for performance');
|
||||
print(' final scope = CherryPick.openRootScope(); // Regular scope');
|
||||
print('');
|
||||
|
||||
@@ -219,7 +226,8 @@ void main() {
|
||||
print('');
|
||||
|
||||
print('🎯 Feature-specific:');
|
||||
print(' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");');
|
||||
print(
|
||||
' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");');
|
||||
print(' // Enable only for critical features');
|
||||
|
||||
// Cleanup
|
||||
|
||||
@@ -29,18 +29,16 @@ class OrderService {
|
||||
class UserModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<UserService>().toProvide(() => UserService(
|
||||
currentScope.resolve<OrderService>()
|
||||
));
|
||||
bind<UserService>()
|
||||
.toProvide(() => UserService(currentScope.resolve<OrderService>()));
|
||||
}
|
||||
}
|
||||
|
||||
class OrderModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<OrderService>().toProvide(() => OrderService(
|
||||
currentScope.resolve<UserService>()
|
||||
));
|
||||
bind<OrderService>()
|
||||
.toProvide(() => OrderService(currentScope.resolve<UserService>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +101,8 @@ class ImprovedUserModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<UserRepository>().singleton().toProvide(() => UserRepository());
|
||||
bind<ImprovedUserService>().toProvide(() => ImprovedUserService(
|
||||
currentScope.resolve<UserRepository>()
|
||||
));
|
||||
bind<ImprovedUserService>().toProvide(
|
||||
() => ImprovedUserService(currentScope.resolve<UserRepository>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +111,8 @@ class ImprovedOrderModule extends Module {
|
||||
void builder(Scope currentScope) {
|
||||
bind<OrderRepository>().singleton().toProvide(() => OrderRepository());
|
||||
bind<ImprovedOrderService>().toProvide(() => ImprovedOrderService(
|
||||
currentScope.resolve<OrderRepository>(),
|
||||
currentScope.resolve<ImprovedUserService>()
|
||||
));
|
||||
currentScope.resolve<OrderRepository>(),
|
||||
currentScope.resolve<ImprovedUserService>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +123,8 @@ void main() {
|
||||
print('1. Attempt to create a scope with circular dependencies:');
|
||||
try {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
|
||||
scope
|
||||
.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
|
||||
|
||||
scope.installModules([
|
||||
UserModule(),
|
||||
@@ -184,7 +181,6 @@ void main() {
|
||||
orderService.createOrder('ORD-001', 'John');
|
||||
final orders = orderService.getOrdersForUser('John');
|
||||
print('✅ Orders for user John: $orders');
|
||||
|
||||
} catch (e) {
|
||||
print('❌ Error: $e');
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -13,41 +13,70 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
/// Represents a direct instance or an async instance ([T] or [Future<T>]).
|
||||
/// Used for both direct and async bindings.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Instance<String> sync = "hello";
|
||||
/// Instance<MyApi> async = Future.value(MyApi());
|
||||
/// ```
|
||||
typedef Instance<T> = FutureOr<T>;
|
||||
|
||||
/// RU: Синхронный или асинхронный провайдер без параметров, возвращающий [T] или [Future<T>].
|
||||
/// ENG: Synchronous or asynchronous provider without parameters, returning [T] or [Future<T>].
|
||||
/// Provider function type for synchronous or asynchronous, parameterless creation of [T].
|
||||
/// Can return [T] or [Future<T>].
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Provider<MyService> provider = () => MyService();
|
||||
/// Provider<Api> asyncProvider = () async => await Api.connect();
|
||||
/// ```
|
||||
typedef Provider<T> = FutureOr<T> Function();
|
||||
|
||||
/// RU: Провайдер с динамическим параметром, возвращающий [T] или [Future<T>] в зависимости от реализации.
|
||||
/// ENG: Provider with dynamic parameter, returning [T] or [Future<T>] depending on implementation.
|
||||
/// Provider function type that accepts a dynamic parameter, for factory/parametrized injection.
|
||||
/// Returns [T] or [Future<T>].
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ProviderWithParams<User> provider = (params) => User(params["name"]);
|
||||
/// ```
|
||||
typedef ProviderWithParams<T> = FutureOr<T> Function(dynamic);
|
||||
|
||||
/// RU: Абстрактный интерфейс для классов, которые разрешают зависимости типа [T].
|
||||
/// ENG: Abstract interface for classes that resolve dependencies of type [T].
|
||||
/// Abstract interface for dependency resolvers used by [Binding].
|
||||
/// Defines how to resolve instances of type [T].
|
||||
///
|
||||
/// You usually don't use this directly; it's used internally for advanced/low-level DI.
|
||||
abstract class BindingResolver<T> {
|
||||
/// RU: Синхронное разрешение зависимости с параметром [params].
|
||||
/// ENG: Synchronous resolution of the dependency with [params].
|
||||
/// Synchronously resolves the dependency, optionally taking parameters (for factory cases).
|
||||
/// Throws if implementation does not support sync resolution.
|
||||
T? resolveSync([dynamic params]);
|
||||
|
||||
/// RU: Асинхронное разрешение зависимости с параметром [params].
|
||||
/// ENG: Asynchronous resolution of the dependency with [params].
|
||||
/// Asynchronously resolves the dependency, optionally taking parameters (for factory cases).
|
||||
/// If instance is already a [Future], returns it directly.
|
||||
Future<T>? resolveAsync([dynamic params]);
|
||||
|
||||
/// RU: Помечает текущий резолвер как синглтон — результат будет закеширован.
|
||||
/// ENG: Marks this resolver as singleton — result will be cached.
|
||||
/// Marks this resolver as singleton: instance(s) will be cached and reused inside the scope.
|
||||
void toSingleton();
|
||||
|
||||
/// Returns true if this resolver is marked as singleton.
|
||||
bool get isSingleton;
|
||||
}
|
||||
|
||||
/// RU: Резолвер, оборачивающий конкретный экземпляр [T] (или Future<T>), без вызова провайдера.
|
||||
/// ENG: Resolver that wraps a concrete instance of [T] (or Future<T>), without provider invocation.
|
||||
/// Concrete resolver for direct instance ([T] or [Future<T>]). No provider is called.
|
||||
///
|
||||
/// Used for [Binding.toInstance].
|
||||
/// Supports both sync and async resolution; sync will throw if underlying instance is [Future].
|
||||
/// Examples:
|
||||
/// ```dart
|
||||
/// var resolver = InstanceResolver("hello");
|
||||
/// resolver.resolveSync(); // == "hello"
|
||||
/// var asyncResolver = InstanceResolver(Future.value(7));
|
||||
/// asyncResolver.resolveAsync(); // Future<int>
|
||||
/// ```
|
||||
class InstanceResolver<T> implements BindingResolver<T> {
|
||||
final Instance<T> _instance;
|
||||
|
||||
/// RU: Создаёт резолвер, оборачивающий значение [instance].
|
||||
/// ENG: Creates a resolver that wraps the given [instance].
|
||||
/// Wraps the given instance (sync or async) in a resolver.
|
||||
InstanceResolver(this._instance);
|
||||
|
||||
@override
|
||||
@@ -62,7 +91,6 @@ class InstanceResolver<T> implements BindingResolver<T> {
|
||||
@override
|
||||
Future<T> resolveAsync([_]) {
|
||||
if (_instance is Future<T>) return _instance;
|
||||
|
||||
return Future.value(_instance);
|
||||
}
|
||||
|
||||
@@ -73,8 +101,23 @@ class InstanceResolver<T> implements BindingResolver<T> {
|
||||
bool get isSingleton => true;
|
||||
}
|
||||
|
||||
/// RU: Резолвер, оборачивающий провайдер, с возможностью синглтон-кеширования.
|
||||
/// ENG: Resolver that wraps a provider, with optional singleton caching.
|
||||
/// Resolver for provider functions (sync/async/factory), with optional singleton caching.
|
||||
/// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton].
|
||||
///
|
||||
/// Examples:
|
||||
/// ```dart
|
||||
/// // No param, sync:
|
||||
/// var r = ProviderResolver((_) => 5, withParams: false);
|
||||
/// r.resolveSync(); // == 5
|
||||
/// // With param:
|
||||
/// var rp = ProviderResolver((p) => p * 2, withParams: true);
|
||||
/// rp.resolveSync(2); // == 4
|
||||
/// // Singleton:
|
||||
/// r.toSingleton();
|
||||
/// // Async:
|
||||
/// var ra = ProviderResolver((_) async => await Future.value(10), withParams: false);
|
||||
/// await ra.resolveAsync(); // == 10
|
||||
/// ```
|
||||
class ProviderResolver<T> implements BindingResolver<T> {
|
||||
final ProviderWithParams<T> _provider;
|
||||
final bool _withParams;
|
||||
@@ -82,8 +125,7 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
||||
FutureOr<T>? _cache;
|
||||
bool _singleton = false;
|
||||
|
||||
/// RU: Создаёт резолвер из произвольной функции [raw], поддерживающей ноль или один параметр.
|
||||
/// ENG: Creates a resolver from arbitrary function [raw], supporting zero or one parameter.
|
||||
/// Creates a resolver from [provider], optionally accepting dynamic params.
|
||||
ProviderResolver(
|
||||
ProviderWithParams<T> provider, {
|
||||
required bool withParams,
|
||||
@@ -93,16 +135,13 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
||||
@override
|
||||
T resolveSync([dynamic params]) {
|
||||
_checkParams(params);
|
||||
|
||||
final result = _cache ?? _provider(params);
|
||||
|
||||
if (result is T) {
|
||||
if (_singleton) {
|
||||
_cache ??= result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
throw StateError(
|
||||
'Provider [$_provider] return Future<$T>. Use resolveAsync() instead.',
|
||||
);
|
||||
@@ -111,14 +150,11 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
||||
@override
|
||||
Future<T> resolveAsync([dynamic params]) {
|
||||
_checkParams(params);
|
||||
|
||||
final result = _cache ?? _provider(params);
|
||||
final target = result is Future<T> ? result : Future<T>.value(result);
|
||||
|
||||
if (_singleton) {
|
||||
_cache ??= target;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -130,8 +166,7 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
||||
@override
|
||||
bool get isSingleton => _singleton;
|
||||
|
||||
/// RU: Проверяет, был ли передан параметр, если провайдер требует его.
|
||||
/// ENG: Checks if parameter is passed when the provider expects it.
|
||||
/// Throws if params required but not supplied.
|
||||
void _checkParams(dynamic params) {
|
||||
if (_withParams && params == null) {
|
||||
throw StateError(
|
||||
|
||||
@@ -14,16 +14,20 @@
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
|
||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||
/// ENG: Exception thrown when a circular dependency is detected.
|
||||
/// Exception thrown when a circular dependency is detected during dependency resolution.
|
||||
///
|
||||
/// Contains a [message] and the [dependencyChain] showing the resolution cycle.
|
||||
///
|
||||
/// Example diagnostic:
|
||||
/// ```
|
||||
/// CircularDependencyException: Circular dependency detected for A
|
||||
/// Dependency chain: A -> B -> C -> A
|
||||
/// ```
|
||||
class CircularDependencyException implements Exception {
|
||||
final String message;
|
||||
final List<String> dependencyChain;
|
||||
|
||||
CircularDependencyException(this.message, this.dependencyChain) {
|
||||
// DEBUG
|
||||
|
||||
}
|
||||
CircularDependencyException(this.message, this.dependencyChain);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -32,8 +36,26 @@ class CircularDependencyException implements Exception {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||
/// Circular dependency detector for CherryPick DI containers.
|
||||
///
|
||||
/// Tracks dependency resolution chains to detect and prevent infinite recursion caused by cycles.
|
||||
/// Whenever a resolve chain re-enters a started dependency, a [CircularDependencyException] is thrown with the full chain.
|
||||
///
|
||||
/// This class is used internally, but you can interact with it through [CycleDetectionMixin].
|
||||
///
|
||||
/// Example usage (pseudocode):
|
||||
/// ```dart
|
||||
/// final detector = CycleDetector(observer: myObserver);
|
||||
/// try {
|
||||
/// detector.startResolving<A>();
|
||||
/// // ... resolving A which depends on B, etc
|
||||
/// detector.startResolving<B>();
|
||||
/// detector.startResolving<A>(); // BOOM: throws exception
|
||||
/// } finally {
|
||||
/// detector.finishResolving<B>();
|
||||
/// detector.finishResolving<A>();
|
||||
/// }
|
||||
/// ```
|
||||
class CycleDetector {
|
||||
final CherryPickObserver _observer;
|
||||
final Set<String> _resolutionStack = HashSet<String>();
|
||||
@@ -41,10 +63,9 @@ class CycleDetector {
|
||||
|
||||
CycleDetector({required CherryPickObserver observer}) : _observer = observer;
|
||||
|
||||
/// RU: Начинает отслеживание разрешения зависимости.
|
||||
/// ENG: Starts tracking dependency resolution.
|
||||
/// Starts tracking dependency resolution for type [T] and optional [named] qualifier.
|
||||
///
|
||||
/// Throws [CircularDependencyException] if circular dependency is detected.
|
||||
/// Throws [CircularDependencyException] if a cycle is found.
|
||||
void startResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
_observer.onDiagnostic(
|
||||
@@ -56,27 +77,21 @@ class CycleDetector {
|
||||
);
|
||||
if (_resolutionStack.contains(dependencyKey)) {
|
||||
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
_observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
_observer.onCycleDetected(cycle);
|
||||
_observer.onError('Cycle detected for $dependencyKey', null, null);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
);
|
||||
}
|
||||
|
||||
_resolutionStack.add(dependencyKey);
|
||||
_resolutionHistory.add(dependencyKey);
|
||||
}
|
||||
|
||||
/// RU: Завершает отслеживание разрешения зависимости.
|
||||
/// ENG: Finishes tracking dependency resolution.
|
||||
/// Stops tracking dependency resolution for type [T] and optional [named] qualifier.
|
||||
/// Should always be called after [startResolving], including for errors.
|
||||
void finishResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
_observer.onDiagnostic(
|
||||
@@ -84,15 +99,14 @@ class CycleDetector {
|
||||
details: {'event': 'finishResolving'},
|
||||
);
|
||||
_resolutionStack.remove(dependencyKey);
|
||||
// Удаляем из истории только если это последний элемент
|
||||
// Only remove from history if it's the last one
|
||||
if (_resolutionHistory.isNotEmpty &&
|
||||
_resolutionHistory.last == dependencyKey) {
|
||||
_resolutionHistory.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Очищает все состояние детектора.
|
||||
/// ENG: Clears all detector state.
|
||||
/// Clears all resolution state and resets the cycle detector.
|
||||
void clear() {
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector clear',
|
||||
@@ -105,33 +119,46 @@ class CycleDetector {
|
||||
_resolutionHistory.clear();
|
||||
}
|
||||
|
||||
/// RU: Проверяет, находится ли зависимость в процессе разрешения.
|
||||
/// ENG: Checks if dependency is currently being resolved.
|
||||
/// Returns true if dependency [T] (and [named], if specified) is being resolved right now.
|
||||
bool isResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
return _resolutionStack.contains(dependencyKey);
|
||||
}
|
||||
|
||||
/// RU: Возвращает текущую цепочку разрешения зависимостей.
|
||||
/// ENG: Returns current dependency resolution chain.
|
||||
List<String> get currentResolutionChain => List.unmodifiable(_resolutionHistory);
|
||||
/// Gets the current dependency resolution chain (for diagnostics or debugging).
|
||||
List<String> get currentResolutionChain =>
|
||||
List.unmodifiable(_resolutionHistory);
|
||||
|
||||
/// RU: Создает уникальный ключ для зависимости.
|
||||
/// ENG: Creates unique key for dependency.
|
||||
/// Returns a unique string key for type [T] (+name).
|
||||
String _createDependencyKey<T>(String? named) {
|
||||
final typeName = T.toString();
|
||||
return named != null ? '$typeName@$named' : typeName;
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Миксин для добавления поддержки обнаружения циклических зависимостей.
|
||||
/// ENG: Mixin for adding circular dependency detection support.
|
||||
/// Mixin for adding circular dependency detection support to custom DI containers/classes.
|
||||
///
|
||||
/// Fields:
|
||||
/// - `observer`: must be implemented by your class (used for diagnostics and error reporting)
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// class MyContainer with CycleDetectionMixin {
|
||||
/// @override
|
||||
/// CherryPickObserver get observer => myObserver;
|
||||
/// }
|
||||
///
|
||||
/// final c = MyContainer();
|
||||
/// c.enableCycleDetection();
|
||||
/// c.withCycleDetection(String, null, () {
|
||||
/// // ... dependency resolution code
|
||||
/// });
|
||||
/// ```
|
||||
mixin CycleDetectionMixin {
|
||||
CycleDetector? _cycleDetector;
|
||||
CherryPickObserver get observer;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей.
|
||||
/// ENG: Enables circular dependency detection.
|
||||
/// Turns on circular dependency detection for this class/container.
|
||||
void enableCycleDetection() {
|
||||
_cycleDetector = CycleDetector(observer: observer);
|
||||
observer.onDiagnostic(
|
||||
@@ -143,8 +170,7 @@ mixin CycleDetectionMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Disables circular dependency detection.
|
||||
/// Shuts off detection and clears any cycle history for this container.
|
||||
void disableCycleDetection() {
|
||||
_cycleDetector?.clear();
|
||||
observer.onDiagnostic(
|
||||
@@ -157,12 +183,17 @@ mixin CycleDetectionMixin {
|
||||
_cycleDetector = null;
|
||||
}
|
||||
|
||||
/// RU: Проверяет, включено ли обнаружение циклических зависимостей.
|
||||
/// ENG: Checks if circular dependency detection is enabled.
|
||||
/// Returns true if detection is currently enabled.
|
||||
bool get isCycleDetectionEnabled => _cycleDetector != null;
|
||||
|
||||
/// RU: Выполняет действие с отслеживанием циклических зависимостей.
|
||||
/// ENG: Executes action with circular dependency tracking.
|
||||
/// Executes [action] while tracking for circular DI cycles for [dependencyType] and [named].
|
||||
///
|
||||
/// Throws [CircularDependencyException] if a dependency cycle is detected.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// withCycleDetection(String, 'api', () => resolveApi());
|
||||
/// ```
|
||||
T withCycleDetection<T>(
|
||||
Type dependencyType,
|
||||
String? named,
|
||||
@@ -177,17 +208,12 @@ mixin CycleDetectionMixin {
|
||||
: dependencyType.toString();
|
||||
|
||||
if (_cycleDetector!._resolutionStack.contains(dependencyKey)) {
|
||||
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||
final cycleStartIndex =
|
||||
_cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
observer.onCycleDetected(cycle);
|
||||
observer.onError('Cycle detected for $dependencyKey', null, null);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -208,8 +234,7 @@ mixin CycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Возвращает текущую цепочку разрешения зависимостей.
|
||||
/// ENG: Returns current dependency resolution chain.
|
||||
/// Gets the current active dependency resolution chain.
|
||||
List<String> get currentResolutionChain =>
|
||||
_cycleDetector?.currentResolutionChain ?? [];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,28 @@
|
||||
//
|
||||
import 'package:cherrypick/src/scope.dart';
|
||||
|
||||
/// Abstract factory interface for creating objects of type [T] using a [Scope].
|
||||
///
|
||||
/// Can be implemented for advanced dependency injection scenarios where
|
||||
/// the resolution requires contextual information from the DI [Scope].
|
||||
///
|
||||
/// Often used to supply complex objects, runtime-bound services,
|
||||
/// or objects depending on dynamic configuration.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// class MyServiceFactory implements Factory<MyService> {
|
||||
/// @override
|
||||
/// MyService createInstance(Scope scope) {
|
||||
/// final db = scope.resolve<Database>(named: "main");
|
||||
/// return MyService(db);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Usage in a module:
|
||||
/// bind<MyService>().toProvide(() => MyServiceFactory().createInstance(scope));
|
||||
/// ```
|
||||
abstract class Factory<T> {
|
||||
/// Implement this to provide an instance of [T], with access to the resolving [scope].
|
||||
T createInstance(Scope scope);
|
||||
}
|
||||
|
||||
@@ -14,34 +14,50 @@
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
|
||||
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
||||
/// ENG: Global circular dependency detector for entire scope hierarchy.
|
||||
/// GlobalCycleDetector detects and prevents circular dependencies across an entire DI scope hierarchy.
|
||||
///
|
||||
/// This is particularly important for modular/feature-based applications
|
||||
/// where subscopes can introduce indirect cycles that span different scopes.
|
||||
///
|
||||
/// The detector tracks resolution chains and throws [CircularDependencyException]
|
||||
/// when a cycle is found, showing the full chain (including scope context).
|
||||
///
|
||||
/// Example usage via [GlobalCycleDetectionMixin]:
|
||||
/// ```dart
|
||||
/// class MyScope with GlobalCycleDetectionMixin { /* ... */ }
|
||||
///
|
||||
/// final scope = MyScope();
|
||||
/// scope.setScopeId('feature');
|
||||
/// scope.enableGlobalCycleDetection();
|
||||
///
|
||||
/// scope.withGlobalCycleDetection(String, null, () {
|
||||
/// // ... resolve dependencies here, will detect cross-scope cycles
|
||||
/// });
|
||||
/// ```
|
||||
class GlobalCycleDetector {
|
||||
static GlobalCycleDetector? _instance;
|
||||
|
||||
final CherryPickObserver _observer;
|
||||
|
||||
// Глобальный стек разрешения зависимостей
|
||||
// Global set and chain history for all resolutions
|
||||
final Set<String> _globalResolutionStack = HashSet<String>();
|
||||
|
||||
// История разрешения для построения цепочки зависимостей
|
||||
final List<String> _globalResolutionHistory = [];
|
||||
|
||||
// Карта активных детекторов по скоупам
|
||||
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
||||
// Map of active detectors for subscopes (rarely used directly)
|
||||
final Map<String, CycleDetector> _scopeDetectors =
|
||||
HashMap<String, CycleDetector>();
|
||||
|
||||
GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer;
|
||||
GlobalCycleDetector._internal({required CherryPickObserver observer})
|
||||
: _observer = observer;
|
||||
|
||||
/// RU: Получить единственный экземпляр глобального детектора.
|
||||
/// ENG: Get singleton instance of global detector.
|
||||
/// Returns the singleton global detector instance, initializing it if needed.
|
||||
static GlobalCycleDetector get instance {
|
||||
_instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
|
||||
_instance ??=
|
||||
GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// RU: Сбросить глобальный детектор (полезно для тестов).
|
||||
/// ENG: Reset global detector (useful for tests).
|
||||
/// Reset internal state (useful for testing).
|
||||
static void reset() {
|
||||
_instance?._globalResolutionStack.clear();
|
||||
_instance?._globalResolutionHistory.clear();
|
||||
@@ -49,24 +65,18 @@ class GlobalCycleDetector {
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
/// RU: Начать отслеживание разрешения зависимости в глобальном контексте.
|
||||
/// ENG: Start tracking dependency resolution in global context.
|
||||
/// Start tracking resolution of dependency [T] with optional [named] and [scopeId].
|
||||
/// Throws [CircularDependencyException] on cycle.
|
||||
void startGlobalResolving<T>({String? named, String? scopeId}) {
|
||||
final dependencyKey = _createDependencyKeyFromType(T, named, scopeId);
|
||||
|
||||
if (_globalResolutionStack.contains(dependencyKey)) {
|
||||
// Найдена глобальная циклическая зависимость
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
_observer.onCycleDetected(cycle, scopeName: scopeId);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
'Global circular dependency detected for $dependencyKey', null, null);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -77,42 +87,35 @@ class GlobalCycleDetector {
|
||||
_globalResolutionHistory.add(dependencyKey);
|
||||
}
|
||||
|
||||
/// RU: Завершить отслеживание разрешения зависимости в глобальном контексте.
|
||||
/// ENG: Finish tracking dependency resolution in global context.
|
||||
/// Finish tracking a dependency. Should always be called after [startGlobalResolving].
|
||||
void finishGlobalResolving<T>({String? named, String? scopeId}) {
|
||||
final dependencyKey = _createDependencyKeyFromType(T, named, scopeId);
|
||||
_globalResolutionStack.remove(dependencyKey);
|
||||
|
||||
// Удаляем из истории только если это последний элемент
|
||||
if (_globalResolutionHistory.isNotEmpty &&
|
||||
_globalResolutionHistory.last == dependencyKey) {
|
||||
_globalResolutionHistory.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Выполнить действие с глобальным отслеживанием циклических зависимостей.
|
||||
/// ENG: Execute action with global circular dependency tracking.
|
||||
/// Internally execute [action] with global cycle detection for [dependencyType], [named], [scopeId].
|
||||
/// Throws [CircularDependencyException] on cycle.
|
||||
T withGlobalCycleDetection<T>(
|
||||
Type dependencyType,
|
||||
String? named,
|
||||
String? scopeId,
|
||||
T Function() action,
|
||||
) {
|
||||
final dependencyKey = _createDependencyKeyFromType(dependencyType, named, scopeId);
|
||||
final dependencyKey =
|
||||
_createDependencyKeyFromType(dependencyType, named, scopeId);
|
||||
|
||||
if (_globalResolutionStack.contains(dependencyKey)) {
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
_observer.onCycleDetected(cycle, scopeName: scopeId);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
'Global circular dependency detected for $dependencyKey', null, null);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -133,31 +136,28 @@ class GlobalCycleDetector {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Получить детектор для конкретного скоупа.
|
||||
/// ENG: Get detector for specific scope.
|
||||
/// Get per-scope detector (not usually needed by consumers).
|
||||
CycleDetector getScopeDetector(String scopeId) {
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
|
||||
return _scopeDetectors.putIfAbsent(
|
||||
scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
|
||||
}
|
||||
|
||||
/// RU: Удалить детектор для скоупа.
|
||||
/// ENG: Remove detector for scope.
|
||||
/// Remove detector for a given scope.
|
||||
void removeScopeDetector(String scopeId) {
|
||||
_scopeDetectors.remove(scopeId);
|
||||
}
|
||||
|
||||
/// RU: Проверить, находится ли зависимость в процессе глобального разрешения.
|
||||
/// ENG: Check if dependency is currently being resolved globally.
|
||||
/// Returns true if dependency [T] is currently being resolved in the global scope.
|
||||
bool isGloballyResolving<T>({String? named, String? scopeId}) {
|
||||
final dependencyKey = _createDependencyKeyFromType(T, named, scopeId);
|
||||
return _globalResolutionStack.contains(dependencyKey);
|
||||
}
|
||||
|
||||
/// RU: Получить текущую глобальную цепочку разрешения зависимостей.
|
||||
/// ENG: Get current global dependency resolution chain.
|
||||
List<String> get globalResolutionChain => List.unmodifiable(_globalResolutionHistory);
|
||||
/// Get current global dependency resolution chain (for debugging or diagnostics).
|
||||
List<String> get globalResolutionChain =>
|
||||
List.unmodifiable(_globalResolutionHistory);
|
||||
|
||||
/// RU: Очистить все состояние детектора.
|
||||
/// ENG: Clear all detector state.
|
||||
/// Clears all global and per-scope state in this detector.
|
||||
void clear() {
|
||||
_globalResolutionStack.clear();
|
||||
_globalResolutionHistory.clear();
|
||||
@@ -167,15 +167,9 @@ class GlobalCycleDetector {
|
||||
|
||||
void _detectorClear(detector) => detector.clear();
|
||||
|
||||
/// RU: Создать уникальный ключ для зависимости с учетом скоупа.
|
||||
/// ENG: Create unique key for dependency including scope.
|
||||
//String _createDependencyKey<T>(String? named, String? scopeId) {
|
||||
// return _createDependencyKeyFromType(T, named, scopeId);
|
||||
//}
|
||||
|
||||
/// RU: Создать уникальный ключ для зависимости по типу с учетом скоупа.
|
||||
/// ENG: Create unique key for dependency by type including scope.
|
||||
String _createDependencyKeyFromType(Type type, String? named, String? scopeId) {
|
||||
/// Creates a unique dependency key string including scope and name (for diagnostics/cycle checks).
|
||||
String _createDependencyKeyFromType(
|
||||
Type type, String? named, String? scopeId) {
|
||||
final typeName = type.toString();
|
||||
final namePrefix = named != null ? '@$named' : '';
|
||||
final scopePrefix = scopeId != null ? '[$scopeId]' : '';
|
||||
@@ -183,40 +177,53 @@ class GlobalCycleDetector {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Улучшенный миксин для глобального обнаружения циклических зависимостей.
|
||||
/// ENG: Enhanced mixin for global circular dependency detection.
|
||||
/// Enhanced mixin for global circular dependency detection, to be mixed into
|
||||
/// DI scopes or containers that want cross-scope protection.
|
||||
///
|
||||
/// Typical usage pattern:
|
||||
/// ```dart
|
||||
/// class MySubscope with GlobalCycleDetectionMixin { ... }
|
||||
///
|
||||
/// final scope = MySubscope();
|
||||
/// scope.setScopeId('user_profile');
|
||||
/// scope.enableGlobalCycleDetection();
|
||||
///
|
||||
/// scope.withGlobalCycleDetection(UserService, null, () {
|
||||
/// // ... resolve user service and friends, auto-detects global cycles
|
||||
/// });
|
||||
/// ```
|
||||
mixin GlobalCycleDetectionMixin {
|
||||
String? _scopeId;
|
||||
bool _globalCycleDetectionEnabled = false;
|
||||
|
||||
/// RU: Установить идентификатор скоупа для глобального отслеживания.
|
||||
/// ENG: Set scope identifier for global tracking.
|
||||
/// Set the scope's unique identifier for global tracking (should be called at scope initialization).
|
||||
void setScopeId(String scopeId) {
|
||||
_scopeId = scopeId;
|
||||
}
|
||||
|
||||
/// RU: Получить идентификатор скоупа.
|
||||
/// ENG: Get scope identifier.
|
||||
/// Get the scope's id, if configured.
|
||||
String? get scopeId => _scopeId;
|
||||
|
||||
/// RU: Включить глобальное обнаружение циклических зависимостей.
|
||||
/// ENG: Enable global circular dependency detection.
|
||||
/// Enable global cross-scope circular dependency detection.
|
||||
void enableGlobalCycleDetection() {
|
||||
_globalCycleDetectionEnabled = true;
|
||||
}
|
||||
|
||||
/// RU: Отключить глобальное обнаружение циклических зависимостей.
|
||||
/// ENG: Disable global circular dependency detection.
|
||||
/// Disable global cycle detection (no cycle checks will be performed globally).
|
||||
void disableGlobalCycleDetection() {
|
||||
_globalCycleDetectionEnabled = false;
|
||||
}
|
||||
|
||||
/// RU: Проверить, включено ли глобальное обнаружение циклических зависимостей.
|
||||
/// ENG: Check if global circular dependency detection is enabled.
|
||||
/// Returns true if global cycle detection is currently enabled for this scope/container.
|
||||
bool get isGlobalCycleDetectionEnabled => _globalCycleDetectionEnabled;
|
||||
|
||||
/// RU: Выполнить действие с глобальным отслеживанием циклических зависимостей.
|
||||
/// ENG: Execute action with global circular dependency tracking.
|
||||
/// Executes [action] with global cycle detection for [dependencyType] and [named].
|
||||
/// Throws [CircularDependencyException] if a cycle is detected.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// withGlobalCycleDetection(UserService, null, () => resolveUser());
|
||||
/// ```
|
||||
T withGlobalCycleDetection<T>(
|
||||
Type dependencyType,
|
||||
String? named,
|
||||
@@ -234,8 +241,7 @@ mixin GlobalCycleDetectionMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// RU: Получить текущую глобальную цепочку разрешения зависимостей.
|
||||
/// ENG: Get current global dependency resolution chain.
|
||||
/// Access the current global dependency resolution chain for diagnostics.
|
||||
List<String> get globalResolutionChain =>
|
||||
GlobalCycleDetector.instance.globalResolutionChain;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
|
||||
Scope? _rootScope;
|
||||
|
||||
/// Global logger for all [Scope]s managed by [CherryPick].
|
||||
@@ -80,7 +79,8 @@ class CherryPick {
|
||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
}
|
||||
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
|
||||
if (_globalCrossScopeCycleDetectionEnabled &&
|
||||
!_rootScope!.isGlobalCycleDetectionEnabled) {
|
||||
_rootScope!.enableGlobalCycleDetection();
|
||||
}
|
||||
return _rootScope!;
|
||||
@@ -96,7 +96,8 @@ class CherryPick {
|
||||
/// ```
|
||||
static Future<void> closeRootScope() async {
|
||||
if (_rootScope != null) {
|
||||
await _rootScope!.dispose(); // Автоматический вызов dispose для rootScope!
|
||||
await _rootScope!
|
||||
.dispose(); // Автоматический вызов dispose для rootScope!
|
||||
_rootScope = null;
|
||||
}
|
||||
}
|
||||
@@ -141,13 +142,15 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// CherryPick.enableCycleDetectionForScope(scopeName: 'api.feature');
|
||||
/// ```
|
||||
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||
static void enableCycleDetectionForScope(
|
||||
{String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
scope.enableCycleDetection();
|
||||
}
|
||||
|
||||
/// Disables cycle detection for a given scope. See [enableCycleDetectionForScope].
|
||||
static void disableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
|
||||
static void disableCycleDetectionForScope(
|
||||
{String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
scope.disableCycleDetection();
|
||||
}
|
||||
@@ -158,7 +161,8 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.api');
|
||||
/// ```
|
||||
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) {
|
||||
static bool isCycleDetectionEnabledForScope(
|
||||
{String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
return scope.isCycleDetectionEnabled;
|
||||
}
|
||||
@@ -171,7 +175,8 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// print(CherryPick.getCurrentResolutionChain(scopeName: 'feature.api'));
|
||||
/// ```
|
||||
static List<String> getCurrentResolutionChain({String scopeName = '', String separator = '.'}) {
|
||||
static List<String> getCurrentResolutionChain(
|
||||
{String scopeName = '', String separator = '.'}) {
|
||||
final scope = _getScope(scopeName, separator);
|
||||
return scope.currentResolutionChain;
|
||||
}
|
||||
@@ -229,14 +234,13 @@ class CherryPick {
|
||||
if (nameParts.isEmpty) {
|
||||
throw Exception('Can not open sub scope because scopeName can not split');
|
||||
}
|
||||
final scope = nameParts.fold(
|
||||
openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element)
|
||||
);
|
||||
final scope = nameParts.fold(openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element));
|
||||
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
|
||||
scope.enableCycleDetection();
|
||||
}
|
||||
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) {
|
||||
if (_globalCrossScopeCycleDetectionEnabled &&
|
||||
!scope.isGlobalCycleDetectionEnabled) {
|
||||
scope.enableGlobalCycleDetection();
|
||||
}
|
||||
return scope;
|
||||
@@ -252,21 +256,21 @@ class CherryPick {
|
||||
/// CherryPick.closeScope(scopeName: 'network.super.api');
|
||||
/// ```
|
||||
@experimental
|
||||
static Future<void> closeScope({String scopeName = '', String separator = '.'}) async {
|
||||
static Future<void> closeScope(
|
||||
{String scopeName = '', String separator = '.'}) async {
|
||||
if (scopeName.isEmpty) {
|
||||
await closeRootScope();
|
||||
return;
|
||||
}
|
||||
final nameParts = scopeName.split(separator);
|
||||
if (nameParts.isEmpty) {
|
||||
throw Exception('Can not close sub scope because scopeName can not split');
|
||||
throw Exception(
|
||||
'Can not close sub scope because scopeName can not split');
|
||||
}
|
||||
if (nameParts.length > 1) {
|
||||
final lastPart = nameParts.removeLast();
|
||||
final scope = nameParts.fold(
|
||||
openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element)
|
||||
);
|
||||
final scope = nameParts.fold(openRootScope(),
|
||||
(Scope previous, String element) => previous.openSubScope(element));
|
||||
await scope.closeSubScope(lastPart);
|
||||
} else {
|
||||
await openRootScope().closeSubScope(nameParts.first);
|
||||
@@ -316,7 +320,8 @@ class CherryPick {
|
||||
/// print('Global cross-scope detection is ON');
|
||||
/// }
|
||||
/// ```
|
||||
static bool get isGlobalCrossScopeCycleDetectionEnabled => _globalCrossScopeCycleDetectionEnabled;
|
||||
static bool get isGlobalCrossScopeCycleDetectionEnabled =>
|
||||
_globalCrossScopeCycleDetectionEnabled;
|
||||
|
||||
/// Returns the current global dependency resolution chain (across all scopes).
|
||||
///
|
||||
@@ -367,7 +372,8 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// final featureScope = CherryPick.openGlobalSafeScope(scopeName: 'featureA.api');
|
||||
/// ```
|
||||
static Scope openGlobalSafeScope({String scopeName = '', String separator = '.'}) {
|
||||
static Scope openGlobalSafeScope(
|
||||
{String scopeName = '', String separator = '.'}) {
|
||||
final scope = openScope(scopeName: scopeName, separator: separator);
|
||||
scope.enableCycleDetection();
|
||||
scope.enableGlobalCycleDetection();
|
||||
|
||||
@@ -15,39 +15,71 @@ import 'dart:collection';
|
||||
import 'package:cherrypick/src/binding.dart';
|
||||
import 'package:cherrypick/src/scope.dart';
|
||||
|
||||
/// RU: Класс Module является основой для пользовательских модулей.
|
||||
/// Этот класс нужен для инициализации [Scope].
|
||||
/// Represents a DI module—a reusable group of dependency bindings.
|
||||
///
|
||||
/// RU: The Module class is the basis for custom modules.
|
||||
/// This class is needed to initialize [Scope].
|
||||
/// Extend [Module] to declaratively group related [Binding] definitions,
|
||||
/// then install your module(s) into a [Scope] for dependency resolution.
|
||||
///
|
||||
/// Modules make it easier to organize your DI configuration for features, layers,
|
||||
/// infrastructure, or integration, and support modular app architecture.
|
||||
///
|
||||
/// Usage example:
|
||||
/// ```dart
|
||||
/// class AppModule extends Module {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<NetworkService>().toProvide(() => NetworkService());
|
||||
/// bind<AuthService>().toProvide(() => AuthService(currentScope.resolve<NetworkService>()));
|
||||
/// bind<Config>().toInstance(Config.dev());
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Installing the module into the root DI scope:
|
||||
/// final rootScope = CherryPick.openRootScope();
|
||||
/// rootScope.installModules([AppModule()]);
|
||||
/// ```
|
||||
///
|
||||
/// Combine several modules and submodules to implement scalable architectures.
|
||||
///
|
||||
abstract class Module {
|
||||
final Set<Binding> _bindingSet = HashSet();
|
||||
|
||||
/// RU: Метод добавляет в коллекцию модуля [Binding] экземпляр.
|
||||
/// Begins the declaration of a new binding within this module.
|
||||
///
|
||||
/// ENG: The method adds an instance to the collection of the [Binding] module.
|
||||
/// Typically used within [builder] to register all needed dependency bindings.
|
||||
///
|
||||
/// return [Binding<T>]
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// bind<Api>().toProvide(() => MockApi());
|
||||
/// bind<Config>().toInstance(Config.dev());
|
||||
/// ```
|
||||
Binding<T> bind<T>() {
|
||||
final binding = Binding<T>();
|
||||
_bindingSet.add(binding);
|
||||
return binding;
|
||||
}
|
||||
|
||||
/// RU: Метод возвращает коллекцию [Binding] экземпляров.
|
||||
/// Returns the set of all [Binding] instances registered in this module.
|
||||
///
|
||||
/// ENG: The method returns a collection of [Binding] instances.
|
||||
///
|
||||
/// return [Set<Binding>]
|
||||
/// This is typically used internally by [Scope] during module installation,
|
||||
/// but can also be used for diagnostics or introspection.
|
||||
Set<Binding> get bindingSet => _bindingSet;
|
||||
|
||||
/// RU: Абстрактный метод для инициализации пользовательских экземпляров.
|
||||
/// В этом методе осуществляется конфигурация зависимостей.
|
||||
/// Abstract method where all dependency bindings are registered.
|
||||
///
|
||||
/// ENG: Abstract method for initializing custom instances.
|
||||
/// This method configures dependencies.
|
||||
/// Override this method in your custom module subclass to declare
|
||||
/// all dependency bindings to be provided by this module.
|
||||
///
|
||||
/// return [void]
|
||||
/// The provided [currentScope] can be used for resolving other dependencies,
|
||||
/// accessing configuration, or controlling binding behavior dynamically.
|
||||
///
|
||||
/// Example (with dependency chaining):
|
||||
/// ```dart
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<ApiClient>().toProvide(() => RestApi());
|
||||
/// bind<UserRepo>().toProvide(() => UserRepo(currentScope.resolve<ApiClient>()));
|
||||
/// }
|
||||
/// ```
|
||||
void builder(Scope currentScope);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ abstract class CherryPickObserver {
|
||||
/// ```dart
|
||||
/// observer.onInstanceCreated('MyService', MyService, instance, scopeName: 'root');
|
||||
/// ```
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName});
|
||||
void onInstanceCreated(String name, Type type, Object instance,
|
||||
{String? scopeName});
|
||||
|
||||
/// Called when an instance is disposed (removed from cache and/or finalized).
|
||||
///
|
||||
@@ -57,7 +58,8 @@ abstract class CherryPickObserver {
|
||||
/// ```dart
|
||||
/// observer.onInstanceDisposed('MyService', MyService, instance, scopeName: 'root');
|
||||
/// ```
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName});
|
||||
void onInstanceDisposed(String name, Type type, Object instance,
|
||||
{String? scopeName});
|
||||
|
||||
// === Module events ===
|
||||
/// Called when modules are installed into the container.
|
||||
@@ -157,19 +159,23 @@ class PrintCherryPickObserver implements CherryPickObserver {
|
||||
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)');
|
||||
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)');
|
||||
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)');
|
||||
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)');
|
||||
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');
|
||||
@@ -178,8 +184,8 @@ class PrintCherryPickObserver implements CherryPickObserver {
|
||||
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)' : ''}');
|
||||
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}) =>
|
||||
@@ -210,9 +216,11 @@ class SilentCherryPickObserver implements CherryPickObserver {
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
void onInstanceCreated(String name, Type type, Object instance,
|
||||
{String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
void onInstanceDisposed(String name, Type type, Object instance,
|
||||
{String? scopeName}) {}
|
||||
@override
|
||||
void onModulesInstalled(List<String> modules, {String? scopeName}) {}
|
||||
@override
|
||||
|
||||
@@ -21,6 +21,37 @@ import 'package:cherrypick/src/module.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
// import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
/// Represents a DI scope (container) for modules, subscopes,
|
||||
/// and dependency resolution (sync/async) in CherryPick.
|
||||
///
|
||||
/// Scopes provide hierarchical DI: you can resolve dependencies from parents,
|
||||
/// override or isolate modules, and manage scope-specific singletons.
|
||||
///
|
||||
/// Each scope:
|
||||
/// - Can install modules ([installModules]) that define [Binding]s
|
||||
/// - Supports parent-child scope tree (see [openSubScope])
|
||||
/// - Can resolve dependencies synchronously ([resolve]) or asynchronously ([resolveAsync])
|
||||
/// - Cleans up resources for [Disposable] objects (see [dispose])
|
||||
/// - Detects dependency cycles (local and global, if enabled)
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final rootScope = CherryPick.openRootScope();
|
||||
/// rootScope.installModules([AppModule()]);
|
||||
///
|
||||
/// // Synchronous resolution:
|
||||
/// final auth = rootScope.resolve<AuthService>();
|
||||
///
|
||||
/// // Asynchronous resolution:
|
||||
/// final db = await rootScope.resolveAsync<Database>();
|
||||
///
|
||||
/// // Open a child scope (for a feature, page, or test):
|
||||
/// final userScope = rootScope.openSubScope('user');
|
||||
/// userScope.installModules([UserModule()]);
|
||||
///
|
||||
/// // Proper resource cleanup (calls dispose() on tracked objects)
|
||||
/// await CherryPick.closeRootScope();
|
||||
/// ```
|
||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
final Scope? _parentScope;
|
||||
|
||||
@@ -32,16 +63,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
|
||||
/// RU: Метод возвращает родительский [Scope].
|
||||
///
|
||||
/// ENG: The method returns the parent [Scope].
|
||||
///
|
||||
/// return [Scope]
|
||||
/// Returns the parent [Scope] if present, or null if this is the root scope.
|
||||
Scope? get parentScope => _parentScope;
|
||||
|
||||
final Map<String, Scope> _scopeMap = HashMap();
|
||||
|
||||
Scope(this._parentScope, {required CherryPickObserver observer}) : _observer = observer {
|
||||
Scope(this._parentScope, {required CherryPickObserver observer})
|
||||
: _observer = observer {
|
||||
setScopeId(_generateScopeId());
|
||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||
observer.onDiagnostic(
|
||||
@@ -60,9 +88,9 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
// индекс для мгновенного поиска binding’ов
|
||||
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
||||
|
||||
|
||||
/// RU: Генерирует уникальный идентификатор для скоупа.
|
||||
/// ENG: Generates unique identifier for scope.
|
||||
/// Generates a unique identifier string for this scope instance.
|
||||
///
|
||||
/// Used internally for diagnostics, logging and global scope tracking.
|
||||
String _generateScopeId() {
|
||||
final random = Random();
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
@@ -70,16 +98,20 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return 'scope_${timestamp}_$randomPart';
|
||||
}
|
||||
|
||||
/// RU: Метод открывает дочерний (дополнительный) [Scope].
|
||||
/// Opens a named child [Scope] (subscope) as a descendant of the current scope.
|
||||
///
|
||||
/// ENG: The method opens child (additional) [Scope].
|
||||
/// Subscopes inherit modules and DI context from their parent, but can override or extend bindings.
|
||||
/// Useful for feature-isolation, screens, request/transaction lifetimes, and test separation.
|
||||
///
|
||||
/// return [Scope]
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final featureScope = rootScope.openSubScope('feature');
|
||||
/// featureScope.installModules([FeatureModule()]);
|
||||
/// final dep = featureScope.resolve<MyDep>();
|
||||
/// ```
|
||||
Scope openSubScope(String name) {
|
||||
if (!_scopeMap.containsKey(name)) {
|
||||
final childScope = Scope(this, observer: observer); // Наследуем observer вниз по иерархии
|
||||
// print removed (trace)
|
||||
// Наследуем настройки обнаружения циклических зависимостей
|
||||
final childScope = Scope(this, observer: observer);
|
||||
if (isCycleDetectionEnabled) {
|
||||
childScope.enableCycleDetection();
|
||||
}
|
||||
@@ -101,16 +133,19 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
|
||||
/// RU: Метод закрывает дочерний (дополнительный) [Scope] асинхронно.
|
||||
/// Asynchronously closes and disposes a named child [Scope] (subscope).
|
||||
///
|
||||
/// ENG: The method closes child (additional) [Scope] asynchronously.
|
||||
/// Ensures all [Disposable] objects and internal modules
|
||||
/// in the subscope are properly cleaned up. Also removes any global cycle detectors associated with the subscope.
|
||||
///
|
||||
/// return [Future<void>]
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await rootScope.closeSubScope('feature');
|
||||
/// ```
|
||||
Future<void> closeSubScope(String name) async {
|
||||
final childScope = _scopeMap[name];
|
||||
if (childScope != null) {
|
||||
await childScope.dispose(); // асинхронный вызов
|
||||
// Очищаем детектор для дочернего скоупа
|
||||
await childScope.dispose();
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
@@ -129,11 +164,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
|
||||
/// RU: Метод инициализирует пользовательские модули в [Scope].
|
||||
/// Installs a list of custom [Module]s into the [Scope].
|
||||
///
|
||||
/// ENG: The method initializes custom modules in [Scope].
|
||||
/// Each module registers bindings and configuration for dependencies.
|
||||
/// After calling this, bindings are immediately available for resolve/tryResolve.
|
||||
///
|
||||
/// return [Scope]
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// rootScope.installModules([AppModule(), NetworkModule()]);
|
||||
/// ```
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
if (modules.isNotEmpty) {
|
||||
@@ -153,7 +192,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
},
|
||||
);
|
||||
module.builder(this);
|
||||
// После builder: для всех новых биндингов
|
||||
// Associate bindings with this scope's observer
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.observer = observer;
|
||||
binding.logAllDeferred();
|
||||
@@ -163,11 +202,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return this;
|
||||
}
|
||||
|
||||
/// RU: Метод удаляет пользовательские модули из [Scope].
|
||||
/// Removes all installed [Module]s and their bindings from this [Scope].
|
||||
///
|
||||
/// ENG: This method removes custom modules from [Scope].
|
||||
/// Typically used in tests or when resetting app configuration/runtime environment.
|
||||
/// Note: this does not dispose resolved [Disposable]s (call [dispose] for that).
|
||||
///
|
||||
/// return [Scope]
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// testScope.dropModules();
|
||||
/// ```
|
||||
Scope dropModules() {
|
||||
if (_modulesList.isNotEmpty) {
|
||||
observer.onModulesRemoved(
|
||||
@@ -188,24 +231,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return this;
|
||||
}
|
||||
|
||||
/// RU: Возвращает найденную зависимость, определенную параметром типа [T].
|
||||
/// Выдает [StateError], если зависимость не может быть разрешена.
|
||||
/// Если вы хотите получить [null], если зависимость не может быть найдена,
|
||||
/// то используйте вместо этого [tryResolve]
|
||||
/// return - возвращает объект типа [T] или [StateError]
|
||||
/// Resolves a dependency of type [T], optionally by name and with params.
|
||||
///
|
||||
/// ENG: Returns the found dependency specified by the type parameter [T].
|
||||
/// Throws [StateError] if the dependency cannot be resolved.
|
||||
/// If you want to get [null] if the dependency cannot be found then use [tryResolve] instead
|
||||
/// return - returns an object of type [T] or [StateError]
|
||||
/// Throws [StateError] if the dependency cannot be resolved. (Use [tryResolve] for fallible lookup).
|
||||
/// Resolves from installed modules or recurses up the parent scope chain.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final logger = scope.resolve<Logger>();
|
||||
/// final special = scope.resolve<Service>(named: 'special');
|
||||
/// ```
|
||||
T resolve<T>({String? named, dynamic params}) {
|
||||
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
try {
|
||||
result = withGlobalCycleDetection<T>(T, named, () {
|
||||
result = withGlobalCycleDetection<T>(T, named, () {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
@@ -232,13 +273,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Разрешение с локальным детектором циклических зависимостей.
|
||||
/// ENG: Resolution with local circular dependency detector.
|
||||
/// Resolves [T] using the local cycle detector for this scope.
|
||||
/// Throws [StateError] if not found or cycle is detected.
|
||||
/// Used internally by [resolve].
|
||||
T _resolveWithLocalDetection<T>({String? named, dynamic params}) {
|
||||
return withCycleDetection<T>(T, named, () {
|
||||
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
|
||||
observer.onInstanceCreated(T.toString(), T, resolved,
|
||||
scopeName: scopeId);
|
||||
observer.onDiagnostic(
|
||||
'Successfully resolved: $T',
|
||||
details: {
|
||||
@@ -262,11 +305,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
});
|
||||
}
|
||||
|
||||
/// RU: Возвращает найденную зависимость типа [T] или null, если она не может быть найдена.
|
||||
/// ENG: Returns found dependency of type [T] or null if it cannot be found.
|
||||
/// Attempts to resolve a dependency of type [T], optionally by name and with params.
|
||||
///
|
||||
/// Returns the resolved dependency, or `null` if not found.
|
||||
/// Does not throw if missing (unlike [resolve]).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final maybeDb = scope.tryResolve<Database>();
|
||||
/// ```
|
||||
T? tryResolve<T>({String? named, dynamic params}) {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = withGlobalCycleDetection<T?>(T, named, () {
|
||||
@@ -279,8 +327,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Попытка разрешения с локальным детектором циклических зависимостей.
|
||||
/// ENG: Try resolution with local circular dependency detector.
|
||||
/// Attempts to resolve [T] using the local cycle detector. Returns null if not found or cycle.
|
||||
/// Used internally by [tryResolve].
|
||||
T? _tryResolveWithLocalDetection<T>({String? named, dynamic params}) {
|
||||
if (isCycleDetectionEnabled) {
|
||||
return withCycleDetection<T?>(T, named, () {
|
||||
@@ -291,48 +339,49 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Внутренний метод для разрешения зависимостей без проверки циклических зависимостей.
|
||||
/// ENG: Internal method for dependency resolution without circular dependency checking.
|
||||
/// Locates and resolves [T] without cycle detection (direct lookup).
|
||||
/// Returns null if not found. Used internally.
|
||||
T? _tryResolveInternal<T>({String? named, dynamic params}) {
|
||||
final resolver = _findBindingResolver<T>(named);
|
||||
|
||||
// 1 Поиск зависимости по всем модулям текущего скоупа
|
||||
// 1 - Try from own modules; 2 - Fallback to parent
|
||||
return resolver?.resolveSync(params) ??
|
||||
// 2 Поиск зависимостей в родительском скоупе
|
||||
_parentScope?.tryResolve(named: named, params: params);
|
||||
}
|
||||
|
||||
/// RU: Асинхронно возвращает найденную зависимость, определенную параметром типа [T].
|
||||
/// Выдает [StateError], если зависимость не может быть разрешена.
|
||||
/// Если хотите получить [null], если зависимость не может быть найдена, используйте [tryResolveAsync].
|
||||
/// return - возвращает объект типа [T] or [StateError]
|
||||
/// Asynchronously resolves a dependency of type [T].
|
||||
///
|
||||
/// ENG: Asynchronously returns the found dependency specified by the type parameter [T].
|
||||
/// Throws [StateError] if the dependency cannot be resolved.
|
||||
/// If you want to get [null] if the dependency cannot be found, use [tryResolveAsync] instead.
|
||||
/// return - returns an object of type [T] or [StateError]
|
||||
/// Throws [StateError] if not found. (Use [tryResolveAsync] for a fallible async resolve.)
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final db = await scope.resolveAsync<Database>();
|
||||
/// final special = await scope.resolveAsync<Service>(named: "special");
|
||||
/// ```
|
||||
Future<T> resolveAsync<T>({String? named, dynamic params}) async {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||
return await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
return await _resolveAsyncWithLocalDetection<T>(
|
||||
named: named, params: params);
|
||||
});
|
||||
} else {
|
||||
result = await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
result = await _resolveAsyncWithLocalDetection<T>(
|
||||
named: named, params: params);
|
||||
}
|
||||
_trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Асинхронное разрешение с локальным детектором циклических зависимостей.
|
||||
/// ENG: Async resolution with local circular dependency detector.
|
||||
Future<T> _resolveAsyncWithLocalDetection<T>({String? named, dynamic params}) async {
|
||||
/// Resolves [T] asynchronously using local cycle detector. Throws if not found.
|
||||
/// Internal implementation for async [resolveAsync].
|
||||
Future<T> _resolveAsyncWithLocalDetection<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
return withCycleDetection<Future<T>>(T, named, () async {
|
||||
var resolved = await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
var resolved =
|
||||
await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
|
||||
observer.onInstanceCreated(T.toString(), T, resolved,
|
||||
scopeName: scopeId);
|
||||
observer.onDiagnostic(
|
||||
'Successfully async resolved: $T',
|
||||
details: {
|
||||
@@ -356,23 +405,32 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempts to asynchronously resolve a dependency of type [T].
|
||||
/// Returns the dependency or null if not present (never throws).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final user = await scope.tryResolveAsync<User>();
|
||||
/// ```
|
||||
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
||||
return await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
return await _tryResolveAsyncWithLocalDetection<T>(
|
||||
named: named, params: params);
|
||||
});
|
||||
} else {
|
||||
result = await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
result = await _tryResolveAsyncWithLocalDetection<T>(
|
||||
named: named, params: params);
|
||||
}
|
||||
if (result != null) _trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Асинхронная попытка разрешения с локальным детектором циклических зависимостей.
|
||||
/// ENG: Async try resolution with local circular dependency detector.
|
||||
Future<T?> _tryResolveAsyncWithLocalDetection<T>({String? named, dynamic params}) async {
|
||||
/// Attempts to resolve [T] asynchronously using local cycle detector. Returns null if missing.
|
||||
/// Internal implementation for async [tryResolveAsync].
|
||||
Future<T?> _tryResolveAsyncWithLocalDetection<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
if (isCycleDetectionEnabled) {
|
||||
return withCycleDetection<Future<T?>>(T, named, () async {
|
||||
return await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||
@@ -382,21 +440,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// RU: Внутренний метод для асинхронного разрешения зависимостей без проверки циклических зависимостей.
|
||||
/// ENG: Internal method for async dependency resolution without circular dependency checking.
|
||||
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) async {
|
||||
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
|
||||
Future<T?> _tryResolveAsyncInternal<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
final resolver = _findBindingResolver<T>(named);
|
||||
|
||||
// 1 Поиск зависимости по всем модулям текущего скоупа
|
||||
// 1 - Try from own modules; 2 - Fallback to parent
|
||||
return resolver?.resolveAsync(params) ??
|
||||
// 2 Поиск зависимостей в родительском скоупе
|
||||
_parentScope?.tryResolveAsync(named: named, params: params);
|
||||
}
|
||||
|
||||
/// Looks up the [BindingResolver] for [T] and [named] within this scope.
|
||||
/// Returns null if none found. Internal use only.
|
||||
BindingResolver<T>? _findBindingResolver<T>(String? named) =>
|
||||
_bindingResolvers[T]?[named] as BindingResolver<T>?;
|
||||
|
||||
// Индексируем все binding’и после каждого installModules/dropModules
|
||||
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
||||
/// Called after [installModules] and [dropModules]. Internal use only.
|
||||
void _rebuildResolversIndex() {
|
||||
_bindingResolvers.clear();
|
||||
for (var module in _modulesList) {
|
||||
@@ -408,14 +467,24 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// INTERNAL: Tracks Disposable objects
|
||||
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
|
||||
/// Internal use only.
|
||||
void _trackDisposable(Object? obj) {
|
||||
if (obj is Disposable && !_disposables.contains(obj)) {
|
||||
_disposables.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls dispose on all tracked disposables and child scopes recursively (async).
|
||||
/// Asynchronously disposes this [Scope], all tracked [Disposable] objects, and recursively
|
||||
/// all its child subscopes.
|
||||
///
|
||||
/// This method should always be called when a scope is no longer needed
|
||||
/// to guarantee timely resource cleanup (files, sockets, streams, handles, etc).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await myScope.dispose();
|
||||
/// ```
|
||||
Future<void> dispose() async {
|
||||
// First dispose children scopes
|
||||
for (final subScope in _scopeMap.values) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: cherrypick
|
||||
description: Cherrypick is a small dependency injection (DI) library for dart/flutter projects.
|
||||
version: 3.0.0-dev.8
|
||||
homepage: https://pese-git.github.io/cherrypick-site/
|
||||
documentation: https://github.com/pese-git/cherrypick/wiki
|
||||
version: 3.0.0-dev.10
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
issue_tracker: https://github.com/pese-git/cherrypick/issues
|
||||
topics:
|
||||
|
||||
@@ -12,6 +12,7 @@ class DummyModule extends Module {
|
||||
}
|
||||
|
||||
class A {}
|
||||
|
||||
class B {}
|
||||
|
||||
class CyclicModule extends Module {
|
||||
@@ -52,10 +53,13 @@ void main() {
|
||||
throwsA(isA<CircularDependencyException>()),
|
||||
);
|
||||
// Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
|
||||
final foundInErrors = observer.errors.any((m) => m.contains('cycle detected'));
|
||||
final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected'));
|
||||
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}');
|
||||
reason:
|
||||
'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
|
||||
});
|
||||
}
|
||||
@@ -15,9 +15,8 @@ class MockObserver implements CherryPickObserver {
|
||||
void onWarning(String message, {Object? details}) => warnings.add(message);
|
||||
|
||||
@override
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) =>
|
||||
errors.add(
|
||||
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) => errors.add(
|
||||
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
|
||||
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) =>
|
||||
@@ -30,9 +29,11 @@ class MockObserver implements CherryPickObserver {
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
void onInstanceCreated(String name, Type type, Object instance,
|
||||
{String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
void onInstanceDisposed(String name, Type type, Object instance,
|
||||
{String? scopeName}) {}
|
||||
@override
|
||||
void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {}
|
||||
@override
|
||||
|
||||
@@ -46,7 +46,9 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('current implementation limitation - may not detect cross-scope cycles', () {
|
||||
test(
|
||||
'current implementation limitation - may not detect cross-scope cycles',
|
||||
() {
|
||||
// Этот тест демонстрирует ограничение текущей реализации
|
||||
final parentScope = CherryPick.openRootScope();
|
||||
parentScope.enableCycleDetection();
|
||||
|
||||
@@ -50,10 +50,9 @@ void main() {
|
||||
expect(
|
||||
() => detector.startResolving<String>(),
|
||||
throwsA(predicate((e) =>
|
||||
e is CircularDependencyException &&
|
||||
e.dependencyChain.contains('String') &&
|
||||
e.dependencyChain.length > 1
|
||||
)),
|
||||
e is CircularDependencyException &&
|
||||
e.dependencyChain.contains('String') &&
|
||||
e.dependencyChain.length > 1)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -161,14 +160,16 @@ class ServiceB {
|
||||
class CircularModuleA extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<ServiceA>().toProvide(() => ServiceA(currentScope.resolve<ServiceB>()));
|
||||
bind<ServiceA>()
|
||||
.toProvide(() => ServiceA(currentScope.resolve<ServiceB>()));
|
||||
}
|
||||
}
|
||||
|
||||
class CircularModuleB extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<ServiceB>().toProvide(() => ServiceB(currentScope.resolve<ServiceA>()));
|
||||
bind<ServiceB>()
|
||||
.toProvide(() => ServiceB(currentScope.resolve<ServiceA>()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ void main() {
|
||||
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isFalse);
|
||||
});
|
||||
|
||||
test('should automatically enable global cycle detection for new root scope', () {
|
||||
test(
|
||||
'should automatically enable global cycle detection for new root scope',
|
||||
() {
|
||||
CherryPick.enableGlobalCrossScopeCycleDetection();
|
||||
|
||||
final scope = CherryPick.openRootScope();
|
||||
@@ -45,7 +47,9 @@ void main() {
|
||||
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('should automatically enable global cycle detection for existing root scope', () {
|
||||
test(
|
||||
'should automatically enable global cycle detection for existing root scope',
|
||||
() {
|
||||
final scope = CherryPick.openRootScope();
|
||||
expect(scope.isGlobalCycleDetectionEnabled, isFalse);
|
||||
|
||||
@@ -56,15 +60,18 @@ void main() {
|
||||
});
|
||||
|
||||
group('Global Safe Scope Creation', () {
|
||||
test('should create global safe root scope with both detections enabled', () {
|
||||
test('should create global safe root scope with both detections enabled',
|
||||
() {
|
||||
final scope = CherryPick.openGlobalSafeRootScope();
|
||||
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('should create global safe sub-scope with both detections enabled', () {
|
||||
final scope = CherryPick.openGlobalSafeScope(scopeName: 'feature.global');
|
||||
test('should create global safe sub-scope with both detections enabled',
|
||||
() {
|
||||
final scope =
|
||||
CherryPick.openGlobalSafeScope(scopeName: 'feature.global');
|
||||
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
|
||||
|
||||
@@ -39,7 +39,9 @@ void main() {
|
||||
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
|
||||
});
|
||||
|
||||
test('should automatically enable cycle detection for new root scope when global is enabled', () {
|
||||
test(
|
||||
'should automatically enable cycle detection for new root scope when global is enabled',
|
||||
() {
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
|
||||
final scope = CherryPick.openRootScope();
|
||||
@@ -47,7 +49,9 @@ void main() {
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('should automatically enable cycle detection for existing root scope when global is enabled', () {
|
||||
test(
|
||||
'should automatically enable cycle detection for existing root scope when global is enabled',
|
||||
() {
|
||||
final scope = CherryPick.openRootScope();
|
||||
expect(scope.isCycleDetectionEnabled, isFalse);
|
||||
|
||||
@@ -56,7 +60,9 @@ void main() {
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('should automatically disable cycle detection for existing root scope when global is disabled', () {
|
||||
test(
|
||||
'should automatically disable cycle detection for existing root scope when global is disabled',
|
||||
() {
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
final scope = CherryPick.openRootScope();
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
@@ -99,21 +105,25 @@ void main() {
|
||||
final scopeName = 'feature.auth';
|
||||
CherryPick.openScope(scopeName: scopeName);
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
|
||||
isFalse);
|
||||
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
|
||||
isTrue);
|
||||
});
|
||||
|
||||
test('should disable cycle detection for specific scope', () {
|
||||
final scopeName = 'feature.auth';
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
|
||||
isTrue);
|
||||
|
||||
CherryPick.disableCycleDetectionForScope(scopeName: scopeName);
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
|
||||
isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,14 +144,17 @@ void main() {
|
||||
// Глобальная настройка отключена
|
||||
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
|
||||
|
||||
final scope = CherryPick.openSafeScope(scopeName: 'feature.independent');
|
||||
final scope =
|
||||
CherryPick.openSafeScope(scopeName: 'feature.independent');
|
||||
|
||||
expect(scope.isCycleDetectionEnabled, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Resolution Chain Tracking', () {
|
||||
test('should return empty resolution chain for scope without cycle detection', () {
|
||||
test(
|
||||
'should return empty resolution chain for scope without cycle detection',
|
||||
() {
|
||||
CherryPick.openRootScope();
|
||||
|
||||
final chain = CherryPick.getCurrentResolutionChain();
|
||||
@@ -149,7 +162,9 @@ void main() {
|
||||
expect(chain, isEmpty);
|
||||
});
|
||||
|
||||
test('should return empty resolution chain for scope with cycle detection but no active resolution', () {
|
||||
test(
|
||||
'should return empty resolution chain for scope with cycle detection but no active resolution',
|
||||
() {
|
||||
CherryPick.enableCycleDetectionForScope();
|
||||
|
||||
final chain = CherryPick.getCurrentResolutionChain();
|
||||
@@ -161,14 +176,17 @@ void main() {
|
||||
final scopeName = 'feature.tracking';
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
|
||||
|
||||
final chain = CherryPick.getCurrentResolutionChain(scopeName: scopeName);
|
||||
final chain =
|
||||
CherryPick.getCurrentResolutionChain(scopeName: scopeName);
|
||||
|
||||
expect(chain, isEmpty); // Пустая, так как нет активного разрешения
|
||||
});
|
||||
});
|
||||
|
||||
group('Integration with Circular Dependencies', () {
|
||||
test('should detect circular dependency with global cycle detection enabled', () {
|
||||
test(
|
||||
'should detect circular dependency with global cycle detection enabled',
|
||||
() {
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
|
||||
final scope = CherryPick.openRootScope();
|
||||
@@ -190,14 +208,16 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('should not detect circular dependency when cycle detection is disabled', () {
|
||||
test(
|
||||
'should not detect circular dependency when cycle detection is disabled',
|
||||
() {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([CircularTestModule()]);
|
||||
|
||||
// Без обнаружения циклических зависимостей не будет выброшено CircularDependencyException,
|
||||
// но может произойти StackOverflowError при попытке создания объекта
|
||||
expect(() => scope.resolve<CircularServiceA>(),
|
||||
throwsA(isA<StackOverflowError>()));
|
||||
throwsA(isA<StackOverflowError>()));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,7 +225,8 @@ void main() {
|
||||
test('should handle empty scope name as root scope', () {
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: '');
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue);
|
||||
expect(
|
||||
CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue);
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue);
|
||||
});
|
||||
|
||||
@@ -213,14 +234,21 @@ void main() {
|
||||
final complexScopeName = 'app.feature.auth.login';
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: complexScopeName);
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: complexScopeName), isTrue);
|
||||
expect(
|
||||
CherryPick.isCycleDetectionEnabledForScope(
|
||||
scopeName: complexScopeName),
|
||||
isTrue);
|
||||
});
|
||||
|
||||
test('should handle custom separator', () {
|
||||
final scopeName = 'app/feature/auth';
|
||||
CherryPick.enableCycleDetectionForScope(scopeName: scopeName, separator: '/');
|
||||
CherryPick.enableCycleDetectionForScope(
|
||||
scopeName: scopeName, separator: '/');
|
||||
|
||||
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName, separator: '/'), isTrue);
|
||||
expect(
|
||||
CherryPick.isCycleDetectionEnabledForScope(
|
||||
scopeName: scopeName, separator: '/'),
|
||||
isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -240,7 +268,9 @@ class CircularServiceB {
|
||||
class CircularTestModule extends Module {
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
bind<CircularServiceA>().toProvide(() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
|
||||
bind<CircularServiceB>().toProvide(() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
|
||||
bind<CircularServiceA>().toProvide(
|
||||
() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
|
||||
bind<CircularServiceB>().toProvide(
|
||||
() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:cherrypick/cherrypick.dart' show Disposable, Module, Scope, CherryPick;
|
||||
import 'package:cherrypick/cherrypick.dart'
|
||||
show Disposable, Module, Scope, CherryPick;
|
||||
import 'dart:async';
|
||||
import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
@@ -18,7 +19,9 @@ class AsyncExampleDisposable implements Disposable {
|
||||
class AsyncExampleModule extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<AsyncExampleDisposable>().toProvide(() => AsyncExampleDisposable()).singleton();
|
||||
bind<AsyncExampleDisposable>()
|
||||
.toProvide(() => AsyncExampleDisposable())
|
||||
.singleton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +52,9 @@ class CountingDisposable implements Disposable {
|
||||
class ModuleCountingDisposable extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<CountingDisposable>().toProvide(() => CountingDisposable()).singleton();
|
||||
bind<CountingDisposable>()
|
||||
.toProvide(() => CountingDisposable())
|
||||
.singleton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +102,9 @@ class AsyncModule extends Module {
|
||||
bind<AsyncCreatedDisposable>()
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
.toProvideAsync(() async {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
return AsyncCreatedDisposable();
|
||||
})
|
||||
.singleton();
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
return AsyncCreatedDisposable();
|
||||
}).singleton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +123,8 @@ void main() {
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
|
||||
});
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () async {
|
||||
test('closeSubScope removes subscope so next openSubScope returns new',
|
||||
() async {
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
final subScope = scope.openSubScope("child");
|
||||
@@ -181,7 +186,8 @@ void main() {
|
||||
});
|
||||
test("After dropModules resolves fail", () {
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)..installModules([TestModule<int>(value: 5)]);
|
||||
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>()));
|
||||
@@ -294,7 +300,8 @@ void main() {
|
||||
await scope.dispose();
|
||||
expect(t.disposed, isTrue);
|
||||
});
|
||||
test('scope.disposeAsync calls dispose on all unique disposables', () async {
|
||||
test('scope.disposeAsync calls dispose on all unique disposables',
|
||||
() async {
|
||||
final scope = Scope(null, observer: MockObserver());
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t1 = scope.resolve<TestDisposable>();
|
||||
@@ -305,7 +312,8 @@ void main() {
|
||||
expect(t1.disposed, isTrue);
|
||||
expect(t2.disposed, isTrue);
|
||||
});
|
||||
test('calling disposeAsync twice does not throw and not call twice', () async {
|
||||
test('calling disposeAsync twice does not throw and not call twice',
|
||||
() async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t = scope.resolve<TestDisposable>();
|
||||
@@ -313,7 +321,8 @@ void main() {
|
||||
await scope.dispose();
|
||||
expect(t.disposed, isTrue);
|
||||
});
|
||||
test('Non-disposable dependency is ignored by scope.disposeAsync', () async {
|
||||
test('Non-disposable dependency is ignored by scope.disposeAsync',
|
||||
() async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final s = scope.resolve<String>();
|
||||
@@ -327,7 +336,8 @@ void main() {
|
||||
group('Scope/subScope dispose edge cases', () {
|
||||
test('Dispose called in closed subScope only', () async {
|
||||
final root = CherryPick.openRootScope();
|
||||
final sub = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
|
||||
final sub = root.openSubScope('feature')
|
||||
..installModules([ModuleCountingDisposable()]);
|
||||
final d = sub.resolve<CountingDisposable>();
|
||||
expect(d.disposeCount, 0);
|
||||
|
||||
@@ -339,7 +349,8 @@ void main() {
|
||||
expect(d.disposeCount, 1);
|
||||
|
||||
// Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова)
|
||||
final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
|
||||
final sub2 = root.openSubScope('feature')
|
||||
..installModules([ModuleCountingDisposable()]);
|
||||
final d2 = sub2.resolve<CountingDisposable>();
|
||||
expect(identical(d, d2), isFalse);
|
||||
await root.closeSubScope('feature');
|
||||
@@ -347,8 +358,14 @@ void main() {
|
||||
});
|
||||
test('Dispose for all nested subScopes on root disposeAsync', () async {
|
||||
final root = CherryPick.openRootScope();
|
||||
root.openSubScope('a').openSubScope('b').installModules([ModuleCountingDisposable()]);
|
||||
final d = root.openSubScope('a').openSubScope('b').resolve<CountingDisposable>();
|
||||
root
|
||||
.openSubScope('a')
|
||||
.openSubScope('b')
|
||||
.installModules([ModuleCountingDisposable()]);
|
||||
final d = root
|
||||
.openSubScope('a')
|
||||
.openSubScope('b')
|
||||
.resolve<CountingDisposable>();
|
||||
await root.dispose();
|
||||
expect(d.disposeCount, 1);
|
||||
});
|
||||
@@ -357,7 +374,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Async disposable (Future test)', () {
|
||||
test('Async Disposable is awaited on disposeAsync', () async {
|
||||
final scope = CherryPick.openRootScope()..installModules([AsyncExampleModule()]);
|
||||
final scope = CherryPick.openRootScope()
|
||||
..installModules([AsyncExampleModule()]);
|
||||
final d = scope.resolve<AsyncExampleDisposable>();
|
||||
expect(d.disposed, false);
|
||||
await scope.dispose();
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## 1.1.2-dev.1
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
## 1.1.2-dev.0
|
||||
|
||||
- **DOCS**(annotations): unify and improve English DartDoc for all DI annotations.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- **FIX**(license): correct urls.
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
analyzer:
|
||||
errors:
|
||||
camel_case_types: ignore
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
|
||||
@@ -13,22 +13,30 @@
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Annotation for field injection in CherryPick DI framework.
|
||||
/// Apply this to a field, and the code generator will automatically inject
|
||||
/// the appropriate dependency into it.
|
||||
/// Marks a field for dependency injection by CherryPick's code generator.
|
||||
///
|
||||
/// ---
|
||||
/// Use `@inject()` on fields within a class marked with `@injectable()`.
|
||||
/// Such fields will be automatically injected from the DI [Scope]
|
||||
/// when using the generated mixin or calling `.injectFields()`.
|
||||
///
|
||||
/// Аннотация для внедрения зависимости в поле через фреймворк CherryPick DI.
|
||||
/// Поместите её на поле класса — генератор кода автоматически подставит нужную зависимость.
|
||||
///
|
||||
/// Example / Пример:
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// @inject()
|
||||
/// late final SomeService service;
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @injectable()
|
||||
/// class LoginScreen with _\$LoginScreen {
|
||||
/// @inject()
|
||||
/// late final AuthService authService;
|
||||
///
|
||||
/// @inject()
|
||||
/// @named('main')
|
||||
/// late final ApiClient api;
|
||||
/// }
|
||||
///
|
||||
/// // After running build_runner, call:
|
||||
/// // LoginScreen().injectFields();
|
||||
/// ```
|
||||
@experimental
|
||||
// ignore: camel_case_types
|
||||
final class inject {
|
||||
const inject();
|
||||
}
|
||||
|
||||
@@ -13,26 +13,31 @@
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks a class as injectable for the CherryPick dependency injection framework.
|
||||
/// If a class is annotated with [@injectable()], the code generator will
|
||||
/// create a mixin to perform automatic injection of fields marked with [@inject].
|
||||
/// Marks a class as injectable, enabling automatic field injection by CherryPick's code generator.
|
||||
///
|
||||
/// ---
|
||||
/// Use `@injectable()` on a class whose fields (marked with `@inject`) you want to be automatically injected from the DI [Scope].
|
||||
/// When used together with code generation (see cherrypick_generator), a mixin will be generated to inject fields.
|
||||
///
|
||||
/// Помечает класс как внедряемый для фреймворка внедрения зависимостей CherryPick.
|
||||
/// Если класс помечен аннотацией [@injectable()], генератор создаст миксин
|
||||
/// для автоматического внедрения полей, отмеченных [@inject].
|
||||
///
|
||||
/// Example / Пример:
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @injectable()
|
||||
/// class MyWidget extends StatelessWidget {
|
||||
/// class ProfileScreen with _\$ProfileScreen {
|
||||
/// @inject()
|
||||
/// late final MyService service;
|
||||
/// late final UserManager manager;
|
||||
///
|
||||
/// @inject()
|
||||
/// @named('main')
|
||||
/// late final ApiClient api;
|
||||
/// }
|
||||
///
|
||||
/// // After running build_runner, call:
|
||||
/// // profileScreen.injectFields();
|
||||
/// ```
|
||||
///
|
||||
/// After running the generator, the mixin (`_\$ProfileScreen`) will be available to help auto-inject all [@inject] fields in your widget/service/controller.
|
||||
@experimental
|
||||
// ignore: camel_case_types
|
||||
final class injectable {
|
||||
const injectable();
|
||||
}
|
||||
|
||||
@@ -11,58 +11,39 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation to specify that a new instance should be provided on each request.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks a provider method or class to always create a new instance (factory) in CherryPick DI.
|
||||
///
|
||||
/// Use the `@instance()` annotation for methods or classes in your DI module
|
||||
/// to declare that the DI container must create a new object every time
|
||||
/// the dependency is injected (i.e., no singleton behavior).
|
||||
/// Use `@instance()` to annotate methods or classes in your DI module/class
|
||||
/// when you want a new object to be created on every injection (no singleton caching).
|
||||
/// The default DI lifecycle is instance/factory unless otherwise specified.
|
||||
///
|
||||
/// Example:
|
||||
/// ### Example (in a module method)
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// abstract class FeatureModule {
|
||||
/// @instance()
|
||||
/// Foo foo() => Foo();
|
||||
/// MyService provideService() => MyService();
|
||||
///
|
||||
/// @singleton()
|
||||
/// Logger provideLogger() => Logger();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will generate:
|
||||
/// ### Example (on a class, with @injectable)
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Foo>().toInstance(() => foo());
|
||||
/// }
|
||||
/// @injectable()
|
||||
/// @instance()
|
||||
/// class MyFactoryClass {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для создания нового экземпляра при каждом запросе.
|
||||
///
|
||||
/// Используйте `@instance()` для методов или классов в DI-модуле,
|
||||
/// чтобы указать, что контейнер внедрения зависимостей должен создавать
|
||||
/// новый объект при каждом обращении к зависимости (то есть, не синглтон).
|
||||
///
|
||||
/// Пример:
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// @instance()
|
||||
/// Foo foo() => Foo();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерирован следующий код:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Foo>().toInstance(() => foo());
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
/// See also: [@singleton]
|
||||
@experimental
|
||||
final class instance {
|
||||
const instance();
|
||||
}
|
||||
|
||||
@@ -11,59 +11,40 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation for marking Dart classes or libraries as modules.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks an abstract Dart class as a dependency injection module for CherryPick code generation.
|
||||
///
|
||||
/// Use the `@module()` annotation on abstract classes (or on a library)
|
||||
/// to indicate that the class represents a DI (Dependency Injection) module.
|
||||
/// This is commonly used in code generation tools to automatically register
|
||||
/// and configure dependencies defined within the module.
|
||||
/// Use `@module()` on your abstract class to indicate it provides DI bindings (via provider methods).
|
||||
/// This enables code generation of a concrete module that registers all bindings from your methods.
|
||||
///
|
||||
/// Example:
|
||||
/// Typical usage:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// // Dependency definitions go here.
|
||||
/// abstract class AppModule {
|
||||
/// @singleton()
|
||||
/// Logger provideLogger() => Logger();
|
||||
///
|
||||
/// @named('mock')
|
||||
/// ApiClient mockApi() => MockApiClient();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Generates code like:
|
||||
/// The generated code will look like:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// // Dependency registration...
|
||||
/// // Dependency registration code...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для пометки классов или библиотек Dart как модуля.
|
||||
///
|
||||
/// Используйте `@module()` для абстрактных классов (или библиотек), чтобы
|
||||
/// показать, что класс реализует DI-модуль (Dependency Injection).
|
||||
/// Обычно используется генераторами кода для автоматической регистрации
|
||||
/// и конфигурирования зависимостей, определённых в модуле.
|
||||
///
|
||||
/// Пример:
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// // Определения зависимостей
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерирован код:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// // Регистрация зависимостей...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
/// See also: [@provide], [@singleton], [@instance], [@named]
|
||||
@experimental
|
||||
final class module {
|
||||
/// Creates a [module] annotation.
|
||||
/// Creates a [module] annotation for use on a DI module class.
|
||||
const module();
|
||||
}
|
||||
|
||||
@@ -11,67 +11,52 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation to assign a name or identifier to a class, method, or other element.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Assigns a name or key identifier to a class, field, factory method or parameter
|
||||
/// for use in multi-registration scenarios (named dependencies) in CherryPick DI.
|
||||
///
|
||||
/// The `@named('value')` annotation allows you to specify a string name
|
||||
/// for a dependency, factory, or injectable. This is useful for distinguishing
|
||||
/// between multiple registrations of the same type in dependency injection,
|
||||
/// code generation, and for providing human-readable metadata.
|
||||
/// Use `@named('key')` to distinguish between multiple bindings/implementations
|
||||
/// of the same type—when registering and when injecting dependencies.
|
||||
///
|
||||
/// Example:
|
||||
/// You can use `@named`:
|
||||
/// - On provider/factory methods in a module
|
||||
/// - On fields with `@inject()` to receive a named instance
|
||||
/// - On function parameters (for method/constructor injection)
|
||||
///
|
||||
/// ### Example: On Provider Method
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// @named('dio')
|
||||
/// Dio dio() => Dio();
|
||||
/// abstract class AppModule {
|
||||
/// @named('main')
|
||||
/// ApiClient apiClient() => ApiClient();
|
||||
///
|
||||
/// @named('mock')
|
||||
/// ApiClient mockApi() => MockApiClient();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will generate:
|
||||
/// ### Example: On Injectable Field
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).withName('dio').singleton();
|
||||
/// }
|
||||
/// @injectable()
|
||||
/// class WidgetModel with _\$WidgetModel {
|
||||
/// @inject()
|
||||
/// @named('main')
|
||||
/// late final ApiClient api;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для задания имени или идентификатора классу, методу или другому элементу.
|
||||
///
|
||||
/// Аннотация `@named('значение')` позволяет указать строковое имя для зависимости,
|
||||
/// фабрики или внедряемого значения. Это удобно для различения нескольких
|
||||
/// регистраций одного типа в DI, генерации кода.
|
||||
///
|
||||
/// Пример:
|
||||
/// ### Example: On Parameter
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// @named('dio')
|
||||
/// Dio dio() => Dio();
|
||||
/// class UserScreen {
|
||||
/// UserScreen(@named('current') User user);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерирован следующий код:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).withName('dio').singleton();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
@experimental
|
||||
final class named {
|
||||
/// EN: The assigned name or identifier for the element.
|
||||
///
|
||||
/// RU: Назначенное имя или идентификатор для элемента.
|
||||
/// The assigned name or identifier for the dependency, provider, or parameter.
|
||||
final String value;
|
||||
|
||||
/// EN: Creates a [named] annotation with the given [value].
|
||||
///
|
||||
/// RU: Создаёт аннотацию [named] с заданным значением [value].
|
||||
/// Creates a [named] annotation with the given [value] key or name.
|
||||
const named(this.value);
|
||||
}
|
||||
|
||||
@@ -11,46 +11,33 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation to mark a method parameter for injection with run-time arguments.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks a parameter in a provider method to receive dynamic runtime arguments when resolving a dependency.
|
||||
///
|
||||
/// Use the `@params()` annotation to specify that a particular parameter of a
|
||||
/// provider method should be assigned a value supplied at resolution time,
|
||||
/// rather than during static dependency graph creation. This is useful in DI
|
||||
/// when a dependency must receive dynamic data passed by the consumer
|
||||
/// (via `.withParams(...)` in the generated code).
|
||||
/// Use `@params()` in a DI module/factory method when the value must be supplied by the user/code at injection time,
|
||||
/// not during static wiring (such as user input, navigation arguments, etc).
|
||||
///
|
||||
/// This enables CherryPick and its codegen to generate .withParams or .toProvideWithParams bindings — so your provider can access runtime values.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// @provide()
|
||||
/// String greet(@params() dynamic params) => 'Hello $params';
|
||||
/// ```
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// This will generate:
|
||||
/// @module()
|
||||
/// abstract class FeatureModule {
|
||||
/// @provide
|
||||
/// UserManager createManager(@params Map<String, dynamic> runtimeParams) {
|
||||
/// return UserManager.forUserId(runtimeParams['userId']);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// Usage at injection/resolution:
|
||||
/// ```dart
|
||||
/// bind<String>().toProvideWithParams((args) => greet(args));
|
||||
/// final manager = scope.resolve<UserManager>(params: {'userId': myId});
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для пометки параметра метода, который будет внедряться со значением во время выполнения.
|
||||
///
|
||||
/// Используйте `@params()` чтобы указать, что конкретный параметр метода-провайдера
|
||||
/// должен получать значение, передаваемое в момент обращения к зависимости,
|
||||
/// а не на этапе построения графа зависимостей. Это полезно, если зависимость
|
||||
/// должна получать данные динамически от пользователя или другого процесса
|
||||
/// через `.withParams(...)` в сгенерированном коде.
|
||||
///
|
||||
/// Пример:
|
||||
/// ```dart
|
||||
/// @provide()
|
||||
/// String greet(@params() dynamic params) => 'Hello $params';
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерировано:
|
||||
/// ```dart
|
||||
/// bind<String>().toProvideWithParams((args) => greet(args));
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
@experimental
|
||||
final class params {
|
||||
/// Marks a method/constructor parameter as supplied at runtime by the caller.
|
||||
const params();
|
||||
}
|
||||
|
||||
@@ -11,60 +11,34 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation to declare a factory/provider method or class as a singleton.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks a method or class as a dependency provider (factory/provider) for CherryPick module code generation.
|
||||
///
|
||||
/// Use the `@singleton()` annotation on methods in your DI module to specify
|
||||
/// that only one instance of the resulting object should be created and shared
|
||||
/// for all consumers. This is especially useful in dependency injection
|
||||
/// frameworks and service locators.
|
||||
/// Use `@provide` on any method inside a `@module()` annotated class when you want that method
|
||||
/// to be used as a DI factory/provider during code generation.
|
||||
///
|
||||
/// This should be used for methods that create dynamic, optional, or complex dependencies, especially
|
||||
/// if you want to control the codegen/injection pipeline explicitly and support parameters.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// abstract class FeatureModule {
|
||||
/// @provide
|
||||
/// Future<Api> provideApi(@params Map<String, dynamic> args) async => ...;
|
||||
///
|
||||
/// @singleton()
|
||||
/// Dio dio() => Dio();
|
||||
/// @provide
|
||||
/// Logger provideLogger() => Logger();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This generates the following code:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).singleton();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для объявления фабричного/провайдерного метода или класса синглтоном.
|
||||
///
|
||||
/// Используйте `@singleton()` для методов внутри DI-модуля, чтобы указать,
|
||||
/// что соответствующий объект (экземпляр класса) должен быть создан только один раз
|
||||
/// и использоваться всеми компонентами приложения (единый общий экземпляр).
|
||||
/// Это характерно для систем внедрения зависимостей и сервис-локаторов.
|
||||
///
|
||||
/// Пример:
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// @singleton()
|
||||
/// Dio dio() => Dio();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерирован следующий код:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).singleton();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
/// See also: [@singleton], [@instance], [@params], [@named]
|
||||
@experimental
|
||||
final class provide {
|
||||
/// Creates a [provide] annotation.
|
||||
const provide();
|
||||
}
|
||||
|
||||
@@ -13,25 +13,41 @@
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Annotation to specify a scope for dependency injection in CherryPick.
|
||||
/// Use this on an injected field to indicate from which scope
|
||||
/// the dependency must be resolved.
|
||||
/// Specifies the DI scope or region from which a dependency should be resolved.
|
||||
///
|
||||
/// ---
|
||||
/// Use `@scope('scopeName')` on an injected field, parameter, or provider method when you want
|
||||
/// to resolve a dependency not from the current scope, but from another named scope/subcontainer.
|
||||
///
|
||||
/// Аннотация для указания области внедрения (scope) в CherryPick.
|
||||
/// Используйте её на инъецируемом поле, чтобы определить из какой области
|
||||
/// должна быть получена зависимость.
|
||||
/// Useful for advanced DI scenarios: multi-feature/state isolation, navigation stacks, explicit subscopes, or testing.
|
||||
///
|
||||
/// Example / Пример:
|
||||
/// Example (injected field):
|
||||
/// ```dart
|
||||
/// @inject()
|
||||
/// @scope('profile')
|
||||
/// late final ProfileManager profileManager;
|
||||
/// @injectable()
|
||||
/// class ProfileScreen with _\$ProfileScreen {
|
||||
/// @inject()
|
||||
/// @scope('profile')
|
||||
/// late final ProfileManager manager;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Example (parameter):
|
||||
/// ```dart
|
||||
/// class TabBarModel {
|
||||
/// TabBarModel(@scope('tabs') TabContext context);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Example (in a module):
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class FeatureModule {
|
||||
/// @provide
|
||||
/// Service service(@scope('shared') SharedConfig config);
|
||||
/// }
|
||||
/// ```
|
||||
@experimental
|
||||
// ignore: camel_case_types
|
||||
final class scope {
|
||||
/// The name/key of the DI scope from which to resolve this dependency.
|
||||
final String? name;
|
||||
const scope(this.name);
|
||||
}
|
||||
|
||||
@@ -11,63 +11,32 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ENGLISH:
|
||||
/// Annotation to declare a dependency as a singleton.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Marks a provider method or class so its instance is created only once and shared (singleton) for DI in CherryPick.
|
||||
///
|
||||
/// Use the `@singleton()` annotation on provider methods inside a module
|
||||
/// to indicate that only a single instance of this dependency should be
|
||||
/// created and shared throughout the application's lifecycle. This is
|
||||
/// typically used in dependency injection frameworks or service locators
|
||||
/// to guarantee a single shared instance.
|
||||
/// Use `@singleton()` on provider methods or classes in your DI module to ensure only one instance is ever created
|
||||
/// and reused across the application's lifetime (or scope lifetime).
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// abstract class AppModule {
|
||||
/// @singleton()
|
||||
/// Dio dio() => Dio();
|
||||
/// ApiClient createApi() => ApiClient();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will generate code like:
|
||||
/// The generated code will ensure:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).singleton();
|
||||
/// }
|
||||
/// }
|
||||
/// bind<ApiClient>().toProvide(() => createApi()).singleton();
|
||||
/// ```
|
||||
///
|
||||
/// RUSSIAN (Русский):
|
||||
/// Аннотация для объявления зависимости как синглтона.
|
||||
///
|
||||
/// Используйте `@singleton()` для методов-провайдеров внутри модуля,
|
||||
/// чтобы указать, что соответствующий объект должен быть создан
|
||||
/// единожды и использоваться во всём приложении (общий синглтон).
|
||||
/// Это характерно для систем внедрения зависимостей и сервис-локаторов,
|
||||
/// чтобы гарантировать один общий экземпляр.
|
||||
///
|
||||
/// Пример:
|
||||
/// ```dart
|
||||
/// @module()
|
||||
/// abstract class AppModule extends Module {
|
||||
/// @singleton()
|
||||
/// Dio dio() => Dio();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Будет сгенерирован следующий код:
|
||||
/// ```dart
|
||||
/// final class $AppModule extends AppModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Dio>().toProvide(() => dio()).singleton();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: camel_case_types
|
||||
/// See also: [@instance], [@provide], [@named]
|
||||
@experimental
|
||||
final class singleton {
|
||||
/// Creates a [singleton] annotation.
|
||||
/// Creates a [singleton] annotation for DI providers/classes.
|
||||
const singleton();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
name: cherrypick_annotations
|
||||
description: |
|
||||
Set of annotations for CherryPick dependency injection library. Enables code generation and declarative DI for Dart & Flutter projects.
|
||||
version: 1.1.1
|
||||
documentation: https://github.com/pese-git/cherrypick/wiki
|
||||
version: 1.1.2-dev.1
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick/cherrypick_annotations
|
||||
issue_tracker: https://github.com/pese-git/cherrypick/issues
|
||||
topics:
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## 1.1.3-dev.10
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
## 1.1.3-dev.9
|
||||
|
||||
- **DOCS**(provider): add detailed English API documentation for CherryPickProvider Flutter integration.
|
||||
|
||||
## 1.1.3-dev.8
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -14,29 +14,87 @@ import 'package:flutter/widgets.dart';
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
/// {@template cherrypick_flutter_provider}
|
||||
/// An [InheritedWidget] that provides convenient integration of CherryPick
|
||||
/// dependency injection scopes into the Flutter widget tree.
|
||||
///
|
||||
/// Place `CherryPickProvider` at the top of your widget subtree to make a
|
||||
/// [Scope] (or its descendants) available via `CherryPickProvider.of(context)`.
|
||||
///
|
||||
/// This is the recommended entry point for connecting CherryPick DI to your
|
||||
/// Flutter app or feature area, enabling context-based scope management and
|
||||
/// DI resolution in child widgets.
|
||||
///
|
||||
/// ### Example: Root Integration
|
||||
/// ```dart
|
||||
/// void main() {
|
||||
/// final rootScope = CherryPick.openRootScope()
|
||||
/// ..installModules([AppModule()]);
|
||||
/// runApp(
|
||||
/// CherryPickProvider(
|
||||
/// child: MyApp(),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
///
|
||||
/// // In any widget:
|
||||
/// final provider = CherryPickProvider.of(context);
|
||||
/// final scope = provider.openRootScope();
|
||||
/// final myService = scope.resolve<MyService>();
|
||||
/// ```
|
||||
///
|
||||
/// ### Example: Subscope for a Feature/Screen
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// final provider = CherryPickProvider.of(context);
|
||||
/// final featureScope = provider.openSubScope(scopeName: 'featureA');
|
||||
/// return MyFeatureScreen(scope: featureScope);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You can use [openRootScope] and [openSubScope] as helpers to get/create scopes.
|
||||
/// {@endtemplate}
|
||||
final class CherryPickProvider extends InheritedWidget {
|
||||
/// Opens (or returns) the application-wide root [Scope].
|
||||
///
|
||||
/// Use to make all dependencies available at the top of your widget tree.
|
||||
Scope openRootScope() => CherryPick.openRootScope();
|
||||
|
||||
/// Opens a subscope (child [Scope]) with the given [scopeName].
|
||||
///
|
||||
/// Useful to create isolated feature/module scopes in widget subtrees.
|
||||
/// If [scopeName] is empty, an unnamed scope is created.
|
||||
Scope openSubScope({String scopeName = '', String separator = '.'}) =>
|
||||
CherryPick.openScope(scopeName: scopeName, separator: separator);
|
||||
|
||||
// Constructor for CherryPickProvider. Initializes with a required rootScope and child widget.
|
||||
/// Creates a [CherryPickProvider] and exposes it to the widget subtree.
|
||||
///
|
||||
/// Place near the root of your widget tree. Use [child] to provide the subtree.
|
||||
const CherryPickProvider({
|
||||
super.key,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
// Method to access the nearest CherryPickProvider instance from the context
|
||||
/// Locates the nearest [CherryPickProvider] up the widget tree from [context].
|
||||
///
|
||||
/// Throws if not found. Use this to access DI [Scope] controls anywhere below the provider.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final provider = CherryPickProvider.of(context);
|
||||
/// final scope = provider.openRootScope();
|
||||
/// ```
|
||||
static CherryPickProvider of(BuildContext context) {
|
||||
// Looks up the widget tree for an instance of CherryPickProvider
|
||||
final CherryPickProvider? result =
|
||||
context.dependOnInheritedWidgetOfExactType<CherryPickProvider>();
|
||||
// Assert to ensure a CherryPickProvider is present in the context
|
||||
assert(result != null, 'No CherryPickProvider found in context');
|
||||
return result!;
|
||||
}
|
||||
|
||||
// Determines whether the widget should notify dependents when it changes
|
||||
/// Controls update notifications for dependent widgets.
|
||||
///
|
||||
/// Always returns false because the provider itself is stateless:
|
||||
/// changes are to the underlying scopes, not the widget.
|
||||
@override
|
||||
bool updateShouldNotify(CherryPickProvider oldWidget) {
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: cherrypick_flutter
|
||||
description: "Flutter library that allows access to the root scope through the context using `CherryPickProvider`."
|
||||
version: 1.1.3-dev.8
|
||||
homepage: https://pese-git.github.io/cherrypick-site/
|
||||
documentation: https://github.com/pese-git/cherrypick/wiki
|
||||
version: 1.1.3-dev.10
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
issue_tracker: https://github.com/pese-git/cherrypick/issues
|
||||
topics:
|
||||
@@ -19,7 +19,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cherrypick: ^3.0.0-dev.8
|
||||
cherrypick: ^3.0.0-dev.10
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
## 2.0.0-dev.1
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
## 2.0.0-dev.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **DOCS**(generator): improve and unify English documentation and examples for all DI source files.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- **FIX**(license): correct urls.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
library;
|
||||
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,6 +10,28 @@ library;
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
library;
|
||||
|
||||
/// CherryPick code generation library: entry point for build_runner DI codegen.
|
||||
///
|
||||
/// This library exports generators for CherryPick dependency injection:
|
||||
/// - [ModuleGenerator]: Generates DI module classes for all `@module()`-annotated classes.
|
||||
/// - [InjectGenerator]: Generates field-injection mixins for classes annotated with `@injectable()`.
|
||||
///
|
||||
/// These generators are hooked into [build_runner] and cherrypick_generator's builder configuration.
|
||||
/// Normally you do not import this directly; instead, add cherrypick_generator
|
||||
/// as a dev_dependency and run `dart run build_runner build`.
|
||||
///
|
||||
/// Example usage in `build.yaml` or your project's workflow:
|
||||
/// ```yaml
|
||||
/// targets:
|
||||
/// $default:
|
||||
/// builders:
|
||||
/// cherrypick_generator|cherrypick_generator:
|
||||
/// generate_for:
|
||||
/// - lib/**.dart
|
||||
/// ```
|
||||
///
|
||||
/// For annotation details, see `package:cherrypick_annotations`.
|
||||
export 'module_generator.dart';
|
||||
export 'inject_generator.dart';
|
||||
|
||||
@@ -20,28 +20,85 @@ import 'package:source_gen/source_gen.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
||||
|
||||
/// InjectGenerator generates a mixin for a class marked with @injectable()
|
||||
/// and injects all fields annotated with @inject(), using CherryPick DI.
|
||||
/// CherryPick DI field injector generator for codegen.
|
||||
///
|
||||
/// For Future<T> fields it calls .resolveAsync<T>(),
|
||||
/// otherwise .resolve<T>() is used. Scope and named qualifiers are supported.
|
||||
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
|
||||
/// which contains the `_inject` method. This method will assign all fields annotated with `@inject()`
|
||||
/// using the CherryPick DI container. Extra annotation qualifiers such as `@named` and `@scope` are respected
|
||||
/// for each field. Nullable fields and Future/injectable async dependencies are also supported automatically.
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// InjectGenerator генерирует миксин для класса с аннотацией @injectable()
|
||||
/// и внедряет все поля, помеченные @inject(), используя DI-фреймворк CherryPick.
|
||||
/// ### Example usage in a project:
|
||||
///
|
||||
/// Для Future<T> полей вызывается .resolveAsync<T>(),
|
||||
/// для остальных — .resolve<T>(). Поддерживаются scope и named qualifier.
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @injectable()
|
||||
/// class MyScreen with _$MyScreen {
|
||||
/// @inject()
|
||||
/// late final Logger logger;
|
||||
///
|
||||
/// @inject()
|
||||
/// @named('test')
|
||||
/// late final HttpClient client;
|
||||
///
|
||||
/// @inject()
|
||||
/// Future<Analytics>? analytics;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// After running build_runner, this mixin will be auto-generated:
|
||||
///
|
||||
/// ```dart
|
||||
/// mixin _$MyScreen {
|
||||
/// void _inject(MyScreen instance) {
|
||||
/// instance.logger = CherryPick.openRootScope().resolve<Logger>();
|
||||
/// instance.client = CherryPick.openRootScope().resolve<HttpClient>(named: 'test');
|
||||
/// instance.analytics = CherryPick.openRootScope().tryResolveAsync<Analytics>(); // nullable async inject
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You may use the mixin (e.g., `myScreen._inject(myScreen)`) or expose your own public helper for instance field injection.
|
||||
///
|
||||
/// **Supported scenarios:**
|
||||
/// - Ordinary injectable fields: `resolve<T>()`.
|
||||
/// - Named qualifiers: `resolve<T>(named: ...)`.
|
||||
/// - Scoping: `CherryPick.openScope(scopeName: ...).resolve<T>()`.
|
||||
/// - Nullable/incomplete fields: `tryResolve`/`tryResolveAsync`.
|
||||
/// - Async dependencies: `Future<T>`/`resolveAsync<T>()`.
|
||||
///
|
||||
/// See also:
|
||||
/// * @inject
|
||||
/// * @injectable
|
||||
class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
const InjectGenerator();
|
||||
|
||||
/// The main entry point for code generation.
|
||||
/// Main entry point for CherryPick field injection code generation.
|
||||
///
|
||||
/// Checks class validity, collects injectable fields, and produces injection code.
|
||||
/// - Only triggers for classes marked with `@injectable()`.
|
||||
/// - Throws an error if used on non-class elements.
|
||||
/// - Scans all fields marked with `@inject()` and gathers qualifiers (if any).
|
||||
/// - Generates Dart code for a mixin that injects all dependencies into the target class instance.
|
||||
///
|
||||
/// Основная точка входа генератора. Проверяет класс, собирает инъектируемые поля и создает код внедрения зависимостей.
|
||||
/// Returns the Dart code as a String defining the new mixin.
|
||||
///
|
||||
/// Example input (user code):
|
||||
/// ```dart
|
||||
/// @injectable()
|
||||
/// class UserBloc with _$UserBloc {
|
||||
/// @inject() late final AuthRepository authRepository;
|
||||
/// }
|
||||
/// ```
|
||||
/// Example output (generated):
|
||||
/// ```dart
|
||||
/// mixin _$UserBloc {
|
||||
/// void _inject(UserBloc instance) {
|
||||
/// instance.authRepository = CherryPick.openRootScope().resolve<AuthRepository>();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@override
|
||||
FutureOr<String> generateForAnnotatedElement(
|
||||
Element element,
|
||||
@@ -63,8 +120,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
..writeln('mixin $mixinName {')
|
||||
..writeln(' void _inject($className instance) {');
|
||||
|
||||
// Collect and process all @inject fields.
|
||||
// Собираем и обрабатываем все поля с @inject.
|
||||
// Collect and process all @inject fields
|
||||
final injectFields =
|
||||
classElement.fields.where(_isInjectField).map(_parseInjectField);
|
||||
|
||||
@@ -79,20 +135,20 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Checks if a field has the @inject annotation.
|
||||
/// Returns true if a field is annotated with `@inject`.
|
||||
///
|
||||
/// Проверяет, отмечено ли поле аннотацией @inject.
|
||||
/// Used to detect which fields should be processed for injection.
|
||||
static bool _isInjectField(FieldElement field) {
|
||||
return field.metadata.any(
|
||||
(m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject',
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses the field for scope/named qualifiers and determines its type.
|
||||
/// Returns a [_ParsedInjectField] describing injection information.
|
||||
/// Parses `@inject()` field and extracts all injection metadata
|
||||
/// (core type, qualifiers, scope, nullability, etc).
|
||||
///
|
||||
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип.
|
||||
/// Возвращает [_ParsedInjectField] с информацией о внедрении.
|
||||
/// Converts Dart field declaration and all parameterizing injection-related
|
||||
/// annotations into a [_ParsedInjectField] which is used for codegen.
|
||||
static _ParsedInjectField _parseInjectField(FieldElement field) {
|
||||
String? scopeName;
|
||||
String? namedValue;
|
||||
@@ -120,8 +176,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
isFuture = false;
|
||||
}
|
||||
|
||||
// ***
|
||||
// Добавим определение nullable для типа (например PostRepository? или Future<PostRepository?>)
|
||||
// Determine nullability for field types like T? or Future<T?>
|
||||
bool isNullable = dartType.nullabilitySuffix ==
|
||||
NullabilitySuffix.question ||
|
||||
(dartType is ParameterizedType &&
|
||||
@@ -139,13 +194,17 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates a line of code that performs the dependency injection for a field.
|
||||
/// Handles resolve/resolveAsync, scoping, and named qualifiers.
|
||||
/// Generates Dart code for a single dependency-injected field based on its metadata.
|
||||
///
|
||||
/// Генерирует строку кода, которая внедряет зависимость для поля.
|
||||
/// Учитывает resolve/resolveAsync, scoping и named qualifier.
|
||||
/// This code will resolve the field from the CherryPick DI container and assign it to the class instance.
|
||||
/// Correctly dispatches to resolve, tryResolve, resolveAsync, or tryResolveAsync methods,
|
||||
/// and applies container scoping or named resolution where required.
|
||||
///
|
||||
/// Returns literal Dart code as string (1 line).
|
||||
///
|
||||
/// Example output:
|
||||
/// `instance.logger = CherryPick.openRootScope().resolve<Logger>();`
|
||||
String _generateInjectionLine(_ParsedInjectField field) {
|
||||
// Используем tryResolve для nullable, иначе resolve
|
||||
final resolveMethod = field.isFuture
|
||||
? (field.isNullable
|
||||
? 'tryResolveAsync<${field.coreType}>'
|
||||
@@ -166,30 +225,29 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Data structure representing all information required to generate
|
||||
/// injection code for a field.
|
||||
/// Internal structure: describes all required information for generating the injection
|
||||
/// assignment for a given field.
|
||||
///
|
||||
/// Структура данных, содержащая всю информацию,
|
||||
/// необходимую для генерации кода внедрения для поля.
|
||||
/// Not exported. Used as a DTO in the generator for each field.
|
||||
class _ParsedInjectField {
|
||||
/// The name of the field / Имя поля.
|
||||
/// The name of the field to be injected.
|
||||
final String fieldName;
|
||||
|
||||
/// The base type name (T or Future<T>) / Базовый тип (T или тип из Future<T>).
|
||||
/// The Dart type to resolve (e.g. `Logger` from `Logger` or `Future<Logger>`).
|
||||
final String coreType;
|
||||
|
||||
/// True if the field type is Future<T>; false otherwise
|
||||
/// Истина, если поле — Future<T>, иначе — ложь.
|
||||
/// True if the field is an async dependency (Future<...>), otherwise false.
|
||||
final bool isFuture;
|
||||
|
||||
/// Optional scope annotation argument / Опциональное имя scope.
|
||||
/// True if the field accepts null (T?), otherwise false.
|
||||
final bool isNullable;
|
||||
|
||||
/// The scoping for DI resolution, or null to use root scope.
|
||||
final String? scopeName;
|
||||
|
||||
/// Optional named annotation argument / Опциональное имя named.
|
||||
/// Name qualifier for named resolution, or null if not set.
|
||||
final String? namedValue;
|
||||
|
||||
final bool isNullable;
|
||||
|
||||
_ParsedInjectField({
|
||||
required this.fieldName,
|
||||
required this.coreType,
|
||||
@@ -200,8 +258,8 @@ class _ParsedInjectField {
|
||||
});
|
||||
}
|
||||
|
||||
/// Builder factory. Used by build_runner.
|
||||
/// Factory for creating the build_runner builder for DI field injection.
|
||||
///
|
||||
/// Фабрика билдера. Используется build_runner.
|
||||
/// Add this builder in your build.yaml if you're invoking CherryPick generators manually.
|
||||
Builder injectBuilder(BuilderOptions options) =>
|
||||
PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart');
|
||||
|
||||
@@ -19,75 +19,89 @@ import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
|
||||
import 'src/generated_class.dart';
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// ModuleGenerator for code generation of dependency-injected modules.
|
||||
/// CherryPick Module Generator — Codegen for DI modules
|
||||
///
|
||||
/// ENGLISH
|
||||
/// This generator scans for Dart classes annotated with `@module()` and
|
||||
/// automatically generates boilerplate code for dependency injection
|
||||
/// (DI) based on the public methods in those classes. Each method can be
|
||||
/// annotated to describe how an object should be provided to the DI container.
|
||||
/// The generated code registers those methods as bindings. This automates the
|
||||
/// creation of factories, singletons, and named instances, reducing repetitive
|
||||
/// manual code.
|
||||
/// This generator scans Dart classes annotated with `@module()` and generates
|
||||
/// boilerplate for dependency injection registration automatically. Each public
|
||||
/// method in such classes can be annotated to describe how an object should be
|
||||
/// bound to the DI container (singleton, factory, named, with parameters, etc).
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Генератор зависимостей для DI-контейнера на основе аннотаций.
|
||||
/// Данный генератор автоматически создаёт код для внедрения зависимостей (DI)
|
||||
/// на основе аннотаций в вашем исходном коде. Когда вы отмечаете класс
|
||||
/// аннотацией `@module()`, этот генератор обработает все его публичные методы
|
||||
/// и автоматически сгенерирует класс с биндингами (регистрациями зависимостей)
|
||||
/// для DI-контейнера. Это избавляет от написания однообразного шаблонного кода.
|
||||
/// The generated code collects all such bind methods and produces a Dart
|
||||
/// companion *module registration class* with a `.bindAll()` method, which you
|
||||
/// can use from your DI root to automatically register those dependencies.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @module()
|
||||
/// abstract class AppModule {
|
||||
/// @singleton()
|
||||
/// Logger logger() => Logger();
|
||||
///
|
||||
/// @provide()
|
||||
/// ApiService api(Logger logger) => ApiService(logger);
|
||||
///
|
||||
/// @named('dev')
|
||||
/// FakeService fake() => FakeService();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// After codegen, you will get (simplified):
|
||||
/// ```dart
|
||||
/// class _\$AppModuleCherrypickModule extend AppModule {
|
||||
/// static void bindAll(CherryPickScope scope, AppModule module) {
|
||||
/// scope.addSingleton<Logger>(() => module.logger());
|
||||
/// scope.addFactory<ApiService>(() => module.api(scope.resolve<Logger>()));
|
||||
/// scope.addFactory<FakeService>(() => module.fake(), named: 'dev');
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Use it e.g. in your bootstrap:
|
||||
/// ```dart
|
||||
/// final scope = CherryPick.openRootScope()..intallModules([_\$AppModuleCherrypickModule()]);
|
||||
/// ```
|
||||
///
|
||||
/// Features supported:
|
||||
/// - Singleton, factory, named, parametric, and async providers
|
||||
/// - Eliminates all boilerplate for DI registration
|
||||
/// - Works with abstract classes and real classes
|
||||
/// - Error if @module() is applied to a non-class
|
||||
///
|
||||
/// See also: [@singleton], [@provide], [@named], [@module]
|
||||
/// ---------------------------------------------------------------------------
|
||||
|
||||
class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
|
||||
/// -------------------------------------------------------------------------
|
||||
/// ENGLISH
|
||||
/// Generates the Dart source for a class marked with the `@module()` annotation.
|
||||
/// - [element]: the original Dart class element.
|
||||
/// - [annotation]: the annotation parameters (not usually used here).
|
||||
/// - [buildStep]: the current build step info.
|
||||
/// Generates Dart source for a class marked with the `@module()` annotation.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Генерирует исходный код для класса-модуля с аннотацией `@module()`.
|
||||
/// [element] — исходный класс, помеченный аннотацией.
|
||||
/// [annotation] — значения параметров аннотации.
|
||||
/// [buildStep] — информация о текущем шаге генерации.
|
||||
/// -------------------------------------------------------------------------
|
||||
/// Throws [InvalidGenerationSourceError] if used on anything except a class.
|
||||
///
|
||||
/// See file-level docs for usage and generated output example.
|
||||
@override
|
||||
String generateForAnnotatedElement(
|
||||
Element element,
|
||||
ConstantReader annotation,
|
||||
BuildStep buildStep,
|
||||
) {
|
||||
// Only classes are supported for @module() annotation
|
||||
// Обрабатываются только классы (другие элементы — ошибка)
|
||||
if (element is! ClassElement) {
|
||||
throw InvalidGenerationSourceError(
|
||||
'@module() can only be applied to classes. / @module() может быть применён только к классам.',
|
||||
'@module() can only be applied to classes.',
|
||||
element: element,
|
||||
);
|
||||
}
|
||||
|
||||
final classElement = element;
|
||||
|
||||
// Build a representation of the generated bindings based on class methods /
|
||||
// Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса
|
||||
final generatedClass = GeneratedClass.fromClassElement(classElement);
|
||||
|
||||
// Generate the resulting Dart code / Генерирует итоговый Dart-код
|
||||
return generatedClass.generate();
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// ENGLISH
|
||||
/// Entry point for build_runner. Returns a Builder used to generate code for
|
||||
/// every file with a @module() annotation.
|
||||
/// Codegen builder entry point: register this builder in build.yaml or your package.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Точка входа для генератора build_runner.
|
||||
/// Возвращает Builder, используемый build_runner для генерации кода для всех
|
||||
/// файлов, где встречается @module().
|
||||
/// Used by build_runner. Generates .module.cherrypick.g.dart files for each
|
||||
/// source file with an annotated @module() class.
|
||||
/// ---------------------------------------------------------------------------
|
||||
Builder moduleBuilder(BuilderOptions options) =>
|
||||
PartBuilder([ModuleGenerator()], '.module.cherrypick.g.dart');
|
||||
|
||||
@@ -15,9 +15,43 @@ import 'package:analyzer/dart/element/element.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'metadata_utils.dart';
|
||||
|
||||
/// Validates annotation combinations and usage patterns
|
||||
/// Provides static utility methods for validating annotation usage in CherryPick
|
||||
/// dependency injection code generation.
|
||||
///
|
||||
/// This validator helps ensure that `@provide`, `@instance`, `@singleton`, `@params`,
|
||||
/// `@inject`, `@named`, `@module`, and `@injectable` annotations are correctly and safely
|
||||
/// combined in your codebase, preventing common configuration and codegen errors before
|
||||
/// code is generated.
|
||||
///
|
||||
/// #### Example Usage
|
||||
/// ```dart
|
||||
/// void processMethod(MethodElement method) {
|
||||
/// AnnotationValidator.validateMethodAnnotations(method);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// All exceptions are thrown as [AnnotationValidationException] and will include
|
||||
/// a helpful message and context.
|
||||
///
|
||||
/// ---
|
||||
/// Typical checks performed by this utility:
|
||||
/// - Mutual exclusivity (`@instance` vs `@provide`)
|
||||
/// - Required presence for fields and methods
|
||||
/// - Proper parameters for `@named` and `@params`
|
||||
/// - Correct usage of injectable fields, module class methods, etc.
|
||||
///
|
||||
class AnnotationValidator {
|
||||
/// Validates annotations on a method element
|
||||
/// Validates annotations for a given [MethodElement].
|
||||
///
|
||||
/// Checks:
|
||||
/// - Mutual exclusivity of `@instance` and `@provide`.
|
||||
/// - That a method is annotated with exactly one DI-producing annotation.
|
||||
/// - If `@params` is present, that it is used together with `@provide`.
|
||||
/// - Appropriate usage of `@singleton`.
|
||||
/// - [@named] syntax and conventions.
|
||||
/// - Parameter validation for method arguments.
|
||||
///
|
||||
/// Throws [AnnotationValidationException] on any violation.
|
||||
static void validateMethodAnnotations(MethodElement method) {
|
||||
final annotations = _getAnnotationNames(method.metadata);
|
||||
|
||||
@@ -26,14 +60,28 @@ class AnnotationValidator {
|
||||
_validateAnnotationParameters(method);
|
||||
}
|
||||
|
||||
/// Validates annotations on a field element
|
||||
/// Validates that a [FieldElement] has correct injection annotations.
|
||||
///
|
||||
/// Specifically, ensures:
|
||||
/// - Injectable fields are of valid type.
|
||||
/// - No `void` injection.
|
||||
/// - Correct scope naming if present.
|
||||
///
|
||||
/// Throws [AnnotationValidationException] if checks fail.
|
||||
static void validateFieldAnnotations(FieldElement field) {
|
||||
final annotations = _getAnnotationNames(field.metadata);
|
||||
|
||||
_validateInjectFieldAnnotations(field, annotations);
|
||||
}
|
||||
|
||||
/// Validates annotations on a class element
|
||||
/// Validates all class-level DI annotations.
|
||||
///
|
||||
/// Executes checks for:
|
||||
/// - Module class validity (e.g. must have public DI methods if `@module`).
|
||||
/// - Injectable class: at least one @inject field, field finalness, etc.
|
||||
/// - Provides helpful context for error/warning reporting.
|
||||
///
|
||||
/// Throws [AnnotationValidationException] if checks fail.
|
||||
static void validateClassAnnotations(ClassElement classElement) {
|
||||
final annotations = _getAnnotationNames(classElement.metadata);
|
||||
|
||||
@@ -41,6 +89,9 @@ class AnnotationValidator {
|
||||
_validateInjectableClassAnnotations(classElement, annotations);
|
||||
}
|
||||
|
||||
// --- Internal helpers follow (private) ---
|
||||
|
||||
/// Helper: Returns the names of all annotation types on `metadata`.
|
||||
static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) {
|
||||
return metadata
|
||||
.map((m) => m.computeConstantValue()?.type?.getDisplayString())
|
||||
@@ -49,6 +100,9 @@ class AnnotationValidator {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Validates that mutually exclusive method annotations are not used together.
|
||||
///
|
||||
/// For example, `@instance` and `@provide` cannot both be present.
|
||||
static void _validateMutuallyExclusiveAnnotations(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
@@ -68,6 +122,10 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates correct annotation combinations, e.g.
|
||||
/// - `@params` must be with `@provide`
|
||||
/// - One of `@instance` or `@provide` must be present for a registration method
|
||||
/// - Validates singleton usage
|
||||
static void _validateAnnotationCombinations(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
@@ -105,6 +163,7 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Singleton-specific method annotation checks.
|
||||
static void _validateSingletonUsage(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
@@ -130,6 +189,7 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates extra requirements or syntactic rules for annotation arguments, like @named.
|
||||
static void _validateAnnotationParameters(MethodElement method) {
|
||||
// Validate @named annotation parameters
|
||||
final namedValue = MetadataUtils.getNamedValue(method.metadata);
|
||||
@@ -170,11 +230,11 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that @params is used with compatible parameter type.
|
||||
static void _validateParamsParameter(
|
||||
ParameterElement param, MethodElement method) {
|
||||
// @params parameter should typically be dynamic or Map<String, dynamic>
|
||||
final paramType = param.type.getDisplayString();
|
||||
|
||||
if (paramType != 'dynamic' &&
|
||||
paramType != 'Map<String, dynamic>' &&
|
||||
paramType != 'Map<String, dynamic>?') {
|
||||
@@ -194,6 +254,7 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks field-level annotation for valid injectable fields.
|
||||
static void _validateInjectFieldAnnotations(
|
||||
FieldElement field,
|
||||
List<String> annotations,
|
||||
@@ -227,6 +288,7 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks @module usage: must have at least one DI method, each with DI-annotation.
|
||||
static void _validateModuleClassAnnotations(
|
||||
ClassElement classElement,
|
||||
List<String> annotations,
|
||||
@@ -268,6 +330,7 @@ class AnnotationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks @injectable usage on classes and their fields.
|
||||
static void _validateInjectableClassAnnotations(
|
||||
ClassElement classElement,
|
||||
List<String> annotations,
|
||||
|
||||
@@ -12,57 +12,59 @@
|
||||
//
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// BindParameterSpec - describes a single method parameter and how to resolve it.
|
||||
/// BindParameterSpec
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Describes a single parameter for a provider/binding method in the DI system.
|
||||
/// Stores the parameter type, its optional `@named` key for named resolution,
|
||||
/// and whether it is a runtime "params" argument. Used to generate code that
|
||||
/// resolves dependencies from the DI scope:
|
||||
/// - If the parameter is a dependency type (e.g. SomeDep), emits:
|
||||
/// currentScope.resolve<SomeDep>()
|
||||
/// - If the parameter is named, emits:
|
||||
/// currentScope.resolve<SomeDep>(named: 'yourName')
|
||||
/// - If it's a runtime parameter (e.g. via @params()), emits:
|
||||
/// args
|
||||
/// Describes a single parameter for a DI provider/factory/binding method,
|
||||
/// specifying how that parameter is to be resolved in generated code.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Описывает один параметр метода в DI, и его способ разрешения из контейнера.
|
||||
/// Сохраняет имя типа, дополнительное имя (если параметр аннотирован через @named),
|
||||
/// и признак runtime-параметра (@params).
|
||||
/// Для обычной зависимости типа (например, SomeDep) генерирует строку вида:
|
||||
/// currentScope.resolve<SomeDep>()
|
||||
/// Для зависимости с именем:
|
||||
/// currentScope.resolve<SomeDep>(named: 'имя')
|
||||
/// Для runtime-параметра:
|
||||
/// args
|
||||
/// Stores the parameter's type name, optional `@named` identifier (for named DI resolution),
|
||||
/// and a flag for runtime (@params) arguments. Used in CherryPick generator
|
||||
/// for creating argument lists when invoking factories or provider methods.
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```dart
|
||||
/// // Binding method: @provide() Logger provideLogger(@named('debug') Config config, @params Map<String, dynamic> args)
|
||||
/// final namedParam = BindParameterSpec('Config', 'debug');
|
||||
/// final runtimeParam = BindParameterSpec('Map<String, dynamic>', null, isParams: true);
|
||||
/// print(namedParam.generateArg()); // prints: currentScope.resolve<Config>(named: 'debug')
|
||||
/// print(runtimeParam.generateArg()); // prints: args
|
||||
/// ```
|
||||
///
|
||||
/// ## Code generation logic
|
||||
/// - Injected: currentScope.resolve<Service>()
|
||||
/// - Named: currentScope.resolve<Service>(named: 'name')
|
||||
/// - @params: args
|
||||
/// ----------------------------------------------------------------------------
|
||||
class BindParameterSpec {
|
||||
/// Type name of the parameter (e.g. SomeService)
|
||||
/// Имя типа параметра (например, SomeService)
|
||||
/// The type name of the parameter (e.g., 'UserRepository')
|
||||
final String typeName;
|
||||
|
||||
/// Optional name for named resolution (from @named)
|
||||
/// Необязательное имя для разрешения по имени (если аннотировано через @named)
|
||||
/// If non-null, this is the named-key for DI resolution (from @named).
|
||||
final String? named;
|
||||
|
||||
/// True if this parameter uses @params and should be provided from runtime args
|
||||
/// Признак, что параметр — runtime (через @params)
|
||||
/// True if this parameter is a runtime param (annotated with @params and
|
||||
/// filled by a runtime argument map).
|
||||
final bool isParams;
|
||||
|
||||
BindParameterSpec(this.typeName, this.named, {this.isParams = false});
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// generateArg
|
||||
/// Generates Dart code to resolve this parameter in the DI container.
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Generates Dart code for resolving the dependency from the DI scope,
|
||||
/// considering type, named, and param-argument.
|
||||
/// - For normal dependencies: resolves by type
|
||||
/// - For named dependencies: resolves by type and name
|
||||
/// - For @params: uses the supplied params variable (default 'args')
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Генерирует строку для получения зависимости из DI scope (с учётом имени
|
||||
/// и типа параметра или runtime-режима @params).
|
||||
/// --------------------------------------------------------------------------
|
||||
/// ## Example
|
||||
/// ```dart
|
||||
/// final a = BindParameterSpec('Api', null); // normal
|
||||
/// print(a.generateArg()); // currentScope.resolve<Api>()
|
||||
///
|
||||
/// final b = BindParameterSpec('Api', 'prod'); // named
|
||||
/// print(b.generateArg()); // currentScope.resolve<Api>(named: 'prod')
|
||||
///
|
||||
/// final c = BindParameterSpec('Map<String,dynamic>', null, isParams: true); // params
|
||||
/// print(c.generateArg()); // args
|
||||
/// ```
|
||||
String generateArg([String paramsVar = 'args']) {
|
||||
if (isParams) {
|
||||
return paramsVar;
|
||||
|
||||
@@ -19,62 +19,64 @@ import 'exceptions.dart';
|
||||
import 'type_parser.dart';
|
||||
import 'annotation_validator.dart';
|
||||
|
||||
/// Enum representing the binding annotation applied to a module method.
|
||||
enum BindingType {
|
||||
/// Direct instance returned from the method (@instance).
|
||||
instance,
|
||||
|
||||
/// Provider/factory function (@provide).
|
||||
provide;
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// BindSpec -- describes a binding specification generated for a dependency.
|
||||
/// BindSpec
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Represents all the data necessary to generate a DI binding for a single
|
||||
/// method in a module class. Each BindSpec corresponds to one public method
|
||||
/// and contains information about its type, provider method, lifecycle (singleton),
|
||||
/// parameters (with their annotations), binding strategy (instance/provide),
|
||||
/// asynchronous mode, and named keys. It is responsible for generating the
|
||||
/// correct Dart code to register this binding with the DI container, in both
|
||||
/// sync and async cases, with and without named or runtime arguments.
|
||||
/// Describes a DI container binding as generated from a single public factory,
|
||||
/// instance, or provider method of a module (annotated with @instance or @provide).
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Описывает параметры для создания одного биндинга зависимости (binding spec).
|
||||
/// Каждый биндинг соответствует одному публичному методу класса-модуля и
|
||||
/// содержит всю информацию для генерации кода регистрации этого биндинга в
|
||||
/// DI-контейнере: тип возвращаемой зависимости, имя метода, параметры, аннотации
|
||||
/// (@singleton, @named, @instance, @provide), асинхронность, признак runtime
|
||||
/// аргументов и др. Генерирует правильный Dart-код для регистрации биндера.
|
||||
/// Includes all annotation-driven parameters required to generate valid DI
|
||||
/// registration Dart code in CherryPick:
|
||||
/// - Return type
|
||||
/// - Provider method name
|
||||
/// - Singleton flag
|
||||
/// - Named identifier (from @named)
|
||||
/// - List of resolved or runtime (@params) parameters
|
||||
/// - Binding mode (instance/provide)
|
||||
/// - Async and parametric variants
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```dart
|
||||
/// // Suppose @provide() Api api(@named('test') Client client)
|
||||
/// final bindSpec = BindSpec.fromMethod(methodElement);
|
||||
/// print(bindSpec.generateBind(2)); // bind<Api>().toProvide(() => api(currentScope.resolve<Client>(named: 'test')));
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class BindSpec {
|
||||
/// The type this binding provides (e.g. SomeService)
|
||||
/// Тип, который предоставляет биндинг (например, SomeService)
|
||||
final String returnType;
|
||||
|
||||
/// Method name that implements the binding
|
||||
/// Имя метода, который реализует биндинг
|
||||
/// Binding provider/factory method name
|
||||
final String methodName;
|
||||
|
||||
/// Optional name for named dependency (from @named)
|
||||
/// Необязательное имя, для именованной зависимости (используется с @named)
|
||||
/// Named identifier for DI resolution (null if unnamed)
|
||||
final String? named;
|
||||
|
||||
/// Whether the dependency is a singleton (@singleton annotation)
|
||||
/// Является ли зависимость синглтоном (имеется ли аннотация @singleton)
|
||||
/// If true, binding is registered as singleton in DI
|
||||
final bool isSingleton;
|
||||
|
||||
/// List of method parameters to inject dependencies with
|
||||
/// Список параметров, которые требуются методу для внедрения зависимостей
|
||||
/// Provider/factory method parameters (in order)
|
||||
final List<BindParameterSpec> parameters;
|
||||
|
||||
/// Binding type: 'instance' or 'provide' (@instance or @provide)
|
||||
final BindingType bindingType; // 'instance' | 'provide'
|
||||
/// Instance vs provider mode, from annotation choice
|
||||
final BindingType bindingType;
|
||||
|
||||
/// True if the method is asynchronous and uses instance binding (Future)
|
||||
/// Async flag for .toInstanceAsync()
|
||||
final bool isAsyncInstance;
|
||||
|
||||
/// True if the method is asynchronous and uses provide binding (Future)
|
||||
/// Async flag for .toProvideAsync()
|
||||
final bool isAsyncProvide;
|
||||
|
||||
/// True if the binding method accepts runtime "params" argument (@params)
|
||||
/// True if a @params runtime parameter is present
|
||||
final bool hasParams;
|
||||
|
||||
BindSpec({
|
||||
@@ -89,20 +91,12 @@ class BindSpec {
|
||||
required this.hasParams,
|
||||
});
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
/// generateBind
|
||||
/// Generates a Dart code line for binding registration.
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Generates a line of Dart code registering the binding with the DI framework.
|
||||
/// Produces something like:
|
||||
/// bind<Type>().toProvide(() => method(args)).withName('name').singleton();
|
||||
/// Indent parameter allows formatted multiline output.
|
||||
/// Example (single-line):
|
||||
/// bind<Api>().toProvide(() => provideApi(currentScope.resolve<Client>(named: 'test'))).withName('prod').singleton();
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Формирует dart-код для биндинга, например:
|
||||
/// bind<Type>().toProvide(() => method(args)).withName('name').singleton();
|
||||
/// Параметр [indent] задаёт отступ для красивого форматирования кода.
|
||||
/// -------------------------------------------------------------------------
|
||||
/// The [indent] argument sets the space indentation for pretty-printing.
|
||||
String generateBind(int indent) {
|
||||
final indentStr = ' ' * indent;
|
||||
final provide = _generateProvideClause(indent);
|
||||
@@ -151,7 +145,7 @@ class BindSpec {
|
||||
return _generatePlainProvideClause(indent);
|
||||
}
|
||||
|
||||
/// EN / RU: Supports runtime parameters (@params).
|
||||
/// Generates code when using runtime parameters (@params).
|
||||
String _generateWithParamsProvideClause(int indent) {
|
||||
// Safe variable name for parameters.
|
||||
const paramVar = 'args';
|
||||
@@ -178,7 +172,7 @@ class BindSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/// EN / RU: Supports only injected dependencies, not runtime (@params).
|
||||
/// Generates code when only resolved (not runtime) arguments used.
|
||||
String _generatePlainProvideClause(int indent) {
|
||||
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
|
||||
|
||||
@@ -241,16 +235,17 @@ class BindSpec {
|
||||
/// -------------------------------------------------------------------------
|
||||
/// fromMethod
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Creates a BindSpec from a module class method by analyzing its return type,
|
||||
/// annotations, list of parameters (with their own annotations), and async-ness.
|
||||
/// Throws if a method does not have the required @instance() or @provide().
|
||||
/// Constructs a [BindSpec] from an analyzer [MethodElement].
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля, анализируя
|
||||
/// возвращаемый тип, аннотации, параметры (и их аннотации), а также факт
|
||||
/// асинхронности. Если нет @instance или @provide — кидает ошибку.
|
||||
/// -------------------------------------------------------------------------
|
||||
/// Validates and parses all type annotations, method/parameter DI hints,
|
||||
/// and derives async and parametric flags as needed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```dart
|
||||
/// final bindSpec = BindSpec.fromMethod(methodElement);
|
||||
/// print(bindSpec.returnType); // e.g., 'Logger'
|
||||
/// ```
|
||||
/// Throws [AnnotationValidationException] or [CodeGenerationException] if invalid.
|
||||
static BindSpec fromMethod(MethodElement method) {
|
||||
try {
|
||||
// Validate method annotations
|
||||
|
||||
@@ -14,10 +14,36 @@
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
/// Enhanced exception class for CherryPick generator with detailed context information
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// CherryPickGeneratorException
|
||||
///
|
||||
/// The base exception for all CherryPick code generation and annotation
|
||||
/// validation errors. This exception provides enhanced diagnostics including
|
||||
/// the error category, helpful suggestions, and additional debugging context.
|
||||
///
|
||||
/// All errors are structured to be as helpful as possible for users
|
||||
/// running build_runner and for CherryPick contributors debugging generators.
|
||||
///
|
||||
/// ## Example usage:
|
||||
/// ```dart
|
||||
/// if (someErrorCondition) {
|
||||
/// throw AnnotationValidationException(
|
||||
/// 'Custom message about what went wrong',
|
||||
/// element: methodElement,
|
||||
/// suggestion: 'Add @provide() or @instance() annotation',
|
||||
/// context: {'found_annotations': annotations},
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
/// A string describing the error category (for grouping).
|
||||
final String category;
|
||||
|
||||
/// An optional suggestion string for resolving the error.
|
||||
final String? suggestion;
|
||||
|
||||
/// Arbitrary key-value map for additional debugging information.
|
||||
final Map<String, dynamic>? context;
|
||||
|
||||
CherryPickGeneratorException(
|
||||
@@ -50,7 +76,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
buffer.writeln(' Type: ${element.runtimeType}');
|
||||
buffer.writeln(' Location: ${element.source?.fullName ?? 'unknown'}');
|
||||
|
||||
// Note: enclosingElement may not be available in all analyzer versions
|
||||
// Try to show enclosing element info for extra context
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing != null) {
|
||||
@@ -60,7 +86,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
// Ignore if enclosingElement is not available
|
||||
}
|
||||
|
||||
// Additional context
|
||||
// Arbitrary user context
|
||||
if (context != null && context.isNotEmpty) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('Additional Context:');
|
||||
@@ -69,7 +95,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
});
|
||||
}
|
||||
|
||||
// Suggestion
|
||||
// Hint/suggestion if present
|
||||
if (suggestion != null) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('💡 Suggestion: $suggestion');
|
||||
@@ -79,7 +105,24 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific exception types for different error categories
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// AnnotationValidationException
|
||||
///
|
||||
/// Thrown when annotation usage is invalid (e.g., missing required annotation,
|
||||
/// mutually exclusive annotations, or incorrect @named format).
|
||||
///
|
||||
/// Grouped as category "ANNOTATION_VALIDATION".
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```dart
|
||||
/// throw AnnotationValidationException(
|
||||
/// '@instance and @provide cannot be used together',
|
||||
/// element: method,
|
||||
/// suggestion: 'Use only one of @instance or @provide.',
|
||||
/// context: {'method_name': method.displayName},
|
||||
/// );
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class AnnotationValidationException extends CherryPickGeneratorException {
|
||||
AnnotationValidationException(
|
||||
super.message, {
|
||||
@@ -89,6 +132,24 @@ class AnnotationValidationException extends CherryPickGeneratorException {
|
||||
}) : super(category: 'ANNOTATION_VALIDATION');
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// TypeParsingException
|
||||
///
|
||||
/// Thrown when a Dart type cannot be interpreted/parsed for DI,
|
||||
/// or if it's not compatible (void, raw Future, etc).
|
||||
///
|
||||
/// Grouped as category "TYPE_PARSING".
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```dart
|
||||
/// throw TypeParsingException(
|
||||
/// 'Cannot parse injected type',
|
||||
/// element: field,
|
||||
/// suggestion: 'Specify a concrete type. Avoid dynamic and raw Future.',
|
||||
/// context: {'type': field.type.getDisplayString()},
|
||||
/// );
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class TypeParsingException extends CherryPickGeneratorException {
|
||||
TypeParsingException(
|
||||
super.message, {
|
||||
@@ -98,6 +159,23 @@ class TypeParsingException extends CherryPickGeneratorException {
|
||||
}) : super(category: 'TYPE_PARSING');
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// CodeGenerationException
|
||||
///
|
||||
/// Thrown on unexpected code generation or formatting failures
|
||||
/// during generator execution.
|
||||
///
|
||||
/// Grouped as category "CODE_GENERATION".
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```dart
|
||||
/// throw CodeGenerationException(
|
||||
/// 'Could not generate module binding',
|
||||
/// element: classElement,
|
||||
/// suggestion: 'Check module class methods and signatures.',
|
||||
/// );
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class CodeGenerationException extends CherryPickGeneratorException {
|
||||
CodeGenerationException(
|
||||
super.message, {
|
||||
@@ -107,6 +185,23 @@ class CodeGenerationException extends CherryPickGeneratorException {
|
||||
}) : super(category: 'CODE_GENERATION');
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// DependencyResolutionException
|
||||
///
|
||||
/// Thrown if dependency information (for example, types or names)
|
||||
/// cannot be resolved during code generation analysis.
|
||||
///
|
||||
/// Grouped as category "DEPENDENCY_RESOLUTION".
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```dart
|
||||
/// throw DependencyResolutionException(
|
||||
/// 'Dependency type not found in scope',
|
||||
/// element: someElement,
|
||||
/// suggestion: 'Check CherryPick registration for this type.',
|
||||
/// );
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class DependencyResolutionException extends CherryPickGeneratorException {
|
||||
DependencyResolutionException(
|
||||
super.message, {
|
||||
|
||||
@@ -12,45 +12,48 @@
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
|
||||
import 'bind_spec.dart';
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// GeneratedClass -- represents the result of processing a single module class.
|
||||
/// GeneratedClass
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Encapsulates all the information produced from analyzing a DI module class:
|
||||
/// - The original class name,
|
||||
/// - Its generated class name (e.g., `$SomeModule`),
|
||||
/// - The collection of bindings (BindSpec) for all implemented provider methods.
|
||||
/// Represents a processed DI module class with all its binding methods analyzed.
|
||||
/// Stores:
|
||||
/// - The original class name,
|
||||
/// - The generated implementation class name (with $ prefix),
|
||||
/// - The list of all BindSpec for the module methods,
|
||||
/// - The source file name for reference or directive generation.
|
||||
///
|
||||
/// Also provides code generation functionality, allowing to generate the source
|
||||
/// code for the derived DI module class, including all binding registrations.
|
||||
/// Provides static and instance methods to construct from a ClassElement
|
||||
/// and generate Dart source code for the resulting DI registration class.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Описывает результат обработки одного класса-модуля DI:
|
||||
/// - Имя оригинального класса,
|
||||
/// - Имя генерируемого класса (например, `$SomeModule`),
|
||||
/// - Список всех бидингов (BindSpec) — по публичным методам модуля.
|
||||
///
|
||||
/// Также содержит функцию генерации исходного кода для этого класса и
|
||||
/// регистрации всех зависимостей через bind(...).
|
||||
/// ## Example usage
|
||||
/// ```dart
|
||||
/// final gen = GeneratedClass.fromClassElement(myModuleClassElement);
|
||||
/// print(gen.generate());
|
||||
/// /*
|
||||
/// Produces:
|
||||
/// final class $MyModule extends MyModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Service>().toProvide(() => provideService(currentScope.resolve<Dep>()));
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// */
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class GeneratedClass {
|
||||
/// The name of the original module class.
|
||||
/// Имя исходного класса-модуля
|
||||
/// Name of the original Dart module class.
|
||||
final String className;
|
||||
|
||||
/// The name of the generated class (e.g., $SomeModule).
|
||||
/// Имя генерируемого класса (например, $SomeModule)
|
||||
/// Name of the generated class, e.g. `$MyModule`
|
||||
final String generatedClassName;
|
||||
|
||||
/// List of all discovered bindings for the class.
|
||||
/// Список всех обнаруженных биндингов
|
||||
/// Binding specs for all provider/factory methods in the class.
|
||||
final List<BindSpec> binds;
|
||||
|
||||
/// Source file name for the part directive
|
||||
/// Имя исходного файла для part директивы
|
||||
/// Source filename of the module class (for code references).
|
||||
final String sourceFile;
|
||||
|
||||
GeneratedClass(
|
||||
@@ -63,16 +66,15 @@ class GeneratedClass {
|
||||
/// -------------------------------------------------------------------------
|
||||
/// fromClassElement
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Static factory: creates a GeneratedClass from a Dart ClassElement (AST representation).
|
||||
/// Discovers all non-abstract methods, builds BindSpec for each, and computes the
|
||||
/// generated class name by prefixing `$`.
|
||||
/// Creates a [GeneratedClass] by analyzing a Dart [ClassElement].
|
||||
/// Collects all public non-abstract methods, creates a [BindSpec] for each,
|
||||
/// and infers the generated class name using a `$` prefix.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Строит объект класса по элементу AST (ClassElement): имя класса,
|
||||
/// сгенерированное имя, список BindSpec по всем не абстрактным методам.
|
||||
/// Имя ген-класса строится с префиксом `$`.
|
||||
/// -------------------------------------------------------------------------
|
||||
/// ## Example usage
|
||||
/// ```dart
|
||||
/// final gen = GeneratedClass.fromClassElement(classElement);
|
||||
/// print(gen.generatedClassName); // e.g. $AppModule
|
||||
/// ```
|
||||
static GeneratedClass fromClassElement(ClassElement element) {
|
||||
final className = element.displayName;
|
||||
// Generated class name with '$' prefix (standard for generated Dart code).
|
||||
@@ -91,16 +93,19 @@ class GeneratedClass {
|
||||
/// -------------------------------------------------------------------------
|
||||
/// generate
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Generates Dart source code for the DI module class. The generated class
|
||||
/// inherits from the original, overrides the 'builder' method, and registers
|
||||
/// all bindings in the DI scope.
|
||||
/// Generates the Dart source code for the DI registration class.
|
||||
/// The generated class extends the original module, and the `builder` method
|
||||
/// registers all bindings (dependencies) into the DI scope.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Генерирует исходный Dart-код для класса-модуля DI.
|
||||
/// Новая версия класса наследует оригинальный, переопределяет builder(Scope),
|
||||
/// и регистрирует все зависимости через методы bind<Type>()...
|
||||
/// -------------------------------------------------------------------------
|
||||
/// ## Example output
|
||||
/// ```dart
|
||||
/// final class $UserModule extends UserModule {
|
||||
/// @override
|
||||
/// void builder(Scope currentScope) {
|
||||
/// bind<Service>().toProvide(() => provideService(currentScope.resolve<Dep>()));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
String generate() {
|
||||
final buffer = StringBuffer()
|
||||
..writeln('final class $generatedClassName extends $className {')
|
||||
@@ -108,7 +113,6 @@ class GeneratedClass {
|
||||
..writeln(' void builder(Scope currentScope) {');
|
||||
|
||||
// For each binding, generate bind<Type>() code string.
|
||||
// Для каждого биндинга — генерируем строку bind<Type>()...
|
||||
for (final bind in binds) {
|
||||
buffer.writeln(bind.generateBind(4));
|
||||
}
|
||||
|
||||
@@ -14,30 +14,32 @@
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// MetadataUtils -- utilities for analyzing method and parameter annotations.
|
||||
/// MetadataUtils
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Provides static utility methods to analyze Dart annotations on methods or
|
||||
/// parameters. For instance, helps to find if an element is annotated with
|
||||
/// `@named()`, `@singleton()`, or other meta-annotations used in this DI framework.
|
||||
/// Static utilities for querying and extracting information from
|
||||
/// Dart annotations ([ElementAnnotation]) in the context of code generation,
|
||||
/// such as checking for the presence of specific DI-related annotations.
|
||||
/// Designed to be used internally by code generation and validation routines.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Утилиты для разбора аннотаций методов и параметров.
|
||||
/// Позволяют находить наличие аннотаций, например, @named() и @singleton(),
|
||||
/// у методов и параметров. Используется для анализа исходного кода при генерации.
|
||||
/// # Example usage
|
||||
/// ```dart
|
||||
/// if (MetadataUtils.anyMeta(method.metadata, 'singleton')) {
|
||||
/// // The method is annotated with @singleton
|
||||
/// }
|
||||
/// final name = MetadataUtils.getNamedValue(field.metadata);
|
||||
/// if (name != null) print('@named value: $name');
|
||||
/// ```
|
||||
/// ---------------------------------------------------------------------------
|
||||
class MetadataUtils {
|
||||
/// -------------------------------------------------------------------------
|
||||
/// anyMeta
|
||||
/// Checks whether any annotation in [meta] matches the [typeName]
|
||||
/// (type name is compared in a case-insensitive manner and can be partial).
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Checks if any annotation in the list has a type name that contains
|
||||
/// [typeName] (case insensitive).
|
||||
/// Returns true if an annotation (such as @singleton, @provide, @named) is found.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName]
|
||||
/// (регистр не учитывается).
|
||||
/// -------------------------------------------------------------------------
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// bool isSingleton = MetadataUtils.anyMeta(myMethod.metadata, 'singleton');
|
||||
/// ```
|
||||
static bool anyMeta(List<ElementAnnotation> meta, String typeName) {
|
||||
return meta.any((m) =>
|
||||
m
|
||||
@@ -49,17 +51,15 @@ class MetadataUtils {
|
||||
false);
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
/// getNamedValue
|
||||
/// Extracts the string value from a `@named('value')` annotation if present in [meta].
|
||||
///
|
||||
/// ENGLISH
|
||||
/// Retrieves the value from a `@named('value')` annotation if present.
|
||||
/// Returns the string value or null if not found.
|
||||
/// Returns the named value or `null` if not annotated.
|
||||
///
|
||||
/// RUSSIAN
|
||||
/// Находит значение из аннотации @named('значение').
|
||||
/// Возвращает строку значения, если аннотация присутствует; иначе null.
|
||||
/// -------------------------------------------------------------------------
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// // For: @named('dev') ApiClient provideApi() ...
|
||||
/// final named = MetadataUtils.getNamedValue(method.metadata); // 'dev'
|
||||
/// ```
|
||||
static String? getNamedValue(List<ElementAnnotation> meta) {
|
||||
for (final m in meta) {
|
||||
final cv = m.computeConstantValue();
|
||||
|
||||
@@ -16,9 +16,35 @@ import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'exceptions.dart';
|
||||
|
||||
/// Enhanced type parser that uses AST analysis instead of regular expressions
|
||||
/// Utility for analyzing and parsing Dart types for CherryPick DI code generation.
|
||||
///
|
||||
/// This type parser leverages the Dart analyzer AST to extract nuanced information
|
||||
/// from Dart types encountered in the source code, in particular for dependency
|
||||
/// injection purposes. It is capable of extracting nullability, generics,
|
||||
/// and Future-related metadata with strong guarantees and handles even nested generics.
|
||||
///
|
||||
/// # Example usage for parsing types:
|
||||
/// ```dart
|
||||
/// final parsed = TypeParser.parseType(method.returnType, method);
|
||||
/// print(parsed);
|
||||
/// print(parsed.resolveMethodName); // e.g. "resolveAsync" or "tryResolve"
|
||||
/// ```
|
||||
///
|
||||
/// # Supported scenarios:
|
||||
/// - Nullable types (e.g., `List<String>?`)
|
||||
/// - Generic types (e.g., `Map<String, User>`)
|
||||
/// - Async types (`Future<T>`, including nested generics)
|
||||
/// - Validation for DI compatibility (throws for `void`, warns on `dynamic`)
|
||||
class TypeParser {
|
||||
/// Parses a DartType and extracts detailed type information
|
||||
/// Parses a [DartType] and extracts detailed type information for use in code generation.
|
||||
///
|
||||
/// If a type is not suitable or cannot be parsed, a [TypeParsingException] is thrown.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final parsed = TypeParser.parseType(field.type, field);
|
||||
/// if (parsed.isNullable) print('Field is nullable');
|
||||
/// ```
|
||||
static ParsedType parseType(DartType dartType, Element context) {
|
||||
try {
|
||||
return _parseTypeInternal(dartType, context);
|
||||
@@ -49,7 +75,7 @@ class TypeParser {
|
||||
return _parseGenericType(dartType, context, isNullable);
|
||||
}
|
||||
|
||||
// Simple type
|
||||
// Simple type (non-generic, non-Future)
|
||||
return ParsedType(
|
||||
displayString: displayString,
|
||||
coreType: displayString.replaceAll('?', ''),
|
||||
@@ -103,7 +129,15 @@ class TypeParser {
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates that a type is suitable for dependency injection
|
||||
/// Validates that a parsed type is suitable for dependency injection.
|
||||
///
|
||||
/// Throws [TypeParsingException] for void and may warn for dynamic.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final parsed = TypeParser.parseType(field.type, field);
|
||||
/// TypeParser.validateInjectableType(parsed, field);
|
||||
/// ```
|
||||
static void validateInjectableType(ParsedType parsedType, Element context) {
|
||||
// Check for void type
|
||||
if (parsedType.coreType == 'void') {
|
||||
@@ -131,7 +165,7 @@ class TypeParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a parsed type with detailed information
|
||||
/// Represents a parsed type with full metadata for code generation.
|
||||
class ParsedType {
|
||||
/// The full display string of the type (e.g., "Future<List<String>?>")
|
||||
final String displayString;
|
||||
@@ -139,19 +173,19 @@ class ParsedType {
|
||||
/// The core type name without nullability and Future wrapper (e.g., "List<String>")
|
||||
final String coreType;
|
||||
|
||||
/// Whether the type is nullable
|
||||
/// True if nullable (has `?`)
|
||||
final bool isNullable;
|
||||
|
||||
/// Whether the type is wrapped in Future
|
||||
/// True if this type is a `Future<T>`
|
||||
final bool isFuture;
|
||||
|
||||
/// Whether the type has generic parameters
|
||||
/// True if the type is a generic type (`List<T>`)
|
||||
final bool isGeneric;
|
||||
|
||||
/// Parsed type arguments for generic types
|
||||
/// List of parsed type arguments in generics, if any.
|
||||
final List<ParsedType> typeArguments;
|
||||
|
||||
/// For Future types, the inner type
|
||||
/// For `Future<T>`, this is the type inside the `Future`.
|
||||
final ParsedType? innerType;
|
||||
|
||||
const ParsedType({
|
||||
@@ -164,7 +198,11 @@ class ParsedType {
|
||||
this.innerType,
|
||||
});
|
||||
|
||||
/// Returns the type string suitable for code generation
|
||||
/// Generates the type string suitable for code generation.
|
||||
///
|
||||
/// - For futures, the codegen type of the inner type is returned
|
||||
/// - For generics, returns e.g. `List<User>`
|
||||
/// - For plain types, just the name
|
||||
String get codeGenType {
|
||||
if (isFuture && innerType != null) {
|
||||
return innerType!.codeGenType;
|
||||
@@ -179,10 +217,10 @@ class ParsedType {
|
||||
return coreType;
|
||||
}
|
||||
|
||||
/// Returns whether this type should use tryResolve instead of resolve
|
||||
/// True if this type should use `tryResolve` instead of `resolve` for DI.
|
||||
bool get shouldUseTryResolve => isNullable;
|
||||
|
||||
/// Returns the appropriate resolve method name
|
||||
/// Returns the method name for DI, e.g. "resolve", "tryResolveAsync", etc.
|
||||
String get resolveMethodName {
|
||||
if (isFuture) {
|
||||
return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync';
|
||||
|
||||
@@ -2,8 +2,9 @@ name: cherrypick_generator
|
||||
description: |
|
||||
Source code generator for the cherrypick dependency injection system. Processes annotations to generate binding and module code for Dart & Flutter projects.
|
||||
|
||||
version: 1.1.1
|
||||
documentation: https://github.com/pese-git/cherrypick/wiki
|
||||
version: 2.0.0-dev.1
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick/cherrypick_generator
|
||||
issue_tracker: https://github.com/pese-git/cherrypick/issues
|
||||
topics:
|
||||
@@ -18,7 +19,7 @@ environment:
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
cherrypick_annotations: ^1.1.1
|
||||
cherrypick_annotations: ^1.1.2-dev.1
|
||||
analyzer: ^7.0.0
|
||||
dart_style: ^3.0.0
|
||||
build: ^2.4.1
|
||||
|
||||
@@ -244,8 +244,7 @@ void main() {
|
||||
final result = bindSpec.generateBind(4);
|
||||
expect(
|
||||
result,
|
||||
equals(
|
||||
" bind<ApiClient>()\n"
|
||||
equals(" bind<ApiClient>()\n"
|
||||
" .toProvideAsync(() => createApiClient())\n"
|
||||
" .withName('mainApi')\n"
|
||||
" .singleton();"));
|
||||
|
||||
@@ -127,28 +127,28 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.7"
|
||||
version: "3.0.0-dev.9"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.1"
|
||||
version: "1.1.2-dev.0"
|
||||
cherrypick_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.3-dev.7"
|
||||
version: "1.1.3-dev.9"
|
||||
cherrypick_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.1"
|
||||
version: "2.0.0-dev.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
import 'router/app_router.dart';
|
||||
@@ -14,9 +13,11 @@ 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;
|
||||
static Talker of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
|
||||
@override
|
||||
bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker;
|
||||
bool updateShouldNotify(TalkerProvider oldWidget) =>
|
||||
oldWidget.talker != talker;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
|
||||
@@ -15,14 +15,16 @@ abstract class AppModule extends Module {
|
||||
@provide()
|
||||
@singleton()
|
||||
TalkerDioLoggerSettings talkerDioLoggerSettings() => TalkerDioLoggerSettings(
|
||||
printRequestHeaders: true,
|
||||
printResponseHeaders: true,
|
||||
printResponseMessage: true,
|
||||
);
|
||||
printRequestHeaders: true,
|
||||
printResponseHeaders: true,
|
||||
printResponseMessage: true,
|
||||
);
|
||||
|
||||
@provide()
|
||||
@singleton()
|
||||
TalkerDioLogger talkerDioLogger(Talker talker, TalkerDioLoggerSettings settings) => TalkerDioLogger(talker: talker, settings: settings);
|
||||
TalkerDioLogger talkerDioLogger(
|
||||
Talker talker, TalkerDioLoggerSettings settings) =>
|
||||
TalkerDioLogger(talker: talker, settings: settings);
|
||||
|
||||
@instance()
|
||||
int timeout() => 1000;
|
||||
|
||||
@@ -13,7 +13,6 @@ void main() {
|
||||
final talker = Talker();
|
||||
final talkerLogger = TalkerCherryPickObserver(talker);
|
||||
|
||||
|
||||
Bloc.observer = TalkerBlocObserver(talker: talker);
|
||||
|
||||
CherryPick.setGlobalObserver(talkerLogger);
|
||||
@@ -24,7 +23,10 @@ void main() {
|
||||
}
|
||||
|
||||
// Используем safe root scope для гарантии защиты
|
||||
CherryPick.openRootScope().installModules([CoreModule(talker: talker), $AppModule()]);
|
||||
CherryPick.openRootScope()
|
||||
.installModules([CoreModule(talker: talker), $AppModule()]);
|
||||
|
||||
runApp(MyApp(talker: talker,));
|
||||
runApp(MyApp(
|
||||
talker: talker,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -175,21 +175,21 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.7"
|
||||
version: "3.0.0-dev.9"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_annotations"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.1"
|
||||
version: "1.1.2-dev.0"
|
||||
cherrypick_generator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../cherrypick_generator"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.1"
|
||||
version: "2.0.0-dev.0"
|
||||
cli_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -864,7 +864,7 @@ packages:
|
||||
path: "../../talker_cherrypick_logger"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
version: "1.1.0-dev.3"
|
||||
talker_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
29
talker_cherrypick_logger/.gitignore
vendored
29
talker_cherrypick_logger/.gitignore
vendored
@@ -1,7 +1,26 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Avoid committing pubspec.lock for library packages; see
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
# Avoid committing generated Javascript files:
|
||||
*.dart.js
|
||||
*.info.json # Produced by the --dump-info flag.
|
||||
*.js # When generated by dart2js. Don't specify *.js if your
|
||||
# project includes source files written in JavaScript.
|
||||
*.js_
|
||||
*.js.deps
|
||||
*.js.map
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
|
||||
pubspec_overrides.yaml
|
||||
@@ -1,3 +1,23 @@
|
||||
## 1.1.0-dev.5
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
## 1.1.0-dev.4
|
||||
|
||||
- **DOCS**(readme): update install instructions to use pub.dev as default method and remove obsolete git example.
|
||||
|
||||
## 1.1.0-dev.3
|
||||
|
||||
## 1.1.0-dev.2
|
||||
|
||||
- Bump "talker_cherrypick_logger" to `1.1.0-dev.2`.
|
||||
|
||||
## 1.1.0-dev.0
|
||||
|
||||
- **FEAT**(logging): add talker_dio_logger and talker_bloc_logger integration, improve cherrypick logger structure, add UI log screen for DI and network/bloc debug.
|
||||
- **DOCS**: add full English documentation and usage guide to README.md.
|
||||
- **DOCS**: add detailed English documentation and usage examples for TalkerCherryPickObserver.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
|
||||
@@ -21,15 +21,14 @@ All CherryPick lifecycle events, instance creations, cache operations, module ac
|
||||
|
||||
### 1. Add dependencies
|
||||
|
||||
Install the package **from [pub.dev](https://pub.dev/packages/talker_cherrypick_logger)**:
|
||||
|
||||
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
|
||||
talker_cherrypick_logger: ^latest
|
||||
```
|
||||
|
||||
### 2. Import the package
|
||||
|
||||
@@ -69,26 +69,32 @@ class TalkerCherryPickObserver implements CherryPickObserver {
|
||||
|
||||
/// 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)');
|
||||
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)');
|
||||
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)');
|
||||
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)');
|
||||
talker.info(
|
||||
'[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
}
|
||||
|
||||
/// Called when a DI scope is opened.
|
||||
@@ -106,7 +112,8 @@ class TalkerCherryPickObserver implements CherryPickObserver {
|
||||
/// 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)' : ''}');
|
||||
talker.warning(
|
||||
'[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
|
||||
}
|
||||
|
||||
/// Called when an instance is found in the cache.
|
||||
@@ -136,6 +143,7 @@ class TalkerCherryPickObserver implements CherryPickObserver {
|
||||
/// 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');
|
||||
talker.handle(error ?? '[CherryPick] $message', stackTrace,
|
||||
'[error][CherryPick] $message');
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
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
|
||||
description: A Talker logger integration for CherryPick DI to observe and log DI events and errors.
|
||||
version: 1.1.0-dev.5
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick
|
||||
issue_tracker: https://github.com/pese-git/cherrypick/issues
|
||||
|
||||
topics:
|
||||
- cherrypick
|
||||
- state
|
||||
- logging
|
||||
- log
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
sdk: ">=3.5.2 <4.0.0"
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
talker: ^4.9.3
|
||||
cherrypick: ^3.0.0-dev.8
|
||||
cherrypick: ^3.0.0-dev.10
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -15,7 +15,8 @@ void main() {
|
||||
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)'));
|
||||
expect(log.message,
|
||||
contains('[request][CherryPick] A — String (scope: test)'));
|
||||
});
|
||||
|
||||
test('onCycleDetected logs warning', () {
|
||||
|
||||
Reference in New Issue
Block a user