gmana_validation

Pure Dart typed validators for email, password, text, and number inputs. Returns Either-based results — no exceptions thrown, no stringly-typed errors.

import 'package:gmana_validation/gmana_validation.dart';

Table of contents


Core types

Every validator returns a ValidationResult, which is an alias for Either<TIssue, TValue> from gmana_functional.

// Aliases
typedef ValidationResult<TIssue, TValue> = Either<TIssue, TValue>;
typedef ValidationMessageResolver<TIssue> = String Function(TIssue issue);
  • Right(value) — validation passed; contains the normalized/parsed value.
  • Left(issue) — validation failed; contains a typed, sealed issue object.

All issue types are sealed, so switch expressions are exhaustively checked by the compiler.

final result = EmailValidator().validate('user@example.com');

// fold — handles both sides
result.fold(
  (issue) => print('Error: ${resolveEmailValidationIssue(issue)}'),
  (email) => print('Normalized: $email'),  // 'user@example.com'
);

// switch on the sealed issue for granular handling
result.fold(
  (issue) => switch (issue) {
    EmailEmptyIssue()          => 'Please enter your email',
    EmailInvalidFormatIssue()  => 'That doesn\'t look like an email',
    EmailTooLongIssue(:final maxLength) => 'Max $maxLength characters',
    EmailBlockedDomainIssue()  => 'That domain isn\'t allowed',
    EmailDisposableDomainIssue() => 'Disposable emails aren\'t accepted',
    _                          => resolveEmailValidationIssue(issue),
  },
  (email) => saveEmail(email),
);

Email

EmailValidator trims whitespace, checks format, enforces length limits, and optionally rejects blocked or disposable domains. On success it returns the normalized email (local@domain lowercased).

Quick start

const validator = EmailValidator();

validator.validate('User@Example.COM').fold(
  (issue) => print(resolveEmailValidationIssue(issue)),
  (email) => print(email), // 'user@example.com'
);

validator.validate('').fold(
  (issue) => print(issue.code), // 'email.empty'
  (_) => {},
);

EmailValidationConfig

// Default — permissive, no domain policies
const EmailValidationConfig()

// Strict preset — rejects disposable domains using the built-in list
EmailValidationConfig.strict()

// Custom — block competitor domains and tighten length limits
EmailValidationConfig(
  maxLength: 100,
  blockedDomains: {'competitor.com', 'spam.org'},
  rejectDisposable: true,
  matchSubdomains: true,    // 'mail.competitor.com' also blocked
)
Parameter Type Default
maxLength int 254
maxLocalPartLength int 64
maxDomainLength int 253
blockedDomains Set<String> {}
rejectDisposable bool false
disposableDomains Set<String> built-in list
matchSubdomains bool true

Issue types

Type Code Carries
EmailEmptyIssue email.empty
EmailInvalidFormatIssue email.invalidFormat
EmailTooLongIssue email.tooLong currentLength, maxLength
EmailLocalPartTooLongIssue email.localPartTooLong currentLength, maxLength
EmailDomainTooLongIssue email.domainTooLong currentLength, maxLength
EmailBlockedDomainIssue email.blockedDomain domain
EmailDisposableDomainIssue email.disposableDomain domain

Password

PasswordValidator enforces length, character requirements, and pattern-based rejection (common passwords, repeated characters, sequential runs). Returns the original password string on success.

Quick start

// Default — strong policy (8+ chars, upper, lower, digit, special)
const validator = PasswordValidator();

validator.validate('MySecure1!').fold(
  (issue) => print(resolvePasswordValidationIssue(issue)),
  (pass)  => print('Valid'),
);

PasswordValidationConfig

// Strong preset (default)
PasswordValidationConfig.strong()
const PasswordValidationConfig()  // equivalent

// Lenient — only minimum length enforced
PasswordValidationConfig.lenient()  // minLength: 4, all checks off

// Custom
PasswordValidationConfig(
  minLength: 12,
  maxLength: 256,
  requireUppercase: true,
  requireLowercase: true,
  requireDigit: true,
  requireSpecialCharacter: false,
  rejectCommonPasswords: true,
  rejectRepeatedCharacters: true,
  rejectSequentialPatterns: true,
  minSequentialRun: 5,                          // e.g. 'abcde' fails
  commonPasswords: {'hunter2', 'letmein123'},   // extend the block list
  commonPrefixes: ['password', 'qwerty'],
)
Parameter Type Default
minLength int 8
maxLength int 128
requireUppercase bool true
requireLowercase bool true
requireDigit bool true
requireSpecialCharacter bool true
rejectCommonPasswords bool true
rejectRepeatedCharacters bool true
rejectSequentialPatterns bool true
minSequentialRun int 4

Issue types

Type Code Carries
PasswordEmptyIssue password.empty
PasswordTooShortIssue password.tooShort currentLength, minLength
PasswordTooLongIssue password.tooLong currentLength, maxLength
PasswordMissingUppercaseIssue password.missingUppercase
PasswordMissingLowercaseIssue password.missingLowercase
PasswordMissingDigitIssue password.missingDigit
PasswordMissingSpecialCharacterIssue password.missingSpecialCharacter
PasswordTooCommonIssue password.tooCommon
PasswordRepeatedCharacterIssue password.repeatedCharacterPattern
PasswordSequentialPatternIssue password.sequentialPattern

PasswordStrength — live UI feedback

Use PasswordStrength to drive a strength meter as the user types, independently of the validator.

