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

discontinuedreplaced by: trust_but_verify

A functional validation library for Dart with support for Either and TaskEither types.

fpvalidate #

A functional validation library for Dart with support for Either and TaskEither types from the fpdart package.

Features #

  • Fluent API: Chain validation rules with a clean, readable syntax
  • Functional Programming: Built on fpdart's Either and TaskEither for type-safe error handling
  • Multiple Validation Modes: Support for both synchronous and asynchronous validation
  • Comprehensive Validators: Built-in validators for strings, numbers, and nullable types
  • Custom Validators: Easy creation of custom validation logic
  • Batch Validation: Validate multiple fields at once
  • Nullable Support: Specialized validators for handling optional fields
  • Type Casting & Transformation: Convert between types while validating (String to int, nullable to non-nullable, etc.)
  • Flutter Form Compatibility: Built-in support for Flutter form validation with asFormValidator() method
  • Error Handling: Detailed error messages with field names and descriptions

Getting Started #

Add fpvalidate to your pubspec.yaml:

dependencies:
  fpvalidate: ^0.1.0
  fpdart: ^1.1.1

Import the library:

import 'package:fpvalidate/fpvalidate.dart';

Usage #

Basic Single Field Validation #

// Functional validation with Either
final result = email
    .field('Email')
    .notEmpty()
    .isEmail()
    .validateEither()
    .mapLeft((error) => 'Validation failed: ${error.message}');

// Simple validation with exception handling
try {
  final validatedEmail = email
      .field('Email')
      .notEmpty()
      .isEmail()
      .validate();
  print('Valid email: $validatedEmail');
} catch (e) {
  if (e is ValidationError) {
    print('Validation failed: ${e.message}');
  }
}

Multiple Field Validation #

final validationResult = [
  email.field('Email').notEmpty().isEmail(),
  password.field('Password').notEmpty().minLength(8),
  age.field('Age').min(13).max(120),
].validateEither()
.mapLeft((error) => 'Validation failed: ${error.message}');

Asynchronous Validation #

// Async validation with TaskEither
final result = await email
    .field('Email')
    .notEmpty()
    .isEmail()
    .toAsync()
    .tryMap((email) async {
      // Check if email exists in database
      final exists = await userService.emailExists(email);
      if (exists) {
        throw Exception('Email already registered');
      }
      return email;
    }, (fieldName) => '$fieldName already registered')
    .validateTaskEither()
    .run();

// Async validation with Either
final asyncResult = await email
    .field('Email')
    .notEmpty()
    .isEmail()
    .toAsync()
    .validateEither();

Nullable Field Validation #

// Optional field that must be valid if provided
final result = optionalEmail
    .field('Optional Email')
    .isNotNull()
    .validateEither();

// Required nullable field
final result = requiredField
    .field('Required Field')
    .isNotNull()
    .notEmpty()
    .validateEither();

Built-in Validators #

String Validators #

email.field('Email')
    .notEmpty()           // Ensures field is not empty
    .notEmpty(allowWhitespace: true)  // Allows whitespace-only strings
    .isEmail()            // Validates email format
    .minLength(5)         // Minimum length
    .maxLength(100)       // Maximum length
    .isPattern(RegExp(r'^[a-z]+$'), 'lowercase letters only') // Custom regex
    .isUrl()              // Validates URL format
    .isPhone()            // Validates phone number format
    .contains('required') // Must contain substring
    .startsWith('https')  // Must start with prefix
    .endsWith('.com')     // Must end with suffix
    .alphanumeric()       // Only alphanumeric characters
    .lettersOnly()        // Only letters
    .digitsOnly()         // Only digits
    .isUuid()             // Valid UUID format
    .isCreditCard()       // Valid credit card number
    .isPostalCode()       // Valid postal code format
    .isIsoDate()          // Valid ISO date (YYYY-MM-DD)
    .isTime24Hour()       // Valid 24-hour time (HH:MM)
    .validate();

These validators provide a fluent, chainable API and automatically use the field name in error messages for better user experience.

Numeric Validators #

age.field('Age')
    .min(0)               // Minimum value
    .max(120)             // Maximum value
    .inRange(13, 65)      // Value within range
    .isPositive()         // Must be positive
    .isNonNegative()      // Must be non-negative
    .isInt()              // Must be an integer
    .isEven()             // Must be even
    .isOdd()              // Must be odd
    .isPowerOfTwo()       // Must be a power of 2
    .isPortNumber()       // Must be a valid port number (1-65535)
    .isWithinPercentage(target, 5.0) // Within 5% of target value
    .validate();

