diff --git a/CHANGELOG.md b/CHANGELOG.md index 165a771..2732955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.2.0 + +* Added 21 new validators inspired by validator.js: `isLowercase`, `isUppercase`, + `isHexadecimal`, `isOctal`, `isDecimal`, `isFloat`, `isMongoId`, `isMD5`, + `isPort`, `isSemVer`, `isSlug`, `isMACAddress`, `isLatLong`, `isJWT`, `isFQDN`, + `isBase64`, `isByteLength`, `isStrongPassword`, `isIn`, `matches` and `contains`. +* Added a new sanitizers module (`package:flutter_validators/sanitizers.dart`): + `trim`, `ltrim`, `rtrim`, `escape`, `unescape`, `blacklist`, `whitelist`, + `stripLow`, `normalizeEmail`, `toBoolean`, `toInt`, `toFloat` and `toDate`. +* Extended the `Validator` form class with methods for every new validator. +* Added test coverage for all new validators and sanitizers. + ## 1.1.0 * Major upgrade! Added 9 new standard validators (URL, UUID, Date, Numeric, Alpha, IP, Hex Color, Credit Card, Length). diff --git a/README.md b/README.md index f249ef4..30751b4 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,64 @@

- Inspired by validator.js · 20+ validators · Works with Flutter Forms out of the box + Inspired by validator.js · 40+ validators & sanitizers · Works with Flutter Forms out of the box

