diff --git a/README.md b/README.md index a324c79..b8e86e1 100644 --- a/README.md +++ b/README.md @@ -2,56 +2,363 @@ Eloquent Regex brings the simplicity and elegance to regular expressions. Designed for Laravel developers, this package offers a fluent, intuitive interface for building and executing regex patterns in your PHP applications. -#### Adding new options +### Table of Contents -All available option classes and option names are hardcoded in `src\OptionsMapper.php`. Refer to it, if you want add new or disable existing one. +- [Overview](#overview) + - [Key Features](#key-features) + - [Getting Started](#getting-started) +- [Basic Usage](#basic-usage) + - [Including EloquentRegex](#including-eloquentregex) + - [Usage Structure](#usage-structure) + - [Ready-to-Use Patterns](#ready-to-use-patterns) + - [Custom Patterns](#custom-patterns) + - [Creating a Custom Pattern](#creating-a-custom-pattern) + - [Applying Quantifiers](#applying-quantifiers) + - [Optional Elements](#optional-elements) + - [Specifying a Range](#specifying-a-range) + - [One or More](#one-or-more) + - [Zero or More](#zero-or-more) + - [Exact Number](#exact-number) + - [Custom Character Sets and Groups](#custom-character-sets-and-groups) + - [Quantifier Values](#quantifier-values) -Think about options as an extra assertions you add to the pattern. To keep it simple, all options (so the option methods too) should have only 1 argument. +# Overview -#### Adding new patterns +#### Dreaming of a world where regex doesn't feel like a rocket science? πŸ˜„πŸš€ -All available redy-to-use pattern classes are hardcoded in `src\Builder.php`. Refer to it, if you want add new or disable existing one. +Regular expressions (regex) are powerful, no doubt. They're the Swiss Army knife for string manipulation and validation. But let's be honest, they can also be a bit of a headache. The syntax is dense, and a tiny mistake can throw everything off. It's like they're designed to be as intimidating as possible, especially when you're just trying to validate an email address! -#### Quantifiers +Enter **EloquentRegex**. Our goal is to make working with regex in Laravel not just bearable, but actually enjoyable. Yes, you heard that rightβ€”**enjoyable**! -Available values for quantifiers as argument: +EloquentRegex is a PHP/Laravel package that offers a fluent, intuitive interface for constructing and executing regular expressions. Whether you need to validate user input, parse text, or extract specific information from strings, EloquentRegex makes it simple and straightforward. For example: -- zeroOrMore = `"zeroOrMore"`, `"0>"`, `"0+"` -- oneOrMore = `"oneOrMore"`, `"1>"`, `"1+"` -- optional = `"optional"`, `"?"`, `"|"` +```php +$isValid = EloquentRegex::source('test@example.com')->email()->check(); +``` -Examples: `->exact("hello world", false, "1+")` +## Key Features + +- **Ready-to-Use Patterns**: Common patterns like emails, URLs, and IP addresses are pre-defined and ready to go. Just a few keystrokes and you're validating. +- **Custom Patterns Made Easy**: Build your own regex patterns with an easy-to-use, fluent interface. Say hello to readable regex! +- **Options and Filters**: Tailor your regex operations with options and filters for precision matching. It's like having a regex wizard at your fingertips. +- **Laravel Integration**: Seamlessly integrates with your Laravel projects, leveraging Laravel's elegant syntax and features. + +## Getting Started + +Simply install the package via Composer, and you're ready to take the pain out of regex in your PHP/Laravel applications. Follow our quick start guide below to dive in. + +```bash +composer require maestroerror/eloquent-regex +``` + +Later, here will be added our quick start guide. + +Remember, regex doesn't have to be a source of frustration. With EloquentRegex, you're on your way to becoming a regex master, all while writing cleaner, more maintainable Laravel code. + +# Basic Usage + +EloquentRegex simplifies regular expressions in Laravel, making it easy to validate data, search text, and extract information. This section introduces the basic usage of EloquentRegex, including leveraging ready-to-use patterns and creating custom patterns. + +First of all, you need to include EloquentRegex class. + +```php +use Maestroerror\EloquentRegex\EloquentRegex; +``` + +**Recomended for Laravel:** + +```php +use Maestroerror\EloquentRegex\Facades\EloquentRegex; +``` + +Usage structure is very similar to Laravel's Eloquent ORM, check this out: + +``` +[Initiator][Pattern][?Optional][Action] +``` + +Let's break it down: + +- **_Initiator_** sets the target string + +```php +EloquentRegex::source($yourString); +``` + +- **_Pattern_** Could be method for one of the ready-to-use patterns or your custom pattern (we will talk about custom pattern later). Let's keep the example simple and add url pattern: + +```php +EloquentRegex::source($yourString)->url(); +``` + +_Note: **?Optional** methods mostly are the expression flags, we will talk about them in next sections_ + +- **_Action_** is the execution method, check the example: + +```php +// get() will return array/collection of URLs if any found in $yourString +EloquentRegex::source($yourString)->url()->get(); + +// check() will return true if $yourString exactly matches the pattern +// In this case, if $yourString is URL, false otherwise +EloquentRegex::source($yourString)->url()->check(); + +// checkString() will return true if $yourString contains any matches of the pattern +// In this case, if $yourString contains minimum 1 URL, false otherwise +EloquentRegex::source($yourString)->url()->checkString(); + +// count() will return count of matches, in this case, amount of URLs in $yourString +EloquentRegex::source($yourString)->url()->count(); + +// toRegex() will return string - raw regex pattern (without options applied) +EloquentRegex::source($yourString)->url()->toRegex(); +``` + +## Ready-to-Use Patterns + +EloquentRegex comes with a set of predefined patterns for common validation/extraction tasks. These patterns are designed to be straightforward and easy to use, requiring minimal effort to implement. + +We have different ways to apply options, but the most common and easy way is to pass them as arguments. Note that all arguments are optional. + +Here you can check all available methods of ready-to-use patterns and their arguments: + +```php +textOrNumbers(int $minLength, int $maxLength, int $minUppercase, int $minLowercase, int $minNumbers, int $maxNumbers) +``` + +```php +// $onlyDomains & $onlyExtensions array or string separated by comma `"example.org,example.com"` +email(int $maxLength, array|string $onlyDomains, array|string $onlyExtensions)` +``` + +```php +url(array|string $onlyProtocol)` +``` + +```php +// $onlyDomains & $onlyExtensions array or string separated by comma "org,com" +domainName(int $maxLength, array|string $onlyDomains, array|string $onlyExtensions)` +``` + +```php +date() +``` + +```php +time() +``` + +```php +ipAddress() +``` + +```php +ipv6Address() +``` + +```php +// $cardTypes string separated by comma "visa, amex" +creditCardNumber(string $cardTypes) +``` + +```php +// $countryCode should passed without "+" sign: phone("1"), phone("995") +phone(string $countryCode) +``` + +```php +username(int $maxLength) +``` + +```php +password(int $minLength, int $minUppercase, int $minNumbers, int $minSpecialChars) +``` + +```php +// $restrictTags & $onlyTags array or string +// separated by comma `"script, style"`. +// It isn't recomended to use both option simultaneously +htmlTag(array|string $restrictTags, array|string $onlyTags) +``` + +```php +// $specificCurrencies array of currency symbols or string separated by comma "$, β‚Ύ" +currency(int $minDigits, int $maxDigits, array|string $specificCurrencies) +``` + +```php +// $pathType allowed values: "absolute" & "relative" +filePath(int $isDirectory, bool $isFile, bool $fileExists,string $pathType) - +``` + +```php +filePathWin(int $isDirectory, bool $isFile, bool $fileExists) +``` + +Didn't it cover all your needs? Let's take a look to the custom patterns section. + +## Custom Patterns + +For scenarios where predefined patterns do not suffice, EloquentRegex allows you to define custom patterns using the start or customPattern methods as initiator: + +```php +EloquentRegex::start($yourString); +// Or +EloquentRegex::customPattern($yourString); +``` + +_Note: They does the exaclty same thing, you can use your favorite one_ + +### Creating a Custom Pattern + +You can start building a custom pattern to match a specific string format, such as a custom ID format that starts with letters followed by digits: + +```php +$result = EloquentRegex::start('ID123456') + ->literal('ID') + ->digitsRange(1, 10) + ->check(); + +if ($result) { + echo "The string matches the custom ID format!"; +} else { + echo "The string does not match the custom ID format."; +} + +``` + +_Note: You can use `EloquentRegex::builder()->pattern()` if you need just build a regex without source string_ + +Custom pattern builder supports a wide range of character classes and all special chars. Also, `literal` or `exact` method could be used to match exact string you need, or `char` method could be used to match exact character. The full list of pattern builder methods is comming soon. Before that, you can check this files out: + +- [Character Classes](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/CharacterClassesTrait.php) +- [Special characters](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php) +- [Groups](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/GroupsTrait.php) +- [Anchors](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/AnchorsTrait.php) + +## Applying Quantifiers + +Quantifiers in regular expressions are symbols or sets of symbols that specify how many instances of a character, group, or character class must be present in the input for a match to be found. EloquentRegex enhances the way quantifiers are used, making it simpler and more intuitive to define the frequency of pattern occurrences. + +### Optional Elements + +To make an element optional, use '?'. This matches zero or one occurrence of the preceding element (`dash` in this example). + +```php +// Matches a string that may or may not contain a dash +$result = EloquentRegex::start($yourString)->exact("123")->dash('?')->exact("456")->check(); +// Result would be true in both cases of $yourString: "123456" & "123-456" +``` + +### Specifying a Range + +For specifying a range of occurrences, use a string with two numbers separated by a comma '2,5'. This matches the preceding element at least and at most the specified times. + +```php +// Matches a string with 2 to 5 spaces +$result = EloquentRegex::start($yourString)->text()->space('2,5')->digits()->check(); +// Result: the "someText 234" would return true, but the "someText 234" false +``` + +### One or More + +To match one or more occurrences of an element, use '+', '1+', '1>' or 'oneOrMore'. This ensures the element appears at least once. + +```php +// Matches strings with one or more backslashes +$result = EloquentRegex::start("\\\\")->backslash('1+')->check(); +// Result: true (if one or more backslashes are found) +``` + +### Zero or More + +The '0+' quantifier matches zero or more occurrences of the preceding element. + +```php +// Matches strings with zero or more forward slashes +$result = EloquentRegex::start($yourString)->alphanumeric()->dot('0+')->check(); +// Result would be true in both cases of $yourString: "test258..." & "test" +``` + +### Exact Number + +To match an exact number of occurrences, directly specify the number. + +```php +// Matches strings with exactly 2 underscores +$result = EloquentRegex::start($yourString)->digits()->underscore('2')->digits()->check(); +// Result would be true in cases $yourString: "1235__158", but "1235___158" and "1235_158" will be false + +``` + +### Custom Character Sets and groups + +You can apply quantifiers to custom character sets and groups as second argument after the callback, matching a specific number of occurrences. + +```php +// Matches strings with exactly 3 periods or colons +$regex = EloquentRegex::builder()->start() + ->charSet(function ($pattern) { + $pattern->period()->colon(); + }, '3')->toRegex(); +// Result: ([\.\:]){3} +``` + +### Quantifier values + +In [Special characters](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php) +and [Groups](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/GroupsTrait.php) - nearly all methods allowing quantifiers with values: + +- Zero or More = `"zeroOrMore"`, `"0>"`, `"0+"`, `"*"` +- One or More = `"oneOrMore"`, `"1>"`, `"1+"`, `"+"` +- Optional (Zero or One) = `"optional"`, `"?"`, `"|"` +- exact amount = `2`, `"5"` +- range = `"{0,5}"` + +Example: `->literal("hello world", false, "1+")` + +But +[Character Classes](https://github.com/MaestroError/eloquent-regex/blob/documentation-and-examples/src/Traits/BuilderPatternTraits/CharacterClassesTrait.php) +have different approach, lets take `digits` as example: + +```php +// By defualt it is set as One or More +EloquentRegex::start($yourString)->digits(); + +// You can completly remove quantifier by passing 0 as first argument +EloquentRegex::start($yourString)->digits(0); + +// You can specify exact amount of digit by passing int +EloquentRegex::start($yourString)->digits(5); + +// You can specify range of digits by adding "Range" to the method +EloquentRegex::start($yourString)->digitsRange(1, 5); // Matches from 1 to 5 digits +``` + +--- ##### To Do -- Add needed options for new patterns: +- Add options for new patterns: - usernameLength: Set minimum and maximum length for the username part of the email. - dateFormat, timeFormat: Specify the format of date and time (e.g., MM-DD-YYYY, HH:MM). - Consider to register Patterns like options using key (name) => value (class) pairs (check performance) βœ”οΈ (_No significant change before 50+ patterns_) +- Return collection on get method if laravel is available. -- Extend BuilderPattern, try to add methods: - - - group(callable $callback): Creates a grouped subpattern.βœ”οΈ - - nonCapturingGroup(callable $callback): Creates a non-capturing group.βœ”οΈ - - orPattern(): Alternation, allowing for multiple possibilities.βœ”οΈ - - lookAhead(callable $callback): Positive lookahead assertion.βœ”οΈ - - lookBehind(callable $callback): Positive lookbehind assertion.βœ”οΈ - - negativeLookAhead(callable $callback): Negative lookahead assertion.βœ”οΈ - - negativeLookBehind(callable $callback): Negative lookbehind assertion.βœ”οΈ - - Raw regex methods for advanced users.βœ”οΈ - - BuilderPattern should be able to reproduce patterns used in HSAβœ”οΈ - -- Add benchmarks and tests for search against large data βœ”οΈ -- Add Feature Tests for BuilderPattern βœ”οΈ -- Remove need for "end" method in BuilderPattern βœ”οΈ -- Add Dockblocs and comments for new methods βœ”οΈ - -- Add facade for Laravel βœ”οΈ -- Wrap Builder in class for static start βœ”οΈ - - "string" and "source" for builder start βœ”οΈ - - "start" and "pattern" for builderPattern start βœ”οΈ - Write documentation (add credit for https://regexr.com/ and ChatGPT) -- Add automated tests on PR creation or on marging to main branch βœ”οΈ + - Create quick start guide and add in Docs. + - Add advanced usage section in Docs: + - Options and Assertions: Detailed explanation of options, how to apply them, and their effects on patterns. + - Filters in Extraction: Using options as filters during extraction and the types of filters available. + - Regex Flags: Guide on applying regex flags to patterns for specialized matching behavior. + - Grouping and Capturing: How to use groups (capturing and non-capturing) and apply quantifiers to them. + - Add section in docs for "lazy" method + - Add sections: + - Testing and Debugging + - Credits + - Contributing + - FAQs + - Creating new patterns ##### Coming next @@ -59,6 +366,6 @@ Examples: `->exact("hello world", false, "1+")` - Implement recursive pattern creation (Using "RI-321" string to create pattern matching this string) - Consider to add Postal Code Pattern - Make options controllable from config or provider (?) -- Make pattern controllable from config or provider (?) +- Make patterns controllable from config or provider (?) - I should be able to make new pattern using BuilderPattern - I should be able to make add custom pattern to the existing one using BuilderPattern diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..d548702 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,254 @@ +# EloquentRegex Structure + +EloquentRegex offers a fluent and intuitive interface for Laravel developers to construct and execute regular expressions (regex) with ease. This document outlines the core components and functionalities of the EloquentRegex package, including ready-to-use patterns, custom pattern creation, and the use of options for added assertions and filtering. The primary purpose of the document is to clarify the inner workings and help you to understand the package better. + +### Table of Contents + +- [EloquentRegex Structure](#eloquentregex-structure) + - [Ready-to-Use Patterns](#ready-to-use-patterns) + - [Custom Patterns](#custom-patterns) + - [Starting Points](#starting-points) + - [Options](#options) + - [Applying Options](#applying-options) + - [Options as Filters](#options-as-filters) + - [Options in Custom Patterns](#options-in-custom-patterns) + - [Regex Flags](#regex-flags) + - [Usage Structure](#usage-structure) +- [Conclusion](#conclusion) + +## Ready-to-Use Patterns + +The inclusion of ready-to-use patterns for common formats like Email, URL, and IP addresses significantly streamlines validation tasks. These patterns are a testament to EloquentRegex's goal of providing developers with a toolkit that simplifies common regex tasks, making code more readable and maintainable. By encapsulating the complexity of regex syntax for these common use cases, EloquentRegex enables developers to perform validations and extractions without needing to manually craft and test these patterns. These patterns can be used directly with minimal setup: + +```php +EloquentRegex::source("test@example.com")->email()->check(); +``` + +They are defined in `src\Patterns` and I tried a lot to make pattern creation process as easy as possible. Here you can check example pattern: + +```php + 5 + ]; +} + +``` + +All available patterns are registered in `src\Builder.php`, inside `$patterns` property + +## Custom Patterns + +_Custom patterns allow for detailed and specific regex definitions._ + +The ability to create custom patterns using methods is central to EloquentRegex's value proposition. This feature leverages a fluent API, allowing developers to construct complex regex expressions through a series of method calls. Each method call builds upon the previous one, enabling the construction of regex patterns in a piecewise, understandable manner. This approach not only makes the creation of regex patterns more intuitive but also enhances code readability by making the patterns' purposes and structures clear at a glance. + +```php +EloquentRegex::start("#hello #world This is a #test")->hash()->text()->get(); +``` + +This functionality mainly depends on the `src\Patterns\BuilderPattern.php`, which technichally is one of the patterns in the system, just haven't predefined regex and allows you to build regex pattern using the chainable methods. + +### Starting Points + +Custom pattern can be initiated with `start` or `customPattern` methods, which are interchangeable and mark the beginning of a custom pattern creation. The `source` and the `string` methods are starting point for ready-to-use patterns, while it returns the main `Builder` class, you can "switch" to the custom pattern from it, of course, if you preffer syntax like this: + +```php +EloquentRegex::source("#hello #world This is a #test")->start()->hash()->text()->end()->get(); // end() method is optional here +``` + +## Options + +Options provide a powerful way to fine-tune patterns. They can be specific to the pattern or applied globally as extra assertions. Patterns define their arguments for easy option application. + +It is quiet easy to add a new option class, mainly it needs 2 methods `validate` and `build`. Validate method is used for validation and build method is optional for cases, where option uses regex for validation (Yes, some options are validating input using PHP methods, some Regex pattern and some both, it depends on the Option). Also, option classes need the "option methods", which should be registered as separated options in the `src\OptionsMapper.php` class. For example: + +```php +validateUsingRegex) { + return $this->validateUsingRegex($input); + } + + if ($this->isFile) { + if ($this->fileExtension) { + if (!preg_match("/\." . preg_quote($this->fileExtension) . "$/", $input)) { + return false; + } + } elseif (!preg_match("/\.[a-zA-Z0-9]+$/", $input)) { + return false; + } + } + + if ($this->isDirectory) { + if (substr($input, -1) != '/') { + return false; + } + } + + return true; + } + + // Builds regex pattern + public function build(): string { + if ($this->isFile) { + if ($this->fileExtension) { + return "[A-Za-z0-9\\/:\.\-\\\\]*\." . preg_quote($this->fileExtension); + } else { + return "[A-Za-z0-9\\/:\.\-\\\\]*\.[a-zA-Z0-9]+"; + } + } + + if ($this->isDirectory) { + return "(?:[a-zA-Z0-9\\/:\-\\\\]+)+"; + } + + return '.*'; + } + + // "option methods": + + public function isFile(string|null $extension = null) { + $this->isFile = true; + $this->fileExtension = $extension; + return $this; + } + + public function isDirectory(int $check = 1) { + $this->isDirectory = $check; + return $this; + } +} +``` + +Later, the "option methods" are registered in OptionsMapper class: + +```php +/** + * @var array Mapping of option names to their corresponding class and method. + */ +public static array $optionMethods = [ + // Other options... + "isFile" => [FileOption::class, "isFile"], + "isDirectory" => [FileOption::class, "isDirectory"], + // Other options... +]; +``` + +_Note: option methods **should** have only one argument._ + +### Applying Options + +- Via Arguments: Directly passing parameters defined by the pattern class in $args property. + +```php +EloquentRegex::source("StrongP@ssw0rd")->password(8, 1, 1, 1)->check(); +``` + +- Via Callback: Using a closure to apply multiple options dynamically (recomended way). + +```php +EloquentRegex::source("StrongP@ssw0rd")->password(function($pattern) { + return $pattern->minLength(8)->minUppercase(1)->minNumbers(1)->minSpecialChars(1); +})->check(); +``` + +- Via Array: Applying options using an associative array. + +```php +EloquentRegex::source("test@example.com")->email(['onlyExtensions' => ['com', 'org']])->check(); // true +``` + +_Note: argument includes pattern-specific option in predefined manner, but while using **array** or **callback** you can apply **any option** to **any pattern**_ + +### Options as Filters + +Options can also act as filters during extraction (`get()`), ensuring only matches that meet specific criteria are returned. + +```php +EloquentRegex::source("Visa: 4111 1111 1111 1111, MasterCard: 5500 0000 0000 0004, Amex: 3400 000000 00009") + ->creditCardNumber("visa, amex")->get(); +// Returns only Visa and Amex card numbers + +``` + +### Options in Custom Patterns + +Sure! You can apply option to your custom pattern using the `end(callback|array $config)` method: + +```php +EloquentRegex::customPattern("Users: user_123, JohnDoe_99") + ->alphanumeric() + ->underscore() + ->digitsRange(0, 5) + ->end(["maxLength" => 8]) // Applied maxLength option + ->get(); // Returns array including "user_123" within +``` + +### Regex Flags + +Regex flags are applied outside the options scope through easy-to-use methods, allowing for global pattern modifiers like Unicode support. + +```php +// Regex flags can be applied only after the end() method +EloquentRegex::start("მზადაა #1 βœ”οΈ და #2 βœ”οΈ")->wordCharRange(0, 2)->end()->asUnicode()->get(); +``` + +## Usage structure + +Here you can check the usage structure: + +``` +[Initiator][Pattern][?Optional][Action] +``` + +- **Initiator** - Sets target string and returns `Builder` for ready-to-use patterns or `BuilderPattern` for building custom patterns (ex: `EloquentRegex::source`) +- **Pattern** - Method for ready to use pattern like `email`, `url`, `filePath` and etc. Or custom pattern created using the `BuilderPattern` methods. +- **?Optional** - Any optional methods like: regex expression flags and `end` method +- **Action** - Final method which performs some action like: `get`, `check`, `toRegex` and etc. + +_Note: Action methods have replicas in `BuilderPattern` to ensure that `end` method remains optional_ + +# Conclusion + +EloquentRegex simplifies the creation and execution of regular expressions in Laravel applications. Through its intuitive API, developers can quickly implement complex regex operations with precision and flexibility. Whether utilizing ready-to-use patterns for common tasks or crafting custom solutions with dynamic options, EloquentRegex enhances productivity and code clarity. diff --git a/src/Builder.php b/src/Builder.php index 7c93db9..686c07e 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -186,20 +186,22 @@ public function toRegex(): string { } // In cases when pattern doesn't allow setting the options (like BuilderPattern) - public function setOptions(array|callable $config): void { + public function setOptions(array|callable $config): self { // Check if the pattern is set if (!$this->patternIsSet()) { throw new \LogicException("Pattern must be set before setting options."); } // Handle options array scenario - if (is_array($config)) { + if (is_array($config) && !empty($config)) { $this->processConfigArray($config); } // Handle callback scenario else if (is_callable($config)) { $this->processCallback($config); } + + return $this; } /** diff --git a/src/Contracts/BuilderContract.php b/src/Contracts/BuilderContract.php index d588f07..9db2b98 100644 --- a/src/Contracts/BuilderContract.php +++ b/src/Contracts/BuilderContract.php @@ -59,7 +59,7 @@ public function toRegex(): string; * @param array|callable $config An array of options (optionName => value (arg)) or a callback function to configure options. * @return void */ - public function setOptions(array|callable $config): void; + public function setOptions(array|callable $config): self; /** * Registers a single pattern in the Builder. diff --git a/src/Patterns/BuilderPattern.php b/src/Patterns/BuilderPattern.php index 5d6db8f..db4b462 100644 --- a/src/Patterns/BuilderPattern.php +++ b/src/Patterns/BuilderPattern.php @@ -7,13 +7,14 @@ use Maestroerror\EloquentRegex\Traits\BuilderPatternTraits\CharacterClassesTrait; use Maestroerror\EloquentRegex\Traits\BuilderPatternTraits\SpecificCharsTrait; use Maestroerror\EloquentRegex\Traits\BuilderPatternTraits\AnchorsTrait; +use Maestroerror\EloquentRegex\Traits\BuilderPatternTraits\GroupsTrait; use Maestroerror\EloquentRegex\Builder; class BuilderPattern extends BasePattern { // BuilderPattern doesn't need the "execute" method (src\Traits\Pattern.php) - use CharacterClassesTrait, SpecificCharsTrait, AnchorsTrait; + use CharacterClassesTrait, SpecificCharsTrait, AnchorsTrait, GroupsTrait; /** * @var array Array of options for the pattern. @@ -46,8 +47,8 @@ public function __construct(BuilderContract $builder = new Builder()) { // Builder class implementation methods START - public function end(): BuilderContract { - return $this->builder; // Return the Builder object + public function end(array|callable $config = []): BuilderContract { + return $this->builder->setOptions($config); // Return the Builder object } public function get(): ?array { @@ -87,19 +88,38 @@ public function getMatchesValidationPattern(): string { * @param string|null $quantifier The quantifier to apply. Can be 'zeroOrMore', 'oneOrMore', or 'optional'. * @return string The modified pattern with the quantifier applied. */ - private function applyQuantifier(string $pattern, string|null $quantifier): string { - switch ($quantifier) { - case 'zeroOrMore' || '0>' || '0+': - $p = $pattern . '*'; - return $this->lazy ? $this->addLazy($p) : $p; - case 'oneOrMore' || '1>' || '1+': - $p = $pattern . '+'; - return $this->lazy ? $this->addLazy($p) : $p; - case 'optional' || '?' || '|': - return $pattern . '?'; - default: - return $pattern; + private function applyQuantifier(string $pattern, string|null $q): string { + + if (!$q) { + return $pattern; + } + + if ($q == 'zeroOrMore' || $q == '0>' || $q == '0+' || $q == '*') { + $p = "(" . $pattern . ')*'; + return $this->lazy ? $this->addLazy($p) : $p; + } elseif ($q == 'oneOrMore' || $q == '1>' || $q == '1+' || $q == '+') { + $p = "(" . $pattern . ')+'; + return $this->lazy ? $this->addLazy($p) : $p; + } elseif ($q == 'optional' || $q == '?' || $q == '|') { + $p = "(" . $pattern . ')?'; + return $this->lazy ? $this->addLazy($p) : $p; } + + if (is_int($q)) { + $p = "(" . $pattern . "){".$q."}"; + return $this->lazy ? $this->addLazy($p) : $p; + } elseif (preg_match("/^\d{1,10}$/", $q)) { + $p = "(" . $pattern . '){'.$q.'}'; + return $this->lazy ? $this->addLazy($p) : $p; + } elseif (preg_match("/^\d{1,10},\d{1,10}$/", $q)) { + $range = explode(",", $q); + $f = $range[0]; + $s = $range[1]; + $p = "(" . $pattern . ")" . "{" . $f . "," . $s ."}"; + return $this->lazy ? $this->addLazy($p) : $p; + } + + return $pattern; } /** @@ -111,9 +131,11 @@ private function applyQuantifier(string $pattern, string|null $quantifier): stri * @return string The generated regex quantifier string. */ private function getLengthOption(int|null $length = null, int $minLength = 0, int $maxLength = 0): string { - if (is_int($length) && $length >= 0) { + if (is_int($length) && $length > 0) { $qntf = "{" . $length . "}"; return $this->lazy ? $this->addLazy($qntf) : $qntf; + } elseif ($length === 0) { + return ""; } if ($minLength > 0 && $maxLength > 0) { @@ -153,142 +175,5 @@ public function lazy(): self { return $this; } - /** - * Adds a new set of characters. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. - * @return self - */ - public function charSet(callable $callback): self { - $subPattern = new self(); - $callback($subPattern); - $this->pattern .= '[' . $subPattern->getPattern() . ']'; - return $this; - } - - /** - * Adds a new set of denied characters. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. - * @return self - */ - public function negativeCharSet(callable $callback): self { - $subPattern = new self(); - $callback($subPattern); - $this->pattern .= '[^' . $subPattern->getPattern() . ']'; - return $this; - } - - /** - * Adds a new grouped subpattern. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. - * @return self - */ - public function group(callable $callback): self { - $subPattern = new self(); - $callback($subPattern); - $this->pattern .= '(' . $subPattern->getPattern() . ')'; - return $this; - } - - /** - * Adds a new non-capturing grouped subpattern. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. - * @return self - */ - public function nonCapturingGroup(callable $callback): self { - $subPattern = new self(); - $callback($subPattern); - $this->pattern .= '(?:' . $subPattern->getPattern() . ')'; - return $this; - } - /** - * Adds an alternation pattern. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the alternation. - * @return self - */ - public function orPattern(callable $callback): self { - $builder = new self(); - $callback($builder); - $this->pattern .= '|' . $builder->getPattern(); - return $this; - } - - /** - * Adds a positive lookahead assertion. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. - * @return self - */ - public function lookAhead(callable $callback): self { - $builder = new self(); - $callback($builder); - $this->pattern .= '(?=' . $builder->getPattern() . ')'; - return $this; - } - - /** - * Adds a positive lookbehind assertion. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. - * @return self - */ - public function lookBehind(callable $callback): self { - $builder = new self(); - $callback($builder); - $this->pattern .= '(?<=' . $builder->getPattern() . ')'; - return $this; - } - - /** - * Adds a negative lookahead assertion. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. - * @return self - */ - public function negativeLookAhead(callable $callback): self { - $builder = new self(); - $callback($builder); - $this->pattern .= '(?!' . $builder->pattern . ')'; - return $this; - } - - /** - * Adds a negative lookbehind assertion. - * - * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. - * @return self - */ - public function negativeLookBehind(callable $callback): self { - $builder = new self(); - $callback($builder); - $this->pattern .= '(?pattern . ')'; - return $this; - } - - /** - * Adds a raw regex string to the pattern. - * - * @param string $regex The raw regex string to add. - * @return self - */ - public function addRawRegex(string $regex): self { - $this->pattern .= $regex; - return $this; - } - - /** - * Wraps a given regex string in a non-capturing group and adds it to the pattern. - * - * @param string $regex The regex string to wrap and add. - * @return self - */ - public function addRawNonCapturingGroup(string $regex): self { - $this->pattern .= '(?:' . $regex . ')'; - return $this; - } } \ No newline at end of file diff --git a/src/Traits/BuilderPatternTraits/GroupsTrait.php b/src/Traits/BuilderPatternTraits/GroupsTrait.php new file mode 100644 index 0000000..a13e270 --- /dev/null +++ b/src/Traits/BuilderPatternTraits/GroupsTrait.php @@ -0,0 +1,156 @@ +getPattern() . ']'; + $this->pattern .= $q ? $this->applyQuantifier($p, $q) : $p; + return $this; + } + + /** + * Adds a new set of denied characters. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. + * @param ?string $q a Quantifier + * @return self + */ + public function negativeCharSet(callable $callback, ?string $q = null): self { + $subPattern = new self(); + $callback($subPattern); + $p = '[^' . $subPattern->getPattern() . ']'; + $this->pattern .= $q ? $this->applyQuantifier($p, $q) : $p; + return $this; + } + + /** + * Adds a new grouped subpattern. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. + * @param ?string $q a Quantifier + * @return self + */ + public function group(callable $callback, ?string $q = null): self { + $subPattern = new self(); + $callback($subPattern); + $p = $subPattern->getPattern(); + $this->pattern .= $q ? $this->applyQuantifier($p, $q) : '(' . $p . ')'; + return $this; + } + + /** + * Adds a new non-capturing grouped subpattern. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the subpattern. + * @param ?string $q a Quantifier + * @return self + */ + public function nonCapturingGroup(callable $callback, ?string $q = null): self { + $subPattern = new self(); + $callback($subPattern); + $p = '(?:' . $subPattern->getPattern() . ')'; + $this->pattern .= $q ? $this->applyQuantifier($p, $q) : $p; + return $this; + } + + /** + * Adds an alternation pattern. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the alternation. + * @param ?string $q a Quantifier + * @return self + */ + public function orPattern(callable $callback, ?string $q = null): self { + $builder = new self(); + $callback($builder); + $p = $builder->getPattern(); + $this->pattern .= $q ? '|' . $this->applyQuantifier($p, $q) : '|' . $p; + return $this; + } + + /** + * Adds a positive lookahead assertion. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. + * @return self + */ + public function lookAhead(callable $callback): self { + $builder = new self(); + $callback($builder); + $this->pattern .= '(?=' . $builder->getPattern() . ')'; + return $this; + } + + /** + * Adds a positive lookbehind assertion. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. + * @return self + */ + public function lookBehind(callable $callback): self { + $builder = new self(); + $callback($builder); + $this->pattern .= '(?<=' . $builder->getPattern() . ')'; + return $this; + } + + /** + * Adds a negative lookahead assertion. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. + * @return self + */ + public function negativeLookAhead(callable $callback): self { + $builder = new self(); + $callback($builder); + $this->pattern .= '(?!' . $builder->pattern . ')'; + return $this; + } + + /** + * Adds a negative lookbehind assertion. + * + * @param callable $callback A callback that receives a BuilderPattern instance to define the assertion. + * @return self + */ + public function negativeLookBehind(callable $callback): self { + $builder = new self(); + $callback($builder); + $this->pattern .= '(?pattern . ')'; + return $this; + } + + /** + * Adds a raw regex string to the pattern. + * + * @param string $regex The raw regex string to add. + * @return self + */ + public function addRawRegex(string $regex): self { + $this->pattern .= $regex; + return $this; + } + + /** + * Wraps a given regex string in a non-capturing group and adds it to the pattern. + * + * @param string $regex The regex string to wrap and add. + * @return self + */ + public function addRawNonCapturingGroup(string $regex): self { + $this->pattern .= '(?:' . $regex . ')'; + return $this; + } + +} diff --git a/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php b/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php index 4112f43..ad2325d 100644 --- a/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php +++ b/src/Traits/BuilderPatternTraits/SpecificCharsTrait.php @@ -18,9 +18,9 @@ private function handleExact(string|array $string, $caseSensitive = true, $quant return $this; } - private function escapeAndAdd(string $char): self { + private function escapeAndAdd(string $char, $quantifier = null): self { $escapedChar = preg_quote($char, '/'); - $this->pattern .= $escapedChar; + $this->pattern .= $quantifier ? $this->applyQuantifier($escapedChar, $quantifier) : $escapedChar; return $this; } @@ -79,132 +79,132 @@ public function formFeed(): self { return $this; } - public function dash() { - return $this->escapeAndAdd("-"); + public function dash(string|null $q = null) { + return $this->escapeAndAdd("-", $q); } - public function dot(): self { - return $this->escapeAndAdd("."); // Matches dot "." character + public function dot(string|null $q = null): self { + return $this->escapeAndAdd(".", $q); // Matches dot "." character } - public function space() { - return $this->escapeAndAdd(" "); + public function space(string|null $q = null) { + return $this->escapeAndAdd(" ", $q); } - public function backslash(): self { - return $this->escapeAndAdd("\\"); + public function backslash(string|null $q = null): self { + return $this->escapeAndAdd("\\", $q); } - public function forwardSlash(): self { - return $this->escapeAndAdd("/"); + public function forwardSlash(string|null $q = null): self { + return $this->escapeAndAdd("/", $q); } - public function slash(): self { - return $this->escapeAndAdd("/"); + public function slash(string|null $q = null): self { + return $this->escapeAndAdd("/", $q); } - public function doubleSlash(): self { - return $this->escapeAndAdd("//"); + public function doubleSlash(string|null $q = null): self { + return $this->escapeAndAdd("//", $q); } - public function underscore(): self { - return $this->escapeAndAdd("_"); + public function underscore(string|null $q = null): self { + return $this->escapeAndAdd("_", $q); } - public function pipe(): self { - return $this->escapeAndAdd("|"); + public function pipe(string|null $q = null): self { + return $this->escapeAndAdd("|", $q); } - public function ampersand(): self { - return $this->escapeAndAdd("&"); + public function ampersand(string|null $q = null): self { + return $this->escapeAndAdd("&", $q); } - public function asterisk(): self { - return $this->escapeAndAdd("*"); + public function asterisk(string|null $q = null): self { + return $this->escapeAndAdd("*", $q); } - public function plus(): self { - return $this->escapeAndAdd("+"); + public function plus(string|null $q = null): self { + return $this->escapeAndAdd("+", $q); } - public function questionMark(): self { - return $this->escapeAndAdd("?"); + public function questionMark(string|null $q = null): self { + return $this->escapeAndAdd("?", $q); } - public function atSign(): self { - return $this->escapeAndAdd("@"); + public function atSign(string|null $q = null): self { + return $this->escapeAndAdd("@", $q); } - public function atSymbol(): self { - return $this->escapeAndAdd("@"); + public function atSymbol(string|null $q = null): self { + return $this->escapeAndAdd("@", $q); } - public function exclamationMark(): self { - return $this->escapeAndAdd("!"); + public function exclamationMark(string|null $q = null): self { + return $this->escapeAndAdd("!", $q); } - public function period(): self { - return $this->escapeAndAdd("."); + public function period(string|null $q = null): self { + return $this->escapeAndAdd(".", $q); } - public function comma(): self { - return $this->escapeAndAdd(","); + public function comma(string|null $q = null): self { + return $this->escapeAndAdd(",", $q); } - public function semicolon(): self { - return $this->escapeAndAdd(";"); + public function semicolon(string|null $q = null): self { + return $this->escapeAndAdd(";", $q); } - public function colon(): self { - return $this->escapeAndAdd(":"); + public function colon(string|null $q = null): self { + return $this->escapeAndAdd(":", $q); } - public function equalSign(): self { - return $this->escapeAndAdd("="); + public function equalSign(string|null $q = null): self { + return $this->escapeAndAdd("=", $q); } - public function tilde(): self { - return $this->escapeAndAdd("~"); + public function tilde(string|null $q = null): self { + return $this->escapeAndAdd("~", $q); } - public function hyphen(): self { - return $this->escapeAndAdd("-"); + public function hyphen(string|null $q = null): self { + return $this->escapeAndAdd("-", $q); } - public function minus(): self { - return $this->escapeAndAdd("-"); + public function minus(string|null $q = null): self { + return $this->escapeAndAdd("-", $q); } - public function doubleQuote(): self { - return $this->escapeAndAdd("\""); + public function doubleQuote(string|null $q = null): self { + return $this->escapeAndAdd("\"", $q); } - public function singleQuote(): self { - return $this->escapeAndAdd("'"); + public function singleQuote(string|null $q = null): self { + return $this->escapeAndAdd("'", $q); } - public function percent(): self { - return $this->escapeAndAdd("%"); + public function percent(string|null $q = null): self { + return $this->escapeAndAdd("%", $q); } - public function dollar(): self { - return $this->escapeAndAdd("$"); + public function dollar(string|null $q = null): self { + return $this->escapeAndAdd("$", $q); } - public function hash(): self { - return $this->escapeAndAdd("#"); + public function hash(string|null $q = null): self { + return $this->escapeAndAdd("#", $q); } - public function hashtag(): self { - return $this->escapeAndAdd("#"); + public function hashtag(string|null $q = null): self { + return $this->escapeAndAdd("#", $q); } - public function backtick(): self { - return $this->escapeAndAdd("`"); + public function backtick(string|null $q = null): self { + return $this->escapeAndAdd("`", $q); } - public function caret(): self { - return $this->escapeAndAdd("^"); + public function caret(string|null $q = null): self { + return $this->escapeAndAdd("^", $q); } public function unicode($code): self { @@ -215,48 +215,48 @@ public function unicode($code): self { // Methods for paired characters with separate open and close methods and an extra method with a boolean argument - public function openSquareBracket(): self { - return $this->escapeAndAdd("["); + public function openSquareBracket(string|null $q = null): self { + return $this->escapeAndAdd("[", $q); } - public function closeSquareBracket(): self { - return $this->escapeAndAdd("]"); + public function closeSquareBracket(string|null $q = null): self { + return $this->escapeAndAdd("]", $q); } public function squareBracket($isOpen = true): self { return $isOpen ? $this->openSquareBracket() : $this->closeSquareBracket(); } - public function openCurlyBrace(): self { - return $this->escapeAndAdd("{"); + public function openCurlyBrace(string|null $q = null): self { + return $this->escapeAndAdd("{", $q); } - public function closeCurlyBrace(): self { - return $this->escapeAndAdd("}"); + public function closeCurlyBrace(string|null $q = null): self { + return $this->escapeAndAdd("}", $q); } public function curlyBrace($isOpen = true): self { return $isOpen ? $this->openCurlyBrace() : $this->closeCurlyBrace(); } - public function openParenthesis(): self { - return $this->escapeAndAdd("("); + public function openParenthesis(string|null $q = null): self { + return $this->escapeAndAdd("(", $q); } - public function closeParenthesis(): self { - return $this->escapeAndAdd(")"); + public function closeParenthesis(string|null $q = null): self { + return $this->escapeAndAdd(")", $q); } public function parenthesis($isOpen = true): self { return $isOpen ? $this->openParenthesis() : $this->closeParenthesis(); } - public function openAngleBracket(): self { - return $this->escapeAndAdd("<"); + public function openAngleBracket(string|null $q = null): self { + return $this->escapeAndAdd("<", $q); } - public function closeAngleBracket(): self { - return $this->escapeAndAdd(">"); + public function closeAngleBracket(string|null $q = null): self { + return $this->escapeAndAdd(">", $q); } public function angleBracket($isOpen = true): self { diff --git a/tests/Feature/BuilderPatternTest.php b/tests/Feature/BuilderPatternTest.php index c2d29a9..7461c28 100644 --- a/tests/Feature/BuilderPatternTest.php +++ b/tests/Feature/BuilderPatternTest.php @@ -101,7 +101,7 @@ $check = $builder->start() ->alphanumeric() - ->underscore("?") + ->underscore() ->digitsRange(0, 2) ->checkString(); diff --git a/tests/Feature/EloquentRegexTest.php b/tests/Feature/EloquentRegexTest.php index 8db7be6..8400f8e 100644 --- a/tests/Feature/EloquentRegexTest.php +++ b/tests/Feature/EloquentRegexTest.php @@ -4,7 +4,7 @@ // Custom Pattern tests: -it('reproduces alt prefix pattern from HSA using facade', function () { +it('reproduces alt prefix pattern from HSA using wrapper', function () { $regex = EloquentRegex::builder()->start() ->exact("alt=") ->group(function ($pattern) { @@ -16,7 +16,7 @@ expect($regex)->toBe("alt\=(\"|')"); }); -it('reproduces hashtag prefix pattern from HSA using facade', function () { +it('reproduces hashtag prefix pattern from HSA using wrapper', function () { $regex = EloquentRegex::builder()->start() ->lookBehind(function ($pattern) { $pattern->charSet(function ($pattern) { @@ -27,7 +27,7 @@ expect($regex)->toBe('(?<=["\>\s])\#'); }); -it('reproduces Text suffix pattern from HSA using facade', function () { +it('reproduces Text suffix pattern from HSA using wrapper', function () { $regex = EloquentRegex::builder()->start() ->openAngleBracket()->slash()->alphanumericRange(0, 10)->closeAngleBracket() ->toRegex(); @@ -35,7 +35,7 @@ expect($regex)->toBe('\<\/[a-zA-Z0-9]{0,10}\>'); }); -it('constructs regex for simple email validation using facade', function () { +it('constructs regex for simple email validation using wrapper', function () { $regex = EloquentRegex::builder()->start() ->textLowercase() ->atSymbol() @@ -47,7 +47,7 @@ expect($regex)->toBe('[a-z]+@[a-z]+\.[a-z]{2,4}'); }); -it('constructs regex for URL validation using facade', function () { +it('constructs regex for URL validation using wrapper', function () { $regex = EloquentRegex::builder()->pattern() ->exact(['http', 'https']) ->colon() @@ -60,7 +60,7 @@ expect($regex)->toBe('(http|https)\:\/\/[a-zA-Z]+\.[a-zA-Z]+'); }); -it('constructs regex for specific phone number format using facade', function () { +it('constructs regex for specific phone number format using wrapper', function () { $regex = EloquentRegex::builder()->pattern(function ($p) { $p->openParenthesis()->digits(3)->closeParenthesis() ->space() @@ -70,7 +70,7 @@ expect($regex)->toBe('\(\d{3}\) \d{3}\-\d{4}'); }); -it('extracts dates in specific format from text using facade', function () { +it('extracts dates in specific format from text using wrapper', function () { $matches = EloquentRegex::customPattern("Meeting on 2021-09-15 and 2021-10-20") ->digits(4) ->dash() @@ -82,17 +82,18 @@ expect($matches)->toEqual(['2021-09-15', '2021-10-20']); }); -it('validates usernames in a string using facade', function () { +it('validates usernames in a string using wrapper and LengthOption', function () { $check = EloquentRegex::customPattern("Users: user_123, JohnDoe99") ->alphanumeric() - ->underscore("?") + ->underscore() ->digitsRange(0, 2) + ->end(["maxLength" => 15]) ->checkString(); expect($check)->toBeTrue(); }); -it('extracts hashtags from text using facade', function () { +it('extracts hashtags from text using wrapper', function () { $matches = EloquentRegex::start("#hello #world This is a #test") ->hash() ->text() @@ -101,7 +102,7 @@ expect($matches)->toEqual(['#hello', '#world', '#test']); }); -it('extracts secret coded messages from text using facade', function () { +it('extracts secret coded messages from text using wrapper', function () { $text = "Normal text {secret: message one} more text {secret: another hidden text} end"; $matches = EloquentRegex::start($text) ->lookBehind(function ($pattern) { @@ -149,7 +150,7 @@ it('validates a date format correctly', function () { $builder = EloquentRegex::string("2023-01-01"); - $check = $builder->date('Y-m-d')->check(); + $check = $builder->date()->check(); expect($check)->toBeTrue(); }); @@ -238,3 +239,91 @@ expect($check)->toBeTrue(); }); + + +// Quantifier tests: +it('matches specific number of dashes', function () { + $result = EloquentRegex::builder()->pattern()->dash('?')->toRegex(); + expect($result)->toBe('(\-)?'); +}); + +it('matches optional dots', function () { + $result = EloquentRegex::builder()->pattern()->dot('?')->toRegex(); + expect($result)->toBe('(\.)?'); +}); + +it('matches multiple spaces', function () { + $result = EloquentRegex::builder()->pattern()->space('2,5')->toRegex(); + expect($result)->toBe('( ){2,5}'); +}); + +it('matches one or more backslashes', function () { + $result = EloquentRegex::start("\\\\\\")->backslash('+')->check(); + expect($result)->toBe(true); +}); + +it('matches zero or more forward slashes', function () { + $result = EloquentRegex::builder()->start()->forwardSlash('*')->toRegex(); + expect($result)->toBe('(\/)*'); +}); + +it('matches exactly 4 underscores', function () { + $result = EloquentRegex::builder()->start()->underscore('4')->toRegex(); + expect($result)->toBe('(_){4}'); +}); + +it('matches one or more pipes', function () { + $result = EloquentRegex::builder()->start()->pipe('+')->toRegex(); + expect($result)->toBe('(\|)+'); +}); + +it('matches a specific number of character sets', function () { + $regex = EloquentRegex::builder()->start() + ->charSet(function ($pattern) { + $pattern->period()->colon(); + }, '3')->toRegex(); + + expect($regex)->toBe('([\.\:]){3}'); +}); + +it('matches a specific number of negative character sets', function () { + $regex = EloquentRegex::builder()->start() + ->negativeCharSet(function ($pattern) { + // "digits" and similar classes adds quantifier+ automaticaly + // Inside set "+" is parsed as symbol, instead of quantifier + // So, inside charSet and negativeCharSet method, you should + // pass 0 as first argument to do not apply quantifier here + $pattern->digits(0); + }, '2,4')->toRegex(); + + expect($regex)->toBe('([^\d]){2,4}'); +}); + +it('applies quantifier to capturing groups correctly', function () { + $regex = EloquentRegex::builder()->start() + ->group(function ($pattern) { + $pattern->text(); + }, '+')->toRegex(); + + expect($regex)->toBe('([a-zA-Z]+)+'); +}); + +it('applies quantifier to non-capturing groups correctly', function () { + $regex = EloquentRegex::builder()->start() + ->nonCapturingGroup(function ($pattern) { + $pattern->digits(); + }, '*')->toRegex(); + + expect($regex)->toBe('((?:\d+))*'); +}); + +it('uses quantifier with alternation patterns correctly', function () { + $regex = EloquentRegex::builder()->start() + ->group(function ($pattern) { + $pattern->text()->orPattern(function ($pattern) { + $pattern->digits(); + }, "?"); + })->toRegex(); + + expect($regex)->toBe('([a-zA-Z]+|(\d+)?)'); +}); diff --git a/tests/Unit/Patterns/BuilderPatternTest.php b/tests/Unit/Patterns/BuilderPatternTest.php index d55adec..35ad574 100644 --- a/tests/Unit/Patterns/BuilderPatternTest.php +++ b/tests/Unit/Patterns/BuilderPatternTest.php @@ -223,12 +223,14 @@ $builder = new BuilderPattern(); $builder->doubleQuote()->exact('quoted', true, '*')->doubleQuote(); - $expectedPattern = '/"quoted*"/'; + // $expectedPattern = '/"quoted*"/'; + $expectedPattern = '/"(quoted)*"/'; // @todo Should return this $regex = $builder->getMatchesValidationPattern(); expect($regex)->toBe($expectedPattern); expect(preg_match($regex, '"quoted"'))->toBe(1); - expect(preg_match($regex, '"quote"'))->toBe(1); // Should match as * allows for zero or more + expect(preg_match($regex, '"quotedquotedquoted"'))->toBe(1); // @todo Should be true + expect(preg_match($regex, '""'))->toBe(1); // Should match as * allows for zero or more expect(preg_match($regex, 'quoted'))->toBe(0); // Should not match without quotes });