From 80476f25f8c394295675f619b7b3675f1d95b69d Mon Sep 17 00:00:00 2001 From: Divyanshu Bhargava Date: Mon, 21 Sep 2026 16:46:38 +0530 Subject: [PATCH 1/3] feat: add Validator.compose and tighten URL, date, and int checks Empty-host URLs, overflow calendar dates, and hex integer literals were accepted as valid. Compose lets forms combine required with other rules without ?? chaining, and the package no longer requires the Flutter SDK. --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 11 ++++++ README.md | 28 ++++++++------ analysis_options.yaml | 4 +- example/lib/main.dart | 9 ++++- example/pubspec.lock | 20 +++++----- lib/form_validator.dart | 67 +++++++++++++++++++++++++++++++-- lib/validators/alpha.dart | 7 +++- lib/validators/ascii.dart | 4 +- lib/validators/base32.dart | 4 +- lib/validators/base58.dart | 4 +- lib/validators/credit_card.dart | 7 +++- lib/validators/date.dart | 24 ++++++++++-- lib/validators/email.dart | 8 ++-- lib/validators/hex_color.dart | 4 +- lib/validators/int.dart | 26 ++++++------- lib/validators/ip.dart | 14 +++---- lib/validators/phone.dart | 42 ++++++++++----------- lib/validators/url.dart | 23 ++++++++--- lib/validators/uuid.dart | 8 ++-- pubspec.lock | 11 +----- pubspec.yaml | 5 +-- test/form_validator_test.dart | 12 ++++++ test/validators/date_test.dart | 4 +- test/validators/int_test.dart | 2 + test/validators/url_test.dart | 6 +++ 26 files changed, 246 insertions(+), 112 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9cca045..c1bc60c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' + - uses: dart-lang/setup-dart@v1 - name: Install dependencies run: dart pub get - name: Analyze code diff --git a/CHANGELOG.md b/CHANGELOG.md index 2732955..01158f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.3.0 + +* Added `Validator.compose` to run multiple form validators in order. +* Fixed `isURL` to require a real host (`https://`, `http://.`, and `http://foo` + are rejected; `localhost` and IP hosts remain valid). +* Fixed `isDate` to reject calendar-invalid dates such as `2023-13-01` and + `2023-02-29`. +* Fixed `isInt` to accept decimal integers only (hex literals like `0x10` are + rejected). +* The package is now a pure Dart package: the Flutter SDK is no longer required. + ## 1.2.0 * Added 21 new validators inspired by validator.js: `isLowercase`, `isUppercase`, diff --git a/README.md b/README.md index 94ce4b0..f0947ae 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Add the package to your `pubspec.yaml`: ```yaml dependencies: - flutter_validators: ^1.2.0 + flutter_validators: ^1.3.0 ``` Then run: @@ -148,7 +148,7 @@ Every validator is available **both** as a top-level function and as a `String` | Validator | Extension | Description | |---|---|---| -| `isInt(str)` | `str.isInt` | Integer (positive or negative) | +| `isInt(str)` | `str.isInt` | Decimal integer (positive or negative) | | `isNumeric(str)` | `str.isNumeric` | Number (integer or float) | | `isFloat(str, {min, max})` | `str.isFloat({min, max})` | Finite float, optionally within a range | | `isDecimal(str)` | `str.isDecimal` | Decimal number | @@ -393,10 +393,10 @@ class _SignUpFormState extends State { TextFormField( decoration: const InputDecoration(labelText: 'Email'), autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - return Validator.required(errorMessage: 'Email is required')(value) ?? - Validator.email(errorMessage: 'Enter a valid email')(value); - }, + validator: Validator.compose([ + Validator.required(errorMessage: 'Email is required'), + Validator.email(errorMessage: 'Enter a valid email'), + ]), ), TextFormField( decoration: const InputDecoration(labelText: 'Website'), @@ -435,6 +435,7 @@ A complete, runnable app is in the [`example/`](example/) directory. Every method on the `Validator` class returns `String? Function(String?)`: ```dart +Validator.compose(Iterable validators) Validator.required({String errorMessage}) Validator.email({String errorMessage}) Validator.url({String errorMessage}) @@ -482,19 +483,24 @@ Validator.strongPassword({int minLength, int minLowercase, int minUppercase, int ## 💡 Behavior Notes and FAQ -**`Validator` methods treat `null` and empty strings as valid.** This is intentional — it lets you compose validators freely. To make a field mandatory, pair it with `Validator.required()`: +**`Validator` methods treat `null` and empty strings as valid.** This is intentional — it lets you compose validators freely. Individual validators still skip empty values; to make a field mandatory, pair them with `Validator.required()` using `Validator.compose`: ```dart -validator: (value) { - return Validator.required()(value) ?? Validator.email()(value); -} +validator: Validator.compose([ + Validator.required(), + Validator.email(), +]) ``` **`contains` is a top-level function only.** Dart's `String` already has a built-in `.contains()` method, so the package does not add a conflicting extension. Use `contains(str, seed)` instead of `str.contains(...)` when you need the case-insensitivity or `minOccurrences` options. **Trimming extensions are named `trimChars` / `ltrimChars` / `rtrimChars`.** Dart's `String` already provides `.trim()`, `.trimLeft()` and `.trimRight()` for whitespace, so the custom-character variants use distinct names to avoid collisions. The top-level functions keep the plain `trim` / `ltrim` / `rtrim` names. -**`isURL` accepts only `http` and `https` schemes.** Other schemes such as `ftp://` are rejected. +**`isURL` accepts only `http` and `https` schemes** and requires a real host. Other schemes such as `ftp://` are rejected. The host must be `localhost`, an IP address, or a name that contains a dot — so `https://` and `http://foo` fail, while `http://localhost` and `https://example.com` pass. + +**`isDate` requires a real calendar date.** ISO-8601 strings whose `YYYY-MM-DD` prefix is not a valid calendar date (for example `2023-13-01` or `2023-02-29`) are rejected. + +**`isInt` accepts decimal integers only.** Hex literals such as `0x10` are rejected. Leading zeros and surrounding whitespace are still allowed. **`isBase64` has a `urlSafe` option.** By default it validates the standard Base64 alphabet (with padding); pass `urlSafe: true` to validate the URL- and filename-safe alphabet instead. diff --git a/analysis_options.yaml b/analysis_options.yaml index 4ff1074..572dd23 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1 @@ -include: package:flutter_lints/flutter.yaml - -analyzer: \ No newline at end of file +include: package:lints/recommended.yaml diff --git a/example/lib/main.dart b/example/lib/main.dart index 753cae4..5819e25 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -43,13 +43,18 @@ class _ExampleScreenState extends State { children: [ TextFormField( decoration: const InputDecoration(labelText: 'Email'), - validator: Validator.email(), + validator: Validator.compose([ + Validator.required(errorMessage: 'Email is required'), + Validator.email(), + ]), autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 16), TextFormField( decoration: const InputDecoration(labelText: 'URL'), - validator: Validator.url(errorMessage: 'Please provide a valid URL'), + validator: Validator.url( + errorMessage: 'Please provide a valid URL', + ), autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 16), diff --git a/example/pubspec.lock b/example/pubspec.lock index 235159d..3e80178 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -81,7 +81,7 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "1.3.0" leak_tracker: dependency: transitive description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.19.0" path: dependency: transitive description: @@ -195,18 +195,18 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.12" vector_math: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.3" vm_service: dependency: transitive description: @@ -216,5 +216,5 @@ packages: source: hosted version: "15.0.2" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.11.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/form_validator.dart b/lib/form_validator.dart index 6e2f089..f6f4c0c 100644 --- a/lib/form_validator.dart +++ b/lib/form_validator.dart @@ -1,5 +1,42 @@ -import 'package:flutter_validators/flutter_validators.dart'; -import 'package:flutter_validators/validators/contains.dart' as contains_fn; +import 'validators/alpha.dart'; +import 'validators/ascii.dart'; +import 'validators/base32.dart'; +import 'validators/base58.dart'; +import 'validators/base64.dart'; +import 'validators/boolean.dart'; +import 'validators/byte_length.dart'; +import 'validators/contains.dart' as contains_fn; +import 'validators/credit_card.dart'; +import 'validators/date.dart'; +import 'validators/decimal.dart'; +import 'validators/email.dart'; +import 'validators/equals.dart'; +import 'validators/float.dart'; +import 'validators/fqdn.dart'; +import 'validators/hex_color.dart'; +import 'validators/hexadecimal.dart'; +import 'validators/in.dart'; +import 'validators/int.dart'; +import 'validators/ip.dart'; +import 'validators/json.dart'; +import 'validators/jwt.dart'; +import 'validators/lat_long.dart'; +import 'validators/length.dart'; +import 'validators/lowercase.dart'; +import 'validators/mac_address.dart'; +import 'validators/matches.dart'; +import 'validators/md5.dart'; +import 'validators/mongo_id.dart'; +import 'validators/numeric.dart'; +import 'validators/octal.dart'; +import 'validators/phone.dart'; +import 'validators/port.dart'; +import 'validators/semver.dart'; +import 'validators/slug.dart'; +import 'validators/strong_password.dart'; +import 'validators/uppercase.dart'; +import 'validators/url.dart'; +import 'validators/uuid.dart'; /// A utility class for Flutter Form validation. /// Provides methods that return a validator function suitable for `TextFormField`. @@ -7,7 +44,10 @@ import 'package:flutter_validators/validators/contains.dart' as contains_fn; /// Example: /// ```dart /// TextFormField( -/// validator: Validator.email(errorMessage: 'Please enter a valid email'), +/// validator: Validator.compose([ +/// Validator.required(errorMessage: 'Email is required'), +/// Validator.email(errorMessage: 'Please enter a valid email'), +/// ]), /// ) /// ``` class Validator { @@ -19,6 +59,27 @@ class Validator { value == null || value.trim().isEmpty ? errorMessage : null; } + /// Runs [validators] in order and returns the first error, or `null` if all pass. + /// + /// Example: + /// ```dart + /// validator: Validator.compose([ + /// Validator.required(errorMessage: 'Email is required'), + /// Validator.email(errorMessage: 'Enter a valid email'), + /// ]) + /// ``` + static String? Function(String?) compose( + Iterable validators, + ) { + return (String? value) { + for (final validator in validators) { + final error = validator(value); + if (error != null) return error; + } + return null; + }; + } + /// Ensures the string is a valid email. static String? Function(String?) email({ String errorMessage = 'Please enter a valid email address', diff --git a/lib/validators/alpha.dart b/lib/validators/alpha.dart index b644533..ea5bca6 100644 --- a/lib/validators/alpha.dart +++ b/lib/validators/alpha.dart @@ -29,10 +29,13 @@ extension AlphaX on String { } } +final _alpha = RegExp(r'^[a-zA-Z]+$'); +final _alphanumeric = RegExp(r'^[a-zA-Z0-9]+$'); + bool _isAlpha(String str) { - return RegExp(r'^[a-zA-Z]+$').hasMatch(str); + return _alpha.hasMatch(str); } bool _isAlphanumeric(String str) { - return RegExp(r'^[a-zA-Z0-9]+$').hasMatch(str); + return _alphanumeric.hasMatch(str); } diff --git a/lib/validators/ascii.dart b/lib/validators/ascii.dart index aa6f807..6671109 100644 --- a/lib/validators/ascii.dart +++ b/lib/validators/ascii.dart @@ -31,6 +31,8 @@ extension AsciiX on String { /// /// Validates if a string contains only ASCII characters using a regular expression /// that matches characters in the range \x00-\x7F (code points 0-127). +final _ascii = RegExp(r'^[\x00-\x7F]+$'); + bool _isAscii(String str) { - return RegExp(r'^[\x00-\x7F]+$').hasMatch(str); + return _ascii.hasMatch(str); } diff --git a/lib/validators/base32.dart b/lib/validators/base32.dart index 3dc32a9..8d130d0 100644 --- a/lib/validators/base32.dart +++ b/lib/validators/base32.dart @@ -36,9 +36,11 @@ extension Base32X on String { /// Validates if a string is base32 encoded using a regular expression /// that matches uppercase letters A-Z, digits 2-7, and optional padding /// with '=' characters at the end. +final _base32 = RegExp(r'^[A-Z2-7]+={0,6}$'); + bool _isBase32(String str) { if (str.isEmpty) return false; - if (!RegExp(r'^[A-Z2-7]+={0,6}$').hasMatch(str)) return false; + if (!_base32.hasMatch(str)) return false; final paddingIdx = str.indexOf('='); if (paddingIdx != -1) { diff --git a/lib/validators/base58.dart b/lib/validators/base58.dart index 2f90006..cf7cf2f 100644 --- a/lib/validators/base58.dart +++ b/lib/validators/base58.dart @@ -38,6 +38,8 @@ extension Base58X on String { /// Validates if a string is base58 encoded using a regular expression /// that matches the base58 character set: A-H, J-N, P-Z, a-k, m-z, and 1-9. /// The omitted characters are: 0, O, I, and l to avoid visual ambiguity. +final _base58 = RegExp(r'^[A-HJ-NP-Za-km-z1-9]+$'); + bool _isBase58(String str) { - return RegExp(r'^[A-HJ-NP-Za-km-z1-9]+$').hasMatch(str); + return _base58.hasMatch(str); } diff --git a/lib/validators/credit_card.dart b/lib/validators/credit_card.dart index bd522f6..737fab7 100644 --- a/lib/validators/credit_card.dart +++ b/lib/validators/credit_card.dart @@ -17,9 +17,12 @@ extension CreditCardX on String { } } +final _creditCardSeparators = RegExp(r'[\s\-]'); +final _creditCardDigits = RegExp(r'^\d{13,19}$'); + bool _isCreditCard(String str) { - final sanitized = str.replaceAll(RegExp(r'[\s\-]'), ''); - if (!RegExp(r'^\d{13,19}$').hasMatch(sanitized)) return false; + final sanitized = str.replaceAll(_creditCardSeparators, ''); + if (!_creditCardDigits.hasMatch(sanitized)) return false; int sum = 0; bool alternate = false; diff --git a/lib/validators/date.dart b/lib/validators/date.dart index 6f93968..3b6c921 100644 --- a/lib/validators/date.dart +++ b/lib/validators/date.dart @@ -1,22 +1,38 @@ -/// Checks if the string represents a valid date. +/// Checks if the string represents a valid calendar date. /// -/// Uses Dart's built-in `DateTime.tryParse` to validate the string formatting. +/// Accepts ISO-8601 strings such as `2023-12-01`, `2023-12-01 12:00:00`, and +/// `2023-12-01T12:00:00Z`. The `YYYY-MM-DD` prefix must be a real calendar +/// date — month/day overflow such as `2023-13-01` or `2023-02-29` is rejected. /// /// Example: /// ```dart /// isDate('2023-12-01'); // true +/// isDate('2023-13-01'); // false +/// isDate('2023-02-29'); // false /// isDate('invalid date'); // false /// ``` bool isDate(String str) => _isDate(str); /// Extension providing date validation methods on [String]. extension DateX on String { - /// Checks if the string represents a valid date. + /// Checks if the string represents a valid calendar date. bool get isDate { return _isDate(this); } } +final _isoDatePrefix = RegExp(r'^(\d{4})-(\d{2})-(\d{2})'); + bool _isDate(String str) { - return DateTime.tryParse(str) != null; + if (DateTime.tryParse(str) == null) return false; + final match = _isoDatePrefix.firstMatch(str); + if (match == null) return false; + + final year = int.parse(match.group(1)!); + final month = int.parse(match.group(2)!); + final day = int.parse(match.group(3)!); + final reconstructed = DateTime.utc(year, month, day); + return reconstructed.year == year && + reconstructed.month == month && + reconstructed.day == day; } diff --git a/lib/validators/email.dart b/lib/validators/email.dart index 4683c05..27cfb1f 100644 --- a/lib/validators/email.dart +++ b/lib/validators/email.dart @@ -40,8 +40,10 @@ extension EmailX on String { /// - Domain part: can be an IP address or domain name with TLD of at least 2 characters /// /// The regex handles common email formats including quoted strings and subdomains. +final _email = RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', +); + bool _isEmail(String str) { - return RegExp( - r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', - ).hasMatch(str); + return _email.hasMatch(str); } diff --git a/lib/validators/hex_color.dart b/lib/validators/hex_color.dart index c5bc5b7..7bf0570 100644 --- a/lib/validators/hex_color.dart +++ b/lib/validators/hex_color.dart @@ -19,6 +19,8 @@ extension HexColorX on String { } } +final _hexColor = RegExp(r'^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$'); + bool _isHexColor(String str) { - return RegExp(r'^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$').hasMatch(str); + return _hexColor.hasMatch(str); } diff --git a/lib/validators/int.dart b/lib/validators/int.dart index 2cc7176..e29d522 100644 --- a/lib/validators/int.dart +++ b/lib/validators/int.dart @@ -1,15 +1,16 @@ -/// Checks if the string represents a valid integer. +/// Checks if the string represents a decimal integer. /// -/// Uses Dart's `int.tryParse` to determine if the string can be parsed as an integer. -/// This supports both positive and negative integers, as well as leading zeros. +/// Accepts an optional sign, leading zeros, and surrounding whitespace. +/// Hex literals such as `0x10` are rejected. /// -/// Returns `true` if the string can be converted to an integer, otherwise returns `false`. +/// Returns `true` if the string is a decimal integer, otherwise returns `false`. /// /// Example: /// ```dart /// isInt('123'); // true /// isInt('-123'); // true /// isInt('01'); // true +/// isInt('0x10'); // false /// isInt('12.34'); // false /// isInt('abc'); // false /// ``` @@ -17,18 +18,19 @@ bool isInt(String str) => _isInt(str); /// Extension providing integer validation methods on [String]. extension IntX on String { - /// Checks if the string represents a valid integer. + /// Checks if the string represents a decimal integer. /// - /// Uses Dart's `int.tryParse` to determine if the string can be parsed as an integer. - /// This supports both positive and negative integers, as well as leading zeros. + /// Accepts an optional sign, leading zeros, and surrounding whitespace. + /// Hex literals such as `0x10` are rejected. /// - /// Returns `true` if the string can be converted to an integer, otherwise returns `false`. + /// Returns `true` if the string is a decimal integer, otherwise returns `false`. /// /// Example: /// ```dart /// '123'.isInt; // true /// '-123'.isInt; // true /// '01'.isInt; // true + /// '0x10'.isInt; // false /// '12.34'.isInt; // false /// 'abc'.isInt; // false /// ``` @@ -37,10 +39,8 @@ extension IntX on String { } } -/// Internal implementation for integer validation. -/// -/// Uses Dart's built-in `int.tryParse` method to attempt parsing the string as an integer. -/// The method returns null if parsing fails, which indicates the string is not a valid integer. +final _decimalInt = RegExp(r'^[+-]?\d+$'); + bool _isInt(String str) { - return int.tryParse(str) != null; + return _decimalInt.hasMatch(str.trim()); } diff --git a/lib/validators/ip.dart b/lib/validators/ip.dart index 9d1b684..05911c7 100644 --- a/lib/validators/ip.dart +++ b/lib/validators/ip.dart @@ -35,9 +35,13 @@ bool _isIP(String str, [int? version]) { return false; } +final _ipv4 = RegExp(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'); +final _ipv6 = RegExp( + r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', +); + bool _isIPv4(String str) { - final reg = RegExp(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'); - final match = reg.firstMatch(str); + final match = _ipv4.firstMatch(str); if (match == null) return false; for (int i = 1; i <= 4; i++) { @@ -48,9 +52,5 @@ bool _isIPv4(String str) { } bool _isIPv6(String str) { - // Common robust regex for IPv6 validation - final reg = RegExp( - r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', - ); - return reg.hasMatch(str); + return _ipv6.hasMatch(str); } diff --git a/lib/validators/phone.dart b/lib/validators/phone.dart index dd960d0..829844e 100644 --- a/lib/validators/phone.dart +++ b/lib/validators/phone.dart @@ -70,31 +70,29 @@ bool _isPhone(String str) { return false; } - // Common phone number patterns - final patterns = [ - // International format with + and country code - r'^\+[1-9]\d{1,14}$', - - // International format with spaces/dashes after country code - r'^\+[1-9][\s.-]?\d{1,3}[\s.-]?\d{3,4}[\s.-]?\d{3,4}$', - - // US/North American format - r'^\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}$', - r'^[0-9]{3}-[0-9]{3}-[0-9]{4}$', - - // Format with dots - r'^[0-9]{3}\.[0-9]{3}\.[0-9]{4}$', - - // Simple sequence of digits (at least 7, at most 15) - r'^[0-9]{7,15}$', - ]; - - for (final pattern in patterns) { - final regExp = RegExp(pattern); - if (regExp.hasMatch(str)) { + for (final pattern in _phonePatterns) { + if (pattern.hasMatch(str)) { return true; } } return false; } + +final _phonePatterns = [ + // International format with + and country code + RegExp(r'^\+[1-9]\d{1,14}$'), + + // International format with spaces/dashes after country code + RegExp(r'^\+[1-9][\s.-]?\d{1,3}[\s.-]?\d{3,4}[\s.-]?\d{3,4}$'), + + // US/North American format + RegExp(r'^\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}$'), + RegExp(r'^[0-9]{3}-[0-9]{3}-[0-9]{4}$'), + + // Format with dots + RegExp(r'^[0-9]{3}\.[0-9]{3}\.[0-9]{4}$'), + + // Simple sequence of digits (at least 7, at most 15) + RegExp(r'^[0-9]{7,15}$'), +]; diff --git a/lib/validators/url.dart b/lib/validators/url.dart index 5ea0e1e..8e57f26 100644 --- a/lib/validators/url.dart +++ b/lib/validators/url.dart @@ -1,17 +1,23 @@ -/// Checks if the string is a valid URL. +import 'ip.dart'; + +/// Checks if the string is a valid HTTP or HTTPS URL. /// -/// Returns `true` if the string can be parsed as a valid Uri, otherwise returns `false`. +/// The URL must have a non-empty host. The host must be `localhost`, an IP +/// address, or a name that contains a dot (a TLD). /// /// Example: /// ```dart /// isURL('https://google.com'); // true +/// isURL('http://localhost'); // true +/// isURL('https://'); // false +/// isURL('http://foo'); // false /// isURL('invalid-url'); // false /// ``` bool isURL(String str) => _isURL(str); /// Extension providing URL validation methods on [String]. extension UrlX on String { - /// Checks if the string is a valid URL. + /// Checks if the string is a valid HTTP or HTTPS URL. bool get isURL { return _isURL(this); } @@ -20,7 +26,12 @@ extension UrlX on String { bool _isURL(String str) { if (str.isEmpty) return false; final uri = Uri.tryParse(str); - return uri != null && - uri.hasAuthority && - (uri.scheme == 'http' || uri.scheme == 'https'); + if (uri == null) return false; + if (uri.scheme != 'http' && uri.scheme != 'https') return false; + + final host = uri.host; + if (host.isEmpty || host == '.') return false; + if (host.toLowerCase() == 'localhost') return true; + if (isIP(host)) return true; + return host.contains('.'); } diff --git a/lib/validators/uuid.dart b/lib/validators/uuid.dart index 8457d36..19d8204 100644 --- a/lib/validators/uuid.dart +++ b/lib/validators/uuid.dart @@ -17,8 +17,10 @@ extension UuidX on String { } } +final _uuid = RegExp( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1345][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$', +); + bool _isUUID(String str) { - return RegExp( - r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1345][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$', - ).hasMatch(str); + return _uuid.hasMatch(str); } diff --git a/pubspec.lock b/pubspec.lock index 977425b..14623b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" frontend_server_client: dependency: transitive description: @@ -146,7 +138,7 @@ packages: source: hosted version: "0.7.2" lints: - dependency: transitive + dependency: "direct dev" description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 @@ -387,4 +379,3 @@ packages: version: "3.1.0" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index a759457..cea96c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,12 @@ name: flutter_validators description: A pure dart package of String validators and sanitizers. Inspired by validator.js. -version: 1.2.0 +version: 1.3.0 homepage: https://github.com/StacDev/flutter_validators repository: https://github.com/StacDev/flutter_validators environment: sdk: ^3.7.0 - flutter: ">=1.17.0" dev_dependencies: test: ^1.25.15 - flutter_lints: ^5.0.0 \ No newline at end of file + lints: ^5.0.0 diff --git a/test/form_validator_test.dart b/test/form_validator_test.dart index 6b941e8..f283426 100644 --- a/test/form_validator_test.dart +++ b/test/form_validator_test.dart @@ -22,6 +22,18 @@ void main() { expect(email(null), isNull); }); + test('compose runs validators in order and returns the first error', () { + final email = Validator.compose([ + Validator.required(errorMessage: 'Email is required'), + Validator.email(errorMessage: 'Enter a valid email'), + ]); + expect(email(''), 'Email is required'); + expect(email(null), 'Email is required'); + expect(email(' '), 'Email is required'); + expect(email('invalid'), 'Enter a valid email'); + expect(email('user@example.com'), isNull); + }); + test('Length validator', () { final len = Validator.length(3, max: 5, errorMessage: 'Bad len'); expect(len('abc'), isNull); diff --git a/test/validators/date_test.dart b/test/validators/date_test.dart index 47c3266..f1c1ec1 100644 --- a/test/validators/date_test.dart +++ b/test/validators/date_test.dart @@ -7,11 +7,13 @@ void main() { expect('2023-12-01'.isDate, isTrue); expect('2023-12-01 12:00:00'.isDate, isTrue); expect('2023-12-01T12:00:00Z'.isDate, isTrue); - expect('2023-13-01'.isDate, isTrue); // DateTime.tryParse allows rollover + expect('2024-02-29'.isDate, isTrue); // leap year }); test('Invalid Dates', () { expect('invalid date'.isDate, isFalse); + expect('2023-13-01'.isDate, isFalse); + expect('2023-02-29'.isDate, isFalse); expect(''.isDate, isFalse); }); }); diff --git a/test/validators/int_test.dart b/test/validators/int_test.dart index a4cb061..c9ace3f 100644 --- a/test/validators/int_test.dart +++ b/test/validators/int_test.dart @@ -14,6 +14,8 @@ void main() { test('Invalid integers', () { expect('12.34'.isInt, isFalse); expect('abc'.isInt, isFalse); + expect('0x10'.isInt, isFalse); + expect('0X10'.isInt, isFalse); expect(''.isInt, isFalse); }); }); diff --git a/test/validators/url_test.dart b/test/validators/url_test.dart index 2b852f0..f707159 100644 --- a/test/validators/url_test.dart +++ b/test/validators/url_test.dart @@ -7,11 +7,17 @@ void main() { expect('https://google.com'.isURL, isTrue); expect('http://example.com/path?query=1'.isURL, isTrue); expect('https://sub.domain.org'.isURL, isTrue); + expect('http://localhost'.isURL, isTrue); + expect('http://127.0.0.1'.isURL, isTrue); }); test('Invalid URLs', () { expect('invalid-url'.isURL, isFalse); expect('ftp://server.com'.isURL, isFalse); // We only support http/https + expect('javascript:alert(1)'.isURL, isFalse); + expect('https://'.isURL, isFalse); + expect('http://.'.isURL, isFalse); + expect('http://foo'.isURL, isFalse); expect(''.isURL, isFalse); }); }); From af0fff21631ef6e08348b5735d994173aceece6a Mon Sep 17 00:00:00 2001 From: Divyanshu Bhargava Date: Mon, 21 Sep 2026 16:51:48 +0530 Subject: [PATCH 2/3] fix: skip Flutter example when resolving CI dependencies dart pub get also resolves example/, which needs the Flutter SDK. The package job is Dart-only, so skip that extra resolution. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1bc60c..d98e56f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 - name: Install dependencies - run: dart pub get + run: dart pub get --no-example - name: Analyze code run: dart analyze - name: Run tests From f721d25b60b1c72321cdcefe455905a31690d3a3 Mon Sep 17 00:00:00 2001 From: Divyanshu Bhargava Date: Mon, 21 Sep 2026 17:00:56 +0530 Subject: [PATCH 3/3] fix: keep Dart CI from analyzing the Flutter example dart analyze at the repo root still walked into example/ without a Flutter SDK. Exclude that tree and analyze only lib and test. --- .github/workflows/test.yml | 2 +- analysis_options.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d98e56f..7669edb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,6 @@ jobs: - name: Install dependencies run: dart pub get --no-example - name: Analyze code - run: dart analyze + run: dart analyze lib test - name: Run tests run: dart test diff --git a/analysis_options.yaml b/analysis_options.yaml index 572dd23..d8969e1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ include: package:lints/recommended.yaml + +analyzer: + exclude: + - example/**