mirror of
https://github.com/Respect/Validation.git
synced 2026-03-14 22:35:45 +01:00
Port Bash scripts to PHP
It makes more sense to use PHP to generate PHP code than to use Bash. I love writing Bash scripts, but I know it's not for everyone, and they can become quite complex. Porting them to PHP code also lowers the barrier for people to change them. While I was making those changes, I also noticed a problem with how we save the domain suffixes. We're converting all of them to ASCII, so we are not preserving languages such as Chinese, Thai, and Hebrew, which use non-ASCII characters.
This commit is contained in:
parent
6dfad94985
commit
7892a7c902
16 changed files with 1120 additions and 556 deletions
12
.gitattributes
vendored
12
.gitattributes
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/*.dist export-ignore
|
||||
/*.yml export-ignore
|
||||
/.* export-ignore
|
||||
/bin export-ignore
|
||||
/docs export-ignore
|
||||
/tests export-ignore
|
||||
/* export-ignore
|
||||
/composer.json -export-ignore
|
||||
/data -export-ignore
|
||||
/library -export-ignore
|
||||
/LICENSE -export-ignore
|
||||
/README.md -export-ignore
|
||||
|
|
|
|||
7
.github/workflows/update-regionals.yaml
vendored
7
.github/workflows/update-regionals.yaml
vendored
|
|
@ -25,10 +25,13 @@ jobs:
|
|||
ref: ${{ secrets.LAST_MINOR_VERSION }}
|
||||
|
||||
- name: Update top level domains
|
||||
run: bin/update-domain-toplevel
|
||||
run: bin/console update:domain-toplevel
|
||||
|
||||
- name: Update public domain suffixes
|
||||
run: bin/update-domain-suffixes
|
||||
run: bin/console update:domain-suffixes
|
||||
|
||||
- name: Update postal codes
|
||||
run: bin/console update:postal-codes
|
||||
|
||||
- name: Create pull request
|
||||
id: cpr
|
||||
|
|
|
|||
29
bin/console
Executable file
29
bin/console
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Respect\Dev\Commands\CreateMixinCommand;
|
||||
use Respect\Dev\Commands\UpdateDocLinksCommand;
|
||||
use Respect\Dev\Commands\UpdateDomainSuffixesCommand;
|
||||
use Respect\Dev\Commands\UpdateDomainToplevelCommand;
|
||||
use Respect\Dev\Commands\UpdatePostalCodesCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
return (static function () {
|
||||
$application = new Application('Respect/Validation', '3.0');
|
||||
$application->addCommand(new CreateMixinCommand());
|
||||
$application->addCommand(new UpdateDocLinksCommand());
|
||||
$application->addCommand(new UpdateDomainSuffixesCommand());
|
||||
$application->addCommand(new UpdateDomainToplevelCommand());
|
||||
$application->addCommand(new UpdatePostalCodesCommand());
|
||||
|
||||
return $application->run();
|
||||
})();
|
||||
248
bin/create-mixin
248
bin/create-mixin
|
|
@ -1,248 +0,0 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Nette\PhpGenerator\InterfaceType;
|
||||
use Nette\PhpGenerator\PhpNamespace;
|
||||
use Nette\PhpGenerator\Printer;
|
||||
use Respect\Validation\Mixins\AllBuilder;
|
||||
use Respect\Validation\Mixins\AllChain;
|
||||
use Respect\Validation\Mixins\Chain;
|
||||
use Respect\Validation\Mixins\KeyBuilder;
|
||||
use Respect\Validation\Mixins\KeyChain;
|
||||
use Respect\Validation\Mixins\LengthBuilder;
|
||||
use Respect\Validation\Mixins\LengthChain;
|
||||
use Respect\Validation\Mixins\MaxBuilder;
|
||||
use Respect\Validation\Mixins\MaxChain;
|
||||
use Respect\Validation\Mixins\MinBuilder;
|
||||
use Respect\Validation\Mixins\MinChain;
|
||||
use Respect\Validation\Mixins\NotBuilder;
|
||||
use Respect\Validation\Mixins\NotChain;
|
||||
use Respect\Validation\Mixins\NullOrBuilder;
|
||||
use Respect\Validation\Mixins\NullOrChain;
|
||||
use Respect\Validation\Mixins\PropertyBuilder;
|
||||
use Respect\Validation\Mixins\PropertyChain;
|
||||
use Respect\Validation\Mixins\UndefOrBuilder;
|
||||
use Respect\Validation\Mixins\UndefOrChain;
|
||||
use Respect\Validation\Validator;
|
||||
use Respect\Validation\ValidatorBuilder;
|
||||
|
||||
function addMethodToInterface(
|
||||
string $originalName,
|
||||
InterfaceType $interfaceType,
|
||||
ReflectionClass $reflection,
|
||||
string|null $prefix,
|
||||
array $allowList,
|
||||
array $denyList,
|
||||
): void {
|
||||
if ($allowList !== [] && !in_array($reflection->getShortName(), $allowList, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($denyList !== [] && in_array($reflection->getShortName(), $denyList, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName);
|
||||
$method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class);
|
||||
if (str_contains($interfaceType->getName(), 'Builder')) {
|
||||
$method->setStatic();
|
||||
}
|
||||
|
||||
if ($prefix === 'key') {
|
||||
$method->addParameter('key')->setType('int|string');
|
||||
}
|
||||
|
||||
if ($prefix === 'property') {
|
||||
$method->addParameter('propertyName')->setType('string');
|
||||
}
|
||||
|
||||
$reflrectionConstructor = $reflection->getConstructor();
|
||||
if ($reflrectionConstructor === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commend = $reflrectionConstructor->getDocComment();
|
||||
if ($commend) {
|
||||
$method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $commend));
|
||||
}
|
||||
|
||||
foreach ($reflrectionConstructor->getParameters() as $reflectionParameter) {
|
||||
if ($reflectionParameter->isVariadic()) {
|
||||
$method->setVariadic();
|
||||
}
|
||||
|
||||
$type = $reflectionParameter->getType();
|
||||
$types = [];
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
foreach ($type->getTypes() as $type) {
|
||||
$types[] = $type->getName();
|
||||
}
|
||||
} elseif ($type instanceof ReflectionNamedType) {
|
||||
$types[] = $type->getName();
|
||||
if (
|
||||
str_starts_with($type->getName(), 'Sokil')
|
||||
|| str_starts_with($type->getName(), 'Egulias')
|
||||
|| $type->getName() === 'finfo'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$parameter = $method->addParameter($reflectionParameter->getName());
|
||||
$parameter->setType(implode('|', $types));
|
||||
|
||||
if (!$reflectionParameter->isDefaultValueAvailable()) {
|
||||
$parameter->setNullable($reflectionParameter->isOptional());
|
||||
}
|
||||
|
||||
if (count($types) > 1 || $reflectionParameter->isVariadic()) {
|
||||
$parameter->setNullable(false);
|
||||
}
|
||||
|
||||
if (!$reflectionParameter->isDefaultValueAvailable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$defaultValue = $reflectionParameter->getDefaultValue();
|
||||
if (is_object($defaultValue)) {
|
||||
$parameter->setDefaultValue(null);
|
||||
$parameter->setNullable(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameter->setDefaultValue($defaultValue);
|
||||
$parameter->setNullable(false);
|
||||
}
|
||||
}
|
||||
|
||||
function overwriteFile(string $content, string $basename): void
|
||||
{
|
||||
file_put_contents(sprintf('%s/../library/Mixins/%s.php', __DIR__, $basename), implode(PHP_EOL . PHP_EOL, [
|
||||
'<?php',
|
||||
file_get_contents(__DIR__ . '/../.docheader'),
|
||||
'declare(strict_types=1);',
|
||||
preg_replace('/extends (.+, )+/', 'extends' . PHP_EOL . '\1', $content),
|
||||
]));
|
||||
}
|
||||
|
||||
(static function (): void {
|
||||
$numberRelatedValidators = [
|
||||
'Between',
|
||||
'BetweenExclusive',
|
||||
'Equals',
|
||||
'Equivalent',
|
||||
'Even',
|
||||
'Factor',
|
||||
'Fibonacci',
|
||||
'Finite',
|
||||
'GreaterThan',
|
||||
'Identical',
|
||||
'In',
|
||||
'Infinite',
|
||||
'LessThan',
|
||||
'LessThanOrEqual',
|
||||
'GreaterThanOrEqual',
|
||||
'Multiple',
|
||||
'Odd',
|
||||
'PerfectSquare',
|
||||
'Positive',
|
||||
'PrimeNumber',
|
||||
];
|
||||
|
||||
$structureRelatedValidators = [
|
||||
'Exists',
|
||||
'Key',
|
||||
'KeyExists',
|
||||
'KeyOptional',
|
||||
'KeySet',
|
||||
'NullOr',
|
||||
'UndefOr',
|
||||
'Property',
|
||||
'PropertyExists',
|
||||
'PropertyOptional',
|
||||
'Attributes',
|
||||
'Templated',
|
||||
'Named',
|
||||
];
|
||||
|
||||
$mixins = [
|
||||
['All', 'all', [], ['All', ...$structureRelatedValidators]],
|
||||
['Key', 'key', [], $structureRelatedValidators],
|
||||
['Length', 'length', $numberRelatedValidators, []],
|
||||
['Max', 'max', $numberRelatedValidators, []],
|
||||
['Min', 'min', $numberRelatedValidators, []],
|
||||
['Not', 'not', [], ['Not', 'NullOr', 'UndefOr', 'Attributes', 'Templated', 'Named']],
|
||||
['NullOr', 'nullOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Templated', 'Named']],
|
||||
['Property', 'property', [], $structureRelatedValidators],
|
||||
['UndefOr', 'undefOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Attributes', 'Templated', 'Named']],
|
||||
['', null, [], []],
|
||||
];
|
||||
|
||||
$names = [];
|
||||
foreach (new DirectoryIterator(__DIR__ . '/../library/Validators') as $file) {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = 'Respect\\Validation\\Validators\\' . $file->getBasename('.php');
|
||||
$reflection = new ReflectionClass($className);
|
||||
if ($reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$names[$reflection->getShortName()] = $reflection;
|
||||
}
|
||||
|
||||
ksort($names);
|
||||
|
||||
foreach ($mixins as [$name, $prefix, $allowList, $denyList]) {
|
||||
$chainedNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
|
||||
$chainedNamespace->addUse(Validator::class);
|
||||
$chainedInterface = $chainedNamespace->addInterface($name . 'Chain');
|
||||
|
||||
$staticNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
|
||||
$staticNamespace->addUse(Validator::class);
|
||||
$staticInterface = $staticNamespace->addInterface($name . 'Builder');
|
||||
|
||||
if ($name === '') {
|
||||
$chainedInterface->addExtend(Validator::class);
|
||||
$chainedInterface->addExtend(AllChain::class);
|
||||
$chainedInterface->addExtend(KeyChain::class);
|
||||
$chainedInterface->addExtend(LengthChain::class);
|
||||
$chainedInterface->addExtend(MaxChain::class);
|
||||
$chainedInterface->addExtend(MinChain::class);
|
||||
$chainedInterface->addExtend(NotChain::class);
|
||||
$chainedInterface->addExtend(NullOrChain::class);
|
||||
$chainedInterface->addExtend(PropertyChain::class);
|
||||
$chainedInterface->addExtend(UndefOrChain::class);
|
||||
$chainedInterface->addComment('@mixin \\' . ValidatorBuilder::class);
|
||||
|
||||
$staticInterface->addExtend(AllBuilder::class);
|
||||
$staticInterface->addExtend(KeyBuilder::class);
|
||||
$staticInterface->addExtend(LengthBuilder::class);
|
||||
$staticInterface->addExtend(MaxBuilder::class);
|
||||
$staticInterface->addExtend(MinBuilder::class);
|
||||
$staticInterface->addExtend(NotBuilder::class);
|
||||
$staticInterface->addExtend(NullOrBuilder::class);
|
||||
$staticInterface->addExtend(PropertyBuilder::class);
|
||||
$staticInterface->addExtend(UndefOrBuilder::class);
|
||||
}
|
||||
|
||||
foreach ($names as $originalName => $reflection) {
|
||||
addMethodToInterface($originalName, $staticInterface, $reflection, $prefix, $allowList, $denyList);
|
||||
addMethodToInterface($originalName, $chainedInterface, $reflection, $prefix, $allowList, $denyList);
|
||||
}
|
||||
|
||||
$printer = new Printer();
|
||||
$printer->wrapLength = 115;
|
||||
|
||||
overwriteFile($printer->printNamespace($staticNamespace), $staticInterface->getName());
|
||||
overwriteFile($printer->printNamespace($chainedNamespace), $chainedInterface->getName());
|
||||
}
|
||||
|
||||
shell_exec(__DIR__ . '/../vendor/bin/phpcbf ' . __DIR__ . '/../library/Mixins');
|
||||
})();
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Usage: {script} DOCS_DIREECTORY
|
||||
# Update list of rules and link related rules.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
declare -r IFS=$'\n'
|
||||
declare -r DOCS_DIREECTORY="${1}"
|
||||
|
||||
declare -A RULES_BY_CATEGORIZATION=()
|
||||
declare -A RELATED_RULES=()
|
||||
|
||||
list_rules()
|
||||
{
|
||||
find "${DOCS_DIREECTORY}/rules" -type f |
|
||||
sed "s,${DOCS_DIREECTORY}/,," |
|
||||
cut -d '/' -f 2 |
|
||||
cut -d '.' -f 1 |
|
||||
sort
|
||||
}
|
||||
|
||||
list_categories()
|
||||
{
|
||||
while read rule; do
|
||||
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
|
||||
sed -n '/## Categorization/,/## Changelog/p' "${filename}"
|
||||
done < <(list_rules) |
|
||||
grep '^-' |
|
||||
sort -u |
|
||||
sed 's,- ,,'
|
||||
}
|
||||
|
||||
create_rule_documentation()
|
||||
{
|
||||
# Usage: create_rule_documentation RULE RELATED_RULES
|
||||
local rule=${1}
|
||||
local related_rules=${2}
|
||||
local filename="${DOCS_DIREECTORY}/rules/${rule}.md"
|
||||
local links=$(ggrep -E '^\[.+\]: .+' "${filename}")
|
||||
local related_links=$(
|
||||
tr ':' '\n' <<< ${related_rules} |
|
||||
sort -u |
|
||||
grep -v '^$' |
|
||||
grep -v "^${rule}$" |
|
||||
sed -E "s,(.+),- [\1](\1.md),g"
|
||||
)
|
||||
|
||||
# "Description" section
|
||||
sed -nE '/^# /,/^## Changelog/p' "${filename}" | ggrep -E -v '^## Changelog'
|
||||
|
||||
# "Changelog" section
|
||||
sed -nE '/^## Changelog/,/^---/p' "${filename}"
|
||||
|
||||
# "See also" section
|
||||
echo
|
||||
echo "See also:"
|
||||
echo
|
||||
echo "${related_links}"
|
||||
|
||||
# Index of links
|
||||
if [[ ! -z "${links}" ]]; then
|
||||
echo
|
||||
echo "${links}"
|
||||
fi
|
||||
}
|
||||
|
||||
create_list_of_rules()
|
||||
{
|
||||
echo "Building list of rules per categorization"
|
||||
while read rule; do
|
||||
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
|
||||
for category in $(sed -n '/## Categorization/,/## Changelog/p' "${filename}" |
|
||||
grep '^-' |
|
||||
sed 's,- ,,'); do
|
||||
RULES_BY_CATEGORIZATION[${category}]=${RULES_BY_CATEGORIZATION[${category}]}:${rule}
|
||||
done
|
||||
done < <(list_rules)
|
||||
|
||||
echo "Creating list of rules"
|
||||
{
|
||||
echo "# List of rules by category"
|
||||
echo
|
||||
for category in $(list_categories); do
|
||||
echo "## ${category}"
|
||||
sed -E 's,:,\n- ,g' <<< "${RULES_BY_CATEGORIZATION[${category}]}" |
|
||||
sort -u |
|
||||
sed -E 's,- (.+),- [\1](rules/\1.md),'
|
||||
echo
|
||||
done
|
||||
|
||||
echo "## Alphabetically"
|
||||
echo
|
||||
ls -1 "${DOCS_DIREECTORY}/rules/" | sort | sed -E 's,^(.+).md$,- [\1](rules/\1.md),'
|
||||
} > "${DOCS_DIREECTORY}/09-list-of-validators-by-category.md"
|
||||
}
|
||||
|
||||
link_related_rules()
|
||||
{
|
||||
local temporaty=$(mktemp)
|
||||
|
||||
echo "Building list of related rules"
|
||||
for rule in $(list_rules); do
|
||||
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
|
||||
declare related_rules=$(ggrep -E '\[.+\]\(.+\.md\)' "${filename}" |
|
||||
sed -E 's,.*\[.+\]\((.+)\.md\).*,\1,' |
|
||||
grep -v 'comparable-values')
|
||||
|
||||
for related_rule in ${related_rules}; do
|
||||
RELATED_RULES[${related_rule}]=${RELATED_RULES[${related_rule}]}:${rule}
|
||||
done
|
||||
|
||||
RELATED_RULES[${rule}]="${RELATED_RULES[${rule}]}:$(tr '\n' ':' <<< ${related_rules})"
|
||||
done
|
||||
|
||||
echo "Recreating rule documentations with related rules"
|
||||
for rule in $(list_rules); do
|
||||
echo "- ${rule}"
|
||||
create_rule_documentation "${rule}" "${RELATED_RULES[${rule}]}" > "${temporaty}"
|
||||
cat "${temporaty}" > "${DOCS_DIREECTORY}/rules/${rule}.md"
|
||||
done
|
||||
}
|
||||
|
||||
create_list_of_rules
|
||||
link_related_rules
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Usage: {script} RULE_FILENAME
|
||||
# Update list of TLD
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
declare -r IFS=$'\n'
|
||||
|
||||
declare -r LIST_URL="https://publicsuffix.org/list/public_suffix_list.dat"
|
||||
declare -r LIST_FILENAME=$(mktemp)
|
||||
|
||||
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
|
||||
|
||||
echo "- Downloading list"
|
||||
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
|
||||
|
||||
echo "- Removing old data"
|
||||
rm -Rf data/domain/*
|
||||
mkdir -p data/domain/public-suffix
|
||||
|
||||
parse_tlds_list () {
|
||||
sed '/^\/\/*/d' |
|
||||
idn2 |
|
||||
tr '[:lower:]' '[:upper:]' |
|
||||
sed '/^$/d' | while read -r suffix
|
||||
do
|
||||
suffix="${suffix#\*\.}"
|
||||
suffix="${suffix#\!}"
|
||||
tld="${suffix##*\.}"
|
||||
if test "$tld" != "$suffix"
|
||||
then
|
||||
prefix="${suffix%.$tld}"
|
||||
echo "$tld $prefix"
|
||||
else
|
||||
prefix=""
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "- Creating files"
|
||||
|
||||
cat "$LIST_FILENAME" |
|
||||
parse_tlds_list |
|
||||
cut -d" " -f1 |
|
||||
sort -u | while read -r tld_with_suffix
|
||||
do
|
||||
suffixlist="$(cat "$LIST_FILENAME" | while read -r line
|
||||
do
|
||||
if test "// ===END ICANN DOMAINS===" = "$line"
|
||||
then break
|
||||
else echo "$line"
|
||||
fi
|
||||
done |
|
||||
parse_tlds_list |
|
||||
grep "^$tld_with_suffix " |
|
||||
cut -d" " -f2 |
|
||||
tr '[:lower:]' '[:upper:]' |
|
||||
LC_ALL=C sort | while read -r suffix
|
||||
do
|
||||
echo " '$suffix.$tld_with_suffix',"
|
||||
done | LC_ALL=C sort || :)"
|
||||
|
||||
if test -n "$suffixlist"
|
||||
then
|
||||
echo "- Creating public-suffix/$tld_with_suffix.php"
|
||||
echo "<?php declare(strict_types=1);
|
||||
// Copyright (c) https://publicsuffix.org
|
||||
// SPDX-License-Identifier: MPL-2.0-no-copyleft-exception
|
||||
return [
|
||||
$suffixlist
|
||||
];" > "data/domain/public-suffix/$tld_with_suffix.php"
|
||||
else
|
||||
echo "- Skipping public-suffix/$tld_with_suffix.php"
|
||||
fi
|
||||
done
|
||||
|
||||
wait
|
||||
|
||||
echo "Finished!"
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Usage: {script} RULE_FILENAME
|
||||
# Update list of TLD
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
declare -r IFS=$'\n'
|
||||
|
||||
declare -r LIST_URL="https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
|
||||
declare -r LIST_FILENAME=$(mktemp)
|
||||
|
||||
declare -r RULE_FILENAME=$(dirname "${BASH_SOURCE}")/../library/Validators/Tld.php
|
||||
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
|
||||
|
||||
echo "- Downloading list"
|
||||
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
|
||||
|
||||
echo "- Creating temporary file"
|
||||
{
|
||||
sed --silent --regexp-extended "/^</,/^\{/p" "${RULE_FILENAME}"
|
||||
echo " /**"
|
||||
echo " * List extracted from ${LIST_URL}"
|
||||
echo " */"
|
||||
echo " private const TLD_LIST = ["
|
||||
grep --invert-match "^#" "${LIST_FILENAME}" |
|
||||
sed --regexp-extended "s,^,',; s/$/', /" |
|
||||
tr --delete "\n" |
|
||||
fold --width=72 --spaces |
|
||||
sed "s,^, ,g; s, $,,g"
|
||||
echo
|
||||
echo " ];"
|
||||
echo
|
||||
echo " /**"
|
||||
echo " * {@inheritDoc}"
|
||||
echo " */"
|
||||
sed --silent --regexp-extended "/^ public function/,/^}/p" "${RULE_FILENAME}"
|
||||
} > "${RULE_FILENAME_TEMPORARY}"
|
||||
|
||||
echo "- Updating content of '$(basename ${RULE_FILENAME})'"
|
||||
mv "${RULE_FILENAME_TEMPORARY}" "${RULE_FILENAME}"
|
||||
|
||||
echo "Finished!"
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Usage: {script}
|
||||
# Update the list of currency codes
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
declare -r IFS=$'\n'
|
||||
|
||||
declare -r LIST_URL="https://download.geonames.org/export/dump/countryInfo.txt"
|
||||
declare -r LIST_FILENAME=$(mktemp)
|
||||
|
||||
declare -r RULE_FILENAME=$(dirname "${BASH_SOURCE}")/../library/Validators/PostalCode.php
|
||||
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
|
||||
|
||||
echo "- Downloading list"
|
||||
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
|
||||
|
||||
declare -r CURRENCY_CODES_COUNT=$(grep "<CcyNtry>" "${LIST_FILENAME}" | wc --lines)
|
||||
|
||||
echo "- Creating temporary file"
|
||||
{
|
||||
sed -n "/^</,/private const POSTAL_CODES = \[/p" "${RULE_FILENAME}"
|
||||
echo ' // phpcs:disable Generic.Files.LineLength.TooLong'
|
||||
cat "$LIST_FILENAME" |
|
||||
sed '/^#/d' |
|
||||
sed '/^$/d' |
|
||||
cut -f1,14,15 |
|
||||
sort -u | while read -r country_postal_code
|
||||
do
|
||||
country_code="${country_postal_code%% *}"
|
||||
country_postal="${country_postal_code#$country_code }"
|
||||
country_format="${country_postal%% *}"
|
||||
country_regex="${country_postal#$country_format }"
|
||||
country_regex="${country_regex%% }"
|
||||
country_format="$(echo "$country_format" | sed 's/#/\\d/g' | sed 's/@/\\w/g')"
|
||||
if test -n "$country_regex"
|
||||
then
|
||||
echo " '$country_code' => ['/^$country_format$/', '/$country_regex/'],"
|
||||
fi
|
||||
done
|
||||
echo ' // phpcs:disable Generic.Files.LineLength.TooLong'
|
||||
sed --silent '/^ \];\/\/end/,/^}/p' "${RULE_FILENAME}"
|
||||
} > "${RULE_FILENAME_TEMPORARY}"
|
||||
|
||||
echo "- Updating content of '$(basename ${RULE_FILENAME})'"
|
||||
mv "${RULE_FILENAME_TEMPORARY}" "${RULE_FILENAME}"
|
||||
|
||||
echo "Finished!"
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
"sokil/php-isocodes": "^4.2.1",
|
||||
"sokil/php-isocodes-db-only": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^4.0",
|
||||
"symfony/console": "^7.4",
|
||||
"symfony/var-exporter": "^6.4 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Respect\\Dev\\": "src-dev/",
|
||||
"Respect\\Validation\\": "tests/unit/",
|
||||
"Respect\\Validation\\Test\\": "tests/library/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,13 @@
|
|||
- [Uploaded](validators/Uploaded.md)
|
||||
- [Writable](validators/Writable.md)
|
||||
|
||||
## ISO codes
|
||||
|
||||
- [CountryCode](validators/CountryCode.md)
|
||||
- [CurrencyCode](validators/CurrencyCode.md)
|
||||
- [LanguageCode](validators/LanguageCode.md)
|
||||
- [SubdivisionCode](validators/SubdivisionCode.md)
|
||||
|
||||
## Identifications
|
||||
|
||||
- [Bsn](validators/Bsn.md)
|
||||
|
|
@ -131,13 +138,6 @@
|
|||
- [Url](validators/Url.md)
|
||||
- [VideoUrl](validators/VideoUrl.md)
|
||||
|
||||
## ISO codes
|
||||
|
||||
- [CountryCode](validators/CountryCode.md)
|
||||
- [CurrencyCode](validators/CurrencyCode.md)
|
||||
- [LanguageCode](validators/LanguageCode.md)
|
||||
- [SubdivisionCode](validators/SubdivisionCode.md)
|
||||
|
||||
## Localization
|
||||
|
||||
- [CountryCode](validators/CountryCode.md)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<arg value="s" />
|
||||
|
||||
<file>library/</file>
|
||||
<file>src-dev/</file>
|
||||
<file>tests/</file>
|
||||
|
||||
<rule ref="Respect" />
|
||||
|
|
|
|||
363
src-dev/Commands/CreateMixinCommand.php
Normal file
363
src-dev/Commands/CreateMixinCommand.php
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Respect\Dev\Commands;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Nette\PhpGenerator\InterfaceType;
|
||||
use Nette\PhpGenerator\Method;
|
||||
use Nette\PhpGenerator\PhpNamespace;
|
||||
use Nette\PhpGenerator\Printer;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use ReflectionUnionType;
|
||||
use Respect\Validation\Mixins\AllBuilder;
|
||||
use Respect\Validation\Mixins\AllChain;
|
||||
use Respect\Validation\Mixins\Chain;
|
||||
use Respect\Validation\Mixins\KeyBuilder;
|
||||
use Respect\Validation\Mixins\KeyChain;
|
||||
use Respect\Validation\Mixins\LengthBuilder;
|
||||
use Respect\Validation\Mixins\LengthChain;
|
||||
use Respect\Validation\Mixins\MaxBuilder;
|
||||
use Respect\Validation\Mixins\MaxChain;
|
||||
use Respect\Validation\Mixins\MinBuilder;
|
||||
use Respect\Validation\Mixins\MinChain;
|
||||
use Respect\Validation\Mixins\NotBuilder;
|
||||
use Respect\Validation\Mixins\NotChain;
|
||||
use Respect\Validation\Mixins\NullOrBuilder;
|
||||
use Respect\Validation\Mixins\NullOrChain;
|
||||
use Respect\Validation\Mixins\PropertyBuilder;
|
||||
use Respect\Validation\Mixins\PropertyChain;
|
||||
use Respect\Validation\Mixins\UndefOrBuilder;
|
||||
use Respect\Validation\Mixins\UndefOrChain;
|
||||
use Respect\Validation\Validator;
|
||||
use Respect\Validation\ValidatorBuilder;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_filter;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function is_object;
|
||||
use function ksort;
|
||||
use function lcfirst;
|
||||
use function preg_replace;
|
||||
use function shell_exec;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_starts_with;
|
||||
use function ucfirst;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'create:mixin',
|
||||
description: 'Generate mixin interfaces from validators',
|
||||
)]
|
||||
final class CreateMixinCommand extends Command
|
||||
{
|
||||
private const array NUMBER_RELATED_VALIDATORS = [
|
||||
'Between',
|
||||
'BetweenExclusive',
|
||||
'Equals',
|
||||
'Equivalent',
|
||||
'Even',
|
||||
'Factor',
|
||||
'Fibonacci',
|
||||
'Finite',
|
||||
'GreaterThan',
|
||||
'Identical',
|
||||
'In',
|
||||
'Infinite',
|
||||
'LessThan',
|
||||
'LessThanOrEqual',
|
||||
'GreaterThanOrEqual',
|
||||
'Multiple',
|
||||
'Odd',
|
||||
'PerfectSquare',
|
||||
'Positive',
|
||||
'PrimeNumber',
|
||||
];
|
||||
|
||||
private const array STRUCTURE_RELATED_VALIDATORS = [
|
||||
'Exists',
|
||||
'Key',
|
||||
'KeyExists',
|
||||
'KeyOptional',
|
||||
'KeySet',
|
||||
'NullOr',
|
||||
'UndefOr',
|
||||
'Property',
|
||||
'PropertyExists',
|
||||
'PropertyOptional',
|
||||
'Attributes',
|
||||
'Templated',
|
||||
'Named',
|
||||
];
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Generating mixin interfaces');
|
||||
|
||||
// Scan validators directory
|
||||
$libraryDir = dirname(__DIR__, 2) . '/library';
|
||||
$validatorsDir = $libraryDir . '/Validators';
|
||||
$validators = $this->scanValidators($validatorsDir);
|
||||
|
||||
$io->text(sprintf('Found %d validators', count($validators)));
|
||||
|
||||
// Define mixins
|
||||
$mixins = [
|
||||
['All', 'all', [], array_merge(['All'], self::STRUCTURE_RELATED_VALIDATORS)],
|
||||
['Key', 'key', [], self::STRUCTURE_RELATED_VALIDATORS],
|
||||
['Length', 'length', self::NUMBER_RELATED_VALIDATORS, []],
|
||||
['Max', 'max', self::NUMBER_RELATED_VALIDATORS, []],
|
||||
['Min', 'min', self::NUMBER_RELATED_VALIDATORS, []],
|
||||
['Not', 'not', [], ['Not', 'NullOr', 'UndefOr', 'Attributes', 'Templated', 'Named']],
|
||||
['NullOr', 'nullOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Templated', 'Named']],
|
||||
['Property', 'property', [], self::STRUCTURE_RELATED_VALIDATORS],
|
||||
['UndefOr', 'undefOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Attributes', 'Templated', 'Named']],
|
||||
['', null, [], []],
|
||||
];
|
||||
|
||||
$io->section('Generating mixin interfaces');
|
||||
|
||||
foreach ($mixins as [$name, $prefix, $allowList, $denyList]) {
|
||||
$io->text(sprintf('Generating %sBuilder and %sChain', $name ?: 'Base', $name ?: 'Base'));
|
||||
|
||||
$chainedNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
|
||||
$chainedNamespace->addUse(Validator::class);
|
||||
$chainedInterface = $chainedNamespace->addInterface($name . 'Chain');
|
||||
|
||||
$staticNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
|
||||
$staticNamespace->addUse(Validator::class);
|
||||
$staticInterface = $staticNamespace->addInterface($name . 'Builder');
|
||||
|
||||
if ($name === '') {
|
||||
$chainedInterface->addExtend(Validator::class);
|
||||
$chainedInterface->addExtend(AllChain::class);
|
||||
$chainedInterface->addExtend(KeyChain::class);
|
||||
$chainedInterface->addExtend(LengthChain::class);
|
||||
$chainedInterface->addExtend(MaxChain::class);
|
||||
$chainedInterface->addExtend(MinChain::class);
|
||||
$chainedInterface->addExtend(NotChain::class);
|
||||
$chainedInterface->addExtend(NullOrChain::class);
|
||||
$chainedInterface->addExtend(PropertyChain::class);
|
||||
$chainedInterface->addExtend(UndefOrChain::class);
|
||||
$chainedInterface->addComment('@mixin \\' . ValidatorBuilder::class);
|
||||
|
||||
$staticInterface->addExtend(AllBuilder::class);
|
||||
$staticInterface->addExtend(KeyBuilder::class);
|
||||
$staticInterface->addExtend(LengthBuilder::class);
|
||||
$staticInterface->addExtend(MaxBuilder::class);
|
||||
$staticInterface->addExtend(MinBuilder::class);
|
||||
$staticInterface->addExtend(NotBuilder::class);
|
||||
$staticInterface->addExtend(NullOrBuilder::class);
|
||||
$staticInterface->addExtend(PropertyBuilder::class);
|
||||
$staticInterface->addExtend(UndefOrBuilder::class);
|
||||
}
|
||||
|
||||
foreach ($validators as $originalName => $reflection) {
|
||||
$this->addMethodToInterface(
|
||||
$originalName,
|
||||
$staticInterface,
|
||||
$reflection,
|
||||
$prefix,
|
||||
$allowList,
|
||||
$denyList,
|
||||
);
|
||||
$this->addMethodToInterface(
|
||||
$originalName,
|
||||
$chainedInterface,
|
||||
$reflection,
|
||||
$prefix,
|
||||
$allowList,
|
||||
$denyList,
|
||||
);
|
||||
}
|
||||
|
||||
$printer = new Printer();
|
||||
$printer->wrapLength = 115;
|
||||
|
||||
$this->overwriteFile($printer->printNamespace($staticNamespace), $staticInterface->getName());
|
||||
$this->overwriteFile($printer->printNamespace($chainedNamespace), $chainedInterface->getName());
|
||||
}
|
||||
|
||||
// Run code beautifier
|
||||
$io->section('Running code beautifier');
|
||||
$mixinsDir = $libraryDir . '/Mixins';
|
||||
$phpcbfPath = dirname(__DIR__, 2) . '/vendor/bin/phpcbf';
|
||||
|
||||
if (file_exists($phpcbfPath)) {
|
||||
shell_exec($phpcbfPath . ' ' . $mixinsDir);
|
||||
$io->success('Code beautified');
|
||||
} else {
|
||||
$io->warning('phpcbf not found, skipping code beautification');
|
||||
}
|
||||
|
||||
$io->success('Mixin interfaces generated successfully');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @return array<string, ReflectionClass> */
|
||||
private function scanValidators(string $directory): array
|
||||
{
|
||||
$names = [];
|
||||
|
||||
foreach (new DirectoryIterator($directory) as $file) {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = 'Respect\\Validation\\Validators\\' . $file->getBasename('.php');
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
if ($reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$names[$reflection->getShortName()] = $reflection;
|
||||
}
|
||||
|
||||
ksort($names);
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $allowList
|
||||
* @param array<string> $denyList
|
||||
*/
|
||||
private function addMethodToInterface(
|
||||
string $originalName,
|
||||
InterfaceType $interfaceType,
|
||||
ReflectionClass $reflection,
|
||||
string|null $prefix,
|
||||
array $allowList,
|
||||
array $denyList,
|
||||
): void {
|
||||
if ($allowList !== [] && !in_array($reflection->getShortName(), $allowList, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($denyList !== [] && in_array($reflection->getShortName(), $denyList, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName);
|
||||
$method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class);
|
||||
|
||||
if (str_contains($interfaceType->getName(), 'Builder')) {
|
||||
$method->setStatic();
|
||||
}
|
||||
|
||||
if ($prefix === 'key') {
|
||||
$method->addParameter('key')->setType('int|string');
|
||||
}
|
||||
|
||||
if ($prefix === 'property') {
|
||||
$method->addParameter('propertyName')->setType('string');
|
||||
}
|
||||
|
||||
$reflectionConstructor = $reflection->getConstructor();
|
||||
if ($reflectionConstructor === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$comment = $reflectionConstructor->getDocComment();
|
||||
if ($comment) {
|
||||
$method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment));
|
||||
}
|
||||
|
||||
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
|
||||
$this->addParameterToMethod($method, $reflectionParameter);
|
||||
}
|
||||
}
|
||||
|
||||
private function addParameterToMethod(Method $method, ReflectionParameter $reflectionParameter): void
|
||||
{
|
||||
if ($reflectionParameter->isVariadic()) {
|
||||
$method->setVariadic();
|
||||
}
|
||||
|
||||
$type = $reflectionParameter->getType();
|
||||
$types = [];
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
foreach ($type->getTypes() as $subType) {
|
||||
$types[] = $subType->getName();
|
||||
}
|
||||
} elseif ($type instanceof ReflectionNamedType) {
|
||||
$types[] = $type->getName();
|
||||
if (
|
||||
str_starts_with($type->getName(), 'Sokil')
|
||||
|| str_starts_with($type->getName(), 'Egulias')
|
||||
|| $type->getName() === 'finfo'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$parameter = $method->addParameter($reflectionParameter->getName());
|
||||
$parameter->setType(implode('|', $types));
|
||||
|
||||
if (!$reflectionParameter->isDefaultValueAvailable()) {
|
||||
$parameter->setNullable($reflectionParameter->isOptional());
|
||||
}
|
||||
|
||||
if (count($types) > 1 || $reflectionParameter->isVariadic()) {
|
||||
$parameter->setNullable(false);
|
||||
}
|
||||
|
||||
if (!$reflectionParameter->isDefaultValueAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultValue = $reflectionParameter->getDefaultValue();
|
||||
if (is_object($defaultValue)) {
|
||||
$parameter->setDefaultValue(null);
|
||||
$parameter->setNullable(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parameter->setDefaultValue($defaultValue);
|
||||
$parameter->setNullable(false);
|
||||
}
|
||||
|
||||
private function overwriteFile(string $content, string $basename): void
|
||||
{
|
||||
$libraryDir = dirname(__DIR__, 2) . '/library';
|
||||
$docheaderPath = dirname(__DIR__, 2) . '/.docheader';
|
||||
$docheader = file_exists($docheaderPath) ? file_get_contents($docheaderPath) : '';
|
||||
|
||||
$finalContent = implode("\n\n", array_filter([
|
||||
'<?php',
|
||||
$docheader,
|
||||
'declare(strict_types=1);',
|
||||
preg_replace('/extends (.+, )+/', 'extends' . "\n" . '\1', $content),
|
||||
]));
|
||||
|
||||
file_put_contents(
|
||||
sprintf('%s/Mixins/%s.php', $libraryDir, $basename),
|
||||
$finalContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
275
src-dev/Commands/UpdateDocLinksCommand.php
Normal file
275
src-dev/Commands/UpdateDocLinksCommand.php
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Respect\Dev\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_unique;
|
||||
use function basename;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function glob;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function preg_split;
|
||||
use function sort;
|
||||
use function sprintf;
|
||||
use function trim;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'update:doc-links',
|
||||
description: 'Update list of validators and link related validators in documentation',
|
||||
)]
|
||||
final class UpdateDocLinksCommand extends Command
|
||||
{
|
||||
/** @var array<string, array<string>> */
|
||||
private array $validatorsByCategory = [];
|
||||
|
||||
/** @var array<string, array<string>> */
|
||||
private array $relatedRules = [];
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$docsDirectory = dirname(__DIR__, 2) . '/docs';
|
||||
|
||||
if (!file_exists($docsDirectory)) {
|
||||
$io->error(sprintf('Documentation directory not found: %s', $docsDirectory));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$validators = $this->listRules($docsDirectory);
|
||||
$this->createListOfRules($docsDirectory, $validators);
|
||||
$this->linkRelatedRules($docsDirectory, $validators, $io);
|
||||
$io->success('Documentation updated successfully');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
private function listRules(string $docsDirectory): array
|
||||
{
|
||||
$files = glob($docsDirectory . '/validators/*.md');
|
||||
if ($files === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$validators = [];
|
||||
foreach ($files as $file) {
|
||||
$validators[] = basename($file, '.md');
|
||||
}
|
||||
|
||||
sort($validators);
|
||||
|
||||
return $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $validators
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function listCategories(string $docsDirectory, array $validators): array
|
||||
{
|
||||
$categories = [];
|
||||
|
||||
foreach ($validators as $validator) {
|
||||
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
|
||||
$content = file_get_contents($filename);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract categories between "## Categorization" and "## Changelog"
|
||||
if (!preg_match('/## Categorization\s*(.*?)\s*## Changelog/s', $content, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match_all('/^-\s*(.+)$/m', $matches[1], $categoryMatches);
|
||||
foreach ($categoryMatches[1] as $category) {
|
||||
$categories[] = trim($category);
|
||||
}
|
||||
}
|
||||
|
||||
$categories = array_unique($categories);
|
||||
sort($categories);
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/** @param array<string> $validators */
|
||||
private function createListOfRules(string $docsDirectory, array $validators): void
|
||||
{
|
||||
// Build validators by category
|
||||
foreach ($validators as $validator) {
|
||||
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
|
||||
$content = file_get_contents($filename);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/## Categorization\s*(.*?)\s*## Changelog/s', $content, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match_all('/^-\s*(.+)$/m', $matches[1], $categoryMatches);
|
||||
foreach ($categoryMatches[1] as $category) {
|
||||
$category = trim($category);
|
||||
if (!isset($this->validatorsByCategory[$category])) {
|
||||
$this->validatorsByCategory[$category] = [];
|
||||
}
|
||||
|
||||
$this->validatorsByCategory[$category][] = $validator;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the list file
|
||||
$categories = $this->listCategories($docsDirectory, $validators);
|
||||
$lines = ['# List of validators by category', ''];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$lines[] = sprintf('## %s', $category);
|
||||
$lines[] = '';
|
||||
|
||||
if (isset($this->validatorsByCategory[$category])) {
|
||||
$categoryRules = $this->validatorsByCategory[$category];
|
||||
sort($categoryRules);
|
||||
foreach ($categoryRules as $validator) {
|
||||
$lines[] = sprintf('- [%s](validators/%s.md)', $validator, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
$lines[] = '## Alphabetically';
|
||||
$lines[] = '';
|
||||
foreach ($validators as $validator) {
|
||||
$lines[] = sprintf('- [%1$s](validators/%1$s.md)', $validator);
|
||||
}
|
||||
|
||||
$outputFile = sprintf('%s/09-list-of-validators-by-category.md', $docsDirectory);
|
||||
file_put_contents($outputFile, trim(implode("\n", $lines)) . PHP_EOL);
|
||||
}
|
||||
|
||||
/** @param array<string> $validators */
|
||||
private function linkRelatedRules(string $docsDirectory, array $validators, SymfonyStyle $io): void
|
||||
{
|
||||
// Build list of related validators
|
||||
foreach ($validators as $validator) {
|
||||
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
|
||||
$content = file_get_contents($filename);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all markdown links
|
||||
preg_match_all('/\[([^\]]+)\]\(([^\)]+)\.md\)/', $content, $matches);
|
||||
foreach ($matches[2] as $relatedRule) {
|
||||
$relatedRule = basename($relatedRule);
|
||||
if ($relatedRule === '08-comparable-values' || $relatedRule === 'comparing-empty-values') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($this->relatedRules[$relatedRule])) {
|
||||
$this->relatedRules[$relatedRule] = [];
|
||||
}
|
||||
|
||||
if (!in_array($validator, $this->relatedRules[$relatedRule], true)) {
|
||||
$this->relatedRules[$relatedRule][] = $validator;
|
||||
}
|
||||
|
||||
if (!isset($this->relatedRules[$validator])) {
|
||||
$this->relatedRules[$validator] = [];
|
||||
}
|
||||
|
||||
if (in_array($relatedRule, $this->relatedRules[$validator], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->relatedRules[$validator][] = $relatedRule;
|
||||
}
|
||||
}
|
||||
|
||||
// Update each validator documentation
|
||||
$io->progressStart(count($validators));
|
||||
foreach ($validators as $validator) {
|
||||
$this->createRuleDocumentation($docsDirectory, $validator);
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
}
|
||||
|
||||
private function createRuleDocumentation(string $docsDirectory, string $validator): void
|
||||
{
|
||||
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
|
||||
$content = file_get_contents($filename);
|
||||
if ($content === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract links at the bottom
|
||||
preg_match_all('/^\[.+\]: .+$/m', $content, $linkMatches);
|
||||
$links = $linkMatches[0] ?? [];
|
||||
|
||||
// Get content before "See also" or "---"
|
||||
$parts = preg_split('/(^## See also:|^---)/m', $content);
|
||||
$mainContent = $parts[0] ?? $content;
|
||||
|
||||
// Build "See also" section
|
||||
$relatedLinks = [];
|
||||
if (isset($this->relatedRules[$validator])) {
|
||||
$related = array_unique($this->relatedRules[$validator]);
|
||||
sort($related);
|
||||
foreach ($related as $relatedRule) {
|
||||
if ($relatedRule === $validator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relatedLinks[] = sprintf('- [%s](%s.md)', $relatedRule, $relatedRule);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the document
|
||||
$lines = [
|
||||
trim($mainContent),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
];
|
||||
|
||||
if ($relatedLinks !== []) {
|
||||
$lines[] = 'See also:';
|
||||
$lines[] = '';
|
||||
$lines = [...$lines, ...$relatedLinks];
|
||||
}
|
||||
|
||||
if ($links !== []) {
|
||||
$lines[] = '';
|
||||
$lines = [...$lines, ...$links];
|
||||
}
|
||||
|
||||
file_put_contents($filename, trim(implode("\n", $lines)) . PHP_EOL);
|
||||
}
|
||||
}
|
||||
207
src-dev/Commands/UpdateDomainSuffixesCommand.php
Normal file
207
src-dev/Commands/UpdateDomainSuffixesCommand.php
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Respect\Dev\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
use function array_keys;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function glob;
|
||||
use function implode;
|
||||
use function is_dir;
|
||||
use function mb_strtoupper;
|
||||
use function mkdir;
|
||||
use function preg_match;
|
||||
use function rmdir;
|
||||
use function sort;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function trim;
|
||||
use function unlink;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'update:domain-suffixes',
|
||||
description: 'Update list of public domain suffixes',
|
||||
)]
|
||||
final class UpdateDomainSuffixesCommand extends Command
|
||||
{
|
||||
private const string LIST_URL = 'https://publicsuffix.org/list/public_suffix_list.dat';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Updating domain suffixes');
|
||||
|
||||
// Download the list
|
||||
$io->section('Downloading list');
|
||||
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
|
||||
|
||||
$listContent = file_get_contents(self::LIST_URL);
|
||||
if ($listContent === false) {
|
||||
$io->error('Failed to download public suffix list');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success('Downloaded successfully');
|
||||
|
||||
// Clean old data
|
||||
$io->section('Removing old data');
|
||||
$dataDir = dirname(__DIR__, 2) . '/data/domain';
|
||||
$this->removeDirectory($dataDir . '/public-suffix');
|
||||
|
||||
if (!is_dir($dataDir)) {
|
||||
mkdir($dataDir, 0777, true);
|
||||
}
|
||||
|
||||
mkdir($dataDir . '/public-suffix', 0777, true);
|
||||
$io->success('Old data removed');
|
||||
|
||||
// Process the list
|
||||
$io->section('Processing public suffix list');
|
||||
|
||||
$suffixes = $this->parseTldsList($listContent);
|
||||
$tlds = array_unique(array_keys($suffixes));
|
||||
sort($tlds);
|
||||
|
||||
$io->text(sprintf('Found %d TLDs with suffixes', count($tlds)));
|
||||
|
||||
// Create files
|
||||
$io->section('Creating suffix files');
|
||||
$progressBar = $io->createProgressBar(count($tlds));
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($tlds as $tld) {
|
||||
$suffixList = $suffixes[$tld];
|
||||
if ($suffixList === []) {
|
||||
$progressBar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
sort($suffixList);
|
||||
|
||||
$fileContent = implode(PHP_EOL, [
|
||||
'<?php declare(strict_types=1);',
|
||||
'// Copyright (c) https://publicsuffix.org',
|
||||
'// SPDX-License-Identifier: MPL-2.0-no-copyleft-exception',
|
||||
'return ' . VarExporter::export($suffixList) . ';' . PHP_EOL,
|
||||
]);
|
||||
|
||||
// Convert IDN TLD to ASCII (Punycode) for filename
|
||||
$filename = sprintf('%s/public-suffix/%s.php', $dataDir, $tld);
|
||||
file_put_contents($filename, $fileContent);
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$io->newLine(2);
|
||||
|
||||
$io->success('Domain suffixes updated successfully');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @return array<string, array<string>> */
|
||||
private function parseTldsList(string $content): array
|
||||
{
|
||||
$lines = explode("\n", $content);
|
||||
$suffixes = [];
|
||||
$icannOnly = true;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Check if we've reached the end of ICANN domains
|
||||
if ($line === '// ===END ICANN DOMAINS===') {
|
||||
$icannOnly = false;
|
||||
}
|
||||
|
||||
// Skip comments and empty lines
|
||||
if ($line === '' || str_starts_with($line, '//')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process the suffix
|
||||
$suffix = $line;
|
||||
|
||||
// Remove wildcards and exceptions
|
||||
$suffix = str_replace('*.', '', $suffix);
|
||||
$suffix = str_replace('!', '', $suffix);
|
||||
|
||||
// Convert to uppercase (using multibyte for international characters)
|
||||
$suffix = mb_strtoupper($suffix, 'UTF-8');
|
||||
|
||||
// Split into TLD and prefix
|
||||
if (!preg_match('/^([^.]+)$|^(.+)\.([^.]+)$/', $suffix, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($matches[3])) {
|
||||
// Has a prefix
|
||||
$tld = $matches[3];
|
||||
$prefix = $matches[2];
|
||||
|
||||
if (!isset($suffixes[$tld])) {
|
||||
$suffixes[$tld] = [];
|
||||
}
|
||||
|
||||
// Only add ICANN domains
|
||||
if ($icannOnly) {
|
||||
$suffixes[$tld][] = $prefix . '.' . $tld;
|
||||
}
|
||||
} else {
|
||||
// Just a TLD
|
||||
$tld = $matches[1];
|
||||
if (!isset($suffixes[$tld])) {
|
||||
$suffixes[$tld] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $suffixes;
|
||||
}
|
||||
|
||||
private function removeDirectory(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($directory . '/*');
|
||||
if ($files === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (is_dir($file)) {
|
||||
$this->removeDirectory($file);
|
||||
} else {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir($directory);
|
||||
}
|
||||
}
|
||||
95
src-dev/Commands/UpdateDomainToplevelCommand.php
Normal file
95
src-dev/Commands/UpdateDomainToplevelCommand.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Respect\Dev\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
use function basename;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use function trim;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'update:domain-toplevel',
|
||||
description: 'Update list of Top Level Domains (TLD) in the Tld validator',
|
||||
)]
|
||||
final class UpdateDomainToplevelCommand extends Command
|
||||
{
|
||||
private const string LIST_URL = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Updating TLD list');
|
||||
|
||||
// Download the list
|
||||
$io->section('Downloading list');
|
||||
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
|
||||
|
||||
$listContent = file_get_contents(self::LIST_URL);
|
||||
if ($listContent === false) {
|
||||
$io->error('Failed to download TLD list');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success('Downloaded successfully');
|
||||
|
||||
// Process the list
|
||||
$io->section('Processing TLD list');
|
||||
|
||||
$lines = explode("\n", trim($listContent));
|
||||
$tlds = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tlds[] = $line;
|
||||
}
|
||||
|
||||
// Create the data file
|
||||
$dataFilename = dirname(__DIR__, 2) . '/data/domain/tld.php';
|
||||
|
||||
$fileContent = implode(PHP_EOL, [
|
||||
'<?php declare(strict_types=1);',
|
||||
'// Copyright (c) https://data.iana.org/TLD/',
|
||||
'// SPDX-License-Identifier: MPL-2.0',
|
||||
'return ' . VarExporter::export($tlds) . ';' . PHP_EOL,
|
||||
]);
|
||||
|
||||
// Write the data file
|
||||
if (file_put_contents($dataFilename, $fileContent) === false) {
|
||||
$io->error('Failed to write data file');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success(sprintf('Updated %s successfully', basename($dataFilename)));
|
||||
$io->text(sprintf('Total TLDs: %d', count($tlds)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
130
src-dev/Commands/UpdatePostalCodesCommand.php
Normal file
130
src-dev/Commands/UpdatePostalCodesCommand.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Respect\Dev\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
use function basename;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function implode;
|
||||
use function ksort;
|
||||
use function preg_replace;
|
||||
use function preg_replace_callback;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
use function trim;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'update:postal-codes',
|
||||
description: 'Update the list of postal codes in the PostalCode validator',
|
||||
)]
|
||||
final class UpdatePostalCodesCommand extends Command
|
||||
{
|
||||
private const string LIST_URL = 'https://download.geonames.org/export/dump/countryInfo.txt';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Updating postal codes list');
|
||||
|
||||
// Download the list
|
||||
$io->section('Downloading list');
|
||||
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
|
||||
|
||||
$listContent = file_get_contents(self::LIST_URL);
|
||||
if ($listContent === false) {
|
||||
$io->error('Failed to download postal codes list');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success('Downloaded successfully');
|
||||
|
||||
// Process the list
|
||||
$io->section('Processing postal codes');
|
||||
|
||||
$lines = explode("\n", $listContent);
|
||||
$postalCodes = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if ($line === '' || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by tab
|
||||
$parts = explode("\t", $line);
|
||||
if (count($parts) < 15) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$countryCode = $parts[0];
|
||||
$countryFormat = $parts[13];
|
||||
$countryRegex = trim($parts[14]);
|
||||
|
||||
if ($countryRegex === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert format
|
||||
$countryFormat = preg_replace_callback('/(#+|@+)/', static function ($matches) {
|
||||
$length = strlen($matches[0]);
|
||||
$regex = str_contains($matches[0], '#') ? '\d' : '\w';
|
||||
if ($length > 1) {
|
||||
$regex .= '{' . $length . '}';
|
||||
}
|
||||
|
||||
return $regex;
|
||||
}, $countryFormat);
|
||||
|
||||
$postalCodes[$countryCode] = ['/^' . $countryFormat . '$/', '/' . $countryRegex . '/'];
|
||||
}
|
||||
|
||||
ksort($postalCodes);
|
||||
|
||||
// Create the data file
|
||||
$dataFilename = dirname(__DIR__, 2) . '/data/domain/postal-code.php';
|
||||
|
||||
$fileContent = implode(PHP_EOL, [
|
||||
'<?php declare(strict_types=1);',
|
||||
'// Copyright (c) https://download.geonames.org/export/dump/countryInfo.txt',
|
||||
'// SPDX-License-Identifier: CC-BY-4.0',
|
||||
'return ' . preg_replace('/\\\([dws])/', '\\1', VarExporter::export($postalCodes)) . ';' . PHP_EOL,
|
||||
]);
|
||||
|
||||
// Write the data file
|
||||
if (file_put_contents($dataFilename, $fileContent) === false) {
|
||||
$io->error('Failed to write data file');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success(sprintf('Updated %s successfully', basename($dataFilename)));
|
||||
$io->text(sprintf('Total postal codes: %d', count($postalCodes)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue