formix_core 0.1.0 copy "formix_core: ^0.1.0" to clipboard
formix_core: ^0.1.0 copied to clipboard

Generic, type-safe validation engine for Dart. Build composable, reusable validators with a fluent API. The core package of the Formix ecosystem.

formix_core #

The core validation engine for the Formix ecosystem. Provides a type-safe, composable, and functional approach to data validation in Dart.

Installation #

dependencies:
  formix_core: ^0.1.0

Features #

  • Type-Safe: Validators are typed for both input T and error E
  • Composable: Build complex logic from simple rules using all, any, chain, etc.
  • Fluent API: Ergonomic extension methods like .andThen(), .when(), .optional()
  • Caching: Built-in performance optimizations with .cached() and .lruCached()
  • Lazy Evaluation: Defer validator creation with LazyFormix
  • No Dependencies: Pure Dart, works in Flutter, server-side, or CLI apps

Core Concepts #

ValidationResult #

The result of validation is one of three types:

sealed class ValidationResult<T, E> {
  // Valid - validation passed
  Valid(T value)
  
  // Invalid - single error (fail-fast)
  Invalid(E error)
  
  // InvalidAll - multiple errors (collect all)
  InvalidAll(List<E> errors)
}

Pattern Matching Results #

final result = validator.validate(input);

switch (result) {
  case Valid(:final value):
    print('Valid: $value');
  case Invalid(:final error):
    print('Error: $error');
  case InvalidAll(:final errors):
    print('All errors: $errors');
}

// Or use convenience getters
if (result.isValid) {
  print(result.valueOrNull);
} else {
  print(result.errors);
}

Creating Validators #

PredicateRule (Simple Checks) #

final notEmpty = PredicateRule<String, String>(
  predicate: (value) => value.isNotEmpty,
  error: 'Cannot be empty',
);

final result = notEmpty.validate('hello'); // Valid('hello')
final result2 = notEmpty.validate('');     // Invalid('Cannot be empty')

FunctionRule (Complex Logic) #

final passwordStrength = FunctionRule<String, String>(
  validator: (password) {
    if (password.length < 8) return Invalid('Too short');
    if (!password.contains(RegExp(r'[A-Z]'))) return Invalid('Need uppercase');
    if (!password.contains(RegExp(r'[0-9]'))) return Invalid('Need digit');
    return Valid(password);
  },
);

Custom Rule Class #

class EmailRule<E> extends Rule<String, E> {
  final E error;
  EmailRule({required this.error});

  static final _emailRegex = RegExp(r'^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$');

  @override
  ValidationResult<String, E> validate(String value) {
    return _emailRegex.hasMatch(value) ? Valid(value) : Invalid(error);
  }
}

Composition #

Validate.all (Fail-Fast) #

Stops at the first error:

final validator = Validate.all<String, String>([
  PredicateRule(predicate: (v) => v.isNotEmpty, error: 'Required'),
  PredicateRule(predicate: (v) => v.contains('@'), error: 'Need @'),
  PredicateRule(predicate: (v) => v.contains('.'), error: 'Need dot'),
]);

validator.validate('');        // Invalid('Required')
validator.validate('test');    // Invalid('Need @')
validator.validate('t@e.com'); // Valid('t@e.com')

Validate.allCollect (Collect All Errors) #

Continues validation to collect all errors:

final validator = Validate.allCollect<String, String>([
  PredicateRule(predicate: (v) => v.isNotEmpty, error: 'Required'),
  PredicateRule(predicate: (v) => v.length >= 8, error: 'Too short'),
  PredicateRule(predicate: (v) => v.contains(RegExp(r'[A-Z]')), error: 'Need uppercase'),
]);

validator.validate('abc'); // InvalidAll(['Too short', 'Need uppercase'])

Validate.any (Alternatives) #

Passes if ANY validator passes:

final contactValidator = Validate.any<String, String>([
  emailValidator,
  phoneValidator,
], fallbackError: 'Must be valid email or phone');

