gmana

Core Dart utilities for production apps: functional result types, focused extensions, validation primitives, ID generation, debounce/throttle helpers, and stream helpers.

gmana is pure Dart, so it works in CLI tools, server apps, packages, and Flutter apps.

For a function-by-function guide with examples, see doc/api.md.

Installation

dart pub add gmana

For Flutter projects:

flutter pub add gmana

Manual pubspec.yaml setup:

dependencies:
  gmana: ^0.2.0

Imports

Use the umbrella import when you want the curated public API:

import 'package:gmana/gmana.dart';

Or import only the area you need:

import 'package:gmana/extensions.dart';
import 'package:gmana/functional.dart';
import 'package:gmana/utilities.dart';
import 'package:gmana/validation.dart';

What You Can Use

Area APIs
Functional results Either, Result, FutureResult, Failure, Unit, UseCase
String extensions casing, parsing, blank handling, slugs, duration parsing, truncation
Number and duration extensions 5.seconds, 2.hours, rounding, normalization, time formatting
Iterable and list extensions sum, average, median, chunked, groupBy, flatten, whereNotNull
Stream extensions debounce, throttle, scan, pairwise, whereNotNull, onErrorReturn
Validation email, password, number, text validators, and predicate functions
Utilities IdGenerator, Debouncer, Throttler

Functional Results

Use Either<L, R> when a function can fail and you want explicit success and failure handling instead of exceptions.

import 'package:gmana/functional.dart';

Either<String, int> divide(int a, int b) {
  if (b == 0) return const Left('Cannot divide by zero');
  return Right(a ~/ b);
}

void main() {
  final result = divide(10, 2);

  final message = result.fold(
    (error) => 'Error: $error',
    (value) => 'Result: $value',
  );

  print(message); // Result: 5
}

Common helpers:

final result = Right<String, int>(10);

final doubled = result.map((value) => value * 2);
final value = doubled.getOrElse((error) => 0);
final isSuccess = doubled.isRight();
final nullable = doubled.rightOrNull();
final hasTwenty = doubled.contains(20);

final saved = await doubled
    .tap((value) => print('Saving $value'))
    .flatMapAsync((value) async => Right<String, int>(value + 1));

Use Cases

UseCase gives clean-architecture style boundaries for asynchronous operations that may fail.

import 'package:gmana/functional.dart';

class LoginParams {
  const LoginParams({
    required this.email,
    required this.password,
  });

  final String email;
  final String password;
}

class LoginUseCase implements UseCase<String, LoginParams> {
  @override
  FutureResult<String> call(LoginParams params) async {
    if (params.email.trim().isEmpty) {
      return const Left(Failure('Email is required'));
    }

    return const Right('auth-token');
  }
}

For operations with no meaningful success value, return unit:

FutureResultUnit saveSettings() async {
  return const Right(unit);
}

Use StreamUseCase when a use case emits fallible values over time:

class WatchCountUseCase implements StreamUseCase<int, NoParams> {
  @override
  StreamResult<int> call(NoParams params) async* {
    yield const Right<Failure, int>(1);
    yield const Left<Failure, int>(Failure('Stream stopped', 'counter_stopped'));
  }
}

String Extensions

import 'package:gmana/extensions.dart';

void main() {
  print('hello world'.toTitleCase); // Hello World
  print('hello world'.toSentenceCase); // Hello world
  print('helloWorld'.toSnakeCase); // hello_world
  print('Hello World! 2026'.toSlug); // hello-world-2026

  print('42'.toIntOrNull); // 42
  print('bad'.toIntOrZero); // 0
  print('12.5'.toDoubleOrNull); // 12.5

  print('01:30'.toDuration()); // 0:01:30.000000
  print('A long article body'.readingTimeMinutes); // 1
  print('Hello World'.truncate(8)); // Hello...
}

Nullable string helpers:

String? name;

final safe = name.orEmpty; // ''
final normalized = '   '.blankToNull; // null
final displayName = name.mapNotBlank((value) => value.toTitleCase);

Number And Duration Extensions

import 'package:gmana/extensions.dart';

final retryDelay = 500.ms;
final timeout = 30.seconds;
final cacheTtl = 2.hours;

await retryDelay.delay;

print(25.celsiusToFahrenheit); // 77.0
print(260.normalized(0, 300).roundTo(2)); // 0.87
print(27.roundToMultiple(5)); // 25
print(3.to(6).toList()); // [3, 4, 5, 6]

Duration formatting:

final duration = Duration(hours: 1, minutes: 2, seconds: 34);

print(duration.toHumanizedString()); // 1:02:34
print(duration.toPaddedString()); // 01:02:34
print(duration.toVerboseString()); // 1h 2m 34s
print(duration.toWordString()); // 1 hour, 2 minutes, 34 seconds
print(Duration(minutes: -5).toRelativeString()); // 5 minutes ago

Iterable, List, And Stream Extensions

import 'package:gmana/extensions.dart';

final scores = [10, 20, 30, 40];

print(scores.sum()); // 100
print(scores.average); // 25.0
print(scores.median); // 25.0
print(scores.top(2)); // [40, 30]
print(scores.runningSum().toList()); // [10, 30, 60, 100]