final strength = PasswordStrength.of('MyPass1');
// or against a custom config:
final strength = PasswordStrength.fromConfig('MyPass1', config);

strength.score           // 0–5
strength.isStrong        // true when all five criteria are met
strength.hasMinLength    // true / false
strength.hasUppercase
strength.hasLowercase
strength.hasDigit
strength.hasSpecial
strength.unmetRequirements // ['One special character']
// Strength indicator bar
LinearProgressIndicator(
  value: strength.score / 5,
  color: switch (strength.score) {
    <= 2 => Colors.red,
    <= 3 => Colors.orange,
    _    => Colors.green,
  },
)

Static character helpers

PasswordValidator.hasUppercase('Hello1!')    // true
PasswordValidator.hasLowercase('Hello1!')    // true
PasswordValidator.hasDigit('Hello1!')        // true
PasswordValidator.hasSpecialCharacter('Hello1!') // true
PasswordValidator.hasOnlyRepeatedCharacters('aaaa') // true
PasswordValidator.hasSequentialRun('abcd', minRun: 4) // true

Text

TextValidator handles general-purpose text fields: required vs optional, length limits, regex patterns, character allowlists, and word blocklists. Returns the (optionally trimmed) string on success.

Quick start

// Optional text — accepts empty input by default
TextValidator().validate('hello').fold(
  (issue) => print(resolveTextValidationIssue(issue)),
  (text)  => print(text),
);

TextValidationConfig

// Required preset — rejects empty and whitespace-only, trims before returning
TextValidationConfig.required()

TextValidationConfig.required(
  minLength: 2,
  maxLength: 50,
)

// Full control
TextValidationConfig(
  allowEmpty: false,
  allowOnlyWhitespace: false,
  trimWhitespace: true,
  minLength: 10,
  maxLength: 500,
  pattern: RegExp(r'^[a-zA-Z\s]+$'),          // letters and spaces only
  allowedCharacters: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ',
  blacklistedWords: {'spam', 'scam'},
  wholeWordBlacklist: true,  // 'scammer' passes; 'scam' fails
)
Parameter Type Default
allowEmpty bool true
allowOnlyWhitespace bool true
trimWhitespace bool false
minLength int? null
maxLength int? null
pattern RegExp? null
allowedCharacters String? null (all allowed)
blacklistedWords Set<String> {}
wholeWordBlacklist bool true

Issue types

Type Code Carries
TextEmptyIssue text.empty
TextOnlyWhitespaceIssue text.onlyWhitespace
TextTooShortIssue text.tooShort currentLength, minLength
TextTooLongIssue text.tooLong currentLength, maxLength
TextInvalidPatternIssue text.invalidPattern
TextInvalidCharactersIssue text.invalidCharacters invalidCharacters
TextContainsBlacklistedIssue text.blacklistedWords foundWords

Number

NumberValidator parses a string to num, then enforces sign, integer, range, and decimal-place constraints. Returns the parsed num on success.

Quick start

const validator = NumberValidator();

validator.validate('42').fold(
  (issue) => print(resolveNumberValidationIssue(issue)),
  (n)     => print(n),  // 42
);

NumberValidationConfig

// Positive integer preset
NumberValidationConfig.positiveInteger()
NumberValidationConfig.positiveInteger(min: 1, max: 100)

// Custom
NumberValidationConfig(
  min: 0,
  max: 9999.99,
  allowNegative: false,
  integerOnly: false,
  maxDecimalPlaces: 2,
)
Parameter Type Default
min num? null
max num? null
allowNegative bool true
integerOnly bool false
maxDecimalPlaces int? null

Issue types

Type Code Carries
NumberEmptyIssue number.empty
NumberInvalidFormatIssue number.invalidFormat
NumberNegativeNotAllowedIssue number.negativeNotAllowed currentValue
NumberNotIntegerIssue number.notInteger currentValue
NumberTooSmallIssue number.tooSmall currentValue, minValue
NumberTooLargeIssue number.tooLarge currentValue, maxValue
NumberDecimalPlacesExceededIssue number.decimalPlacesExceeded currentPlaces, maxPlaces

Custom message resolvers

Each domain ships a default resolver (resolveEmailValidationIssue, etc.) that returns English strings. Override per-issue for localization or custom copy.

String myEmailMessages(EmailValidationIssue issue) => switch (issue) {
  EmailEmptyIssue()           => 'សូមបញ្ចូលអ៊ីម៉ែល',           // Khmer
  EmailInvalidFormatIssue()   => 'អ៊ីម៉ែលមិនត្រឹមត្រូវ',
  EmailBlockedDomainIssue()   => 'Domain មិនត្រូវបានអនុញ្ញាត',
  _                           => resolveEmailValidationIssue(issue), // fallback
};

Using with Flutter forms

Wire any validator into a TextFormField using the asFormValidator adapter from gmana_form:

import 'package:gmana_form/gmana_form.dart';

TextFormField(
  validator: asFormValidator(
    validate: (input) => EmailValidator().validate(input),
    resolve: resolveEmailValidationIssue,
  ),
)

// With a custom resolver
TextFormField(
  validator: asFormValidator(
    validate: (input) => PasswordValidator(
      PasswordValidationConfig.strong(),
    ).validate(input),
    resolve: myPasswordMessages,
  ),
)

Or use the pre-wired field widgets from gmana_form (GEmailField, GPasswordField, GTextField, GNumberField) which handle this internally.

Libraries

gmana_validation
Pure Dart typed validators with Either-based results.