contactValidator.validate('test@example.com'); // Valid
contactValidator.validate('+1234567890');      // Valid
contactValidator.validate('invalid');          // Invalid('Must be valid email or phone')

Chaining with andThen #

final validator = requiredRule
    .andThen(minLengthRule)
    .andThen(emailFormatRule);

Conditional with when/whenNot #

// Only validate if not empty
final optionalEmail = emailValidator.when((v) => v.isNotEmpty);

// Skip validation if empty
final optionalAge = ageValidator.whenNot((v) => v == null);

Optional Fields #

// Skips validation for empty strings
final optionalEmail = emailValidator.optional();

optionalEmail.validate('');                  // Valid('')
optionalEmail.validate('test@example.com');  // Runs validation

Caching #

CachedFormix (Single Value) #

Caches the last validated value - perfect for form fields:

final cached = emailValidator.cached();

cached.validate('test@example.com'); // Computed
cached.validate('test@example.com'); // Cached! (instant)
cached.validate('other@example.com'); // Computed (value changed)

LruCachedFormix (Multiple Values) #

LRU cache for multiple recent values - perfect for search/autocomplete:

final lruCached = searchValidator.lruCached(maxSize: 20);

// User types and backtracks:
lruCached.validate('h');     // Computed
lruCached.validate('he');    // Computed
lruCached.validate('hel');   // Computed
lruCached.validate('he');    // Cache hit! (backspace)
lruCached.validate('h');     // Cache hit! (backspace)

LazyFormix (Deferred Creation) #

Defer expensive validator creation until first use:

final lazy = Validate.lazy<String, String>(
  () => buildExpensiveValidator(), // Only called on first validate()
);

// Validator not created yet
lazy.validate('test'); // Now created and cached
lazy.validate('test2'); // Reuses same instance

Transformation #

Transform input before validation or change the type:

// Parse string to int and validate
final ageValidator = Validate.transform<String, int, String>(
  transform: (value) => int.tryParse(value),
  onNull: 'Must be a number',
  then: NumberRules.range(0, 150, error: 'Invalid age'),
);

ageValidator.validate('25');   // Valid(25)
ageValidator.validate('abc');  // Invalid('Must be a number')
ageValidator.validate('200');  // Invalid('Invalid age')

Error Mapping #

Convert error types for display or localization:

enum AuthError { required, invalidEmail }

final validator = Validate.all<String, AuthError>([...]);

// Map to strings
final stringValidator = validator.mapError((error) => switch (error) {
  AuthError.required => 'Email is required',
  AuthError.invalidEmail => 'Invalid email format',
});

// Or use a formatter class
final formatted = validator.withMessageFormatter(AuthErrorFormatter());

API Reference #

Extension Methods on Formix<T, E> #

Method Description
.andThen(next) Chain validators sequentially
.when(condition) Run only if condition is true
.whenNot(condition) Skip if condition is true
.optional() Skip if value is empty
.mapError(mapper) Transform error type
.cached() Single-value caching
.lruCached(maxSize) LRU multi-value caching
.lazy() Wrap in lazy evaluator

Validate Static Factory #

Method Description
Validate.all([...]) All must pass, fail-fast
Validate.allCollect([...]) All must pass, collect errors
Validate.any([...]) Any must pass
Validate.chain([...]) Sequential validation
Validate.when(test, then, orElse) Conditional validation
Validate.transform(...) Transform and validate
Validate.cached(validator) Create cached validator
Validate.lruCached(validator, maxSize) Create LRU cached validator
Validate.lazy(factory) Lazy validator creation

License #

MIT License - see LICENSE file for details.

0
likes
150
points
112
downloads

Publisher

unverified uploader

Weekly Downloads

Generic, type-safe validation engine for Dart. Build composable, reusable validators with a fluent API. The core package of the Formix ecosystem.

Homepage
Repository (GitHub)
View/report issues

Topics

#validation #form #dart #input-validation #form-validation

Documentation

Documentation
API reference

License

MIT (license)

More

Packages that depend on formix_core