Nullable Validators #

optionalField.field('Optional Field')
    .isNotNull()
    .validate();

Type Casting and Transformation Validators #

Some validators not only validate but also transform the value type, enabling different validation chains:

// String to Integer transformation
final result = '123'
    .field('Number String')
    .toInt()              // Converts String to int, enables numeric validators
    .min(100)             // Now we can use numeric validators
    .max(200)
    .isEven()
    .validateEither();

// Nullable to Non-nullable transformation
final result = (someNullableString as String?)
    .field('Optional String')
    .isNotNull()          // Converts String? to String, enables string validators
    .notEmpty()           // Now we can use string validators
    .isEmail()
    .validateEither();

// Custom transformation with tryMap
final result = '2023-12-25'
    .field('Date String')
    .tryMap(
      (value) => DateTime.parse(value),  // Converts String to DateTime
      (fieldName) => '$fieldName must be a valid date',
    )
    .validateEither();

These transformation validators are powerful because they allow you to:

  • Convert between types while validating
  • Chain different types of validators
  • Handle nullable to non-nullable conversions
  • Create custom type transformations with tryMap()

Advanced Features #

Custom Validation #

// Custom validation with check()
final result = 'hello world'
    .field('Custom String')
    .check(
      (value) => value.contains('world'),
      (fieldName) => '$fieldName must contain "world"',
    )
    .validateEither();

// Custom transformation with tryMap()
final result = '123'
    .field('Number String')
    .tryMap(
      (value) => int.tryParse(value) ?? throw Exception('Invalid number'),
      (fieldName) => '$fieldName must be a valid number',
    )
    .validateEither();

// Type conversion with toInt()
final result = '123'
    .field('Number String')
    .toInt()
    .validateEither();

Using the bind() Method #

The bind() method allows you to chain validation steps by passing the current value to a function that returns an Either. This is useful for complex validation logic that requires multiple steps or conditional validation.

// Complex validation with bind()
final result = 'user@example.com'
    .field('Email')
    .bind((email) {
      // Check if email is from allowed domains
      final allowedDomains = ['example.com', 'company.org'];
      final domain = email.split('@').last;

      if (!allowedDomains.contains(domain)) {
        return Left(ValidationError('Email', 'Email must be from an allowed domain'));
      }

      // Check if email is not too long
      if (email.length > 50) {
        return Left(ValidationError('Email', 'Email must be less than 50 characters'));
      }

      return Right(email);
    })
    .validateEither();

// Conditional validation with bind()
final result = age
    .field('Age')
    .bind((value) {
      if (value < 18) {
        return Left(ValidationError('Age', 'Must be at least 18 years old'));
      }

      if (value > 65) {
        return Left(ValidationError('Age', 'Must be under 65 years old'));
      }

      // Additional business logic
      if (value == 25) {
        return Left(ValidationError('Age', 'Age 25 is not allowed for this application'));
      }

      return Right(value);
    })
    .validateEither();

Creating Custom Extensions #

You can create custom extensions for specific types of ValidationStep to add domain-specific validators. Use the pass() and fail() helper methods to create success and failure results.

// Custom extension for String validation
extension CustomStringExtension on SyncValidationStep<String> {
  /// Validates that the string is a strong password
  SyncValidationStep<String> isStrongPassword() => bind((value) {
    if (value.length < 8) {
      return fail('$fieldName must be at least 8 characters long');
    }

    if (!RegExp(r'[A-Z]').hasMatch(value)) {
      return fail('$fieldName must contain at least one uppercase letter');
    }

    if (!RegExp(r'[a-z]').hasMatch(value)) {
      return fail('$fieldName must contain at least one lowercase letter');
    }

    if (!RegExp(r'[0-9]').hasMatch(value)) {
      return fail('$fieldName must contain at least one number');
    }

    if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) {
      return fail('$fieldName must contain at least one special character');
    }

    return pass(value);
  });
}

