Dart / Integration: targets, resources, validation, and evidence / Lesson 18
Review a typed offline batch-processing pipeline
Combine parsing, sealed outcomes, record summaries, immutable boundaries, and explicit Future ownership without claiming a production service.
On this page
Lesson goals
- Preserve one understandable outcome for each supplied input before aggregation.
- Separate local policy evidence from platform, persistence, authorization, and recovery requirements.
Write the small promise the capstone actually implements
This capstone accepts fixed text counts, labels valid positive values, and summarizes a small list. It does not reserve real resources, administer samples, write files, contact providers, or persist work. The invented lab policy permits one through one thousand units, so the source has an explicit bounded rule rather than an unexplained default. Dart 3 sealed classes, object patterns, and records require a compatible language version. Keep those requirements visible alongside the missing operational responsibilities. A correct local program is useful evidence for its rule, not a certificate that a distributed application is ready to publish.
Retain why an input was rejected
The parser distinguishes malformed integer text from a count outside the allowed range. Each sealed alternative carries the data it needs, and an exhaustive switch converts it into safe display text. Trimming is an intentional policy for this text count, not a universal identifier-normalization rule. The constructors in this teaching model are public within the source, so callers could create a ValidCount directly without parsing; a larger application should decide where trusted construction belongs. The example reports one outcome per input instead of silently dropping invalid entries and claiming complete success.
sealed class CountResult {}
final class ValidCount extends CountResult {
final int value;
ValidCount(this.value);
}
final class InvalidCount extends CountResult {
final String reason;
InvalidCount(this.reason);
}
CountResult parseCount(String text) {
final value = int.tryParse(text.trim());
if (value == null) return InvalidCount('not-an-int');
if (value < 1 || value > 1000) return InvalidCount('out-of-range');
return ValidCount(value);
}
String label(CountResult result) => switch (result) {
ValidCount(value: final value) => 'valid:$value',
InvalidCount(reason: final reason) => 'invalid:$reason',
};
void main() {
for (final input in <String>[' 2 ', 'bad', '0']) {
print(label(parseCount(input)));
}
}
valid:2
invalid:not-an-int
invalid:out-of-range
Expected output is valid:2, invalid:not-an-int, invalid:out-of-range. No Dart analyzer or runtime was executed, and no real batch is created.
Aggregate only after choosing an acceptance policy
A summary can count accepted and rejected values while totaling only accepted units. That is different from returning every rejection reason, so preserve detailed results if the product needs them. The second example returns a named record with accepted, rejected, and total fields. Its fixed small inputs keep arithmetic within a simple range; the code does not claim arbitrary-size numeric safety across all targets. A reusable summary should document whether it consumes the source once, whether lazy input can change between traversals, and whether partial acceptance is permitted rather than inferred from a filter operation.
Await the declared result without inventing background durability
The async wrapper below performs local work and returns its record through a Future. It demonstrates that the caller waits before printing the summary; it does not move the loop into another isolate or persist a background job. If real I/O is added later, keep it behind an explicit owned boundary with error and cancellation policy. Future timeout alone would not undo that operation. Keep parsing and the summary independently checkable with fixed values so platform failures do not become mislabeled as malformed counts.
import 'dart:async';
Future<({int accepted, int rejected, int total})> summarize(List<String> inputs) async {
var accepted = 0;
var rejected = 0;
var total = 0;
for (final text in inputs) {
final count = int.tryParse(text.trim());
if (count == null || count < 1 || count > 1000) {
rejected += 1;
} else {
accepted += 1;
total += count;
}
}
return (accepted: accepted, rejected: rejected, total: total);
}
Future<void> main() async {
final result = await summarize(<String>['2', 'bad', '0', '3']);
print('accepted=${result.accepted}');
print('rejected=${result.rejected}');
print('total=${result.total}');
}
accepted=2
rejected=2
total=5
Expected output is accepted=2, rejected=2, total=5. The Future represents local completion only. This source was not compiled, run, benchmarked, or integrated with any service.
End with an evidence plan that matches the actual claims
Check malformed text, zero, negatives, the upper boundary, whitespace, and ordinary values in the project’s declared SDK. Review whether constructors can bypass validation, whether every result is handled, and whether output fields are stable for callers. Test alias mutation and repeated traversal if inputs become richer or lazy. A real service adds resource authorization, authoritative concurrency control, persistence, request identity, and recovery evidence. The authoring validation here checks JSON structure and bilingual technical parity, not Dart execution. Report those limits honestly and avoid calling a planned test passed merely because its expected output is easy to predict.
Common errors and fixes
Invalid entries vanish and the summary is labeled complete success.
- Why it happens
- Partial acceptance was introduced without a product contract.
- How to fix it
- Retain rejection outcomes or explicitly report partial acceptance.
- Prevent it next time
- Define completeness before choosing filtering or aggregation.
An async summary is assumed to run on another CPU worker.
- Why it happens
- Future completion was confused with isolate execution.
- How to fix it
- Keep the local model explicit and add an isolate only for a justified supported target.
- Prevent it next time
- Separate performance hypotheses from language-syntax examples.
The capstone is presented as a durable production batch service.
- Why it happens
- Missing persistence, authority, and recovery boundaries were ignored.
- How to fix it
- State the offline scope and request separate implementation evidence for real effects.
- Prevent it next time
- Tie every readiness claim to tests actually executed in the declared environment.
Check your understanding
1. What changes if callers need the exact reason for every rejected count?
Show worked solution
Keep one typed result per input and aggregate that validated result list while retaining the detailed records. A count of rejections alone cannot recover the original reasons or input association. Define whether sensitive raw input is retained, and preserve stable error codes separately from display wording. Do not replace the richer contract with a silent map-and-filter shortcut.
2. List three distinct evidence boundaries before this capstone becomes a real service.
Show worked solution
First compile and test its local policy in the declared SDK and target. Then verify authenticated resource authorization and authoritative state transitions, including concurrency. Finally verify persistence, request identity, and recovery for external or asynchronous effects. These may require several tests each. Printing the predicted total in a local example proves none of the missing service guarantees.
Key takeaways
- Typed local stages are valuable when their guarantees and limits remain explicit.
- A final report must distinguish source reasoning, executed checks, and untested service obligations.