formix_core 0.1.0
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
Tand errorE - 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.