dey_dev_utils

Small, focused utilities for Dart development, with explicit APIs for common functional programming and control-flow patterns.

This package is also a place to explore ideas with AI assistance (many of things here came up from there). AI can help with development concepts, suggest classes and utilities, explain trade-offs, and help implement or test those ideas. The package itself has no AI runtime dependency: the AI guidance lives in the repository's skills and documentation.

Features

The package's tested core includes five libraries:

  • Result: represent success and typed failure explicitly with Ok and Err.
  • Scope helpers: run blocks, manage cleanup, observe values, transform values, and filter them fluently.
  • Type-safe comparison: use Comparison.less, Comparison.equal, and Comparison.greater instead of unexplained comparison integers.
  • Trampoline: evaluate synchronous bounce chains iteratively to avoid call stack growth in deep computations.
  • Specification pattern: express and compose Boolean business rules.

It also includes experimental libraries for options, validation, lenses, isomorphisms, middleware, sagas, tracing, rate limiting, contracts, monoids, and lazy initialization.

Getting started

Requires Dart SDK 3.13.7 or newer.

Add the package to your project:

dart pub add dey_dev_utils

Then run:

dart pub get

There is currently no umbrella library. Import the utility you need directly:

import 'package:dey_dev_utils/result.dart';

Usage

Explicit results

Use Result<T, E> when an operation can fail and the failure should remain visible in its return type:

import 'package:dey_dev_utils/result.dart';

Result<int, FormatException> parseNumber(String input) {
	final value = int.tryParse(input);
	return value == null
			? Result.err(FormatException('Not a number: $input'), StackTrace.current)
			: Result.ok(value);
}

final result = parseNumber('30');
final number = result.unwrapOr(0);

Result.guardSync / Result.guardAsync convert a matching exception type into Ok/Err while allowing unrelated errors to propagate, and the guardExceptionSync / guardExceptionAsync variants keep Error values uncaught.

Scope helpers

Scope utilities support readable transformations and resource cleanup:

import 'package:dey_dev_utils/scope.dart';

final label = 30
		.let((value) => value * 2)
		.also((value) => print('Computed: $value'))
		.toString();

final content = use(
	'temporary resource',
	(resource) => resource.length,
	dispose: (resource) => print('Releasing $resource'),
);

Use applyIf for type-preserving conditional changes and takeIf or takeUnless when a value should be kept or discarded.

Readable comparisons

import 'package:dey_dev_utils/comparison.dart';

final ordering = 3.compareWith(5);
if (ordering == Comparison.less) {
	print('3 is smaller than 5');
}

final isReady = 10.isGreaterThan(5);

Stack-safe synchronous steps

Build each next step lazily with Bounce.more, then evaluate the chain with trampoline:

import 'package:dey_dev_utils/trampoline.dart';

Bounce<int> sumDown(int value, int total) {
	if (value == 0) return Bounce.done(total);
	return Bounce.more(() => sumDown(value - 1, total + value));
}

final result = trampoline(sumDown(3, 0)); // 6

The trampoline is synchronous. It does not schedule asynchronous work or automatically make arbitrary recursive code stack-safe.

Composable specifications

Represent each business rule as a reusable Boolean specification. Create rules with predicate, always, or never, then combine them with &, |, ^, nand, nor, xnor, implies, iff, or ~/negated():

import 'package:dey_dev_utils/spec.dart';

final isPositive = Spec<int>.predicate((value) => value > 0);
final isUnderOneHundred = Spec<int>.predicate((value) => value < 100);
final accepted = Spec<int>.allOf([isPositive, isUnderOneHundred]);

// .call for tear-offs, instead of using .isSatisfiedBy
final acceptedValues = [12, -3, 100].where(accepted.call).toList(); // [12]

For a named rule, use a final, base, or sealed subclass because Spec<T> is an abstract base class:

final class IsAdult extends Spec<int> {
	const IsAdult();

	@override
	bool isSatisfiedBy(int age) => age >= 18;
}

final canVote = IsAdult() & Spec<int>.predicate((age) => age.isEven);

contramap adapts a specification to another candidate type through a projection. Aggregate factories snapshot their input when created; allOf and noneOf of an empty iterable are satisfied, while anyOf is not. AND, OR, NAND, and NOR short-circuit in evaluation order. XOR, XNOR, and iff evaluate both operands. Predicate callbacks run on every evaluation and are not cached.

Experimental APIs

The following libraries are not exported and may change or be removed in future releases without the same compatibility guarantees as the tested core:

  • iso.dart for reversible transformations between types
  • lens.dart for composable access to nested immutable data
  • middleware.dart for asynchronous handlers and pipelines
  • monoid.dart for identity and associative combination
  • once.dart for lazy one-time initialization
  • rate_limit.dart for debouncing and throttling
  • saga.dart for compensating asynchronous workflows
  • trace.dart for timing and logging wrappers
  • validate.dart for accumulating validation results

The experimental option.dart for Some and None values.

Contract checks

runWithChecking and runWithCheckingAsync evaluate a precondition, run an operation, and check its result. They default to ContractCheckingMode.assertionsOnly, where failed checks use Dart assertions and callbacks are not evaluated when assertions are disabled. Pass checkingMode: ContractCheckingMode.alwaysThrow to enforce checks with ContractViolationError regardless of assertion settings. The Invariant mixin provides callback-based pre and post checks, explicit verifyInvariant, and sync/async runWithInvariant helpers. Those helpers evaluate the invariant even when the body throws; if both the body and invariant check fail, the invariant failure takes precedence.

import 'package:dey_dev_utils/contract.dart';

final input = '  value  ';
final result = runWithChecking(
  preCondition: () => input.isNotEmpty,
  operation: () => input.trim(),
  postCondition: (value) => value.isNotEmpty,
  checkingMode: ContractCheckingMode.alwaysThrow,
);

Use runWithCheckingAsync for an asynchronous operation:

final result = await runWithCheckingAsync(
  preCondition: () => input.isNotEmpty,
  operation: () async => input.trim(),
  postCondition: (value) => value.isNotEmpty,
  checkingMode: ContractCheckingMode.alwaysThrow,
);

AI skills for stable libraries

The repository includes reusable AI skills for the stable libraries. They give an AI coding assistant the API model, important semantics, examples, testing guidance, and review checklists for:

To use them, open the relevant SKILL.md file in your AI coding assistant or copy its path into the assistant's context. Then ask for the task while naming the library. For example:

Use the dev-utils Result skill to review this function and add focused tests for its success, typed failure, and stack-trace behavior.

These skills are useful for implementation, refactoring, documentation, testing, and code review. They are guidance for development tools, not code that is loaded by your Dart application.

Development

Run the test suite and static analysis locally:

dart analyze
dart test

GitHub Actions runs formatting, analysis, and tests on every push and pull request. Dedupe, cognitive complexity, and undead reports are published as workflow artifacts and summaries for review; they are advisory while the APIs are evolving. Mutation testing runs weekly and on manual workflow dispatch, and its report is advisory as well. Package contents are validated before release publishing.

Contributions are welcome. Please open an issue for bugs or ideas, and include the smallest reproducible example when reporting a problem. See the repository for source code and project discussions.

The tested core is released as version 1.0.0.

License

dey_dev_utils is available under the MIT License. Copyright (c) 2026 ElanDeyan.