--- -A pure Dart package with 20+ string validators and sanitizers, from emails and URLs to credit cards and UUIDs. Use them as simple functions, convenient `String` extensions, or plug them directly into Flutter's `TextFormField` with the built-in `Validator` class. Zero dependencies, fully tested. +**Flutter Validators** is a pure Dart package with **40+ string validators** and **13 sanitizers** — from emails and URLs to credit cards, UUIDs, JWTs and strong-password checks. Every validator works three ways: + +- as a **top-level function** — `isEmail('foo@bar.com')` +- as a **`String` extension** — `'foo@bar.com'.isEmail` +- as a **Flutter form validator** — `Validator.email()` plugs straight into `TextFormField` + +Zero runtime dependencies. Fully tested. Works with both Dart and Flutter. + +--- + +## 📚 Table of Contents + +- [✨ Features](#-features) +- [📦 Installation](#-installation) +- [🚀 Quick Start](#-quick-start) +- [🧩 Validators](#-validators) + - [Contact and Web](#contact-and-web) + - [Numbers](#numbers) + - [Text and Format](#text-and-format) + - [Encoding and Data](#encoding-and-data) + - [Identifiers and Crypto](#identifiers-and-crypto) + - [Security](#security) +- [🧹 Sanitizers](#-sanitizers) + - [Trimming](#trimming) + - [HTML Escaping](#html-escaping) + - [Character Filtering](#character-filtering) + - [Type Conversion](#type-conversion) + - [Email Normalization](#email-normalization) +- [📝 Flutter Form Integration](#-flutter-form-integration) +- [💡 Behavior Notes and FAQ](#-behavior-notes-and-faq) +- [🤝 Contributing](#-contributing) +- [📄 License](#-license) + +--- + +## ✨ Features + +- **40+ validators** covering email, URL, numbers, encodings, identifiers, crypto hashes and more. +- **13 sanitizers** for trimming, HTML escaping, character filtering and type conversion. +- **Three usage styles** — top-level functions, `String` extensions, and Flutter form validators — pick whatever reads best. +- **First-class Flutter form support** via the `Validator` class, which returns `String? Function(String?)` closures with customizable error messages. +- **Pure Dart, zero runtime dependencies** — lightweight and safe to add to any project. +- **Fully tested** — every validator and sanitizer has dedicated test coverage. --- ## 📦 Installation +Add the package to your `pubspec.yaml`: + ```yaml dependencies: - flutter_validators: ^1.1.0 + flutter_validators: ^1.2.0 ``` Then run: @@ -44,121 +88,433 @@ dart pub get ## 🚀 Quick Start +Import the package: + ```dart import 'package:flutter_validators/flutter_validators.dart'; ``` -### Use as String Extensions +**As `String` extensions** — the most concise style: ```dart -'foo@bar.com'.isEmail; // true -'https://google.com'.isURL; // true +'foo@bar.com'.isEmail; // true +'https://google.com'.isURL; // true '4111111111111111'.isCreditCard; // true -'abc123'.isAlphanumeric; // true +'abc123'.isAlphanumeric; // true ``` -### Use as Top-Level Functions +**As top-level functions** — handy when the value isn't a literal: ```dart -isEmail('foo@bar.com'); // true +isEmail('foo@bar.com'); // true isURL('https://google.com'); // true -isIP('192.168.1.1'); // true +isIP('192.168.1.1'); // true ``` ---- - -## 📝 Flutter Form Integration - -The `Validator` class returns `String? Function(String?)` closures — exactly what `TextFormField.validator` expects. Each method accepts a custom `errorMessage`. +**As Flutter form validators** — drop straight into `TextFormField`: ```dart -Form( - child: Column( - children: [ - TextFormField( - decoration: const InputDecoration(labelText: 'Email'), - validator: Validator.email(errorMessage: 'Enter a valid email'), - ), - TextFormField( - decoration: const InputDecoration(labelText: 'Website'), - validator: Validator.url(), - ), - TextFormField( - decoration: const InputDecoration(labelText: 'Age'), - validator: Validator.numeric(errorMessage: 'Must be a number'), - ), - ], - ), +TextFormField( + validator: Validator.email(errorMessage: 'Enter a valid email'), ) ``` -> **Tip:** Use `Validator.required()` alongside other validators to enforce non-empty fields. - -See the [`example/`](example/) directory for a complete working app. - --- -## 📋 All Validators +## 🧩 Validators + +Every validator is available **both** as a top-level function and as a `String` extension. Parameterized validators accept their options as named/positional arguments. -Every validator is available **both** as a top-level function and as a `String` extension. +### Contact and Web | Validator | Extension | Description | |---|---|---| | `isEmail(str)` | `str.isEmail` | Valid email address | | `isURL(str)` | `str.isURL` | Valid HTTP/HTTPS URL | -| `isIP(str, [version])` | `str.isIP` / `str.isIPv4` / `str.isIPv6` | Valid IP address (v4 or v6) | -| `isUUID(str)` | `str.isUUID` | Valid UUID | -| `isCreditCard(str)` | `str.isCreditCard` | Credit card number (Luhn algorithm) | -| `isDate(str)` | `str.isDate` | Parseable date string | -| `isJson(str)` | `str.isJson` | Valid JSON | -| `isInt(str)` | `str.isInt` | Valid integer | -| `isNumeric(str)` | `str.isNumeric` | Valid number (int or float) | +| `isFQDN(str)` | `str.isFQDN` | Fully qualified domain name | +| `isPhone(str)` | `str.isPhone` | Phone number (international & US formats) | +| `isLatLong(str)` | `str.isLatLong` | `latitude,longitude` coordinate pair | + +```dart +'user@example.com'.isEmail; // true +'https://dart.dev'.isURL; // true +'sub.example.co.uk'.isFQDN; // true +'localhost'.isFQDN; // false (no TLD) +'(123) 456-7890'.isPhone; // true +'40.7128,-74.0060'.isLatLong; // true +``` + +### Numbers + +| Validator | Extension | Description | +|---|---|---| +| `isInt(str)` | `str.isInt` | 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 | +| `isHexadecimal(str)` | `str.isHexadecimal` | Hexadecimal number | +| `isOctal(str)` | `str.isOctal` | Octal number | +| `isPort(str)` | `str.isPort` | Port number (0–65535) | + +```dart +'42'.isInt; // true +'3.14'.isNumeric; // true +'1.5'.isFloat(); // true +'5'.isFloat(min: 0, max: 2); // false (out of range) +'.5'.isDecimal; // true +'deadBEEF'.isHexadecimal; // true +'0o17'.isOctal; // true +'8080'.isPort; // true +'65536'.isPort; // false (out of range) +``` + +### Text and Format + +| Validator | Extension | Description | +|---|---|---| | `isAlpha(str)` | `str.isAlpha` | Letters only (a–z, A–Z) | | `isAlphanumeric(str)` | `str.isAlphanumeric` | Letters and numbers only | | `isAscii(str)` | `str.isAscii` | ASCII characters only | +| `isLowercase(str)` | `str.isLowercase` | Entirely lowercase | +| `isUppercase(str)` | `str.isUppercase` | Entirely uppercase | +| `isLength(str, min, [max])` | `str.isLength(min, [max])` | Length within a range | +| `isByteLength(str, min, [max])` | `str.isByteLength(min, [max])` | UTF-8 byte length within a range | +| `isSlug(str)` | `str.isSlug` | URL slug (`my-blog-post`) | +| `isIn(str, values)` | `str.isIn(values)` | One of an allowed set of values | +| `matches(str, pattern)` | `str.matches(pattern)` | Matches a `Pattern` / `RegExp` | +| `contains(str, seed, {ignoreCase, minOccurrences})` | — | Contains a substring | +| `equals(str, comparison)` | `str.equals(comparison)` | Exact (case-sensitive) string match | + +```dart +'Hello'.isAlpha; // true +'abc123'.isAlphanumeric; // true +'héllo'.isAscii; // false +'hello'.isLowercase; // true +'abc'.isLength(2, 5); // true +'é'.isByteLength(2, 2); // true ('é' is 2 bytes in UTF-8) +'my-blog-post'.isSlug; // true +'red'.isIn(['red', 'green', 'blue']); // true +'abc123'.matches(RegExp(r'\d+')); // true +'foo'.equals('foo'); // true + +// `contains` is a top-level function only (see Behavior Notes) +contains('hello world', 'world'); // true +contains('Hello World', 'world', ignoreCase: true); // true +contains('a-a-a', 'a', minOccurrences: 3); // true +``` + +### Encoding and Data + +| Validator | Extension | Description | +|---|---|---| | `isBase32(str)` | `str.isBase32` | Base32 encoded | | `isBase58(str)` | `str.isBase58` | Base58 encoded | -| `isBoolean(str)` | `str.isBoolean` | Boolean string (`true`/`false`/`1`/`0`) | +| `isBase64(str, {urlSafe})` | `str.isBase64({urlSafe})` | Base64 encoded (standard or URL-safe) | +| `isJson(str)` | `str.isJson` | Valid JSON | | `isHexColor(str)` | `str.isHexColor` | Hex color code (`#fff`, `ff0000`) | -| `isPhone(str)` | `str.isPhone` | Valid phone number | -| `isLength(str, min, [max])` | `str.isLength(min, [max])` | Length within range | -| `equals(str, comparison)` | `str.equals(comparison)` | Exact string match | +| `isBoolean(str)` | `str.isBoolean` | Boolean string (`true`/`false`/`1`/`0`) | +| `isDate(str)` | `str.isDate` | Parseable date string | + +```dart +'JBSWY3DP'.isBase32; // true +'aGVsbG8='.isBase64(); // true +'a-b_cdef'.isBase64(urlSafe: true); // true +'{"name":"Dart"}'.isJson; // true +'#ff0000'.isHexColor; // true +'true'.isBoolean; // true +'2024-01-15'.isDate; // true +``` + +### Identifiers and Crypto + +| Validator | Extension | Description | +|---|---|---| +| `isUUID(str)` | `str.isUUID` | UUID (v1, v3, v4, v5) | +| `isMongoId(str)` | `str.isMongoId` | MongoDB ObjectId (24-char hex) | +| `isMD5(str)` | `str.isMD5` | MD5 hash | +| `isJWT(str)` | `str.isJWT` | JSON Web Token | +| `isCreditCard(str)` | `str.isCreditCard` | Credit card number (Luhn algorithm) | +| `isMACAddress(str)` | `str.isMACAddress` | MAC address (EUI-48 / EUI-64) | +| `isSemVer(str)` | `str.isSemVer` | Semantic version | + +```dart +'550e8400-e29b-41d4-a716-446655440000'.isUUID; // true +'507f1f77bcf86cd799439011'.isMongoId; // true +'d41d8cd98f00b204e9800998ecf8427e'.isMD5; // true +'eyJhbGci.eyJzdWIi.SflKxwRJ'.isJWT; // true +'4111111111111111'.isCreditCard; // true +'00:1B:44:11:3A:B7'.isMACAddress; // true +'2.1.0-alpha.1'.isSemVer; // true +``` + +### Security + +| Validator | Extension | Description | +|---|---|---| +| `isStrongPassword(str, {...})` | `str.isStrongPassword({...})` | Password meets configurable strength rules | + +`isStrongPassword` accepts five options, all with sensible defaults: + +| Option | Default | Meaning | +|---|---|---| +| `minLength` | `8` | Minimum total length | +| `minLowercase` | `1` | Minimum lowercase letters | +| `minUppercase` | `1` | Minimum uppercase letters | +| `minNumbers` | `1` | Minimum digits | +| `minSymbols` | `1` | Minimum non-alphanumeric symbols | + +```dart +'Abcd1234!'.isStrongPassword(); // true +'weak'.isStrongPassword(); // false + +// Relax the rules — e.g. allow passphrases with no symbols or digits +'abcdefghij'.isStrongPassword( + minUppercase: 0, + minNumbers: 0, + minSymbols: 0, +); // true +``` --- -## 🏗️ Form Validator API Reference +## 🧹 Sanitizers -All methods on the `Validator` class return `String? Function(String?)`: +Sanitizers transform or coerce strings. Like validators, they're available as both top-level functions and `String` extensions. Import them via the main library or directly: + +```dart +import 'package:flutter_validators/flutter_validators.dart'; +// or, sanitizers only: +import 'package:flutter_validators/sanitizers.dart'; +``` + +### Trimming + +| Sanitizer | Extension | Description | +|---|---|---| +| `trim(str, [chars])` | `str.trimChars(chars)` | Trim whitespace/chars from both ends | +| `ltrim(str, [chars])` | `str.ltrimChars(chars)` | Trim from the start | +| `rtrim(str, [chars])` | `str.rtrimChars(chars)` | Trim from the end | + +```dart +trim(' hello '); // 'hello' +trim('xxhelloxx', 'x'); // 'hello' +ltrim('00042', '0'); // '42' +rtrim('hello!!!', '!'); // 'hello' +``` + +### HTML Escaping + +| Sanitizer | Extension | Description | +|---|---|---| +| `escape(str)` | `str.escape()` | Escape HTML-unsafe characters | +| `unescape(str)` | `str.unescape()` | Reverse of `escape` | + +```dart +escape(''); +// '<script>alert(1)</script>' + +unescape('<b>hi</b>'); // 'hi' +``` + +### Character Filtering + +| Sanitizer | Extension | Description | +|---|---|---| +| `blacklist(str, chars)` | `str.blacklist(chars)` | Remove the listed characters | +| `whitelist(str, chars)` | `str.whitelist(chars)` | Keep only the listed characters | +| `stripLow(str, {keepNewLines})` | `str.stripLow({keepNewLines})` | Remove ASCII control characters | + +```dart +blacklist('hello world', 'lo'); // 'he wrd' +whitelist('a1b2c3', '0123456789'); // '123' +stripLow('line1\nline2'); // 'line1line2' +stripLow('line1\nline2', keepNewLines: true); // 'line1\nline2' +``` + +### Type Conversion + +| Sanitizer | Extension | Returns | Description | +|---|---|---|---| +| `toBoolean(str, {strict})` | `str.toBoolean({strict})` | `bool` | Convert to a boolean | +| `toInt(str, {radix})` | `str.toInt({radix})` | `int?` | Parse to an integer | +| `toFloat(str)` | `str.toFloat()` | `double?` | Parse to a double | +| `toDate(str)` | `str.toDate()` | `DateTime?` | Parse to a `DateTime` | + +```dart +toBoolean('true'); // true +toBoolean('0'); // false +toBoolean('yes', strict: true); // false (strict: only '1'/'true' are true) +toInt('42'); // 42 +toInt('ff', radix: 16); // 255 +toInt('abc'); // null +toFloat('3.14'); // 3.14 +toDate('2024-01-15'); // DateTime(2024, 1, 15) +``` + +### Email Normalization + +| Sanitizer | Extension | Returns | Description | +|---|---|---|---| +| `normalizeEmail(str)` | `str.normalizeEmail()` | `String?` | Canonicalize an email address | + +```dart +normalizeEmail('Test.User+promo@GMAIL.com'); // 'testuser@gmail.com' +normalizeEmail('User@Example.COM'); // 'User@example.com' +normalizeEmail('not-an-email'); // null +``` + +--- + +## 📝 Flutter Form Integration + +The `Validator` class returns `String? Function(String?)` closures — exactly the type `TextFormField.validator` expects. A closure returns `null` when the value is valid, or the error message when it isn't. Every method accepts a custom `errorMessage`. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_validators/flutter_validators.dart'; + +class SignUpForm extends StatefulWidget { + const SignUpForm({super.key}); + + @override + State createState() => _SignUpFormState(); +} + +class _SignUpFormState extends State { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + children: [ + // Combine `required` with `email` to enforce a non-empty, valid email. + 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); + }, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Website'), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: Validator.url(), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: Validator.strongPassword( + errorMessage: 'Use 8+ chars with upper, lower, number & symbol', + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Form is valid!')), + ); + } + }, + child: const Text('Sign Up'), + ), + ], + ), + ); + } +} +``` + +A complete, runnable app is in the [`example/`](example/) directory. + +### Validator API Reference + +Every method on the `Validator` class returns `String? Function(String?)`: ```dart Validator.required({String errorMessage}) Validator.email({String errorMessage}) Validator.url({String errorMessage}) Validator.ip({int? version, String errorMessage}) +Validator.fqdn({String errorMessage}) +Validator.phone({String errorMessage}) +Validator.latLong({String errorMessage}) Validator.date({String errorMessage}) Validator.numeric({String errorMessage}) Validator.integer({String errorMessage}) +Validator.float({double? min, double? max, String errorMessage}) +Validator.decimal({String errorMessage}) +Validator.hexadecimal({String errorMessage}) +Validator.octal({String errorMessage}) +Validator.port({String errorMessage}) Validator.alpha({String errorMessage}) Validator.alphanumeric({String errorMessage}) -Validator.phone({String errorMessage}) -Validator.creditCard({String errorMessage}) -Validator.json({String errorMessage}) -Validator.uuid({String errorMessage}) -Validator.hexColor({String errorMessage}) Validator.ascii({String errorMessage}) +Validator.lowercase({String errorMessage}) +Validator.uppercase({String errorMessage}) +Validator.slug({String errorMessage}) +Validator.length(int min, {int? max, String errorMessage}) +Validator.byteLength(int min, {int? max, String errorMessage}) +Validator.contains(String seed, {bool ignoreCase, int minOccurrences, String errorMessage}) +Validator.matches(Pattern pattern, {String errorMessage}) +Validator.inList(Iterable allowed, {String errorMessage}) +Validator.equals(String comparison, {String errorMessage}) Validator.base32({String errorMessage}) Validator.base58({String errorMessage}) +Validator.base64({bool urlSafe, String errorMessage}) +Validator.json({String errorMessage}) +Validator.hexColor({String errorMessage}) Validator.boolean({String errorMessage}) -Validator.equals(String comparison, {String errorMessage}) -Validator.length(int min, {int? max, String errorMessage}) +Validator.uuid({String errorMessage}) +Validator.mongoId({String errorMessage}) +Validator.md5({String errorMessage}) +Validator.jwt({String errorMessage}) +Validator.creditCard({String errorMessage}) +Validator.macAddress({String errorMessage}) +Validator.semVer({String errorMessage}) +Validator.strongPassword({int minLength, int minLowercase, int minUppercase, int minNumbers, int minSymbols, String errorMessage}) ``` --- +## 💡 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()`: + +```dart +validator: (value) { + return Validator.required()(value) ?? Validator.email()(value); +} +``` + +**`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. + +**`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. + +**`isBoolean` accepts `'true'`, `'false'`, `'1'` and `'0'`.** Any other value is not a boolean string. + +**`isFloat` rejects non-finite values.** `'Infinity'` and `'NaN'` return `false`, even though Dart's `double.tryParse` can parse them. + +**`normalizeEmail` applies Gmail-specific rules.** For `gmail.com` / `googlemail.com` addresses it lowercases the local part, removes dots, and strips any `+tag` suffix. For other providers it only lowercases the domain. It returns `null` if the input isn't a valid email. + +--- + ## 🤝 Contributing -Contributions, issues, and feature requests are welcome! -Feel free to check the [issues page](https://github.com/divyanshub024/flutter_validators/issues). +Contributions, issues and feature requests are welcome! Check the [issues page](https://github.com/divyanshub024/flutter_validators/issues) to get started. + +Before opening a pull request, please run the test suite: + +```sh +dart test +``` --- diff --git a/lib/flutter_validators.dart b/lib/flutter_validators.dart index de1d79e..eb74ecb 100644 --- a/lib/flutter_validators.dart +++ b/lib/flutter_validators.dart @@ -1,22 +1,45 @@ library; +export 'validators/alpha.dart'; export 'validators/ascii.dart'; export 'validators/base32.dart'; export 'validators/base58.dart'; +export 'validators/base64.dart'; export 'validators/boolean.dart'; -export 'validators/email.dart'; -export 'validators/equals.dart'; -export 'validators/int.dart'; -export 'validators/json.dart'; -export 'validators/phone.dart'; -export 'validators/alpha.dart'; +export 'validators/byte_length.dart'; +export 'validators/contains.dart'; export 'validators/credit_card.dart'; export 'validators/date.dart'; +export 'validators/decimal.dart'; +export 'validators/email.dart'; +export 'validators/equals.dart'; +export 'validators/float.dart'; +export 'validators/fqdn.dart'; export 'validators/hex_color.dart'; +export 'validators/hexadecimal.dart'; +export 'validators/in.dart'; +export 'validators/int.dart'; export 'validators/ip.dart'; +export 'validators/json.dart'; +export 'validators/jwt.dart'; +export 'validators/lat_long.dart'; export 'validators/length.dart'; +export 'validators/lowercase.dart'; +export 'validators/mac_address.dart'; +export 'validators/matches.dart'; +export 'validators/md5.dart'; +export 'validators/mongo_id.dart'; export 'validators/numeric.dart'; +export 'validators/octal.dart'; +export 'validators/phone.dart'; +export 'validators/port.dart'; +export 'validators/semver.dart'; +export 'validators/slug.dart'; +export 'validators/strong_password.dart'; +export 'validators/uppercase.dart'; export 'validators/url.dart'; export 'validators/uuid.dart'; +export 'sanitizers.dart'; + export 'form_validator.dart'; diff --git a/lib/form_validator.dart b/lib/form_validator.dart index e2cd843..6e2f089 100644 --- a/lib/form_validator.dart +++ b/lib/form_validator.dart @@ -1,4 +1,5 @@ import 'package:flutter_validators/flutter_validators.dart'; +import 'package:flutter_validators/validators/contains.dart' as contains_fn; /// A utility class for Flutter Form validation. /// Provides methods that return a validator function suitable for `TextFormField`. @@ -155,6 +156,185 @@ class Validator { return _build(errorMessage, (v) => v.isLength(min, max)); } + /// Ensures the string is entirely lowercase. + static String? Function(String?) lowercase({ + String errorMessage = 'Must be lowercase', + }) { + return _build(errorMessage, (v) => v.isLowercase); + } + + /// Ensures the string is entirely uppercase. + static String? Function(String?) uppercase({ + String errorMessage = 'Must be uppercase', + }) { + return _build(errorMessage, (v) => v.isUppercase); + } + + /// Ensures the string is a hexadecimal number. + static String? Function(String?) hexadecimal({ + String errorMessage = 'Please enter a valid hexadecimal number', + }) { + return _build(errorMessage, (v) => v.isHexadecimal); + } + + /// Ensures the string is an octal number. + static String? Function(String?) octal({ + String errorMessage = 'Please enter a valid octal number', + }) { + return _build(errorMessage, (v) => v.isOctal); + } + + /// Ensures the string is a valid MongoDB ObjectId. + static String? Function(String?) mongoId({ + String errorMessage = 'Please enter a valid MongoDB ObjectId', + }) { + return _build(errorMessage, (v) => v.isMongoId); + } + + /// Ensures the string is a valid MD5 hash. + static String? Function(String?) md5({ + String errorMessage = 'Please enter a valid MD5 hash', + }) { + return _build(errorMessage, (v) => v.isMD5); + } + + /// Ensures the string is a valid port number. + static String? Function(String?) port({ + String errorMessage = 'Please enter a valid port number', + }) { + return _build(errorMessage, (v) => v.isPort); + } + + /// Ensures the string is a valid Semantic Version. + static String? Function(String?) semVer({ + String errorMessage = 'Please enter a valid semantic version', + }) { + return _build(errorMessage, (v) => v.isSemVer); + } + + /// Ensures the string is a valid URL slug. + static String? Function(String?) slug({ + String errorMessage = 'Please enter a valid slug', + }) { + return _build(errorMessage, (v) => v.isSlug); + } + + /// Ensures the string is a valid MAC address. + static String? Function(String?) macAddress({ + String errorMessage = 'Please enter a valid MAC address', + }) { + return _build(errorMessage, (v) => v.isMACAddress); + } + + /// Ensures the string is a valid `latitude,longitude` pair. + static String? Function(String?) latLong({ + String errorMessage = 'Please enter valid coordinates', + }) { + return _build(errorMessage, (v) => v.isLatLong); + } + + /// Ensures the string is a valid JSON Web Token. + static String? Function(String?) jwt({ + String errorMessage = 'Please enter a valid JWT', + }) { + return _build(errorMessage, (v) => v.isJWT); + } + + /// Ensures the string is a fully qualified domain name. + static String? Function(String?) fqdn({ + String errorMessage = 'Please enter a valid domain name', + }) { + return _build(errorMessage, (v) => v.isFQDN); + } + + /// Ensures the string is Base64 encoded. + static String? Function(String?) base64({ + bool urlSafe = false, + String errorMessage = 'Please enter a valid Base64 encoded string', + }) { + return _build(errorMessage, (v) => v.isBase64(urlSafe: urlSafe)); + } + + /// Ensures the string represents a decimal number. + static String? Function(String?) decimal({ + String errorMessage = 'Please enter a valid decimal number', + }) { + return _build(errorMessage, (v) => v.isDecimal); + } + + /// Ensures the string contains the [seed] substring. + static String? Function(String?) contains( + String seed, { + bool ignoreCase = false, + int minOccurrences = 1, + String errorMessage = 'Required text is missing', + }) { + return _build( + errorMessage, + (v) => contains_fn.contains( + v, + seed, + ignoreCase: ignoreCase, + minOccurrences: minOccurrences, + ), + ); + } + + /// Ensures the string matches the given [pattern]. + static String? Function(String?) matches( + Pattern pattern, { + String errorMessage = 'Invalid format', + }) { + return _build(errorMessage, (v) => v.matches(pattern)); + } + + /// Ensures the string is one of the [allowed] values. + static String? Function(String?) inList( + Iterable allowed, { + String errorMessage = 'Value is not allowed', + }) { + return _build(errorMessage, (v) => isIn(v, allowed)); + } + + /// Ensures the string is a finite floating-point number. + static String? Function(String?) float({ + double? min, + double? max, + String errorMessage = 'Please enter a valid number', + }) { + return _build(errorMessage, (v) => v.isFloat(min: min, max: max)); + } + + /// Ensures the string's UTF-8 byte length falls within a range. + static String? Function(String?) byteLength( + int min, { + int? max, + String errorMessage = 'Length is out of range', + }) { + return _build(errorMessage, (v) => v.isByteLength(min, max)); + } + + /// Ensures the string is a strong password. + static String? Function(String?) strongPassword({ + int minLength = 8, + int minLowercase = 1, + int minUppercase = 1, + int minNumbers = 1, + int minSymbols = 1, + String errorMessage = 'Password is not strong enough', + }) { + return _build( + errorMessage, + (v) => v.isStrongPassword( + minLength: minLength, + minLowercase: minLowercase, + minUppercase: minUppercase, + minNumbers: minNumbers, + minSymbols: minSymbols, + ), + ); + } + /// Internal helper to construct the validator closure static String? Function(String?) _build( String errorMessage, diff --git a/lib/sanitizers.dart b/lib/sanitizers.dart new file mode 100644 index 0000000..ce4a15d --- /dev/null +++ b/lib/sanitizers.dart @@ -0,0 +1,13 @@ +/// String sanitizers for the flutter_validators package. +/// +/// Provides functions that transform or coerce strings, mirroring the +/// sanitizers offered by validator.js. +library; + +export 'validators/sanitizers/blacklist.dart'; +export 'validators/sanitizers/escape.dart'; +export 'validators/sanitizers/normalize_email.dart'; +export 'validators/sanitizers/strip_low.dart'; +export 'validators/sanitizers/to_boolean.dart'; +export 'validators/sanitizers/to_number.dart'; +export 'validators/sanitizers/trim.dart'; diff --git a/lib/validators/base64.dart b/lib/validators/base64.dart new file mode 100644 index 0000000..ea8c44b --- /dev/null +++ b/lib/validators/base64.dart @@ -0,0 +1,36 @@ +/// Checks if the string is Base64 encoded. +/// +/// By default the standard Base64 alphabet is used. Set [urlSafe] to `true` +/// to validate against the URL- and filename-safe alphabet. +/// +/// Example: +/// ```dart +/// isBase64('aGVsbG8='); // true +/// isBase64('aGVsbG8'); // false (invalid padding) +/// isBase64('aGVsbG8', urlSafe: true); // true +/// ``` +bool isBase64(String str, {bool urlSafe = false}) => _isBase64(str, urlSafe); + +/// Extension providing Base64 validation methods on [String]. +extension Base64X on String { + /// Checks if the string is Base64 encoded. + bool isBase64({bool urlSafe = false}) { + return _isBase64(this, urlSafe); + } +} + +final _base64Standard = RegExp( + r'^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$', +); +final _base64UrlSafe = RegExp(r'^[A-Za-z0-9_-]+$'); + +bool _isBase64(String str, bool urlSafe) { + if (str.isEmpty) return false; + if (urlSafe) { + // A base64 string can never have a length of `4n + 1`. + if (str.length % 4 == 1) return false; + return _base64UrlSafe.hasMatch(str); + } + if (str.length % 4 != 0) return false; + return _base64Standard.hasMatch(str); +} diff --git a/lib/validators/byte_length.dart b/lib/validators/byte_length.dart new file mode 100644 index 0000000..db58562 --- /dev/null +++ b/lib/validators/byte_length.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +/// Checks if the string's UTF-8 byte length falls in a range. +/// +/// The byte length must be at least [min]. If [max] is provided, it must +/// also be at most [max]. +/// +/// Example: +/// ```dart +/// isByteLength('abc', 2); // true +/// isByteLength('abc', 4); // false +/// isByteLength('é', 1, 2); // true ('é' is 2 bytes in UTF-8) +/// ``` +bool isByteLength(String str, int min, [int? max]) => + _isByteLength(str, min, max); + +/// Extension providing byte-length validation methods on [String]. +extension ByteLengthX on String { + /// Checks if the string's UTF-8 byte length falls in a range. + bool isByteLength(int min, [int? max]) { + return _isByteLength(this, min, max); + } +} + +bool _isByteLength(String str, int min, int? max) { + final length = utf8.encode(str).length; + return length >= min && (max == null || length <= max); +} diff --git a/lib/validators/contains.dart b/lib/validators/contains.dart new file mode 100644 index 0000000..40752a7 --- /dev/null +++ b/lib/validators/contains.dart @@ -0,0 +1,25 @@ +/// Checks if the string contains the [seed] substring. +/// +/// Set [ignoreCase] to `true` for a case-insensitive match. [minOccurrences] +/// requires the seed to appear at least that many times. +/// +/// Note: this is exposed only as a top-level function because [String] +/// already provides a built-in `contains` method. +/// +/// Example: +/// ```dart +/// contains('hello world', 'world'); // true +/// contains('hello world', 'WORLD', ignoreCase: true); // true +/// contains('a-a-a', 'a', minOccurrences: 3); // true +/// ``` +bool contains( + String str, + String seed, { + bool ignoreCase = false, + int minOccurrences = 1, +}) { + if (seed.isEmpty) return false; + final haystack = ignoreCase ? str.toLowerCase() : str; + final needle = ignoreCase ? seed.toLowerCase() : seed; + return needle.allMatches(haystack).length >= minOccurrences; +} diff --git a/lib/validators/decimal.dart b/lib/validators/decimal.dart new file mode 100644 index 0000000..788e57e --- /dev/null +++ b/lib/validators/decimal.dart @@ -0,0 +1,28 @@ +/// Checks if the string represents a decimal number. +/// +/// Accepts an optional sign followed by digits with an optional fractional +/// part (e.g. `1`, `-1.5`, `.5`, `+10.0`). +/// +/// Example: +/// ```dart +/// isDecimal('1.5'); // true +/// isDecimal('-0.25'); // true +/// isDecimal('.5'); // true +/// isDecimal('1.'); // false +/// isDecimal('abc'); // false +/// ``` +bool isDecimal(String str) => _isDecimal(str); + +/// Extension providing decimal validation methods on [String]. +extension DecimalX on String { + /// Checks if the string represents a decimal number. + bool get isDecimal { + return _isDecimal(this); + } +} + +final _decimal = RegExp(r'^[+-]?(\d+(\.\d+)?|\.\d+)$'); + +bool _isDecimal(String str) { + return _decimal.hasMatch(str); +} diff --git a/lib/validators/float.dart b/lib/validators/float.dart new file mode 100644 index 0000000..0296e51 --- /dev/null +++ b/lib/validators/float.dart @@ -0,0 +1,30 @@ +/// Checks if the string represents a finite floating-point number. +/// +/// If [min] and/or [max] are provided, the parsed value must also fall +/// within that inclusive range. +/// +/// Example: +/// ```dart +/// isFloat('1.5'); // true +/// isFloat('-3'); // true +/// isFloat('1.5', min: 0, max: 2); // true +/// isFloat('5', min: 0, max: 2); // false +/// isFloat('abc'); // false +/// ``` +bool isFloat(String str, {double? min, double? max}) => _isFloat(str, min, max); + +/// Extension providing float validation methods on [String]. +extension FloatX on String { + /// Checks if the string represents a finite floating-point number. + bool isFloat({double? min, double? max}) { + return _isFloat(this, min, max); + } +} + +bool _isFloat(String str, double? min, double? max) { + final value = double.tryParse(str); + if (value == null || !value.isFinite) return false; + if (min != null && value < min) return false; + if (max != null && value > max) return false; + return true; +} diff --git a/lib/validators/fqdn.dart b/lib/validators/fqdn.dart new file mode 100644 index 0000000..67719d6 --- /dev/null +++ b/lib/validators/fqdn.dart @@ -0,0 +1,43 @@ +/// Checks if the string is a fully qualified domain name (FQDN). +/// +/// Requires at least two labels separated by dots, a valid top-level domain, +/// and labels that do not start or end with a hyphen. +/// +/// Example: +/// ```dart +/// isFQDN('example.com'); // true +/// isFQDN('sub.example.co.uk'); // true +/// isFQDN('localhost'); // false (no TLD) +/// isFQDN('-bad.com'); // false +/// ``` +bool isFQDN(String str) => _isFQDN(str); + +/// Extension providing FQDN validation methods on [String]. +extension FQDNX on String { + /// Checks if the string is a fully qualified domain name (FQDN). + bool get isFQDN { + return _isFQDN(this); + } +} + +final _fqdnLabel = RegExp(r'^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$'); +final _fqdnTld = RegExp(r'^[a-zA-Z]{2,}$|^xn--[a-zA-Z0-9]+$'); + +bool _isFQDN(String str) { + if (str.isEmpty) return false; + var domain = str; + if (domain.endsWith('.')) { + domain = domain.substring(0, domain.length - 1); + } + final parts = domain.split('.'); + if (parts.length < 2) return false; + + final tld = parts.removeLast(); + if (!_fqdnTld.hasMatch(tld)) return false; + + for (final part in parts) { + if (part.isEmpty || part.length > 63) return false; + if (!_fqdnLabel.hasMatch(part)) return false; + } + return true; +} diff --git a/lib/validators/hexadecimal.dart b/lib/validators/hexadecimal.dart new file mode 100644 index 0000000..3a6366d --- /dev/null +++ b/lib/validators/hexadecimal.dart @@ -0,0 +1,26 @@ +/// Checks if the string is a hexadecimal number. +/// +/// Accepts an optional `0x` or `0h` prefix followed by one or more +/// hexadecimal digits. +/// +/// Example: +/// ```dart +/// isHexadecimal('deadBEEF'); // true +/// isHexadecimal('0xff'); // true +/// isHexadecimal('xyz'); // false +/// ``` +bool isHexadecimal(String str) => _isHexadecimal(str); + +/// Extension providing hexadecimal validation methods on [String]. +extension HexadecimalX on String { + /// Checks if the string is a hexadecimal number. + bool get isHexadecimal { + return _isHexadecimal(this); + } +} + +final _hexadecimal = RegExp(r'^(0x|0h)?[0-9A-Fa-f]+$'); + +bool _isHexadecimal(String str) { + return _hexadecimal.hasMatch(str); +} diff --git a/lib/validators/in.dart b/lib/validators/in.dart new file mode 100644 index 0000000..ffeaad8 --- /dev/null +++ b/lib/validators/in.dart @@ -0,0 +1,20 @@ +/// Checks if the string is one of the [values]. +/// +/// Example: +/// ```dart +/// isIn('red', ['red', 'green', 'blue']); // true +/// isIn('yellow', ['red', 'green', 'blue']); // false +/// ``` +bool isIn(String str, Iterable values) => _isIn(str, values); + +/// Extension providing membership validation methods on [String]. +extension InX on String { + /// Checks if the string is one of the [values]. + bool isIn(Iterable values) { + return _isIn(this, values); + } +} + +bool _isIn(String str, Iterable values) { + return values.contains(str); +} diff --git a/lib/validators/ip.dart b/lib/validators/ip.dart index 9461377..9d1b684 100644 --- a/lib/validators/ip.dart +++ b/lib/validators/ip.dart @@ -16,12 +16,12 @@ extension IpX on String { bool get isIP { return _isIP(this); } - + /// Checks if the string is a valid IPv4 address. bool get isIPv4 { return _isIP(this, 4); } - + /// Checks if the string is a valid IPv6 address. bool get isIPv6 { return _isIP(this, 6); @@ -49,6 +49,8 @@ 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]))$'); + 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); } diff --git a/lib/validators/jwt.dart b/lib/validators/jwt.dart new file mode 100644 index 0000000..66e50be --- /dev/null +++ b/lib/validators/jwt.dart @@ -0,0 +1,31 @@ +/// Checks if the string is a valid JSON Web Token (JWT). +/// +/// A JWT consists of three base64url-encoded segments separated by dots. +/// The signature segment may be empty (for unsigned tokens). +/// +/// Example: +/// ```dart +/// isJWT('eyJhbGci.eyJzdWIi.SflKxwRJ'); // true +/// isJWT('eyJhbGci.eyJzdWIi'); // false (only two segments) +/// ``` +bool isJWT(String str) => _isJWT(str); + +/// Extension providing JWT validation methods on [String]. +extension JWTX on String { + /// Checks if the string is a valid JSON Web Token (JWT). + bool get isJWT { + return _isJWT(this); + } +} + +final _base64Url = RegExp(r'^[A-Za-z0-9_-]+$'); + +bool _isJWT(String str) { + final parts = str.split('.'); + if (parts.length != 3) return false; + if (parts[0].isEmpty || parts[1].isEmpty) return false; + for (var i = 0; i < 2; i++) { + if (!_base64Url.hasMatch(parts[i])) return false; + } + return parts[2].isEmpty || _base64Url.hasMatch(parts[2]); +} diff --git a/lib/validators/lat_long.dart b/lib/validators/lat_long.dart new file mode 100644 index 0000000..a37ef8d --- /dev/null +++ b/lib/validators/lat_long.dart @@ -0,0 +1,30 @@ +/// Checks if the string is a valid `latitude,longitude` pair. +/// +/// Latitude must be in the range -90 to 90 and longitude in the range +/// -180 to 180. The two values are separated by a comma. +/// +/// Example: +/// ```dart +/// isLatLong('40.7128,-74.0060'); // true +/// isLatLong('0,0'); // true +/// isLatLong('91,0'); // false (latitude out of range) +/// ``` +bool isLatLong(String str) => _isLatLong(str); + +/// Extension providing latitude/longitude validation methods on [String]. +extension LatLongX on String { + /// Checks if the string is a valid `latitude,longitude` pair. + bool get isLatLong { + return _isLatLong(this); + } +} + +bool _isLatLong(String str) { + final parts = str.split(','); + if (parts.length != 2) return false; + final lat = double.tryParse(parts[0].trim()); + final long = double.tryParse(parts[1].trim()); + if (lat == null || long == null) return false; + if (!lat.isFinite || !long.isFinite) return false; + return lat >= -90 && lat <= 90 && long >= -180 && long <= 180; +} diff --git a/lib/validators/lowercase.dart b/lib/validators/lowercase.dart new file mode 100644 index 0000000..5358730 --- /dev/null +++ b/lib/validators/lowercase.dart @@ -0,0 +1,25 @@ +/// Checks if the string is entirely lowercase. +/// +/// A string is considered lowercase if it is unchanged when converted to +/// lower case. Strings without cased characters (digits, symbols) and the +/// empty string are considered lowercase. +/// +/// Example: +/// ```dart +/// isLowercase('hello'); // true +/// isLowercase('hello123'); // true +/// isLowercase('Hello'); // false +/// ``` +bool isLowercase(String str) => _isLowercase(str); + +/// Extension providing lowercase validation methods on [String]. +extension LowercaseX on String { + /// Checks if the string is entirely lowercase. + bool get isLowercase { + return _isLowercase(this); + } +} + +bool _isLowercase(String str) { + return str == str.toLowerCase(); +} diff --git a/lib/validators/mac_address.dart b/lib/validators/mac_address.dart new file mode 100644 index 0000000..cd42c47 --- /dev/null +++ b/lib/validators/mac_address.dart @@ -0,0 +1,32 @@ +/// Checks if the string is a valid MAC address. +/// +/// Accepts EUI-48 (6-byte) and EUI-64 (8-byte) addresses using `:` or `-` +/// as separators, as well as the separator-less form. +/// +/// Example: +/// ```dart +/// isMACAddress('00:1B:44:11:3A:B7'); // true +/// isMACAddress('00-1B-44-11-3A-B7'); // true +/// isMACAddress('001B44113AB7'); // true +/// isMACAddress('00:1B:44:11:3A'); // false +/// ``` +bool isMACAddress(String str) => _isMACAddress(str); + +/// Extension providing MAC address validation methods on [String]. +extension MACAddressX on String { + /// Checks if the string is a valid MAC address. + bool get isMACAddress { + return _isMACAddress(this); + } +} + +final _macAddress = RegExp( + r'^(?:[0-9A-Fa-f]{2}([:-]))(?:[0-9A-Fa-f]{2}\1){4}[0-9A-Fa-f]{2}$' + r'|^(?:[0-9A-Fa-f]{2}([:-]))(?:[0-9A-Fa-f]{2}\2){6}[0-9A-Fa-f]{2}$' + r'|^[0-9A-Fa-f]{12}$' + r'|^[0-9A-Fa-f]{16}$', +); + +bool _isMACAddress(String str) { + return _macAddress.hasMatch(str); +} diff --git a/lib/validators/matches.dart b/lib/validators/matches.dart new file mode 100644 index 0000000..a474967 --- /dev/null +++ b/lib/validators/matches.dart @@ -0,0 +1,23 @@ +/// Checks if the string matches the given [pattern]. +/// +/// The [pattern] may be a [RegExp] or any [Pattern]. A match anywhere in the +/// string is sufficient. +/// +/// Example: +/// ```dart +/// matches('abc123', RegExp(r'\d+')); // true +/// matches('abc', RegExp(r'^\d+$')); // false +/// ``` +bool matches(String str, Pattern pattern) => _matches(str, pattern); + +/// Extension providing pattern matching methods on [String]. +extension MatchesX on String { + /// Checks if the string matches the given [pattern]. + bool matches(Pattern pattern) { + return _matches(this, pattern); + } +} + +bool _matches(String str, Pattern pattern) { + return str.contains(pattern); +} diff --git a/lib/validators/md5.dart b/lib/validators/md5.dart new file mode 100644 index 0000000..0ee4ce7 --- /dev/null +++ b/lib/validators/md5.dart @@ -0,0 +1,24 @@ +/// Checks if the string is a valid MD5 hash. +/// +/// An MD5 hash is a 32-character hexadecimal string. +/// +/// Example: +/// ```dart +/// isMD5('d41d8cd98f00b204e9800998ecf8427e'); // true +/// isMD5('d41d8cd98f00b204e9800998ecf8427'); // false (too short) +/// ``` +bool isMD5(String str) => _isMD5(str); + +/// Extension providing MD5 hash validation methods on [String]. +extension MD5X on String { + /// Checks if the string is a valid MD5 hash. + bool get isMD5 { + return _isMD5(this); + } +} + +final _md5 = RegExp(r'^[0-9a-fA-F]{32}$'); + +bool _isMD5(String str) { + return _md5.hasMatch(str); +} diff --git a/lib/validators/mongo_id.dart b/lib/validators/mongo_id.dart new file mode 100644 index 0000000..09f06ee --- /dev/null +++ b/lib/validators/mongo_id.dart @@ -0,0 +1,25 @@ +/// Checks if the string is a valid MongoDB ObjectId. +/// +/// A MongoDB ObjectId is a 24-character hexadecimal string. +/// +/// Example: +/// ```dart +/// isMongoId('507f1f77bcf86cd799439011'); // true +/// isMongoId('507f1f77bcf86cd79943901'); // false (too short) +/// isMongoId('zzzf1f77bcf86cd799439011'); // false +/// ``` +bool isMongoId(String str) => _isMongoId(str); + +/// Extension providing MongoDB ObjectId validation methods on [String]. +extension MongoIdX on String { + /// Checks if the string is a valid MongoDB ObjectId. + bool get isMongoId { + return _isMongoId(this); + } +} + +final _mongoId = RegExp(r'^[0-9a-fA-F]{24}$'); + +bool _isMongoId(String str) { + return _mongoId.hasMatch(str); +} diff --git a/lib/validators/octal.dart b/lib/validators/octal.dart new file mode 100644 index 0000000..f6dafcf --- /dev/null +++ b/lib/validators/octal.dart @@ -0,0 +1,26 @@ +/// Checks if the string is an octal number. +/// +/// Accepts an optional `0o` prefix followed by one or more octal digits +/// (0–7). +/// +/// Example: +/// ```dart +/// isOctal('0o123'); // true +/// isOctal('777'); // true +/// isOctal('088'); // false +/// ``` +bool isOctal(String str) => _isOctal(str); + +/// Extension providing octal validation methods on [String]. +extension OctalX on String { + /// Checks if the string is an octal number. + bool get isOctal { + return _isOctal(this); + } +} + +final _octal = RegExp(r'^(0o)?[0-7]+$'); + +bool _isOctal(String str) { + return _octal.hasMatch(str); +} diff --git a/lib/validators/port.dart b/lib/validators/port.dart new file mode 100644 index 0000000..cdbd822 --- /dev/null +++ b/lib/validators/port.dart @@ -0,0 +1,27 @@ +/// Checks if the string is a valid port number. +/// +/// A valid port is an integer in the range 0–65535 without leading zeros. +/// +/// Example: +/// ```dart +/// isPort('8080'); // true +/// isPort('0'); // true +/// isPort('65536'); // false (out of range) +/// isPort('080'); // false (leading zero) +/// ``` +bool isPort(String str) => _isPort(str); + +/// Extension providing port validation methods on [String]. +extension PortX on String { + /// Checks if the string is a valid port number. + bool get isPort { + return _isPort(this); + } +} + +bool _isPort(String str) { + if (str.isEmpty) return false; + if (str.length > 1 && str.startsWith('0')) return false; + final port = int.tryParse(str); + return port != null && port >= 0 && port <= 65535; +} diff --git a/lib/validators/sanitizers/blacklist.dart b/lib/validators/sanitizers/blacklist.dart new file mode 100644 index 0000000..4fdaa16 --- /dev/null +++ b/lib/validators/sanitizers/blacklist.dart @@ -0,0 +1,35 @@ +/// Removes all characters that appear in [chars] from the string. +/// +/// Example: +/// ```dart +/// blacklist('hello world', 'lo'); // 'he wrd' +/// ``` +String blacklist(String str, String chars) => _blacklist(str, chars); + +/// Removes all characters that do not appear in [chars] from the string. +/// +/// Example: +/// ```dart +/// whitelist('hello world', 'lo'); // 'llool' +/// ``` +String whitelist(String str, String chars) => _whitelist(str, chars); + +/// Extension providing character filtering sanitizers on [String]. +extension BlacklistX on String { + /// Removes all characters that appear in [chars] from the string. + String blacklist(String chars) => _blacklist(this, chars); + + /// Removes all characters that do not appear in [chars] from the string. + String whitelist(String chars) => _whitelist(this, chars); +} + +String _blacklist(String str, String chars) { + if (chars.isEmpty) return str; + final set = chars.split('').toSet(); + return str.split('').where((c) => !set.contains(c)).join(); +} + +String _whitelist(String str, String chars) { + final set = chars.split('').toSet(); + return str.split('').where(set.contains).join(); +} diff --git a/lib/validators/sanitizers/escape.dart b/lib/validators/sanitizers/escape.dart new file mode 100644 index 0000000..58cde31 --- /dev/null +++ b/lib/validators/sanitizers/escape.dart @@ -0,0 +1,52 @@ +/// Replaces HTML-unsafe characters with their entity equivalents. +/// +/// Escapes `&`, `<`, `>`, `"`, `'`, `` ` ``, `/` and `\`. +/// +/// Example: +/// ```dart +/// escape('