print([1, 2, 3, 4, 5].chunked(2).toList()); // [[1, 2], [3, 4], [5]]
print(['a', 'bb', 'c'].groupBy((value) => value.length)); // {1: [a, c], 2: [bb]}
print([[1, 2], [3, 4]].flattenToList()); // [1, 2, 3, 4]
print([1, null, 2].whereNotNull.toList()); // [1, 2]

Stream helpers:

final values = Stream.fromIterable([1, 1, 2, 3]);

values
    .distinctUntilChanged()
    .scan(0, (total, value) => total + value)
    .listen(print); // 1, 3, 6

List-emitting streams:

Stream.value([1, 2, 3, 4])
    .filter((value) => value.isEven)
    .listen(print); // [2, 4]

Validation

The canonical validators return Either<Issue, Value>. A Right contains the normalized value. A Left contains a typed validation issue.

import 'package:gmana/validation.dart';

final emailResult = const EmailValidator().validate(' User@Example.com ');

emailResult.fold(
  (issue) => print(resolveEmailValidationIssue(issue)),
  (email) => print(email), // user@example.com
);

Strict email validation can reject disposable domains:

final validator = EmailValidator(EmailValidationConfig.strict());
final result = validator.validate('person@mailinator.com');
print(result.leftOrNull()?.code); // email.disposableDomain

Email validation is designed for product forms and domain input, not SMTP delivery checks. It trims whitespace, lowercases the returned address, rejects invalid local-part dot placement, enforces RFC email length limits, and returns typed issues you can map to your own localized messages.

const validator = EmailValidator(
  EmailValidationConfig(
    blockedDomains: {'example.test', 'internal.company'},
    disposableDomains: {'mailinator.com', 'tempmail.com'},
    rejectDisposable: true,
  ),
);

final result = validator.validate(' Person@MAIL.Example.Test ');

result.fold(
  (issue) {
    print(issue.code); // email.blockedDomain
    print(resolveEmailValidationIssue(issue));
  },
  (email) => print(email),
);

Configured domains are trimmed and compared case-insensitively. By default a configured domain also matches subdomains, so example.com blocks mail.example.com. Set matchSubdomains: false when you only want exact domain matches.

const exactOnly = EmailValidator(
  EmailValidationConfig(
    blockedDomains: {'example.com'},
    matchSubdomains: false,
  ),
);

print(exactOnly.validate('user@mail.example.com').rightOrNull());
// user@mail.example.com

Password validation:

final password = const PasswordValidator().validate('StrongP@ssw0rd');
final message = password.fold(resolvePasswordValidationIssue, (_) => null);
print(message); // null

final lenient = PasswordValidator(PasswordValidationConfig.lenient());
print(lenient.validate('abcd').isRight()); // true

Number and text validation:

final number = const NumberValidator(
  NumberValidationConfig(allowNegative: false, integerOnly: true),
).validate('42');

final text = TextValidator(
  TextValidationConfig(trimWhitespace: true, minLength: 3),
).validate('  abc  ');

print(number.rightOrNull()); // 42
print(text.rightOrNull()); // abc

For Flutter FormField.validator adapters, use package:gmana_form/gmana_form.dart.

Instant String Validation

Use validation.dart for format checks and regex helpers:

import 'package:gmana/validation.dart';

print('test@example.com'.isValidEmail); // true
print('Jane Doe'.isValidName); // true
print('550e8400-e29b-41d4-a716-446655440000'.isValidUuid); // true
print('#F57224'.isValidHexColor); // true

You can also call the lower-level functions directly:

print(isEmail('test@example.com')); // true
print(isInt('42')); // true
print(isBase64('YWJjZA==')); // true
print(isHexColor('#F57224')); // true
print(isLength('hello', 2, 10)); // true

Utilities

Generate IDs and encoded values:

import 'package:gmana/utilities.dart';

final uuid = IdGenerator.uuidV4Like();
final nanoid = IdGenerator.nanoid(size: 10);
final timestampId = IdGenerator.timestampId();
final random = IdGenerator.randomString(length: 12);
final encoded = IdGenerator.encodeToBase64(['user', 123]);

IdGenerator uses non-cryptographic randomness. Do not use these helpers for secrets, reset tokens, or security-sensitive identifiers.

Debounce and throttle repeated work:

final debouncer = Debouncer(milliseconds: 300);

debouncer.run(() {
  print('Runs after the user stops triggering events');
});

final throttler = Throttler(milliseconds: 1000);

throttler.run(() {
  print('Runs at most once per interval');
});

Package Relationship

  • Use gmana for pure Dart extensions, validation, functional results, and utilities.
  • Use gmana_form for Flutter form fields and validator adapters.
  • Use gmana_flutter for Flutter widgets, theme helpers, color helpers, and UI convenience APIs.
  • Use gmana_value_objects for typed domain values when that package is part of your project.

Libraries

extensions
Re-exports string/date validation extensions.
functional
Re-exports functional programming primitives from gmana_functional.
gmana
utilities
Re-exports runtime utilities from gmana_utils.
validation
Re-exports predicates and typed validators.