// Custom extension for numeric validation
extension CustomNumExtension<T extends num> on SyncValidationStep<T> {
  /// Validates that the number is a valid age for employment
  SyncValidationStep<T> isEmploymentAge() => bind((value) {
    if (value < 16) {
      return fail('$fieldName must be at least 16 years old for employment');
    }

    if (value > 70) {
      return fail('$fieldName must be under 70 years old for employment');
    }

    return pass(value);
  });

  /// Validates that the number is a valid percentage (0-100)
  SyncValidationStep<T> isPercentage() => bind((value) {
    if (value < 0 || value > 100) {
      return fail('$fieldName must be between 0 and 100');
    }

    return pass(value);
  });
}

// Usage of custom extensions
final passwordResult = 'MyP@ssw0rd'
    .field('Password')
    .isStrongPassword()
    .validateEither();

final ageResult = 25
    .field('Age')
    .isEmploymentAge()
    .validateEither();

Error Handling #

// Get error message or null (useful for Flutter forms)
final error = email
    .field('Email')
    .notEmpty()
    .isEmail()
    .errorOrNull();

if (error != null) {
  // Display error in UI
  print('Error: $error');
}

// Convenience method for Flutter forms
final formValidator = email
    .field('Email')
    .notEmpty()
    .isEmail()
    .asFormValidator(); // Same as errorOrNull()

Async Validation #

// Convert sync validation to async
final asyncResult = await email
    .field('Email')
    .notEmpty()
    .isEmail()
    .toAsync()
    .tryMap((email) async {
      // Simulate async validation
      await Future.delayed(Duration(milliseconds: 100));
      if (email.contains('async')) {
        return email;
      }
      throw Exception('Email must contain "async"');
    }, (fieldName) => '$fieldName must contain "async"')
    .validateTaskEither()
    .run();

Examples #

Form Validation #

class UserRegistrationForm {
  final String email;
  final String password;
  final int age;
  final String? phone;

  UserRegistrationForm({
    required this.email,
    required this.password,
    required this.age,
    this.phone,
  });

  Either<ValidationError, UserRegistrationForm> validate() {
    return [
      email.field('Email').notEmpty().isEmail(),
      password.field('Password').notEmpty().minLength(8),
      age.field('Age').min(13).max(120),
      if (phone != null) phone!.field('Phone').isPhone(),
    ].validateEither().map((_) => this);
  }
}

API Response Validation #

Future<Either<ValidationError, User>> validateUserResponse(Map<String, dynamic> json) async {
  return await json['email']
      .toString()
      .field('Email')
      .notEmpty()
      .isEmail()
      .toAsync()
      .tryMap((email) async {
        // Additional async validation
        final userExists = await userService.exists(email);
        if (!userExists) {
          throw Exception('User not found');
        }
        return User(email: email);
      }, (fieldName) => '$fieldName not found')
      .validateTaskEither()
      .run();
}

Flutter Form Integration #

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  String? _validateEmail(String? value) {
    if (value == null) return null;
    return value
        .field('Email')
        .notEmpty()
        .isEmail()
        .asFormValidator();
  }

  String? _validatePassword(String? value) {
    if (value == null) return null;
    return value
        .field('Password')
        .notEmpty()
        .minLength(8)
        .asFormValidator();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            validator: _validateEmail,
            decoration: InputDecoration(labelText: 'Email'),
          ),
          TextFormField(
            controller: _passwordController,
            validator: _validatePassword,
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
          ),
        ],
      ),
    );
  }
}

Error Messages #

The library provides descriptive error messages that include the field name:

  • "Email must be a valid email address"
  • "Password must be at least 8 characters long"
  • "Age must be between 13 and 120"
  • "Phone must be a valid phone number"

Localization #

Currently, all error messages in fpvalidate are provided in English only and are not localizable. However, I recognize the importance of internationalization for a validation library and am actively interested in community contributions to implement localization support.

I'd welcome contributions in the following areas:

  • Design Discussion: Help us design the best approach for implementing localization in the validation library
  • Implementation: Contribute code to add localization infrastructure and support
  • Translations: Provide translations for error messages once localization is implemented

If you're interested in contributing to localization efforts, please:

  1. Open an issue to discuss the design approach
  2. Share your thoughts on the best way to integrate localization with the existing API
  3. Contribute translations for your target languages

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

3
likes
0
points
36
downloads

Publisher

unverified uploader

Weekly Downloads

A functional validation library for Dart with support for Either and TaskEither types.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

fpdart

More

Packages that depend on fpvalidate