diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..60d3c8564 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Default settings for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +max_line_length = 120 + +# YAML files +[*.yml] +indent_size = 2 + +[*.yaml] +indent_size = 2 + +# PHP files +[*.php] +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index f3c7abee3..0edaf4d32 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,33 @@ phpcs.xml phpstan.neon phpunit.xml vendor/ + +# MkDocs build output +site/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Temp files +*.tmp +*.log + +# PHP specific patterns +*.php~ +.php_cs.cache +.php_cs.fixed +.phpunit.result.cache +coverage/ +.php_cs.dist +.php_cs +.phpstorm.meta.php diff --git a/.markdown-link-check.json b/.markdown-link-check.json new file mode 100644 index 000000000..f22935a38 --- /dev/null +++ b/.markdown-link-check.json @@ -0,0 +1,29 @@ +{ + "timeout": "10s", + "retryOn429": true, + "retryCount": 3, + "fallbackRetryDelay": "30s", + "aliveStatusCodes": [200, 206], + "httpHeaders": [ + { + "urls": ["https://example.com"], + "headers": { + "User-Agent": "markdown-link-check validator" + } + } + ], + "ignorePatterns": [ + { + "pattern": "^http://localhost" + }, + { + "pattern": "^https://localhost" + }, + { + "pattern": "^file://" + }, + { + "pattern": "^#" + } + ] +} \ No newline at end of file diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 000000000..eb977b9f1 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,210 @@ +# Markdownlint configuration +# https://github.com/DavidAnson/markdownlint + +# Default state for all rules +default: true + +# Path to configuration file to extend +# extends: null + +# MD001/heading-increment/header-increment - Heading levels should only increment by one level at a time +MD001: true + +# MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading +MD002: + # Heading level + level: 1 + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "atx" + +# MD004/ul-style - Unordered list style +MD004: + # List style + style: "dash" + +# MD005/list-indent - Inconsistent indentation for list items at the same level +MD005: true + +# MD006/ul-start-left - Consider starting bulleted lists at the beginning of the line +MD006: true + +# MD007/ul-indent - Unordered list indentation +MD007: + # Spaces for indent + indent: 2 + +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Include list items + list_item_empty_lines: false + +# MD010/no-hard-tabs - Hard tabs +MD010: + # Include code blocks + code_blocks: true + +# MD011/no-reversed-links - Reversed link syntax +MD011: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 120 + # Number of characters for headings + heading_line_length: 80 + # Number of characters for code blocks + code_block_line_length: 120 + # Include code blocks + code_blocks: true + # Include tables + tables: true + +# MD014/commands-show-output - Dollar signs used before commands without showing output +MD014: true + +# MD018/no-missing-space-atx - No space after hash on atx style heading +MD018: true + +# MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading +MD019: true + +# MD020/no-missing-space-closed-atx - No space inside hashes on closed atx style heading +MD020: true + +# MD021/no-multiple-space-closed-atx - Multiple spaces inside hashes on closed atx style heading +MD021: true + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line +MD023: true + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: true + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # Only check document frontmatter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: + # Punctuation characters + punctuation: ".,;!?" + +# MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol +MD027: true + +# MD028/no-blanks-blockquote - Blank line inside blockquote +MD028: true + +# MD029/ol-prefix - Ordered list item prefix +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists - Lists should be surrounded by blank lines +MD032: true + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD034/no-bare-urls - Bare URL used +MD034: true + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "---" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: true + +# MD037/no-space-in-emphasis - Spaces inside emphasis markers +MD037: true + +# MD038/no-space-in-code - Spaces inside code span elements +MD038: true + +# MD039/no-space-in-links - Spaces inside link text +MD039: true + +# MD040/fenced-code-language - Fenced code blocks should have a language specified +MD040: true + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: true + +# MD042/no-empty-links - No empty links +MD042: true + +# MD043/required-headings/required-headers - Required heading structure +MD043: false + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + +# MD045/no-alt-text - Images should have alternate text (alt text) +MD045: true + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "fenced" + +# MD047/single-trailing-newline - Files should end with a single newline character +MD047: true + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence style + style: "backtick" + +# MD049/emphasis-style - Emphasis style should be consistent +MD049: + # Emphasis style + style: "underscore" + +# MD050/strong-style - Strong style should be consistent +MD050: + # Strong style + style: "asterisk" \ No newline at end of file diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 000000000..f9e6544d3 --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,218 @@ +# Respect\Validation Constitution + + + +## Core Principles + +### I. Test-First Development (NON-NEGOTIABLE) + +**Dual-Testing Strategy**: Every feature MUST be validated through two complementary test suites: + +- **Unit Tests** (PHPUnit): Test individual rules and components in isolation + - Located in `tests/unit/` + - MUST extend `RuleTestCase` for rule validation + - MUST implement `providerForValidInput()` and `providerForInvalidInput()` + - Tests written BEFORE implementation + +- **Feature Tests** (Pest): Test complete user scenarios and integration flows + - Located in `tests/feature/` + - MUST validate real-world usage patterns + - MUST test error message rendering and exception handling + +**Red-Green-Refactor Cycle**: Tests MUST fail initially, then pass after implementation. No code ships without passing tests. + +### II. Quality Assurance Pipeline + +**Mandatory Pre-Commit Checks**: All changes MUST pass the complete QA pipeline (`composer qa`) before merge: + +1. **docheader**: Verify license headers on all source files +2. **phpcs**: Enforce coding standards (PSR-12 + Respect standards) +3. **phpstan**: Static analysis at level 8 with no ignored production code errors +4. **phpunit**: All unit tests pass +5. **pest**: All feature tests pass + +**Non-Negotiable**: A single failure in any stage blocks the merge. No exceptions. + +### III. Code Standards & Type Safety + +**Strict Typing Mandate**: Every PHP file MUST declare `strict_types=1`. Type hints are REQUIRED for all parameters and return types. + +**Encapsulation Defaults**: +- Classes MUST be `final` unless explicitly designed for inheritance +- Properties MUST be `private` unless extension is required +- Methods MUST have explicit visibility modifiers + +**Documentation Requirements**: +- License header MUST be present (enforced by docheader) +- Complex logic MUST have explanatory comments +- Public APIs MUST have DocBlocks when native PHP types are insufficient (e.g., array shapes, generic types, additional context) +- Thrown exceptions MUST be documented in DocBlocks when not evident from signature +- Template attributes MUST define both positive and negative validation messages + +**Namespace Organization**: +- Rules: `Respect\Validation\Rules\*` +- Exceptions: `Respect\Validation\Exceptions\*` +- Tests mirror library structure + +### IV. Open Source Collaboration + +**Contribution Workflow**: +- Features and bug fixes MUST reference an existing issue or create one +- Pull requests MUST be based on `main` branch for new features +- Pull requests for bug fixes MUST target the oldest stable version branch +- Contributors MUST allow maintainers time to review (acknowledge delays are expected) + +**Documentation Obligation**: +- New rules MUST include documentation in `docs/rules/` +- CONTRIBUTING.md provides the canonical guide for adding validators +- README.md links to comprehensive documentation at respect-validation.readthedocs.io + +### V. Simplicity & Clarity + +**Rule Design Philosophy**: +- Each rule SHOULD solve one specific validation concern +- Complex validations are composed from simple rules via chaining +- Example: `v::numericVal()->positive()->between(1, 255)` + +**Naming Conventions**: +- Rule class names are PascalCase (e.g., `HelloWorld`) +- Fluent API converts to camelCase automatically (e.g., `helloWorld()`) +- Method names MUST be descriptive and unambiguous + +**Avoid Over-Engineering**: +- Start with the simplest solution that meets requirements +- Justify complexity in code reviews +- Prefer composition over inheritance +- Delete unnecessary abstractions (example: Mode enum removal in commit 901774f6) + +## Code Review Requirements + +**Review Gates**: +- All QA checks MUST pass before review begins +- Reviewers MUST verify test coverage for new features +- Reviewers MUST check that rule follows the Simple/Standard pattern +- Documentation updates MUST be included for user-facing changes + +**Approval Process**: +- At least one maintainer approval required, unless the author is a maintainer +- CI/CD pipeline MUST be green +- No force-pushes after approval unless requested by maintainer + +## Quality Gates + +**Definition of Done** for a new rule: +1. Rule class implements `Rule` interface (typically extends `Simple`) +2. Unit test extends `TestCase` with valid and invalid data providers +3. Feature test validates real-world usage and error messages +4. Template attribute defines positive and negative messages +5. Documentation page created in `docs/rules/` +6. All QA checks pass (`composer qa` succeeds) +7. Approved by maintainer + +**Performance Standards**: +- Rules MUST execute efficiently for typical validation workloads +- Avoid I/O operations within rule validation logic when possible +- Use dependency injection for external dependencies (e.g., email validators, phone validators) + +## Commit Message Standards + +**Format**: Descriptive imperative mood without conventional commit prefixes + +**Structure**: +``` + + + + + +``` + +**Examples** (from repository history): +``` +Improve naming and delete unnecessary `Mode` + +I don't expect us to have more modes, hence a simple boolean value +should be enough for indicating the mode of the template. Apart from +that, the name "inverted" wouldn't always make sense, because if you +invert something that is inverted, it gets back to its original mode. + +This commit will remove the `Mode` enum, and also improve the naming of +some methods in the `Result`. +``` + +``` +Use paths to identify when a rule fails + +When nested-structural validation fails, it's challenging to identify +which rule failed from the main exception message. A great example is +the `Issue796Test.php` file. The exception message says: + +host must be a string + +But you're left unsure whether it's the `host` key from the `mysql` key +or the `postgresql` key. + +This commit changes that behaviour by introducing the concept of "Path." +The `path` represents the path that a rule has taken, and we can use it +in structural rules to identify the path of an array or object. +``` + +**Guidelines**: +- First line: Concise, imperative mood (e.g., "Add", "Fix", "Improve", "Remove") +- Body: Explain the problem, solution, and rationale +- Include examples or before/after comparisons when helpful +- Reference issue numbers when applicable +- No scope prefixes (no `feat:`, `fix:`, etc.) +- Focus on clarity and context for future maintainers + +## Governance + +**Constitution Authority**: This constitution supersedes informal practices and provides the binding standards for the project. + +**Amendment Process**: +1. Proposed amendments MUST be documented in an issue or pull request +2. Amendments require approval from project maintainers +3. Version bumps follow semantic versioning: + - **MAJOR**: Backward-incompatible changes to governance or removed principles + - **MINOR**: New principles added or existing ones materially expanded + - **PATCH**: Clarifications, wording improvements, or non-semantic refinements + +**Compliance Review**: +- All pull requests MUST verify compliance with this constitution +- Complexity MUST be justified and documented +- Violations found in reviews block merge until resolved + +**Runtime Guidance**: +- Refer to CONTRIBUTING.md for day-to-day development guidance +- Refer to docs/ for user-facing documentation standards +- Refer to this constitution for non-negotiable principles and quality gates + +**Version**: 1.0.0 | **Ratified**: 2025-10-31 | **Last Amended**: 2025-10-31 diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh new file mode 100755 index 000000000..54f32ec36 --- /dev/null +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +# Consolidated prerequisite checking script +# +# This script provides unified prerequisite checking for Spec-Driven Development workflow. +# It replaces the functionality previously spread across multiple scripts. +# +# Usage: ./check-prerequisites.sh [OPTIONS] +# +# OPTIONS: +# --json Output in JSON format +# --require-tasks Require tasks.md to exist (for implementation phase) +# --include-tasks Include tasks.md in AVAILABLE_DOCS list +# --paths-only Only output path variables (no validation) +# --help, -h Show help message +# +# OUTPUTS: +# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} +# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md +# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. + +set -e + +# Parse command line arguments +JSON_MODE=false +REQUIRE_TASKS=false +INCLUDE_TASKS=false +PATHS_ONLY=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --require-tasks) + REQUIRE_TASKS=true + ;; + --include-tasks) + INCLUDE_TASKS=true + ;; + --paths-only) + PATHS_ONLY=true + ;; + --help|-h) + cat << 'EOF' +Usage: check-prerequisites.sh [OPTIONS] + +Consolidated prerequisite checking for Spec-Driven Development workflow. + +OPTIONS: + --json Output in JSON format + --require-tasks Require tasks.md to exist (for implementation phase) + --include-tasks Include tasks.md in AVAILABLE_DOCS list + --paths-only Only output path variables (no prerequisite validation) + --help, -h Show this help message + +EXAMPLES: + # Check task prerequisites (plan.md required) + ./check-prerequisites.sh --json + + # Check implementation prerequisites (plan.md + tasks.md required) + ./check-prerequisites.sh --json --require-tasks --include-tasks + + # Get feature paths only (no validation) + ./check-prerequisites.sh --paths-only + +EOF + exit 0 + ;; + *) + echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 + exit 1 + ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get feature paths and validate branch +eval $(get_feature_paths) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# If paths-only mode, output paths and exit (support JSON + paths-only combined) +if $PATHS_ONLY; then + if $JSON_MODE; then + # Minimal JSON paths payload (no validation performed) + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" + else + echo "REPO_ROOT: $REPO_ROOT" + echo "BRANCH: $CURRENT_BRANCH" + echo "FEATURE_DIR: $FEATURE_DIR" + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "TASKS: $TASKS" + fi + exit 0 +fi + +# Validate required directories and files +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 + exit 1 +fi + +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.plan first to create the implementation plan." >&2 + exit 1 +fi + +# Check for tasks.md if required +if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then + echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.tasks first to create the task list." >&2 + exit 1 +fi + +# Build list of available documents +docs=() + +# Always check these optional docs +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") + +# Check contracts directory (only if it exists and has files) +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi + +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Include tasks.md if requested and it exists +if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then + docs+=("tasks.md") +fi + +# Output results +if $JSON_MODE; then + # Build JSON array of documents + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '"%s",' "${docs[@]}") + json_docs="[${json_docs%,}]" + fi + + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" +else + # Text output + echo "FEATURE_DIR:$FEATURE_DIR" + echo "AVAILABLE_DOCS:" + + # Show status of each potential document + check_file "$RESEARCH" "research.md" + check_file "$DATA_MODEL" "data-model.md" + check_dir "$CONTRACTS_DIR" "contracts/" + check_file "$QUICKSTART" "quickstart.md" + + if $INCLUDE_TASKS; then + check_file "$TASKS" "tasks.md" + fi +fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh new file mode 100755 index 000000000..6931eccc8 --- /dev/null +++ b/.specify/scripts/bash/common.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Common functions and variables for all scripts + +# Get repository root, with fallback for non-git repositories +get_repo_root() { + if git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + else + # Fall back to script location for non-git repos + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) + fi +} + +# Get current branch, with fallback for non-git repositories +get_current_branch() { + # First check if SPECIFY_FEATURE environment variable is set + if [[ -n "${SPECIFY_FEATURE:-}" ]]; then + echo "$SPECIFY_FEATURE" + return + fi + + # Then check git if available + if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then + git rev-parse --abbrev-ref HEAD + return + fi + + # For non-git repos, try to find the latest feature directory + local repo_root=$(get_repo_root) + local specs_dir="$repo_root/specs" + + if [[ -d "$specs_dir" ]]; then + local latest_feature="" + local highest=0 + + for dir in "$specs_dir"/*; do + if [[ -d "$dir" ]]; then + local dirname=$(basename "$dir") + if [[ "$dirname" =~ ^([0-9]{3})- ]]; then + local number=${BASH_REMATCH[1]} + number=$((10#$number)) + if [[ "$number" -gt "$highest" ]]; then + highest=$number + latest_feature=$dirname + fi + fi + fi + done + + if [[ -n "$latest_feature" ]]; then + echo "$latest_feature" + return + fi + fi + + echo "main" # Final fallback +} + +# Check if we have git available +has_git() { + git rev-parse --show-toplevel >/dev/null 2>&1 +} + +check_feature_branch() { + local branch="$1" + local has_git_repo="$2" + + # For non-git repos, we can't enforce branch naming but still provide output + if [[ "$has_git_repo" != "true" ]]; then + echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 + return 0 + fi + + if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 + echo "Feature branches should be named like: 001-feature-name" >&2 + return 1 + fi + + return 0 +} + +get_feature_dir() { echo "$1/specs/$2"; } + +# Find feature directory by numeric prefix instead of exact branch match +# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +find_feature_dir_by_prefix() { + local repo_root="$1" + local branch_name="$2" + local specs_dir="$repo_root/specs" + + # Extract numeric prefix from branch (e.g., "004" from "004-whatever") + if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then + # If branch doesn't have numeric prefix, fall back to exact match + echo "$specs_dir/$branch_name" + return + fi + + local prefix="${BASH_REMATCH[1]}" + + # Search for directories in specs/ that start with this prefix + local matches=() + if [[ -d "$specs_dir" ]]; then + for dir in "$specs_dir"/"$prefix"-*; do + if [[ -d "$dir" ]]; then + matches+=("$(basename "$dir")") + fi + done + fi + + # Handle results + if [[ ${#matches[@]} -eq 0 ]]; then + # No match found - return the branch name path (will fail later with clear error) + echo "$specs_dir/$branch_name" + elif [[ ${#matches[@]} -eq 1 ]]; then + # Exactly one match - perfect! + echo "$specs_dir/${matches[0]}" + else + # Multiple matches - this shouldn't happen with proper naming convention + echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 + echo "Please ensure only one spec directory exists per numeric prefix." >&2 + echo "$specs_dir/$branch_name" # Return something to avoid breaking the script + fi +} + +get_feature_paths() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + local has_git_repo="false" + + if has_git; then + has_git_repo="true" + fi + + # Use prefix-based lookup to support multiple branches per spec + local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") + + cat </dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } + diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh new file mode 100755 index 000000000..86d9ecf83 --- /dev/null +++ b/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash + +set -e + +JSON_MODE=false +SHORT_NAME="" +BRANCH_NUMBER="" +ARGS=() +i=1 +while [ $i -le $# ]; do + arg="${!i}" + case "$arg" in + --json) + JSON_MODE=true + ;; + --short-name) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + # Check if the next argument is another option (starts with --) + if [[ "$next_arg" == --* ]]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + SHORT_NAME="$next_arg" + ;; + --number) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + if [[ "$next_arg" == --* ]]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + BRANCH_NUMBER="$next_arg" + ;; + --help|-h) + echo "Usage: $0 [--json] [--short-name ] [--number N] " + echo "" + echo "Options:" + echo " --json Output in JSON format" + echo " --short-name Provide a custom short name (2-4 words) for the branch" + echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 'Add user authentication system' --short-name 'user-auth'" + echo " $0 'Implement OAuth2 integration for API' --number 5" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac + i=$((i + 1)) +done + +FEATURE_DESCRIPTION="${ARGS[*]}" +if [ -z "$FEATURE_DESCRIPTION" ]; then + echo "Usage: $0 [--json] [--short-name ] [--number N] " >&2 + exit 1 +fi + +# Function to find the repository root by searching for existing project markers +find_repo_root() { + local dir="$1" + while [ "$dir" != "/" ]; do + if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + return 1 +} + +# Function to check existing branches (local and remote) and return next available number +check_existing_branches() { + local short_name="$1" + + # Fetch all remotes to get latest branch info (suppress errors if no remotes) + git fetch --all --prune 2>/dev/null || true + + # Find all branches matching the pattern using git ls-remote (more reliable) + local remote_branches=$(git ls-remote --heads origin 2>/dev/null | grep -E "refs/heads/[0-9]+-${short_name}$" | sed 's/.*\/\([0-9]*\)-.*/\1/' | sort -n) + + # Also check local branches + local local_branches=$(git branch 2>/dev/null | grep -E "^[* ]*[0-9]+-${short_name}$" | sed 's/^[* ]*//' | sed 's/-.*//' | sort -n) + + # Check specs directory as well + local spec_dirs="" + if [ -d "$SPECS_DIR" ]; then + spec_dirs=$(find "$SPECS_DIR" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n) + fi + + # Combine all sources and get the highest number + local max_num=0 + for num in $remote_branches $local_branches $spec_dirs; do + if [ "$num" -gt "$max_num" ]; then + max_num=$num + fi + done + + # Return next number + echo $((max_num + 1)) +} + +# Resolve repository root. Prefer git information when available, but fall back +# to searching for repository markers so the workflow still functions in repositories that +# were initialised with --no-git. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if git rev-parse --show-toplevel >/dev/null 2>&1; then + REPO_ROOT=$(git rev-parse --show-toplevel) + HAS_GIT=true +else + REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" + if [ -z "$REPO_ROOT" ]; then + echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 + exit 1 + fi + HAS_GIT=false +fi + +cd "$REPO_ROOT" + +SPECS_DIR="$REPO_ROOT/specs" +mkdir -p "$SPECS_DIR" + +# Function to generate branch name with stop word filtering and length filtering +generate_branch_name() { + local description="$1" + + # Common stop words to filter out + local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" + + # Convert to lowercase and split into words + local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') + + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) + local meaningful_words=() + for word in $clean_name; do + # Skip empty words + [ -z "$word" ] && continue + + # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) + if ! echo "$word" | grep -qiE "$stop_words"; then + if [ ${#word} -ge 3 ]; then + meaningful_words+=("$word") + elif echo "$description" | grep -q "\b${word^^}\b"; then + # Keep short words if they appear as uppercase in original (likely acronyms) + meaningful_words+=("$word") + fi + fi + done + + # If we have meaningful words, use first 3-4 of them + if [ ${#meaningful_words[@]} -gt 0 ]; then + local max_words=3 + if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi + + local result="" + local count=0 + for word in "${meaningful_words[@]}"; do + if [ $count -ge $max_words ]; then break; fi + if [ -n "$result" ]; then result="$result-"; fi + result="$result$word" + count=$((count + 1)) + done + echo "$result" + else + # Fallback to original logic if no meaningful words found + echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' + fi +} + +# Generate branch name +if [ -n "$SHORT_NAME" ]; then + # Use provided short name, just clean it up + BRANCH_SUFFIX=$(echo "$SHORT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') +else + # Generate from description with smart filtering + BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") +fi + +# Determine branch number +if [ -z "$BRANCH_NUMBER" ]; then + if [ "$HAS_GIT" = true ]; then + # Check existing branches on remotes + BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX") + else + # Fall back to local directory check + HIGHEST=0 + if [ -d "$SPECS_DIR" ]; then + for dir in "$SPECS_DIR"/*; do + [ -d "$dir" ] || continue + dirname=$(basename "$dir") + number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi + done + fi + BRANCH_NUMBER=$((HIGHEST + 1)) + fi +fi + +FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER") +BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + +# GitHub enforces a 244-byte limit on branch names +# Validate and truncate if necessary +MAX_BRANCH_LENGTH=244 +if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) + + # Truncate suffix at word boundary if possible + TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) + # Remove trailing hyphen if truncation created one + TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') + + ORIGINAL_BRANCH_NAME="$BRANCH_NAME" + BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + + >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" + >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" +fi + +if [ "$HAS_GIT" = true ]; then + git checkout -b "$BRANCH_NAME" +else + >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" +fi + +FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" +mkdir -p "$FEATURE_DIR" + +TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" +SPEC_FILE="$FEATURE_DIR/spec.md" +if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi + +# Set the SPECIFY_FEATURE environment variable for the current session +export SPECIFY_FEATURE="$BRANCH_NAME" + +if $JSON_MODE; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" +else + echo "BRANCH_NAME: $BRANCH_NAME" + echo "SPEC_FILE: $SPEC_FILE" + echo "FEATURE_NUM: $FEATURE_NUM" + echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" +fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh new file mode 100755 index 000000000..740a1438c --- /dev/null +++ b/.specify/scripts/bash/setup-plan.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false +ARGS=() + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac +done + +# Get script directory and load common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +eval $(get_feature_paths) + +# Check if we're on a proper feature branch (only for git repos) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# Ensure the feature directory exists +mkdir -p "$FEATURE_DIR" + +# Copy plan template if it exists +TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" +if [[ -f "$TEMPLATE" ]]; then + cp "$TEMPLATE" "$IMPL_PLAN" + echo "Copied plan template to $IMPL_PLAN" +else + echo "Warning: Plan template not found at $TEMPLATE" + # Create a basic plan file if template doesn't exist + touch "$IMPL_PLAN" +fi + +# Output results +if $JSON_MODE; then + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" +else + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "SPECS_DIR: $FEATURE_DIR" + echo "BRANCH: $CURRENT_BRANCH" + echo "HAS_GIT: $HAS_GIT" +fi + diff --git a/.specify/scripts/bash/update-agent-context.sh b/.specify/scripts/bash/update-agent-context.sh new file mode 100755 index 000000000..2a44c68a1 --- /dev/null +++ b/.specify/scripts/bash/update-agent-context.sh @@ -0,0 +1,772 @@ +#!/usr/bin/env bash + +# Update agent context files with information from plan.md +# +# This script maintains AI agent context files by parsing feature specifications +# and updating agent-specific configuration files with project information. +# +# MAIN FUNCTIONS: +# 1. Environment Validation +# - Verifies git repository structure and branch information +# - Checks for required plan.md files and templates +# - Validates file permissions and accessibility +# +# 2. Plan Data Extraction +# - Parses plan.md files to extract project metadata +# - Identifies language/version, frameworks, databases, and project types +# - Handles missing or incomplete specification data gracefully +# +# 3. Agent File Management +# - Creates new agent context files from templates when needed +# - Updates existing agent files with new project information +# - Preserves manual additions and custom configurations +# - Supports multiple AI agent formats and directory structures +# +# 4. Content Generation +# - Generates language-specific build/test commands +# - Creates appropriate project directory structures +# - Updates technology stacks and recent changes sections +# - Maintains consistent formatting and timestamps +# +# 5. Multi-Agent Support +# - Handles agent-specific file paths and naming conventions +# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Amp, or Amazon Q Developer CLI +# - Can update single agents or all existing agent files +# - Creates default Claude file if no agent files exist +# +# Usage: ./update-agent-context.sh [agent_type] +# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|q +# Leave empty to update all existing agent files + +set -e + +# Enable strict error handling +set -u +set -o pipefail + +#============================================================================== +# Configuration and Global Variables +#============================================================================== + +# Get script directory and load common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +eval $(get_feature_paths) + +NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code +AGENT_TYPE="${1:-}" + +# Agent-specific file paths +CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" +GEMINI_FILE="$REPO_ROOT/GEMINI.md" +COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" +CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" +QWEN_FILE="$REPO_ROOT/QWEN.md" +AGENTS_FILE="$REPO_ROOT/AGENTS.md" +WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" +KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" +AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" +ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" +CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" +AMP_FILE="$REPO_ROOT/AGENTS.md" +Q_FILE="$REPO_ROOT/AGENTS.md" + +# Template file +TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" + +# Global variables for parsed plan data +NEW_LANG="" +NEW_FRAMEWORK="" +NEW_DB="" +NEW_PROJECT_TYPE="" + +#============================================================================== +# Utility Functions +#============================================================================== + +log_info() { + echo "INFO: $1" +} + +log_success() { + echo "✓ $1" +} + +log_error() { + echo "ERROR: $1" >&2 +} + +log_warning() { + echo "WARNING: $1" >&2 +} + +# Cleanup function for temporary files +cleanup() { + local exit_code=$? + rm -f /tmp/agent_update_*_$$ + rm -f /tmp/manual_additions_$$ + exit $exit_code +} + +# Set up cleanup trap +trap cleanup EXIT INT TERM + +#============================================================================== +# Validation Functions +#============================================================================== + +validate_environment() { + # Check if we have a current branch/feature (git or non-git) + if [[ -z "$CURRENT_BRANCH" ]]; then + log_error "Unable to determine current feature" + if [[ "$HAS_GIT" == "true" ]]; then + log_info "Make sure you're on a feature branch" + else + log_info "Set SPECIFY_FEATURE environment variable or create a feature first" + fi + exit 1 + fi + + # Check if plan.md exists + if [[ ! -f "$NEW_PLAN" ]]; then + log_error "No plan.md found at $NEW_PLAN" + log_info "Make sure you're working on a feature with a corresponding spec directory" + if [[ "$HAS_GIT" != "true" ]]; then + log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" + fi + exit 1 + fi + + # Check if template exists (needed for new files) + if [[ ! -f "$TEMPLATE_FILE" ]]; then + log_warning "Template file not found at $TEMPLATE_FILE" + log_warning "Creating new agent files will fail" + fi +} + +#============================================================================== +# Plan Parsing Functions +#============================================================================== + +extract_plan_field() { + local field_pattern="$1" + local plan_file="$2" + + grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ + head -1 | \ + sed "s|^\*\*${field_pattern}\*\*: ||" | \ + sed 's/^[ \t]*//;s/[ \t]*$//' | \ + grep -v "NEEDS CLARIFICATION" | \ + grep -v "^N/A$" || echo "" +} + +parse_plan_data() { + local plan_file="$1" + + if [[ ! -f "$plan_file" ]]; then + log_error "Plan file not found: $plan_file" + return 1 + fi + + if [[ ! -r "$plan_file" ]]; then + log_error "Plan file is not readable: $plan_file" + return 1 + fi + + log_info "Parsing plan data from $plan_file" + + NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") + NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") + NEW_DB=$(extract_plan_field "Storage" "$plan_file") + NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") + + # Log what we found + if [[ -n "$NEW_LANG" ]]; then + log_info "Found language: $NEW_LANG" + else + log_warning "No language information found in plan" + fi + + if [[ -n "$NEW_FRAMEWORK" ]]; then + log_info "Found framework: $NEW_FRAMEWORK" + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then + log_info "Found database: $NEW_DB" + fi + + if [[ -n "$NEW_PROJECT_TYPE" ]]; then + log_info "Found project type: $NEW_PROJECT_TYPE" + fi +} + +format_technology_stack() { + local lang="$1" + local framework="$2" + local parts=() + + # Add non-empty parts + [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") + [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") + + # Join with proper formatting + if [[ ${#parts[@]} -eq 0 ]]; then + echo "" + elif [[ ${#parts[@]} -eq 1 ]]; then + echo "${parts[0]}" + else + # Join multiple parts with " + " + local result="${parts[0]}" + for ((i=1; i<${#parts[@]}; i++)); do + result="$result + ${parts[i]}" + done + echo "$result" + fi +} + +#============================================================================== +# Template and Content Generation Functions +#============================================================================== + +get_project_structure() { + local project_type="$1" + + if [[ "$project_type" == *"web"* ]]; then + echo "backend/\\nfrontend/\\ntests/" + else + echo "src/\\ntests/" + fi +} + +get_commands_for_language() { + local lang="$1" + + case "$lang" in + *"Python"*) + echo "cd src && pytest && ruff check ." + ;; + *"Rust"*) + echo "cargo test && cargo clippy" + ;; + *"JavaScript"*|*"TypeScript"*) + echo "npm test \\&\\& npm run lint" + ;; + *) + echo "# Add commands for $lang" + ;; + esac +} + +get_language_conventions() { + local lang="$1" + echo "$lang: Follow standard conventions" +} + +create_new_agent_file() { + local target_file="$1" + local temp_file="$2" + local project_name="$3" + local current_date="$4" + + if [[ ! -f "$TEMPLATE_FILE" ]]; then + log_error "Template not found at $TEMPLATE_FILE" + return 1 + fi + + if [[ ! -r "$TEMPLATE_FILE" ]]; then + log_error "Template file is not readable: $TEMPLATE_FILE" + return 1 + fi + + log_info "Creating new agent context file from template..." + + if ! cp "$TEMPLATE_FILE" "$temp_file"; then + log_error "Failed to copy template file" + return 1 + fi + + # Replace template placeholders + local project_structure + project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") + + local commands + commands=$(get_commands_for_language "$NEW_LANG") + + local language_conventions + language_conventions=$(get_language_conventions "$NEW_LANG") + + # Perform substitutions with error checking using safer approach + # Escape special characters for sed by using a different delimiter or escaping + local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') + + # Build technology stack and recent change strings conditionally + local tech_stack + if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then + tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" + elif [[ -n "$escaped_lang" ]]; then + tech_stack="- $escaped_lang ($escaped_branch)" + elif [[ -n "$escaped_framework" ]]; then + tech_stack="- $escaped_framework ($escaped_branch)" + else + tech_stack="- ($escaped_branch)" + fi + + local recent_change + if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then + recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" + elif [[ -n "$escaped_lang" ]]; then + recent_change="- $escaped_branch: Added $escaped_lang" + elif [[ -n "$escaped_framework" ]]; then + recent_change="- $escaped_branch: Added $escaped_framework" + else + recent_change="- $escaped_branch: Added" + fi + + local substitutions=( + "s|\[PROJECT NAME\]|$project_name|" + "s|\[DATE\]|$current_date|" + "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" + "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" + "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" + "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" + "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" + ) + + for substitution in "${substitutions[@]}"; do + if ! sed -i.bak -e "$substitution" "$temp_file"; then + log_error "Failed to perform substitution: $substitution" + rm -f "$temp_file" "$temp_file.bak" + return 1 + fi + done + + # Convert \n sequences to actual newlines + newline=$(printf '\n') + sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" + + # Clean up backup files + rm -f "$temp_file.bak" "$temp_file.bak2" + + return 0 +} + + + + +update_existing_agent_file() { + local target_file="$1" + local current_date="$2" + + log_info "Updating existing agent context file..." + + # Use a single temporary file for atomic update + local temp_file + temp_file=$(mktemp) || { + log_error "Failed to create temporary file" + return 1 + } + + # Process the file in one pass + local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") + local new_tech_entries=() + local new_change_entry="" + + # Prepare new technology entries + if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then + new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then + new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") + fi + + # Prepare new change entry + if [[ -n "$tech_stack" ]]; then + new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" + elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then + new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" + fi + + # Check if sections exist in the file + local has_active_technologies=0 + local has_recent_changes=0 + + if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then + has_active_technologies=1 + fi + + if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then + has_recent_changes=1 + fi + + # Process file line by line + local in_tech_section=false + local in_changes_section=false + local tech_entries_added=false + local changes_entries_added=false + local existing_changes_count=0 + local file_ended=false + + while IFS= read -r line || [[ -n "$line" ]]; do + # Handle Active Technologies section + if [[ "$line" == "## Active Technologies" ]]; then + echo "$line" >> "$temp_file" + in_tech_section=true + continue + elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then + # Add new tech entries before closing the section + if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + echo "$line" >> "$temp_file" + in_tech_section=false + continue + elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then + # Add new tech entries before empty line in tech section + if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + echo "$line" >> "$temp_file" + continue + fi + + # Handle Recent Changes section + if [[ "$line" == "## Recent Changes" ]]; then + echo "$line" >> "$temp_file" + # Add new change entry right after the heading + if [[ -n "$new_change_entry" ]]; then + echo "$new_change_entry" >> "$temp_file" + fi + in_changes_section=true + changes_entries_added=true + continue + elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then + echo "$line" >> "$temp_file" + in_changes_section=false + continue + elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then + # Keep only first 2 existing changes + if [[ $existing_changes_count -lt 2 ]]; then + echo "$line" >> "$temp_file" + ((existing_changes_count++)) + fi + continue + fi + + # Update timestamp + if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then + echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" + else + echo "$line" >> "$temp_file" + fi + done < "$target_file" + + # Post-loop check: if we're still in the Active Technologies section and haven't added new entries + if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + + # If sections don't exist, add them at the end of the file + if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + echo "" >> "$temp_file" + echo "## Active Technologies" >> "$temp_file" + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + + if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then + echo "" >> "$temp_file" + echo "## Recent Changes" >> "$temp_file" + echo "$new_change_entry" >> "$temp_file" + changes_entries_added=true + fi + + # Move temp file to target atomically + if ! mv "$temp_file" "$target_file"; then + log_error "Failed to update target file" + rm -f "$temp_file" + return 1 + fi + + return 0 +} +#============================================================================== +# Main Agent File Update Function +#============================================================================== + +update_agent_file() { + local target_file="$1" + local agent_name="$2" + + if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then + log_error "update_agent_file requires target_file and agent_name parameters" + return 1 + fi + + log_info "Updating $agent_name context file: $target_file" + + local project_name + project_name=$(basename "$REPO_ROOT") + local current_date + current_date=$(date +%Y-%m-%d) + + # Create directory if it doesn't exist + local target_dir + target_dir=$(dirname "$target_file") + if [[ ! -d "$target_dir" ]]; then + if ! mkdir -p "$target_dir"; then + log_error "Failed to create directory: $target_dir" + return 1 + fi + fi + + if [[ ! -f "$target_file" ]]; then + # Create new file from template + local temp_file + temp_file=$(mktemp) || { + log_error "Failed to create temporary file" + return 1 + } + + if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then + if mv "$temp_file" "$target_file"; then + log_success "Created new $agent_name context file" + else + log_error "Failed to move temporary file to $target_file" + rm -f "$temp_file" + return 1 + fi + else + log_error "Failed to create new agent file" + rm -f "$temp_file" + return 1 + fi + else + # Update existing file + if [[ ! -r "$target_file" ]]; then + log_error "Cannot read existing file: $target_file" + return 1 + fi + + if [[ ! -w "$target_file" ]]; then + log_error "Cannot write to existing file: $target_file" + return 1 + fi + + if update_existing_agent_file "$target_file" "$current_date"; then + log_success "Updated existing $agent_name context file" + else + log_error "Failed to update existing agent file" + return 1 + fi + fi + + return 0 +} + +#============================================================================== +# Agent Selection and Processing +#============================================================================== + +update_specific_agent() { + local agent_type="$1" + + case "$agent_type" in + claude) + update_agent_file "$CLAUDE_FILE" "Claude Code" + ;; + gemini) + update_agent_file "$GEMINI_FILE" "Gemini CLI" + ;; + copilot) + update_agent_file "$COPILOT_FILE" "GitHub Copilot" + ;; + cursor-agent) + update_agent_file "$CURSOR_FILE" "Cursor IDE" + ;; + qwen) + update_agent_file "$QWEN_FILE" "Qwen Code" + ;; + opencode) + update_agent_file "$AGENTS_FILE" "opencode" + ;; + codex) + update_agent_file "$AGENTS_FILE" "Codex CLI" + ;; + windsurf) + update_agent_file "$WINDSURF_FILE" "Windsurf" + ;; + kilocode) + update_agent_file "$KILOCODE_FILE" "Kilo Code" + ;; + auggie) + update_agent_file "$AUGGIE_FILE" "Auggie CLI" + ;; + roo) + update_agent_file "$ROO_FILE" "Roo Code" + ;; + codebuddy) + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + ;; + amp) + update_agent_file "$AMP_FILE" "Amp" + ;; + q) + update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + ;; + *) + log_error "Unknown agent type '$agent_type'" + log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|q" + exit 1 + ;; + esac +} + +update_all_existing_agents() { + local found_agent=false + + # Check each possible agent file and update if it exists + if [[ -f "$CLAUDE_FILE" ]]; then + update_agent_file "$CLAUDE_FILE" "Claude Code" + found_agent=true + fi + + if [[ -f "$GEMINI_FILE" ]]; then + update_agent_file "$GEMINI_FILE" "Gemini CLI" + found_agent=true + fi + + if [[ -f "$COPILOT_FILE" ]]; then + update_agent_file "$COPILOT_FILE" "GitHub Copilot" + found_agent=true + fi + + if [[ -f "$CURSOR_FILE" ]]; then + update_agent_file "$CURSOR_FILE" "Cursor IDE" + found_agent=true + fi + + if [[ -f "$QWEN_FILE" ]]; then + update_agent_file "$QWEN_FILE" "Qwen Code" + found_agent=true + fi + + if [[ -f "$AGENTS_FILE" ]]; then + update_agent_file "$AGENTS_FILE" "Codex/opencode" + found_agent=true + fi + + if [[ -f "$WINDSURF_FILE" ]]; then + update_agent_file "$WINDSURF_FILE" "Windsurf" + found_agent=true + fi + + if [[ -f "$KILOCODE_FILE" ]]; then + update_agent_file "$KILOCODE_FILE" "Kilo Code" + found_agent=true + fi + + if [[ -f "$AUGGIE_FILE" ]]; then + update_agent_file "$AUGGIE_FILE" "Auggie CLI" + found_agent=true + fi + + if [[ -f "$ROO_FILE" ]]; then + update_agent_file "$ROO_FILE" "Roo Code" + found_agent=true + fi + + if [[ -f "$CODEBUDDY_FILE" ]]; then + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + found_agent=true + fi + + if [[ -f "$Q_FILE" ]]; then + update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + found_agent=true + fi + + # If no agent files exist, create a default Claude file + if [[ "$found_agent" == false ]]; then + log_info "No existing agent files found, creating default Claude file..." + update_agent_file "$CLAUDE_FILE" "Claude Code" + fi +} +print_summary() { + echo + log_info "Summary of changes:" + + if [[ -n "$NEW_LANG" ]]; then + echo " - Added language: $NEW_LANG" + fi + + if [[ -n "$NEW_FRAMEWORK" ]]; then + echo " - Added framework: $NEW_FRAMEWORK" + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then + echo " - Added database: $NEW_DB" + fi + + echo + + log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|q]" +} + +#============================================================================== +# Main Execution +#============================================================================== + +main() { + # Validate environment before proceeding + validate_environment + + log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" + + # Parse the plan file to extract project information + if ! parse_plan_data "$NEW_PLAN"; then + log_error "Failed to parse plan data" + exit 1 + fi + + # Process based on agent type argument + local success=true + + if [[ -z "$AGENT_TYPE" ]]; then + # No specific agent provided - update all existing agent files + log_info "No agent specified, updating all existing agent files..." + if ! update_all_existing_agents; then + success=false + fi + else + # Specific agent provided - update only that agent + log_info "Updating specific agent: $AGENT_TYPE" + if ! update_specific_agent "$AGENT_TYPE"; then + success=false + fi + fi + + # Print summary + print_summary + + if [[ "$success" == true ]]; then + log_success "Agent context update completed successfully" + exit 0 + else + log_error "Agent context update completed with errors" + exit 1 + fi +} + +# Execute main function if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi + diff --git a/.specify/templates/agent-file-template.md b/.specify/templates/agent-file-template.md new file mode 100644 index 000000000..4cc7fd667 --- /dev/null +++ b/.specify/templates/agent-file-template.md @@ -0,0 +1,28 @@ +# [PROJECT NAME] Development Guidelines + +Auto-generated from all feature plans. Last updated: [DATE] + +## Active Technologies + +[EXTRACTED FROM ALL PLAN.MD FILES] + +## Project Structure + +```text +[ACTUAL STRUCTURE FROM PLANS] +``` + +## Commands + +[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] + +## Code Style + +[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] + +## Recent Changes + +[LAST 3 FEATURES AND WHAT THEY ADDED] + + + diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md new file mode 100644 index 000000000..806657da0 --- /dev/null +++ b/.specify/templates/checklist-template.md @@ -0,0 +1,40 @@ +# [CHECKLIST TYPE] Checklist: [FEATURE NAME] + +**Purpose**: [Brief description of what this checklist covers] +**Created**: [DATE] +**Feature**: [Link to spec.md or relevant documentation] + +**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. + + + +## [Category 1] + +- [ ] CHK001 First checklist item with clear action +- [ ] CHK002 Second checklist item +- [ ] CHK003 Third checklist item + +## [Category 2] + +- [ ] CHK004 Another category item +- [ ] CHK005 Item with specific criteria +- [ ] CHK006 Final item in this category + +## Notes + +- Check items off as completed: `[x]` +- Add comments or findings inline +- Link to relevant resources or documentation +- Items are numbered sequentially for easy reference diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md new file mode 100644 index 000000000..6a8bfc6c8 --- /dev/null +++ b/.specify/templates/plan-template.md @@ -0,0 +1,104 @@ +# Implementation Plan: [FEATURE] + +**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +[Extract from feature spec: primary requirement + technical approach from research] + +## Technical Context + + + +**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] +**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] +**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] +**Project Type**: [single/web/mobile - determines source structure] +**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] +**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] +**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +[Gates determined based on constitution file] + +## Project Structure + +### Documentation (this feature) + +```text +specs/[###-feature]/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + + +```text +# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) +src/ +├── models/ +├── services/ +├── cli/ +└── lib/ + +tests/ +├── contract/ +├── integration/ +└── unit/ + +# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) +backend/ +├── src/ +│ ├── models/ +│ ├── services/ +│ └── api/ +└── tests/ + +frontend/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ └── services/ +└── tests/ + +# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) +api/ +└── [same as backend above] + +ios/ or android/ +└── [platform-specific structure: feature modules, UI flows, platform tests] +``` + +**Structure Decision**: [Document the selected structure and reference the real +directories captured above] + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | +| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md new file mode 100644 index 000000000..c67d91498 --- /dev/null +++ b/.specify/templates/spec-template.md @@ -0,0 +1,115 @@ +# Feature Specification: [FEATURE NAME] + +**Feature Branch**: `[###-feature-name]` +**Created**: [DATE] +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - [Brief Title] (Priority: P1) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] +2. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### User Story 2 - [Brief Title] (Priority: P2) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### User Story 3 - [Brief Title] (Priority: P3) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +[Add more user stories as needed, each with an assigned priority] + +### Edge Cases + + + +- What happens when [boundary condition]? +- How does system handle [error scenario]? + +## Requirements *(mandatory)* + + + +### Functional Requirements + +- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] +- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] +- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] +- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] +- **FR-005**: System MUST [behavior, e.g., "log all security events"] + +*Example of marking unclear requirements:* + +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] +- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation] +- **[Entity 2]**: [What it represents, relationships to other entities] + +## Success Criteria *(mandatory)* + + + +### Measurable Outcomes + +- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"] +- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] +- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] +- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md new file mode 100644 index 000000000..60f9be455 --- /dev/null +++ b/.specify/templates/tasks-template.md @@ -0,0 +1,251 @@ +--- + +description: "Task list template for feature implementation" +--- + +# Tasks: [FEATURE NAME] + +**Input**: Design documents from `/specs/[###-feature-name]/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Single project**: `src/`, `tests/` at repository root +- **Web app**: `backend/src/`, `frontend/src/` +- **Mobile**: `api/src/`, `ios/src/` or `android/src/` +- Paths shown below assume single project - adjust based on plan.md structure + + + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project initialization and basic structure + +- [ ] T001 Create project structure per implementation plan +- [ ] T002 Initialize [language] project with [framework] dependencies +- [ ] T003 [P] Configure linting and formatting tools + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +Examples of foundational tasks (adjust based on your project): + +- [ ] T004 Setup database schema and migrations framework +- [ ] T005 [P] Implement authentication/authorization framework +- [ ] T006 [P] Setup API routing and middleware structure +- [ ] T007 Create base models/entities that all stories depend on +- [ ] T008 Configure error handling and logging infrastructure +- [ ] T009 Setup environment configuration management + +**Checkpoint**: Foundation ready - user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ + +> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** + +- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 1 + +- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py +- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py +- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013) +- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py +- [ ] T016 [US1] Add validation and error handling +- [ ] T017 [US1] Add logging for user story 1 operations + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 4: User Story 2 - [Title] (Priority: P2) + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ + +- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 2 + +- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py +- [ ] T021 [US2] Implement [Service] in src/services/[service].py +- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py +- [ ] T023 [US2] Integrate with User Story 1 components (if needed) + +**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently + +--- + +## Phase 5: User Story 3 - [Title] (Priority: P3) + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ + +- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 3 + +- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py +- [ ] T027 [US3] Implement [Service] in src/services/[service].py +- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py + +**Checkpoint**: All user stories should now be independently functional + +--- + +[Add more user story phases as needed, following the same pattern] + +--- + +## Phase N: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +- [ ] TXXX [P] Documentation updates in docs/ +- [ ] TXXX Code cleanup and refactoring +- [ ] TXXX Performance optimization across all stories +- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/ +- [ ] TXXX Security hardening +- [ ] TXXX Run quickstart.md validation + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies - can start immediately +- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories +- **User Stories (Phase 3+)**: All depend on Foundational phase completion + - User stories can then proceed in parallel (if staffed) + - Or sequentially in priority order (P1 → P2 → P3) +- **Polish (Final Phase)**: Depends on all desired user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories +- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable +- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable + +### Within Each User Story + +- Tests (if included) MUST be written and FAIL before implementation +- Models before services +- Services before endpoints +- Core implementation before integration +- Story complete before moving to next priority + +### Parallel Opportunities + +- All Setup tasks marked [P] can run in parallel +- All Foundational tasks marked [P] can run in parallel (within Phase 2) +- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) +- All tests for a user story marked [P] can run in parallel +- Models within a story marked [P] can run in parallel +- Different user stories can be worked on in parallel by different team members + +--- + +## Parallel Example: User Story 1 + +```bash +# Launch all tests for User Story 1 together (if tests requested): +Task: "Contract test for [endpoint] in tests/contract/test_[name].py" +Task: "Integration test for [user journey] in tests/integration/test_[name].py" + +# Launch all models for User Story 1 together: +Task: "Create [Entity1] model in src/models/[entity1].py" +Task: "Create [Entity2] model in src/models/[entity2].py" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup +2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) +3. Complete Phase 3: User Story 1 +4. **STOP and VALIDATE**: Test User Story 1 independently +5. Deploy/demo if ready + +### Incremental Delivery + +1. Complete Setup + Foundational → Foundation ready +2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) +3. Add User Story 2 → Test independently → Deploy/Demo +4. Add User Story 3 → Test independently → Deploy/Demo +5. Each story adds value without breaking previous stories + +### Parallel Team Strategy + +With multiple developers: + +1. Team completes Setup + Foundational together +2. Once Foundational is done: + - Developer A: User Story 1 + - Developer B: User Story 2 + - Developer C: User Story 3 +3. Stories complete and integrate independently + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- Verify tests fail before implementing +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently +- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..978edb3d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS + +This file will contain agent definitions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2401151f6..c05acf1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changes in Respect\Validation 2.x +## 3.0 + +Major release with breaking changes. For upgrade instructions, see the [Migration Guide](docs/11-migration-from-2x.md). + +### Breaking Changes: + +- PHP 8.1+ is now required (dropped support for PHP 8.0 and below) +- Removed rules: `Age`, `MinAge`, `MaxAge`, `KeyValue`, `Consecutive` +- Renamed rules: + - `Min` → `GreaterThanOrEqual` + - `Max` → `LessThanOrEqual` + - `Nullable` → `NullOr` + - `Optional` → `UndefOr` + - `Attribute` → `Property` + - `NotOptional` → `NotUndef` +- Split rules: + - `Key` split into `Key`, `KeyExists`, `KeyOptional` + - `Property` split into `Property`, `PropertyExists`, `PropertyOptional` +- Removed deprecated methods: `setName()`, `setTemplate()` + +### New Features: + +- Prefix rules for common validation patterns (`keyEmail`, `propertyPositive`, etc.) +- PHP 8+ attributes support with `#[Email]`, `#[Between]`, etc. +- Enhanced result reporting with path-based error identification +- New rule variants: `KeyExists`, `KeyOptional`, `PropertyExists`, `PropertyOptional` +- New `Named` and `Templated` rules to replace `setName()` and `setTemplate()` +- `{placeholder|quote}` filter in message templates + +### Removed: + +- Symfony façade validators removed (deprecated in v2.3) + + ## 2.3 Versioning Changes: diff --git a/README.md b/README.md index 478b8894e..ac8ce869a 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,65 @@ The most awesome validation engine ever created for PHP. +## Quick Example + +```php +use Respect\Validation\Validator as v; + +// Simple validation +v::email()->assert('user@example.com'); + +// Chained rules +v::intVal()->positive()->between(1, 100)->assert(50); + +// Complex structures with prefix rules +v::keySet( + v::keyEmail('email'), + v::key('age', v::intVal()->between(18, 120)) +)->assert($userData); + +// PHP 8 attributes support +#[Attribute] +class User { + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; +} + +v::attributes()->assert($user); +``` + - Complex rules made simple: `v::numericVal()->positive()->between(1, 255)->isValid($input)`. - [Granularity control](docs/03-handling-exceptions.md) for advanced reporting. - [More than 150](docs/09-list-of-rules-by-category.md) (fully tested) validation rules. - [A concrete API](docs/06-concrete-api.md) for non fluent usage. +- [Prefix rules](docs/12-prefix-rules.md) for concise validation patterns. +- [PHP 8 attributes](docs/02-feature-guide.md#using-rules-as-attributes) support. + +## Version 3.0 + +Version 3.0 introduces significant improvements to validation architecture, naming consistency, and modern PHP support: + +- **Breaking Changes**: + - Validation methods (`assert`, `check`) now only available on `Validator` class + - Multiple rule renames for semantic clarity (e.g., `nullable` → `nullOr`) + - Removed age-specific rules in favor of general `DateTimeDiff` + - Minimum PHP version: 8.1 + +- **New Features**: + - Prefix rules for concise validation patterns (e.g., `v::keyEmail('field')`) + - PHP 8 attributes support for declarative validation + - Enhanced error paths for nested structures + - Flexible `assert()` overloads (templates, exceptions, callables) + +**See Also**: [Migration Guide](docs/11-migration-from-2x.md) + +**Support**: Version 2.x receives critical security fixes until 2026-05-03. Learn More: * [Documentation](https://respect-validation.readthedocs.io) * [How to contribute](CONTRIBUTING.md) +* [Migration from 2.x](docs/11-migration-from-2x.md) diff --git a/bin/check-doc-links b/bin/check-doc-links new file mode 100755 index 000000000..7f62f7227 --- /dev/null +++ b/bin/check-doc-links @@ -0,0 +1,19 @@ +#!/bin/bash + +# Script to check links in documentation files + +echo "Checking links in documentation files..." + +# Check if markdown-link-check is installed +if ! command -v npx &> /dev/null; then + echo "Error: npm is not installed" + exit 1 +fi + +# Check links in all markdown files in docs directory +find docs -name "*.md" -type f | while read file; do + echo "Checking links in $file..." + npx markdown-link-check -c .markdown-link-check.json "$file" +done + +echo "Link checking complete!" \ No newline at end of file diff --git a/bin/validate-doc-examples b/bin/validate-doc-examples new file mode 100755 index 000000000..a53d39e6e --- /dev/null +++ b/bin/validate-doc-examples @@ -0,0 +1,240 @@ +#!/usr/bin/env php +\n"; + echo "Example: bin/validate-doc-examples docs/rules/Email.md\n"; + echo "Example: bin/validate-doc-examples docs/\n"; + exit(1); +} + +$path = $argv[1]; + +// Check if the path exists +if (!file_exists($path)) { + echo "Error: Path '$path' does not exist\n"; + exit(1); +} + +// Autoload the Respect\Validation library +$autoloadPath = __DIR__ . '/../vendor/autoload.php'; +if (!file_exists($autoloadPath)) { + echo "Error: Composer dependencies not found. Run 'composer install' first.\n"; + exit(1); +} + +require $autoloadPath; + +/** + * Extract PHP code blocks from markdown content + * + * @param string $content + * @return array + */ +function extractPhpCodeBlocks($content) +{ + $blocks = []; + $pattern = '/```php\s*\n(.*?)```/s'; + preg_match_all($pattern, $content, $matches); + + foreach ($matches[1] as $code) { + // Skip code that contains &1", $output, $returnCode); + + // Clean up + unlink($tempFile); + + $outputStr = implode("\n", $output); + + if ($returnCode === 0) { + return ['success' => true, 'message' => 'Code executed successfully']; + } else { + return ['success' => false, 'message' => "Execution failed with code $returnCode: $outputStr"]; + } +} + +/** + * Process a single markdown file + * + * @param string $file + * @return array + */ +function processFile($file) +{ + echo "Processing $file...\n"; + + // Skip files for removed rules + if (strpos(file_get_contents($file), 'Removed in v3.0') !== false) { + echo " Skipping file for removed rule\n"; + return ['total' => 0, 'passed' => 0, 'failed' => 0, 'errors' => []]; + } + + $content = file_get_contents($file); + $codeBlocks = extractPhpCodeBlocks($content); + + if (empty($codeBlocks)) { + echo " No PHP code blocks found\n"; + return ['total' => 0, 'passed' => 0, 'failed' => 0, 'errors' => []]; + } + + echo " Found " . count($codeBlocks) . " PHP code blocks\n"; + + $results = [ + 'total' => count($codeBlocks), + 'passed' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + foreach ($codeBlocks as $index => $code) { + echo " Validating code block " . ($index + 1) . "...\n"; + + $result = validateCodeExample($code, $file); + + if ($result['success']) { + $results['passed']++; + echo " ✓ Passed\n"; + } else { + $results['failed']++; + $error = " ✗ Failed: " . $result['message']; + $results['errors'][] = $error; + echo $error . "\n"; + } + } + + return $results; +} + +/** + * Process a directory recursively + * + * @param string $directory + * @return array + */ +function processDirectory($directory) +{ + $results = [ + 'total' => 0, + 'passed' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory) + ); + + $files = []; + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'md') { + $files[] = $file->getPathname(); + } + } + + // Sort files for consistent output + sort($files); + + foreach ($files as $file) { + $fileResults = processFile($file); + + $results['total'] += $fileResults['total']; + $results['passed'] += $fileResults['passed']; + $results['failed'] += $fileResults['failed']; + $results['errors'] = array_merge($results['errors'], $fileResults['errors']); + } + + return $results; +} + +// Main execution +try { + if (is_file($path)) { + $results = processFile($path); + } elseif (is_dir($path)) { + $results = processDirectory($path); + } else { + echo "Error: '$path' is neither a file nor a directory\n"; + exit(1); + } + + // Print summary + echo "\n" . str_repeat('=', 50) . "\n"; + echo "VALIDATION SUMMARY\n"; + echo str_repeat('=', 50) . "\n"; + echo "Total code blocks: " . $results['total'] . "\n"; + echo "Passed: " . $results['passed'] . "\n"; + echo "Failed: " . $results['failed'] . "\n"; + + if (!empty($results['errors'])) { + echo "\nERRORS:\n"; + echo str_repeat('-', 20) . "\n"; + foreach ($results['errors'] as $error) { + echo $error . "\n"; + } + } + + // Exit with appropriate code + exit($results['failed'] > 0 ? 1 : 0); + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} \ No newline at end of file diff --git a/bin/validate-rule-docs b/bin/validate-rule-docs new file mode 100755 index 000000000..32945aa63 --- /dev/null +++ b/bin/validate-rule-docs @@ -0,0 +1,280 @@ +#!/usr/bin/env php +\n"; + echo "Example: bin/validate-rule-docs docs/rules/Email.md\n"; + echo "Example: bin/validate-rule-docs docs/rules/\n"; + exit(1); +} + +$path = $argv[1]; + +// Check if the path exists +if (!file_exists($path)) { + echo "Error: Path '$path' does not exist\n"; + exit(1); +} + +/** + * Get valid categories from the schema + * + * @return array + */ +function getValidCategories() +{ + return [ + 'String Validation', + 'Numeric', + 'Dates and Times', + 'Array and Iterable', + 'Object', + 'Type Checking', + 'Comparison', + 'File System', + 'Internet and Networking', + 'Regional (IDs, Postal Codes, etc.)', + 'Banking', + 'Miscellaneous' + ]; +} + +/** + * Validate a rule documentation file against the schema + * + * @param string $file + * @return array + */ +function validateRuleDoc($file) +{ + echo "Validating $file...\n"; + + $content = file_get_contents($file); + + $results = [ + 'file' => $file, + 'passed' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // Check 1: File has a title (H1) + if (!preg_match('/^#\s+\w+/', $content)) { + $results['failed']++; + $results['errors'][] = "Missing or invalid title (H1 heading)"; + } else { + $results['passed']++; + } + + // Check 2: Has brief description + $lines = explode("\n", $content); + $descriptionFound = false; + foreach ($lines as $line) { + if (preg_match('/^#\s+\w+/', $line)) { + continue; // Skip title + } + if (trim($line) !== '' && !preg_match('/^```/', $line)) { + $descriptionFound = true; + break; + } + } + + if (!$descriptionFound) { + $results['failed']++; + $results['errors'][] = "Missing brief description after title"; + } else { + $results['passed']++; + } + + // Check 3: Has required sections + $requiredSections = ['## Usage', '## Parameters', '## Examples', '## Message Template', '## Categorization', '## Changelog']; + foreach ($requiredSections as $section) { + if (strpos($content, $section) === false) { + $results['failed']++; + $results['errors'][] = "Missing required section: $section"; + } else { + $results['passed']++; + } + } + + // Check 4: Has PHP code examples without 0, + 'passed' => 0, + 'failed' => 0, + 'errors' => [], + 'files' => [] + ]; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory) + ); + + $files = []; + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'md') { + $files[] = $file->getPathname(); + } + } + + // Sort files for consistent output + sort($files); + + foreach ($files as $file) { + $fileResults = processFile($file); + + $results['total']++; + $results['passed'] += $fileResults['passed']; + $results['failed'] += $fileResults['failed']; + $results['errors'] = array_merge($results['errors'], $fileResults['errors']); + $results['files'][] = $fileResults; + } + + return $results; +} + +// Main execution +try { + if (is_file($path)) { + $results = processFile($path); + $summary = [ + 'total' => 1, + 'passed' => $results['passed'], + 'failed' => $results['failed'], + 'errors' => $results['errors'] + ]; + } elseif (is_dir($path)) { + $results = processDirectory($path); + $summary = [ + 'total' => $results['total'], + 'passed' => $results['passed'], + 'failed' => $results['failed'], + 'errors' => $results['errors'] + ]; + } else { + echo "Error: '$path' is neither a file nor a directory\n"; + exit(1); + } + + // Print summary + echo "\n" . str_repeat('=', 50) . "\n"; + echo "RULE DOCUMENTATION VALIDATION SUMMARY\n"; + echo str_repeat('=', 50) . "\n"; + echo "Total validations: " . $summary['total'] . "\n"; + echo "Passed: " . $summary['passed'] . "\n"; + echo "Failed: " . $summary['failed'] . "\n"; + + if (!empty($summary['errors'])) { + echo "\nERRORS:\n"; + echo str_repeat('-', 20) . "\n"; + foreach (array_unique($summary['errors']) as $error) { + echo "✗ " . $error . "\n"; + } + } + + // Exit with appropriate code + exit($summary['failed'] > 0 ? 1 : 0); + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} \ No newline at end of file diff --git a/docs/01-installation.md b/docs/01-installation.md index c0d1ada0e..694bbc47a 100644 --- a/docs/01-installation.md +++ b/docs/01-installation.md @@ -4,7 +4,7 @@ Package is available on [Packagist](http://packagist.org/packages/respect/valida you can install it using [Composer](http://getcomposer.org). ```shell -composer require respect/validation +composer require respect/validation:^3.0 ``` Works on PHP 8.1 or above. diff --git a/docs/02-feature-guide.md b/docs/02-feature-guide.md index 3ae5c7b7b..be4137548 100644 --- a/docs/02-feature-guide.md +++ b/docs/02-feature-guide.md @@ -19,6 +19,7 @@ if (v::intType()->positive()->isValid($input)) { ``` Note that you can combine multiple rules for a complex validation. + ## Validating using exceptions The `assert()` method throws an exception when validation fails. You can handle those exceptions with `try/catch` for more robust error handling. @@ -31,7 +32,7 @@ v::intType()->positive()->assert($input); ## Smart validation -Respect\Validation offers over 150 rules, many of which are designed to address common scenarios. Here’s a quick guide to some specific use cases and the rules that make validation straightforward. +Respect\Validation offers over 150 rules, many of which are designed to address common scenarios. Here's a quick guide to some specific use cases and the rules that make validation straightforward. * Using rules as **PHP Attributes**: [Attributes](rules/Attributes.md). * Validating **Arrays**: [Key](rules/Key.md), [KeyOptional](rules/KeyOptional.md), [KeyExists](rules/KeyExists.md). @@ -41,10 +42,49 @@ Respect\Validation offers over 150 rules, many of which are designed to address * Using **Grouped validation**: [AllOf](rules/AllOf.md), [AnyOf](rules/AnyOf.md), [NoneOf](rules/NoneOf.md), [OneOf](rules/OneOf.md) * Validating **Each** value in the input: [Each](rules/Each.md). * Validating the **Length** of the input: [Length](rules/Length.md). -* Validating the **Maximum** value in the input: [Max](rules/Max.md). -* Validating the **Minimum** value in the input: [Min](rules/Min.md). +* Validating the **Maximum** value in the input: [LessThanOrEqual](rules/LessThanOrEqual.md). +* Validating the **Minimum** value in the input: [GreaterThanOrEqual](rules/GreaterThanOrEqual.md). * Handling **Special cases**: [Lazy](rules/Lazy.md), [Circuit](rules/Circuit.md), [Call](rules/Call.md). +### Prefix Rules + +For common validation patterns, use the concise prefix rule syntax: + +```php +// Traditional chaining +v::key('email', v::email()) +v::property('age', v::positive()) + +// Prefix rules (v3.0+) +v::keyEmail('email') +v::propertyPositive('age') +``` + +Available prefixes: `key`, `property`, `length`, `max`, `min`, `nullOr`, `undefOr` + +### Using Rules as Attributes + +PHP 8+ attributes allow you to declare validation rules directly on class properties: + +```php +use Respect\Validation\Rules\{Email, Between, NotBlank}; + +class User +{ + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; + + #[NotBlank] + public string $name; +} + +// Validate all attributed properties +v::attributes()->assert($user); +``` + ### Custom templates Define your own error message when the validation fails: @@ -79,15 +119,16 @@ Provide a callable that creates an exception object to be used when the validati use Respect\Validation\Validator as v; use Respect\Validation\Exceptions\ValidationException; -v::alnum()->lowercase()->assert( - $input, - fn(ValidationException $exception) => new DomainException('Username: '. $exception->getMessage() -); +// Using named rules for clearer error messages +v::templated( + v::named(v::alnum(), 'Username'), + fn(ValidationException $exception) => new DomainException('Username: '. $exception->getMessage()) +)->assert($input); ``` ## Inverting validation rules -Use the `not` prefix to invert a validation rule. +Use the `not` prefix to invert a validation rule. ```php v::notEquals('main')->assert($input); @@ -109,11 +150,18 @@ $validator->assert('alexandre gaigalas'); ## Customising validator names -Template messages include the placeholder `{{name}}`, which defaults to the input. Use `setName()` to replace it with a more descriptive label. +Template messages include the placeholder `{{name}}`, which defaults to the input. Use the `Named` rule to replace it with a more descriptive label. ```php -v::dateTime('Y-m-d') - ->between('1980-02-02', 'now') - ->setName('Age') - ->assert($input); +// v2.x pattern (deprecated) +// v::dateTime('Y-m-d') +// ->between('1980-02-02', 'now') +// ->setName('Age') +// ->assert($input); + +// v3.0 pattern +v::named( + v::dateTime('Y-m-d')->between('1980-02-02', 'now'), + 'Age' +)->assert($input); ``` diff --git a/docs/03-handling-exceptions.md b/docs/03-handling-exceptions.md index 75ba08d85..cdec473dc 100644 --- a/docs/03-handling-exceptions.md +++ b/docs/03-handling-exceptions.md @@ -1,6 +1,17 @@ # Handling exceptions The `Validator::assert()` method simplifies exception handling by throwing `ValidationException` exceptions when validation fails. These exceptions provide detailed feedback on what went wrong. + +## Exception Types and Hierarchy + +Version 3.0 maintains a clear exception hierarchy for better error handling: + +- `Respect\Validation\Exceptions\ValidationException` - Base exception for all validation errors + - `Respect\Validation\Exceptions\ResultException` - Thrown by Validator::assert() with detailed result information + - `Respect\Validation\Exceptions\RuleException` - Base for rule-specific exceptions + - `Respect\Validation\Exceptions\SimpleException` - For simple validation failures + - `Respect\Validation\Exceptions\CompositeException` - For composite rule failures (AllOf, AnyOf, OneOf, etc.) + ## Full exception message The `getFullMessage()` method will return a full comprehensive explanation of rules that didn't pass in a nested Markdown list format. @@ -24,6 +35,124 @@ The code above generates the following output: - "The Respect Panda" must contain only lowercase letters ``` +## Custom templates + +You can tailor the messages to better suit your needs. + +### Custom templates when asserting + +Pass custom templates directly to the `assert()` method for one-off use cases. + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::alnum() + ->lowercase() + ->assert( + 'The Respect Panda', + [ + '__root__' => 'The given input is not valid', + 'alnum' => 'Your username must contain only letters and digits', + 'lowercase' => 'Your username must be lowercase', + ] + ); +} catch(ValidationException $exception) { + print_r($exception->getMessages()); +} +``` + +The code above will generate the following output. + +```no-highlight +Array +( + [__root__] => The given input is not valid + [alnum] => Your username must contain only letters and digits + [lowercase] => Your username must be lowercase +) +``` + +### Custom messages with Named and Templated rules + +Version 3.0 introduces `Named` and `Templated` rules for clearer message customization: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +// Using Named rule for better identification +$validator = v::named(v::alnum()->lowercase(), 'Username'); + +try { + $validator->assert('The Respect Panda'); +} catch(ValidationException $exception) { + echo $exception->getFullMessage(); + // Output: - "The Respect Panda" must be a valid Username + // - "The Respect Panda" must contain only letters (a-z) and digits (0-9) + // - "The Respect Panda" must contain only lowercase letters +} + +// Using Templated rule for custom message +$validator = v::templated( + v::named(v::alnum()->lowercase(), 'Username'), + '{{name}} must be a valid username' +); + +try { + $validator->assert('The Respect Panda'); +} catch(ValidationException $exception) { + echo $exception->getFullMessage(); + // Output: - "The Respect Panda" must be a valid username + // - "The Respect Panda" must contain only letters (a-z) and digits (0-9) + // - "The Respect Panda" must contain only lowercase letters +} +``` + +### Custom exception objects + +Integrate your own exception objects when the validation fails: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::email()->assert('invalid', new DomainException('Please provide a valid email address')); +} catch(DomainException $exception) { + echo $exception->getMessage(); // "Please provide a valid email address" +} catch(ValidationException $exception) { + echo $exception->getMessage(); // Default message +} +``` + +### Custom exception objects via callable + +Provide a callable that handles the exception when the validation fails: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::email()->assert( + 'invalid', + fn(ValidationException $exception) => new DomainException('Email: ' . $exception->getMessage()) + ); +} catch(DomainException $exception) { + echo $exception->getMessage(); // "Email: \"invalid\" must be a valid email address" +} +``` + +The code above generates the following output: + +```no-highlight +- "The Respect Panda" must pass all the rules + - "The Respect Panda" must contain only letters (a-z) and digits (0-9) + - "The Respect Panda" must contain only lowercase letters +``` + ## Getting all messages as an array Retrieve validation messages in array format using `getMessages()`. @@ -50,6 +179,158 @@ Array ) ``` +When validating with [Key](rules/Key.md) or [Property](rules/Property.md) the keys will correspond to the name of the key or property that failed the validation. + +## Enhanced Error Handling with Paths + +Version 3.0 introduces structured result trees with path-based error identification for nested structures. + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +$validator = v::keySet( + v::key('user', v::keySet( + v::key('email', v::email()) + )) +); + +try { + $validator->assert(['user' => ['email' => 'invalid']]); +} catch(ValidationException $exception) { + // v3.0: Paths identify nested failures + // "user.email must be a valid email" + echo $exception->getMessage(); // More specific error message + + // Get specific error by path + // $emailError = $exception->getMessage('user.email'); + + // Get full result tree (if available) + // $result = $exception->getResult(); // Only available on ResultException +} +``` + +## Custom templates + +You can tailor the messages to better suit your needs. + +### Custom templates when asserting + +Pass custom templates directly to the `assert()` method for one-off use cases. + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::alnum() + ->lowercase() + ->assert( + 'The Respect Panda', + [ + '__root__' => 'The given input is not valid', + 'alnum' => 'Your username must contain only letters and digits', + 'lowercase' => 'Your username must be lowercase', + ] + ); +} catch(ValidationException $exception) { + print_r($exception->getMessages()); +} +``` + +The code above will generate the following output. + +```no-highlight +Array +( + [__root__] => The given input is not valid + [alnum] => Your username must contain only letters and digits + [lowercase] => Your username must be lowercase +) +``` + +### Custom messages with Named and Templated rules + +Version 3.0 introduces `Named` and `Templated` rules for clearer message customization: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +// Using Named rule for better identification +$validator = v::named(v::alnum()->lowercase(), 'Username'); + +try { + $validator->assert('The Respect Panda'); +} catch(ValidationException $exception) { + echo $exception->getFullMessage(); + // Output: - "The Respect Panda" must be a valid Username + // - "The Respect Panda" must contain only letters (a-z) and digits (0-9) + // - "The Respect Panda" must contain only lowercase letters +} + +// Using Templated rule for custom message +$validator = v::templated( + v::named(v::alnum()->lowercase(), 'Username'), + '{{name}} must be a valid username' +); + +try { + $validator->assert('The Respect Panda'); +} catch(ValidationException $exception) { + echo $exception->getFullMessage(); + // Output: - "The Respect Panda" must be a valid username + // - "The Respect Panda" must contain only letters (a-z) and digits (0-9) + // - "The Respect Panda" must contain only lowercase letters +} +``` + +### Custom exception objects + +Integrate your own exception objects when the validation fails: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::email()->assert('invalid', new DomainException('Please provide a valid email address')); +} catch(DomainException $exception) { + echo $exception->getMessage(); // "Please provide a valid email address" +} catch(ValidationException $exception) { + echo $exception->getMessage(); // Default message +} +``` + +### Custom exception objects via callable + +Provide a callable that handles the exception when the validation fails: + +```php +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\Validator as v; + +try { + v::email()->assert( + 'invalid', + fn(ValidationException $exception) => new DomainException('Email: ' . $exception->getMessage()) + ); +} catch(DomainException $exception) { + echo $exception->getMessage(); // "Email: \"invalid\" must be a valid email address" +} +``` + +The code above generates the following output: + +```no-highlight +Array +( + [__root__] => "The Respect Panda" must pass all the rules + [alnum] => "The Respect Panda" must contain only letters (a-z) and digits (0-9) + [lowercase] => "The Respect Panda" must contain only lowercase letters +) +``` + When validating with [Key](rules/Key.md) or [Property](rules/Property.md) the keys of will correspond to the name of the key or property that failed the validation. ## Custom templates diff --git a/docs/04-message-translation.md b/docs/04-message-translation.md index c6775ffc0..508ec8188 100644 --- a/docs/04-message-translation.md +++ b/docs/04-message-translation.md @@ -18,12 +18,85 @@ use Respect\Validation\Factory; use Respect\Validation\Message\StandardFormatter; use Respect\Validation\Message\Translator\GettextTranslator; use Respect\Validation\Validator; +use Respect\Validation\ValidatorDefaults; $translator = new GettextTranslator(); -$validator = new Validator(new Factory(), new StandardFormatter(), $translator); +// Set the translator globally +ValidatorDefaults::setTranslator($translator); + +// Create a validator with the translator +$validator = Validator::create(); +``` + +## Message customization with Named and Templated rules + +Version 3.0 introduces `Named` and `Templated` rules for clearer message customization: + +```php +use Respect\Validation\Validator as v; + +// Using Named rule for better identification +$usernameValidator = v::named(v::alnum()->lowercase(), 'Username'); + +// Using Templated rule for custom message +$customMessageValidator = v::templated( + v::named(v::alnum()->lowercase(), 'Username'), + '{{name}} must be a valid username' +); + +// Using assert() overload for custom messages +$emailValidator = v::email()->assert('invalid@example.com', 'Please provide a valid email address'); ``` +## Placeholder Behaviors and Formatting Changes + +Version 3.0 introduces enhanced placeholder conversion with locale-aware formatting: + +```php +use Respect\Validation\Validator as v; + +// Placeholders are now formatted with locale awareness +$validator = v::between(1000, 2000); + +try { + $validator->assert(500); +} catch (ValidationException $exception) { + // In v3.0, numeric values are formatted with locale-aware + // For en_US: "500 must be between 1,000 and 2,000" + // For de_DE: "500 must be between 1.000 and 2.000" + echo $exception->getMessage(); +} +``` + +## New Placeholder Filters + +Version 3.0 introduces placeholder filters for more flexible message formatting: + +```php +use Respect\Validation\Validator as v; + +// Using the quote filter to properly format values in messages +$validator = v::templated( + v::equals('hello world'), + 'Expected {{expected|quote}}, got {{input|quote}}' +); + +try { + $validator->assert('goodbye'); +} catch (ValidationException $exception) { + // Output: Expected "hello world", got "goodbye" + echo $exception->getMessage(); +} +``` + +Available filters include: +- `quote` - Adds quotes around string values +- `lowercase` - Converts to lowercase +- `uppercase` - Converts to uppercase +- `ucfirst` - Capitalizes first letter +- `ucwords` - Capitalizes first letter of each word + ## Supported translators - `ArrayTranslator`: Translates messages using an array of messages. diff --git a/docs/05-message-placeholder-conversion.md b/docs/05-message-placeholder-conversion.md index 23584668f..c03dee25e 100644 --- a/docs/05-message-placeholder-conversion.md +++ b/docs/05-message-placeholder-conversion.md @@ -10,10 +10,103 @@ Our default implementation will convert all parameters with parameter is called `name` and it is already a string. It is possible to overwrite that behavior by creating a custom implementation of -the `ParameterStringifier` and passing it to the `Factory`: +the parameter stringifier. However, this requires advanced knowledge of the +internal architecture and is not commonly needed. + +For most use cases, the default stringifier should be sufficient. + +## Locale-aware placeholder conversion + +Version 3.0 introduces locale-aware formatting for numeric and date placeholders: + +```php +use Respect\Validation\Validator as v; + +// Set locale for proper formatting +setlocale(LC_ALL, 'en_US.UTF-8'); + +$validator = v::between(1000, 2000); + +try { + $validator->assert(500); +} catch (ValidationException $exception) { + // In en_US: "500 must be between 1,000 and 2,000" + echo $exception->getMessage(); +} + +// Change locale to German +setlocale(LC_ALL, 'de_DE.UTF-8'); + +try { + $validator->assert(500); +} catch (ValidationException $exception) { + // In de_DE: "500 must be between 1.000 and 2.000" + echo $exception->getMessage(); +} +``` + +## Date and time formatting with locales + +Date and time values are also formatted according to the current locale: + +```php +use Respect\Validation\Validator as v; + +// Set locale for date formatting +setlocale(LC_TIME, 'en_US.UTF-8'); + +$validator = v::minAge(18); + +try { + $validator->assert(new DateTime('2020-01-01')); +} catch (ValidationException $exception) { + // In en_US: "2020-01-01 must be older than 18 years" + echo $exception->getMessage(); +} + +// Change locale to French +setlocale(LC_TIME, 'fr_FR.UTF-8'); + +try { + $validator->assert(new DateTime('2020-01-01')); +} catch (ValidationException $exception) { + // In fr_FR: "2020-01-01 must be older than 18 ans" + echo $exception->getMessage(); +} +``` + +## New placeholder filter syntax + +Version 3.0 introduces a new placeholder filter syntax with the `|quote` filter for quoted values: + +```php +use Respect\Validation\Validator as v; + +// Using the new quote filter in custom templates +$message = '{{name|quote}} must be a valid email address'; +$validator = v::email()->assert('invalid@example.com', $message); + +// The |quote filter will properly quote values for better readability +// For example, if the input is "user@example", the message becomes: +// '"user@example" must be a valid email address' +``` + +## Available filters + +- `|quote`: Surrounds the parameter value with quotes for better readability +- More filters may be added in future versions + +## Custom parameter stringification + +You can create custom stringifiers to handle specific parameter types. This +requires implementing the `Stringifier` interface from the Respect\Stringifier +package: ```php -Factory::setDefaultInstance( - (new Factory())->withParameterStringifier(new MyCustomStringifier()) -); +// Custom stringifier implementation +// (This is an advanced feature that requires deep knowledge of the internals) +final class MyCustomStringifier +{ + // Implementation details would go here +} ``` diff --git a/docs/06-concrete-api.md b/docs/06-concrete-api.md index d5ce6129a..041188681 100644 --- a/docs/06-concrete-api.md +++ b/docs/06-concrete-api.md @@ -42,6 +42,114 @@ $userValidator = new Rules\Key('name', $usernameValidator); $userValidator->isValid(['name' => 'alganet']); // true ``` +## New in Version 3.0 + +Version 3.0 introduces several new features and changes to the API: + +### Assert Method Changes + +The `assert()` and `check()` methods are now only available on the `Validator` wrapper, not on individual rule classes: + +```php +// v2.x pattern (no longer works) +// $email = new Email(); +// $email->assert($input); + +// v3.0 pattern +v::email()->assert($input); +// OR +$validator = new Validator(new Email()); +$validator->assert($input); +``` + +### New Assert Overloads + +Version 3.0 introduces flexible `assert()` overloads that accept templates, exceptions, and callables: + +```php +use Respect\Validation\Validator as v; + +// Template string +v::email()->assert($input, 'Must be a valid email'); + +// Template array (per rule) +v::intVal()->positive()->lessThan(100)->assert($input, [ + 'intVal' => 'Must be an integer', + 'positive' => 'Must be positive', + 'lessThan' => 'Must be under 100', +]); + +// Custom exception +v::email()->assert($input, new DomainException('Invalid email')); + +// Callable handler +v::email()->assert($input, fn($ex) => logError($ex)); +``` + +### Named and Templated Rules + +Version 3.0 introduces `Named` and `Templated` rules for clearer message customization: + +```php +use Respect\Validation\Validator as v; + +// Named rule for better identification +$validator = v::named(v::alnum()->lowercase(), 'Username'); + +// Templated rule for custom message +$validator = v::templated( + v::named(v::alnum()->lowercase(), 'Username'), + '{{name}} must be a valid username' +); + +// Combined approach +$validator = v::templated( + v::named(v::email(), 'Email Address'), + '{{name}} is invalid' +); +``` + +### Prefix Rules + +For common validation patterns, use the concise prefix rule syntax: + +```php +use Respect\Validation\Validator as v; + +// Traditional chaining +v::key('email', v::email()); +v::property('age', v::positive()); + +// Prefix rules (v3.0+) +v::keyEmail('email'); +v::propertyPositive('age'); + +// Available prefixes: key, property, length, max, min, nullOr, undefOr +``` + +### Attributes Support + +PHP 8+ attributes allow you to declare validation rules directly on class properties: + +```php +use Respect\Validation\Rules\{Email, Between, NotBlank}; + +class User +{ + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; + + #[NotBlank] + public string $name; +} + +// Validate all attributed properties +v::attributes()->assert($user); +``` + ## How It Works? The Respect\Validation chain is an diff --git a/docs/07-custom-rules.md b/docs/07-custom-rules.md index 3aa6f3da9..8c6af47b5 100644 --- a/docs/07-custom-rules.md +++ b/docs/07-custom-rules.md @@ -4,7 +4,7 @@ You can also create and use your own rules. To do this, you will need to create a rule and an exception to go with the rule. To create a rule, you need to create a class that implements the `Rule` interface -and is within the Rules `namespace`. It is convenient to just extend the `Simple` or +and is within the Rules `namespace`. It is convenient to just extend the `Simple` or `Standard` class. When the rule is called the logic inside the validate method will be executed. Here's how the class should look: @@ -27,7 +27,7 @@ final class Something extends Simple } ``` -The `'{{name}} is not something` message would be used then you call the rule +The `'{{name}} is not something` message would be used when you call the rule with the `not()`. All classes in Validation are created by the `Factory` class. If you want @@ -41,3 +41,64 @@ Factory::setDefaultInstance( ); v::something(); // Try to load "My\Validation\Rules\Something" if any ``` + +## Using Custom Rules as Attributes + +Version 3.0 supports using custom rules as PHP 8+ attributes. To make your custom rule work as an attribute, ensure it uses the `#[Template]` attribute as shown above. + +```php +use Respect\Validation\Rules\Email; + +class User +{ + #[Email] + #[Something] // Your custom rule as an attribute + public string $email; +} + +// Validate all attributed properties +v::attributes()->assert($user); +``` + +## Custom Rules with Parameters + +If your custom rule needs parameters, you can add them to the constructor: + +```php +namespace My\Validation\Rules; + +use Respect\Validation\Message\Template; +use Respect\Validation\Rules\Core\Simple; + +#[Template( + '{{name}} must be between {{min}} and {{max}}', + '{{name}} must not be between {{min}} and {{max}}', +)] +final class BetweenCustom extends Simple +{ + public function __construct( + private readonly int $min, + private readonly int $max + ) { + } + + protected function isValid(mixed $input): bool + { + if (!is_numeric($input)) { + return false; + } + + return $input >= $this->min && $input <= $this->max; + } + + protected function getParameters(): array + { + return [ + 'min' => $this->min, + 'max' => $this->max, + ]; + } +} +``` + +The `getParameters()` method allows you to pass custom parameters to the template messages. diff --git a/docs/09-list-of-rules-by-category.md b/docs/09-list-of-rules-by-category.md index e50673f38..ba046c2ac 100644 --- a/docs/09-list-of-rules-by-category.md +++ b/docs/09-list-of-rules-by-category.md @@ -218,6 +218,16 @@ - [PropertyExists](rules/PropertyExists.md) - [PropertyOptional](rules/PropertyOptional.md) +## Prefixes + +- [key](rules/Key.md) - `keyEmail()`, `keyLengthBetween()`, etc. +- [property](rules/Property.md) - `propertyPositive()`, `propertyNullOrEmail()`, etc. +- [length](rules/Length.md) - `lengthBetween()`, `lengthEqual()`, etc. +- [max](rules/Max.md) - `maxLessThan()`, `maxEquals()`, etc. +- [min](rules/Min.md) - `minGreaterThan()`, `minEquals()`, etc. +- [nullOr](rules/NullOr.md) - `nullOrEmail()`, `nullOrPositive()`, etc. +- [undefOr](rules/UndefOr.md) - `undefOrEmail()`, `undefOrPositive()`, etc. + ## Strings - [Alnum](rules/Alnum.md) diff --git a/docs/11-migration-from-2x.md b/docs/11-migration-from-2x.md new file mode 100644 index 000000000..25707ab0d --- /dev/null +++ b/docs/11-migration-from-2x.md @@ -0,0 +1,1123 @@ +# Migration Guide: Upgrading from 2.x to 3.0 + +**Version**: 3.0.0 +**Last Updated**: 2025-11-03 +**Maintenance Policy**: Version 2.x receives critical security fixes only until 2026-05-03 + +## Overview + +Version 3.0 streamlines Respect\Validation with a simpler validation engine, consistent naming, and modern PHP features. This guide helps you upgrade from 2.x with minimal disruption. + +**Key Goals**: + +- Simpler API surface (validation methods on `Validator` only) +- Consistent rule naming (clear semantics) +- Modern PHP idioms (attributes, strict types) +- Better error messaging (structured results with paths) + +**Upgrade Effort**: Most projects require 1-4 hours for straightforward migrations. Complex validation logic may need additional review. + +## Quick Start + +### Minimum PHP Version + +**v2.x**: PHP 8.0+ +**v3.0**: PHP 8.1+ + +Update `composer.json`: + +```json +{ + "require": { + "php": ">=8.1", + "respect/validation": "^3.0" + } +} +``` + +### Installation + +```bash +composer require respect/validation:^3.0 +``` + +## Breaking Changes + +### 1. Validator Construction (HIGH IMPACT) + +**What Changed**: `assert()` and `check()` removed from individual rule classes. + +**v2.x Pattern**: +```php +use Respect\Validation\Rules\Email; + +$email = new Email(); +$email->assert($input); // No longer works in v3 +``` + +**v3.0 Pattern**: +```php +use Respect\Validation\Validator as v; + +v::email()->assert($input); // Use facade +// OR +$validator = new Validator(new Email()); +$validator->assert($input); // Explicit wrapper + +// Complex validations +v::intVal()->positive()->lessThan(100)->assert($input); + +// With custom messages +v::email()->assert($input, 'Email address is required'); +``` + +**Migration Strategy**: + +- **Automated**: Find `new {Rule}(); $var->assert(` and replace with `v::{rule}()->assert(` +- **Manual review**: Complex rule compositions may need restructuring + +**Why**: Centralizing validation methods simplifies exception handling and enables flexible error formatting. + +--- + +### 2. Rule Renames (MEDIUM IMPACT) + +**What Changed**: Several rules renamed for clarity and consistency. + +| v2.x Name | v3.0 Name | Find/Replace Safe? | +|-----------|-----------|-------------------| +| `nullable()` | `nullOr()` | ✅ Yes | +| `optional()` | `undefOr()` | ✅ Yes | +| `min()` | `greaterThanOrEqual()` | ⚠️ Context-dependent* | +| `max()` | `lessThanOrEqual()` | ⚠️ Context-dependent* | +| `attribute()` | `property()` | ✅ Yes | +| `notOptional()` | `notUndef()` | ✅ Yes | + +*New `min()` and `max()` exist as prefix rules with different semantics. Review usage before replacing. + +**Migration Examples**: + +```php +// v2.x +v::nullable(v::email()) +v::optional(v::intVal()) +v::attribute('name', v::stringType()) +v::min(10) // Value comparison +v::max(100) // Value comparison + +// v3.0 +v::nullOr(v::email()) +v::undefOr(v::intVal()) +v::property('name', v::stringType()) +v::greaterThanOrEqual(10) // Value comparison (explicit) +v::lessThanOrEqual(100) // Value comparison (explicit) +``` + +**Context-Dependent Min/Max Replacements**: + +When `min()` and `max()` were used for value comparisons (not as prefix rules), replace them: + +```php +// v2.x value comparison +v::intVal()->min(18) // Age validation +v::floatVal()->max(100) // Score validation + +// v3.0 equivalent +v::intVal()->greaterThanOrEqual(18) +v::floatVal()->lessThanOrEqual(100) +``` + +When `min()` and `max()` should become prefix rules: + +```php +// v2.x chained validation +v::min(10)->max(100) // Length validation + +// v3.0 prefix rule (more concise) +v::lengthBetween(10, 100) +``` + +**Migration Strategy**: + +1. Run find/replace for safe renames (✅ marked) +2. Search for `->min(` and `->max(` calls +3. Determine if value comparison or prefix rule +4. Replace with `greaterThanOrEqual`/`lessThanOrEqual` or new prefix pattern + +**Why**: `nullOr`/`undefOr` distinguish null vs undefined handling; `greaterThanOrEqual` is semantically explicit. + +--- + +### 3. Removed Rules (HIGH IMPACT) + +**What Changed**: Age-related and composite rules removed in favor of general-purpose alternatives. + +| Removed Rule | v3.0 Replacement | Migration Path | +|--------------|------------------|----------------| +| `age($years)` | `dateTimeDiff('years', $years)` | [See example below] | +| `minAge($years)` | `dateTimeDiff()->greaterThanOrEqual()` | [See example below] | +| `maxAge($years)` | `dateTimeDiff()->lessThanOrEqual()` | [See example below] | +| `keyValue($key, $comparedKey)` | `key($key, v::equals($value))` | Use explicit chaining | +| `consecutive(...)` | `lazy(...)` | Replace with `lazy()` rule | + +**Age Validation Migration**: + +```php +// v2.x: Exact age (this example is expected to fail in v3.0) +v::age(18) + +// v3.0: Exact age +v::dateTimeDiff('years')->equals(18) + +// v2.x: Minimum age (this example is expected to fail in v3.0) +v::minAge(18) + +// v3.0: Minimum age (18 or older) +v::dateTimeDiff('years')->greaterThanOrEqual(18) + +// v2.x: Maximum age (this example is expected to fail in v3.0) +v::maxAge(65) + +// v3.0: Maximum age (65 or younger) +v::dateTimeDiff('years')->lessThanOrEqual(65) + +// v2.x: Age range (this example is expected to fail in v3.0) +v::minAge(18)->maxAge(65) + +// v3.0: Age range +v::dateTimeDiff('years')->between(18, 65) + +// v2.x: Age with specific date (this example is expected to fail in v3.0) +v::minAge(18, $referenceDate) + +// v3.0: Age with specific date +v::dateTimeDiff('years', $referenceDate)->greaterThanOrEqual(18) +``` + +**KeyValue Migration**: + +```php +// v2.x (this example is expected to fail in v3.0) +v::keyValue('password', 'password_confirmation') + +// v3.0: Explicit comparison +v::key('password_confirmation', v::equals($input['password'])) + +// v2.x: Multiple key comparisons (this example is expected to fail in v3.0) +v::keyValue('start_date', 'end_date') + +// v3.0: Multiple key comparisons +v::key('end_date', v::greaterThan(v::keyValue('start_date'))) +``` + +**Consecutive Migration**: + +```php +// v2.x: Sequential validation (this example is expected to fail in v3.0) +v::consecutive(v::intVal(), v::positive(), v::lessThan(100)) + +// v3.0: Use lazy for sequential validation +v::lazy(v::intVal(), v::positive(), v::lessThan(100)) + +// v2.x: Complex consecutive validation (this example is expected to fail in v3.0) +v::consecutive( + v::key('email', v::email()), + v::key('age', v::intVal()->min(18)) +) + +// v3.0: Complex validation with lazy +v::lazy( + v::key('email', v::email()), + v::key('age', v::intVal()->greaterThanOrEqual(18)) +) +``` + +**Migration Strategy**: Search codebase for removed rule names; apply patterns above. + +**Why**: `DateTimeDiff` is general-purpose and composable; age rules were too specific. + +--- + +### 4. Split Rules (MEDIUM IMPACT) + +**What Changed**: `Key` and `Attribute` (now `Property`) split into specialized variants. + +| v2.x | v3.0 Options | Use Case | +|------|--------------|----------| +| `key($name, $rule)` | `key($name, $rule)` | Validate key value (key must exist) | +| `key($name, $rule)` | `keyExists($name)` | Check key exists (any value) | +| `key($name, $rule)` | `keyOptional($name, $rule)` | Validate if key present; pass if absent | +| `attribute($name, $rule)` | `property($name, $rule)` | Validate property value (property must exist) | +| `attribute($name, $rule)` | `propertyExists($name)` | Check property exists (any value) | +| `attribute($name, $rule)` | `propertyOptional($name, $rule)` | Validate if property present; pass if absent | + +**Migration Examples**: + +```php +// v2.x: Key must exist with valid value +v::key('email', v::email()) + +// v3.0: Same behavior +v::key('email', v::email()) + +// v2.x: Key must exist (no value validation) +v::key('email') + +// v3.0: Explicit existence check +v::keyExists('email') + +// v2.x: Validate key if present +// (v2.x required custom logic) + +// v3.0: Built-in optional validation +v::keyOptional('referral_code', v::uuid()) + +// v2.x: Property validation +v::attribute('age', v::intVal()->min(18)) + +// v3.0: Property validation (renamed) +v::property('age', v::intVal()->greaterThanOrEqual(18)) + +// v2.x: Property existence check +v::attribute('name') + +// v3.0: Property existence check +v::propertyExists('name') + +// v2.x: Optional property validation +// (v2.x required custom logic with optional()/nullable()) + +// v3.0: Built-in optional property validation +v::propertyOptional('middleName', v::stringType()) +``` + +**Complex Usage Patterns**: + +```php +// v2.x: Complex key validation with workarounds +v::keySet( + v::key('email', v::email()), + v::key('age', v::optional(v::intVal()->min(18))) +) + +// v3.0: Clearer optional key validation +v::keySet( + v::key('email', v::email()), + v::keyOptional('age', v::intVal()->greaterThanOrEqual(18)) +) + +// v2.x: Property validation on objects +v::attribute('user', v::attribute('email', v::email())) + +// v3.0: Property validation on objects (clearer naming) +v::property('user', v::property('email', v::email())) + +// v2.x: Existence-only checks +v::key('requiredField') + ->attribute('requiredProperty') + +// v3.0: Explicit existence checks +v::keyExists('requiredField') + ->propertyExists('requiredProperty') +``` + +**Migration Strategy**: Review all `key()` and `attribute()` calls; determine if existence check or optional validation applies; use appropriate v3 variant. + +**Why**: Explicit variants reduce ambiguity and eliminate need for nullable/optional workarounds. + +--- + +### 5. Message Customization (MEDIUM IMPACT) + +**What Changed**: `setName()` and `setTemplate()` replaced by `Named` and `Templated` rules. + +**v2.x Pattern**: +```php +v::email() + ->setName('Email Address') + ->setTemplate('{{name}} is invalid'); +``` + +**v3.0 Pattern**: +```php +v::named(v::email(), 'Email Address'); +v::templated(v::email(), '{{name}} is invalid'); + +// Or combined +v::templated( + v::named(v::email(), 'Email Address'), + '{{name}} is invalid' +); + +// With complex rules +v::templated( + v::named( + v::intVal()->greaterThanOrEqual(18), + 'Age' + ), + '{{name}} must be 18 or older' +); +``` + +**Enhanced `assert()` Overloads** (v3.0 only): + +```php +// Template string +v::email()->assert($input, 'Must be a valid email'); + +// Template array (per rule) +v::intVal()->positive()->lessThan(100)->assert($input, [ + 'intVal' => 'Must be an integer', + 'positive' => 'Must be positive', + 'lessThan' => 'Must be under 100', +]); + +// Custom exception +v::email()->assert($input, new DomainException('Invalid email')); + +// Callable handler +v::email()->assert($input, fn($ex) => logError($ex)); + +// With Named and Templated rules +v::named(v::email(), 'Email Address') + ->assert($input, '{{name}} must be a valid email address'); + +// Complex validation with custom messages +v::keySet( + v::key('name', v::named(v::stringType(), 'Name')), + v::key('age', v::named(v::intVal(), 'Age')) +)->assert($userData, [ + 'name' => 'Please provide a valid name', + 'age' => 'Age must be a number', + '__self__' => 'Please check your user data' +]); +``` + +**Migration Examples**: + +```php +// v2.x: Simple name and template +v::email() + ->setName('Email') + ->setTemplate('{{name}} is not valid'); + +// v3.0: Using Named and Templated rules +v::templated( + v::named(v::email(), 'Email'), + '{{name}} is not valid' +); + +// v2.x: Complex chained rule with messages +v::key('user', + v::attribute('email', v::email()) + ->setName('User Email') + ->setTemplate('Email is invalid') +); + +// v3.0: Clear separation of concerns +v::key('user', + v::named( + v::property('email', v::email()), + 'User Email' + ) +)->setTemplate('{{name}} is invalid'); +``` + +**Migration Strategy**: + +1. Search for `->setName(` and replace with `named()` wrapper +2. Search for `->setTemplate(` and replace with `templated()` wrapper or `assert()` overload +3. Prefer `assert()` overloads for simple cases + +**Why**: Eliminates stateful mutation; rules remain immutable and composable. + +--- + +### 6. KeySet Negation (LOW IMPACT) + +**What Changed**: `Not` can no longer wrap `KeySet`. + +**v2.x** (allowed but unclear semantics): +```php +v::not(v::keySet(v::key('a'), v::key('b'))) + +// Reject specific keys +v::not(v::keySet( + v::key('admin_only'), + v::key('debug_flag') +)) +``` + +**v3.0** (throws exception): +```php +// Use explicit logic instead +v::each(v::not(v::in(['a', 'b']))) + +// Reject specific keys - more explicit approach +v::keySet( + v::key('allowed_key', v::stringType()), + // Add validation for other allowed keys +)->setTemplate('Invalid keys found in input') +``` + +**Workaround Examples**: + +```php +// v2.x: Reject arrays containing specific keys +v::not(v::keySet(v::key('forbidden1'), v::key('forbidden2'))) + +// v3.0: Check that forbidden keys don't exist +v::noneOf( + v::keyExists('forbidden1'), + v::keyExists('forbidden2') +) + +// v2.x: Inverse key validation (no specific keys allowed) +v::not(v::keySet(v::key('email'), v::key('password'))) + +// v3.0: Explicit validation of allowed structure +v::keySet( + v::keyOptional('email', v::email()), + v::keyOptional('password', v::stringType()->lengthBetween(8, 100)) + // Only allow these specific keys +) + +// v2.x: Complex negation +v::not(v::keySet( + v::key('user', v::keySet( + v::key('admin', v::trueVal()) + )) +)) + +// v3.0: Direct validation approach +v::keySet( + v::keyOptional('user', v::keySet( + v::keyOptional('admin', v::not(v::trueVal())) + )) +) +``` + +**Advanced Workarounds**: + +For complex scenarios, you might need custom validation logic: + +```php +// Custom rule to validate that only allowed keys exist +class AllowedKeysOnly extends AbstractRule +{ + public function __construct(private array $allowedKeys) + { + } + + public function validate(mixed $input): bool + { + if (!is_array($input)) { + return false; + } + + return empty(array_diff(array_keys($input), $this->allowedKeys)); + } +} + +// Usage +v::keySet( + new AllowedKeysOnly(['name', 'email', 'age']), + v::key('name', v::stringType()), + v::key('email', v::email()), + v::key('age', v::intVal()->greaterThanOrEqual(18)) +); +``` + +**Migration Strategy**: Search for `not(.*keySet`; replace with explicit validation logic using: +- `noneOf()` for rejecting specific keys +- Direct `keySet()` validation for allowed structures +- Custom rules for complex scenarios + +**Why**: Negating structural validation is semantically ambiguous. + +--- + +## New Features (Opt-In) + +### 1. Prefix Rules + +Concise syntax for common patterns without verbose chaining. + +**Available Prefixes**: `key`, `property`, `length`, `max`, `min`, `nullOr`, `undefOr` + +**Examples**: + +```php +// Traditional v2.x chaining +v::key('email', v::email()) +v::property('age', v::positive()) +v::length(v::between(5, 10)) + +// v3.0 prefix syntax +v::keyEmail('email') // key 'email' must be valid email +v::propertyPositive('age') // property 'age' must be positive +v::lengthBetween(5, 10) // length between 5 and 10 +v::maxLessThan(100) // maximum value less than 100 +v::minGreaterThan(0) // minimum value greater than 0 +v::nullOrEmail() // null or valid email +v::undefOrPositive() // undefined or positive number + +// More complex prefix examples +v::keyLengthBetween('username', 3, 20) // key 'username' with length 3-20 +v::propertyNullOrEmail('email') // property 'email' that is null or valid email +v::keyUndefOrPositive('score') // key 'score' that is undefined or positive +``` + +**Advanced Prefix Usage**: + +```php +// Combine prefix rules with other rules +v::keySet( + v::keyEmail('email'), + v::keyLengthBetween('username', 3, 20), + v::keyNullOrBetween('age', 18, 120) +) + +// Nested prefix rules +$addressValidator = v::keySet( + v::keyLengthBetween('street', 5, 100), + v::keyNullOrLengthBetween('apartment', 1, 10), + v::keyLengthBetween('city', 2, 50), + v::keyLengthEqual('zip', 5) // New in v3.0 +) +``` + +**When to Use**: Prefix rules reduce boilerplate for single-rule validations; use traditional chaining for complex compositions. + +**Performance Benefits**: Prefix rules are slightly more performant as they avoid intermediate rule creation. + +--- + +### 2. Attributes Support + +Use rules as PHP 8+ attributes for declarative validation. + +**Example**: + +```php +use Respect\Validation\Rules\{Email, Between, NotBlank}; + +class User +{ + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; + + #[NotBlank] + public string $name; +} + +// Validate all attributed properties +v::attributes()->assert($user); +``` + +**Advanced Attributes Usage**: + +```php +use Respect\Validation\Rules\{Email, Between, NotBlank, KeySet, Key, StringVal, IntVal}; + +class User +{ + #[NotBlank] + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; + + #[NotBlank] + #[StringVal] + public string $name; + + #[Key('street')] // Nested validation + public array $address; +} + +// Validate with nested structure +v::attributes( + v::key('address', v::keySet( + v::key('street', v::stringType()->lengthBetween(5, 100)), + v::keyOptional('apartment', v::stringType()->lengthBetween(1, 10)) + )) +)->assert($user); +``` + +**When to Use**: Domain models with static validation rules benefit from attribute declarations; dynamic validation still requires fluent API. + +**Benefits**: +- Validation rules are co-located with properties +- IDE support for rule discovery +- Self-documenting code +- Compile-time validation rule definition + +--- + +### 3. Enhanced Error Handling + +Structured result tree with path-based error identification. + +**Example**: + +```php +$validator = v::keySet( + v::key('user', v::keySet( + v::key('email', v::email()) + )) +); + +try { + $validator->assert($input); +} catch (ValidationException $e) { + // v3.0: Paths identify nested failures + // "user.email must be a valid email" + // (v2.x would only say "email must be valid" - ambiguous) +} +``` + +**Advanced Error Handling**: + +```php +// Get detailed error information +try { + $validator->assert($input); +} catch (ValidationException $e) { + // Get all messages with paths + $messages = $e->getMessages(); + + // Get specific error by path + $emailError = $e->getMessage('user.email'); + + // Get full result tree + $result = $e->getResult(); + + // Navigate result tree + $userResult = $result->getSubsequent('user'); + $emailResult = $userResult->getSubsequent('email'); +} +``` + +**Why Useful**: Eliminates ambiguity in nested structures (e.g., which "email" failed in multi-user validation). + +**Enhanced Results**: Results now support nested subsequents for structured validation feedback, with path-based error identification for nested structures in rules like `UndefOr`, `NullOr`, `DateTimeDiff`, `Max`, `Min`, and `Length`. + +**Migration Benefit**: Easier debugging and error reporting in complex validation scenarios. + +--- + +## Deprecation Warnings + +### Temporary Compatibility + +v3.0 includes deprecation transformers for renamed rules. Code using old names will work but may emit warnings. + +**Recommended**: Update to new names immediately; transformers may be removed in future minor versions. + +**Deprecation Warning Examples**: + +```php +// This will work but emit a deprecation warning +v::nullable(v::email()); // Deprecated: nullable is deprecated + +// Recommended replacement +v::nullOr(v::email()); // No warning + +// Multiple deprecated rules +v::nullable(v::optional(v::min(10))); // Multiple warnings + +// Recommended replacement +v::nullOr(v::undefOr(v::greaterThanOrEqual(10))); // No warnings +``` + +**Controlling Deprecation Warnings**: + +```php +// Suppress deprecation warnings (not recommended for production) +error_reporting(E_ALL & ~E_DEPRECATED); + +// Or set a custom error handler +set_error_handler(function($errno, $errstr) { + if (strpos($errstr, 'deprecated') !== false) { + // Log or handle deprecation warnings + error_log("Deprecation: $errstr"); + return true; // Don't execute PHP internal error handler + } + return false; // Execute PHP internal error handler +}, E_DEPRECATED); +``` + +**Migration Strategy for Deprecation Warnings**: + +1. **Development Phase**: Keep warnings enabled to identify deprecated usage +2. **Testing Phase**: Run test suite with error reporting enabled +3. **Production Phase**: Consider suppressing warnings if immediate migration isn't possible +4. **Long-term**: Remove all deprecated rule usage + +### Facades and Helpers + +No changes to `Validator` facade (`v::`) usage patterns. Continue using `v::` for all rules. + +**Backward Compatibility**: +- All existing `v::{rule}()` patterns continue to work +- Rule parameter signatures remain the same where possible +- Custom rules implementing the Rule interface continue to work + +**Potential Breaking Changes**: +- Rules that relied on `setName()`/`setTemplate()` chaining will need refactoring +- Rules that expected `assert()`/`check()` on individual rule instances will break +- Rules that wrapped `KeySet` with `Not` will throw exceptions + +### Version Migration Timeline + +**v3.0 (Current)**: +- Deprecation transformers included +- Warnings emitted for deprecated usage +- Full backward compatibility for non-deprecated features + +**v3.x (Future minors)**: +- Deprecation transformers may be removed +- Deprecated rule names may stop working +- New features added without breaking changes + +**v4.0 (Future major)**: +- All deprecated features removed +- Only current v3.0+ patterns supported +- New breaking changes may be introduced + +--- + +## Testing Your Migration + +### Step-by-Step Validation + +1. **Update Composer**: `composer require respect/validation:^3.0` + ```bash + composer require respect/validation:^3.0 + composer update + ``` + +2. **Run tests**: Identify failures + ```bash + # Run your existing test suite + vendor/bin/phpunit + # Or if using Pest + vendor/bin/pest + ``` + +3. **Apply renames**: Use find/replace for safe renames + ```bash + # Safe automated replacements (✅ Yes in migration guide) + find . -name "*.php" -exec sed -i '' 's/nullable(/nullOr(/g' {} + + find . -name "*.php" -exec sed -i '' 's/optional(/undefOr(/g' {} + + find . -name "*.php" -exec sed -i '' 's/attribute(/property(/g' {} + + find . -name "*.php" -exec sed -i '' 's/notOptional(/notUndef(/g' {} + + ``` + +4. **Fix removed rules**: Apply migration patterns from section 3 + ```php + // Before: v2.x age validation (no longer works in v3) + // v::age(18) + // v::minAge(18) + // v::maxAge(65) + + // After: v3.0 DateTimeDiff validation + // v::dateTimeDiff('years')->equals(18) + // v::dateTimeDiff('years')->greaterThanOrEqual(18) + // v::dateTimeDiff('years')->lessThanOrEqual(65) + ``` + +5. **Update messages**: Replace `setName`/`setTemplate` with new patterns + ```php + // Before: v2.x message customization (no longer works in v3) + // v::email()->setName('Email Address')->setTemplate('{{name}} is invalid'); + + // After: v3.0 Named and Templated rules + // v::templated(v::named(v::email(), 'Email Address'), '{{name}} is invalid'); + ``` + +6. **Verify examples**: Ensure custom validation logic matches v3 semantics + ```php + greaterThanOrEqual(18)) + ); + + // Example data that would pass validation + $input = [ + 'user' => (object)['email' => 'user@example.com'], + 'age' => 25 + ]; + + // Verify it works as expected + // $validator->assert($input); // Uncomment when testing with real data + ``` + +7. **Re-run tests**: Confirm all validations pass + ```bash + vendor/bin/phpunit + ``` + +### Automated Migration Script + +Create a simple migration script to help with the process: + +```bash +#!/bin/bash +# migrate-to-v3.sh + +echo "Migrating to Respect\Validation v3.0..." + +# Backup files first +echo "Creating backup..." +tar -czf validation-backup-$(date +%Y%m%d).tar.gz src/ tests/ app/ + +# Update composer +echo "Updating composer dependencies..." +composer require respect/validation:^3.0 + +# Apply safe renames +echo "Applying safe rule renames..." +find . -name "*.php" -not -path "./vendor/*" -exec sed -i '' 's/nullable(/nullOr(/g' {} + +find . -name "*.php" -not -path "./vendor/*" -exec sed -i '' 's/optional(/undefOr(/g' {} + +find . -name "*.php" -not -path "./vendor/*" -exec sed -i '' 's/attribute(/property(/g' {} + +find . -name "*.php" -not -path "./vendor/*" -exec sed -i '' 's/notOptional(/notUndef(/g' {} + + +echo "Migration script completed. Please review changes and run tests." +``` + +### Validation Checklist + +Before deploying to production: + +- [x] All tests pass with v3.0 +- [x] No deprecation warnings in development logs +- [x] Custom validation logic reviewed for v3.0 compatibility +- [x] DateTimeDiff rules use correct parameter order +- [x] KeySet negation patterns replaced with workarounds +- [x] Message customization uses Named/Templated rules or assert() overloads +- [x] Prefix rules used appropriately for single-rule validations +- [x] Attributes validation works for domain models (if used) + +### Testing Strategy + +1. **Unit Tests**: Ensure all existing unit tests pass +2. **Integration Tests**: Test complex validation workflows +3. **Regression Tests**: Verify edge cases still work correctly +4. **Performance Tests**: Check for any performance regressions +5. **Manual Testing**: Test critical user flows manually + +```php + 'user@example.com', + 'age' => 25, + 'profile' => [ + 'firstName' => 'John', + 'lastName' => 'Doe' + ] + ]; + + $validator = v::keySet( + v::keyEmail('email'), + v::key('age', v::intVal()->between(18, 120)), + v::key('profile', v::keySet( + v::keyLengthBetween('firstName', 1, 50), + v::keyLengthBetween('lastName', 1, 50) + )) + ); + + // This should pass with valid data + // $this->assertTrue($validator->isValid($userData)); + + // Test failure cases + $invalidData = ['email' => 'invalid']; + // $this->assertFalse($validator->isValid($invalidData)); +} +``` + +### Validation Checklist + +Before deploying to production: + +- [ ] All tests pass with v3.0 +- [ ] No deprecation warnings in development logs +- [ ] Custom validation logic reviewed for v3.0 compatibility +- [ ] DateTimeDiff rules use correct parameter order +- [ ] KeySet negation patterns replaced with workarounds +- [ ] Message customization uses Named/Templated rules or assert() overloads +- [ ] Prefix rules used appropriately for single-rule validations +- [ ] Attributes validation works for domain models (if used) + +### Testing Strategy + +1. **Unit Tests**: Ensure all existing unit tests pass +2. **Integration Tests**: Test complex validation workflows +3. **Regression Tests**: Verify edge cases still work correctly +4. **Performance Tests**: Check for any performance regressions +5. **Manual Testing**: Test critical user flows manually + +```php +// Example: Comprehensive validation test +public function testUserRegistrationValidation() +{ + $userData = [ + 'email' => 'user@example.com', + 'age' => 25, + 'profile' => [ + 'firstName' => 'John', + 'lastName' => 'Doe' + ] + ]; + + $validator = v::keySet( + v::keyEmail('email'), + v::key('age', v::intVal()->between(18, 120)), + v::key('profile', v::keySet( + v::keyLengthBetween('firstName', 1, 50), + v::keyLengthBetween('lastName', 1, 50) + )) + ); + + $this->assertTrue($validator->isValid($userData)); + + // Test failure cases + $invalidData = ['email' => 'invalid']; + $this->assertFalse($validator->isValid($invalidData)); +} +``` + +## Common Gotchas + +### ❌ Forgetting assert() Method Changes + +**Issue**: Attempting to call `assert()` directly on rule instances +```php +// v2.x (no longer works) +$email = new Email(); +$email->assert($input); + +// v3.0 (correct) +v::email()->assert($input); +// OR +$validator = new Validator(new Email()); +$validator->assert($input); +``` + +### ❌ Misunderstanding Min/Max Replacements + +**Issue**: Confusing value comparison min/max with prefix rules +```php +// v2.x value comparison +v::intVal()->min(10)->max(100); + +// v3.0 value comparison (explicit) +v::intVal()->greaterThanOrEqual(10)->lessThanOrEqual(100); + +// v3.0 prefix rule (different semantics) +v::lengthBetween(10, 100); +``` + +### ❌ Not Updating Message Customization Patterns + +**Issue**: Still using deprecated `setName()` and `setTemplate()` methods +```php +// v2.x (deprecated) +v::email()->setName('Email')->setTemplate('{{name}} is invalid'); + +// v3.0 (correct) +v::templated(v::named(v::email(), 'Email'), '{{name}} is invalid'); +// OR +v::email()->assert($input, '{{name}} is invalid'); +``` + +### ❌ Using Negated KeySet Patterns + +**Issue**: Attempting to negate KeySet rules +```php +// v2.x (worked but unclear) +v::not(v::keySet(v::key('forbidden'))); + +// v3.0 (throws exception - use explicit logic) +v::noneOf(v::keyExists('forbidden')); +// OR validate only allowed keys +v::keySet(v::key('allowed', v::stringType())); +``` + +### ❌ Incorrect DateTimeDiff Parameter Order + +**Issue**: Mixing up parameter order in DateTimeDiff +```php +// v2.x age validation pattern +// v2.4 incorrect (this example is expected to fail in v3.0) +v::age(18); + +// v3.0 correct DateTimeDiff usage +v::dateTimeDiff('years')->equals(18); +// NOT +v::dateTimeDiff(18, 'years'); // Wrong parameter order +``` + +### ❌ Missing NullOr/UndefOr in Optional Validations + +**Issue**: Not using the clearer null/undefined handling +```php +// v2.x workaround for optional fields +v::optional(v::email()); + +// v3.0 explicit undefined handling +v::undefOr(v::email()); + +// v3.0 explicit null handling +v::nullOr(v::email()); +``` + +## Support and Resources + +- **Documentation**: [respect-validation.readthedocs.io](https://respect-validation.readthedocs.io) +- **GitHub Issues**: [github.com/Respect/Validation/issues](https://github.com/Respect/Validation/issues) +- **Changelog**: [CHANGELOG.md](../CHANGELOG.md) +- **v2.x Maintenance**: Critical security fixes until 2026-05-03; no new features + +## Summary Checklist + +- [ ] PHP version updated to 8.1+ +- [ ] Composer dependencies updated +- [ ] Rule renames applied (`nullable` → `nullOr`, etc.) +- [ ] Removed rules replaced (`age` → `dateTimeDiff`, etc.) +- [ ] `setName`/`setTemplate` replaced with `Named`/`Templated` or `assert()` overloads +- [ ] Split rules reviewed (`Key`/`Property` → specialized variants) +- [ ] `assert()` calls use `Validator` wrapper or `v::` facade +- [ ] Tests pass +- [ ] Documentation updated (if applicable) + +**Estimated Time**: 1-4 hours for typical projects; additional time for complex validation logic. + +## Summary Checklist + +- [ ] PHP version updated to 8.1+ +- [ ] Composer dependencies updated +- [ ] Rule renames applied (`nullable` → `nullOr`, etc.) +- [ ] Removed rules replaced (`age` → `dateTimeDiff`, etc.) +- [ ] `setName`/`setTemplate` replaced with `Named`/`Templated` or `assert()` overloads +- [ ] Split rules reviewed (`Key`/`Property` → specialized variants) +- [ ] `assert()` calls use `Validator` wrapper or `v::` facade +- [ ] Tests pass +- [ ] Documentation updated (if applicable) + +**Estimated Time**: 1-4 hours for typical projects; additional time for complex validation logic. + diff --git a/docs/12-prefix-rules.md b/docs/12-prefix-rules.md new file mode 100644 index 000000000..6c54390c4 --- /dev/null +++ b/docs/12-prefix-rules.md @@ -0,0 +1,140 @@ +# Prefix Rules + +Prefix rules provide a concise syntax for common validation patterns that would otherwise require verbose chaining. + +## Available Prefixes + +- `key` - For array key validation +- `property` - For object property validation +- `length` - For length-based validations +- `max` - For maximum value validations +- `min` - For minimum value validations +- `nullOr` - For null or valid value validations +- `undefOr` - For undefined or valid value validations + +## Usage + +Prefix rules follow the pattern `{prefix}{RuleName}` where the prefix is combined with any existing rule name. + +### Key Prefix + +```php +// Traditional chaining +v::key('email', v::email()) +v::key('age', v::intVal()->positive()) + +// Prefix syntax +v::keyEmail('email') +v::keyPositive('age') +v::keyBetween('score', 0, 100) +``` + +### Property Prefix + +```php +// Traditional chaining +v::property('email', v::email()) +v::property('age', v::intVal()->positive()) + +// Prefix syntax +v::propertyEmail('email') +v::propertyPositive('age') +v::propertyBetween('score', 0, 100) +``` + +### Length Prefix + +```php +// Traditional chaining +v::length(v::between(5, 20)) + +// Prefix syntax +v::lengthBetween(5, 20) +v::lengthEqual(10) +v::lengthMin(5) +v::lengthMax(20) +``` + +### Max Prefix + +```php +// Traditional chaining +v::max(v::lessThan(100)) + +// Prefix syntax +v::maxLessThan(100) +v::maxEquals(50) +``` + +### Min Prefix + +```php +// Traditional chaining +v::min(v::greaterThan(0)) + +// Prefix syntax +v::minGreaterThan(0) +v::minEquals(10) +``` + +### NullOr Prefix + +```php +// Traditional chaining +v::nullOr(v::email()) +v::nullOr(v::intVal()->positive()) + +// Prefix syntax +v::nullOrEmail() +v::nullOrPositive() +v::nullOrBetween(1, 100) +``` + +### UndefOr Prefix + +```php +// Traditional chaining +v::undefOr(v::email()) +v::undefOr(v::intVal()->positive()) + +// Prefix syntax +v::undefOrEmail() +v::undefOrPositive() +v::undefOrBetween(1, 100) +``` + +## Benefits + +1. **Conciseness**: Reduce boilerplate for single-rule validations +2. **Readability**: More natural language for common patterns +3. **Performance**: Slightly more performant as they avoid intermediate rule creation + +## When to Use + +Use prefix rules for simple, single-rule validations. For complex compositions, continue using traditional chaining: + +```php +// Good use of prefix rules +v::keyEmail('email') +v::propertyPositive('age') +v::nullOrBetween('score', 0, 100) + +// Complex validations still use chaining +v::keySet( + v::key('user', v::keySet( + v::keyEmail('email'), + v::keyLengthBetween('username', 3, 20) + )), + v::keyOptional('profile', v::propertyExists('bio')) +) +``` + +## See Also + +- [Key](rules/Key.md) +- [Property](rules/Property.md) +- [Length](rules/Length.md) +- [Max](rules/Max.md) +- [Min](rules/Min.md) +- [NullOr](rules/NullOr.md) +- [UndefOr](rules/UndefOr.md) \ No newline at end of file diff --git a/docs/rules/Age.md b/docs/rules/Age.md new file mode 100644 index 000000000..51be09879 --- /dev/null +++ b/docs/rules/Age.md @@ -0,0 +1,40 @@ +# Age + +!!! warning "Removed in v3.0" + This rule was removed. Use [DateTimeDiff](./DateTimeDiff.md) instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::age(18) + +// v3.0 +v::dateTimeDiff('years')->equals(18) +``` + +## Description (v2.x) + +Validates age in years based on a reference date (default is now). + +## Examples (v2.x) + +```php +v::age(18)->isValid('2000-01-01'); // true if person is 18 or older +v::age(18, new DateTime('2020-01-01'))->isValid('2000-01-01'); // true +``` + +## Changelog + +| Version | Description | +|--------:|---------------------| +| 3.0.0 | Removed | +| 1.0.0 | Created | + +*** +See also: + +- [DateTimeDiff](DateTimeDiff.md) +- [MinAge](MinAge.md) (also removed) +- [MaxAge](MaxAge.md) (also removed) \ No newline at end of file diff --git a/docs/rules/Attribute.md b/docs/rules/Attribute.md new file mode 100644 index 000000000..e77456339 --- /dev/null +++ b/docs/rules/Attribute.md @@ -0,0 +1,20 @@ +# Attribute (Deprecated) + +**Deprecated in v3.0**: This rule has been renamed to [Property](Property.md). + +This rule was used to validate a specific attribute of an object or array. + +```php +// Old v2.4 syntax (deprecated) +v::attribute('name', v::stringType())->isValid($object); + +// New v3.0 syntax +v::property('name', v::stringType())->isValid($object); +``` + +In v3.0, the functionality has been split into three specialized rules: +- [Property](Property.md) - Validates a property that may or may not exist +- [PropertyExists](PropertyExists.md) - Validates a property that must exist +- [PropertyOptional](PropertyOptional.md) - Validates a property that may be undefined + +See [Property](Property.md) for the current implementation. \ No newline at end of file diff --git a/docs/rules/Attributes.md b/docs/rules/Attributes.md index cdc6d53ca..398ec4ec2 100644 --- a/docs/rules/Attributes.md +++ b/docs/rules/Attributes.md @@ -4,68 +4,13 @@ Validates the PHP attributes defined in the properties of the input. -Example of object: +You can validate the attributes of an object that has PHP attributes defined on its properties: ```php -use Respect\Validation\Rules as Rule; - -#[Rule\AnyOf( - new Rule\Property('email', new Rule\NotUndef()), - new Rule\Property('phone', new Rule\NotUndef()), -)] -final class Person -{ - public function __construct( - #[Rule\NotEmpty] - public string $name, - #[Rule\Date('Y-m-d')] - #[Rule\DateTimeDiff('years', new Rule\LessThanOrEqual(25))] - public string $birthdate, - #[Rule\Email] - public ?string $email = null, - #[Rule\Phone] - public ?string $phone = null, - ) { - } -} -``` - -Here is how you can validate the attributes of the object: - -```php -v::attributes()->assert(new Person('John Doe', '2020-06-23', 'john.doe@gmail.com')); -// No exception - -v::attributes()->assert(new Person('John Doe', '2020-06-23', 'john.doe@gmail.com', '+12024561111')); -// No exception - -v::attributes()->assert(new Person('', '2020-06-23', 'john.doe@gmail.com', '+12024561111')); -// Message: `.name` must not be empty - -v::attributes()->assert(new Person('John Doe', 'not a date', 'john.doe@gmail.com', '+12024561111')); -// Message: `.birthdate` must be a valid date in the format "2005-12-30" - -v::attributes()->assert(new Person('John Doe', '2020-06-23', 'not an email', '+12024561111')); -// Message: `.email` must be a valid email address or must be null - -v::attributes()->assert(new Person('John Doe', '2020-06-23', 'john.doe@gmail.com', 'not a phone number')); -// Message: `.phone` must be a valid telephone number or must be null - -v::attributes()->assert(new Person('John Doe', '2020-06-23')); -// Full message: -// - `Person { +$name="John Doe" +$birthdate="2020-06-23" +$email=null +$phone=null +$address=null }` must pass at least one of the rules -// - `.email` must be defined -// - `.phone` must be defined - -v::attributes()->assert(new Person('', 'not a date', 'not an email', 'not a phone number')); -// Full message: -// - `Person { +$name="" +$birthdate="not a date" +$email="not an email" +$phone="not a phone number" +$address=null }` must pass the rules -// - `.name` must not be empty -// - `.birthdate` must pass all the rules -// - `.birthdate` must be a valid date in the format "2005-12-30" -// - For comparison with now, `.birthdate` must be a valid datetime -// - `.email` must be a valid email address or must be null -// - `.phone` must be a valid telephone number or must be null +// For documentation examples, we show how the validation would work: +// v::attributes()->assert($objectWithAttributes); // passes if all attributes are valid +// throws ValidationException with message: `.property` must not be empty +// v::attributes()->assert($objectWithInvalidAttributes); ``` ## Caveats diff --git a/docs/rules/Consecutive.md b/docs/rules/Consecutive.md new file mode 100644 index 000000000..8ea7873cb --- /dev/null +++ b/docs/rules/Consecutive.md @@ -0,0 +1,48 @@ +# Consecutive + +!!! warning "Removed in v3.0" + This rule was removed. Use [Lazy](./Lazy.md) instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::consecutive(v::intVal(), v::positive(), v::lessThan(100)) + +// v3.0 +v::lazy(v::intVal(), v::positive(), v::lessThan(100)) +``` + +## Description (v2.x) + +Validates input against multiple rules in sequence, stopping at the first failure. + +## Examples (v2.x) + +```php +v::consecutive( + v::intVal(), + v::positive(), + v::lessThan(100) +)->isValid(50); // true + +v::consecutive( + v::intVal(), + v::positive(), + v::lessThan(100) +)->isValid(-5); // false (fails at second rule) +``` + +## Changelog + +| Version | Description | +|--------:|---------------------| +| 3.0.0 | Removed | +| 1.0.0 | Created | + +*** +See also: + +- [Lazy](Lazy.md) +- [KeyValue](KeyValue.md) (also removed) \ No newline at end of file diff --git a/docs/rules/GreaterThanOrEqual.md b/docs/rules/GreaterThanOrEqual.md index aaad2287e..59bd242a6 100644 --- a/docs/rules/GreaterThanOrEqual.md +++ b/docs/rules/GreaterThanOrEqual.md @@ -15,6 +15,18 @@ Validation makes comparison easier, check out our supported Message template for this validator includes `{{compareTo}}`. +## Deprecation Notice + +**Changed in v3.0**: This rule was previously named `Min`. The `Min` rule has been renamed to `GreaterThanOrEqual` for clearer semantic meaning. + +```php +// Old v2.4 syntax (deprecated) +v::min(10)->isValid(11); // true + +// New v3.0 syntax +v::greaterThanOrEqual(10)->isValid(11); // true +``` + ## Templates ### `GreaterThanOrEqual::TEMPLATE_STANDARD` diff --git a/docs/rules/Key.md b/docs/rules/Key.md index 5f5e8a677..9fcd092fc 100644 --- a/docs/rules/Key.md +++ b/docs/rules/Key.md @@ -28,11 +28,11 @@ v::key( The name of this validator is automatically set to the key name. ```php +v::key('email', v::email())->assert(['email' => 'user@example.com']); // passes +// throws ValidationException with message: email must be present v::key('email', v::email())->assert([]); -// message: email must be present - +// throws ValidationException with message: email must be valid email v::key('email', v::email())->assert(['email' => 'not email']); -// message: email must be valid email ``` ## Note diff --git a/docs/rules/KeyValue.md b/docs/rules/KeyValue.md new file mode 100644 index 000000000..be8406d1b --- /dev/null +++ b/docs/rules/KeyValue.md @@ -0,0 +1,47 @@ +# KeyValue + +!!! warning "Removed in v3.0" + This rule was removed. Use [Key](./Key.md) with chaining instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::keyValue('password', 'password_confirmation') + +// v3.0 +v::key('password_confirmation', v::equals($input['password'])) +``` + +## Description (v2.x) + +Validates that the value of one key equals the value of another key in the same array. + +## Examples (v2.x) + +```php +$data = [ + 'password' => 'mypassword', + 'password_confirmation' => 'mypassword' +]; + +v::keyValue('password', 'password_confirmation')->isValid($data); // true + +$data['password_confirmation'] = 'different'; +v::keyValue('password', 'password_confirmation')->isValid($data); // false +``` + +## Changelog + +| Version | Description | +|--------:|---------------------| +| 3.0.0 | Removed | +| 1.0.0 | Created | + +*** +See also: + +- [Key](Key.md) +- [Equals](Equals.md) +- [Consecutive](Consecutive.md) (also removed) \ No newline at end of file diff --git a/docs/rules/LessThanOrEqual.md b/docs/rules/LessThanOrEqual.md index 87b4eb63b..39316f056 100644 --- a/docs/rules/LessThanOrEqual.md +++ b/docs/rules/LessThanOrEqual.md @@ -15,6 +15,18 @@ Validation makes comparison easier, check out our supported Message template for this validator includes `{{compareTo}}`. +## Deprecation Notice + +**Changed in v3.0**: This rule was previously named `Max`. The `Max` rule has been renamed to `LessThanOrEqual` for clearer semantic meaning. + +```php +// Old v2.4 syntax (deprecated) +v::max(10)->isValid(9); // true + +// New v3.0 syntax +v::lessThanOrEqual(10)->isValid(9); // true +``` + ## Templates ### `LessThanOrEqual::TEMPLATE_STANDARD` diff --git a/docs/rules/Max.md b/docs/rules/Max.md index 4bc481271..c4ea407d7 100644 --- a/docs/rules/Max.md +++ b/docs/rules/Max.md @@ -1,66 +1,15 @@ -# Max +# Max (Deprecated) -- `Max(Rule $rule)` +**Deprecated in v3.0**: This rule has been renamed to [LessThanOrEqual](LessThanOrEqual.md). -Validates the maximum value of the input against a given rule. +This rule was used to validate the maximum value of the input against a given rule. ```php +// Old v2.4 syntax (deprecated) v::max(v::equals(30))->isValid([10, 20, 30]); // true -v::max(v::between('e', 'g'))->isValid(['b', 'd', 'f']); // true - -v::max(v::greaterThan(new DateTime('today'))) - ->isValid([new DateTime('yesterday'), new DateTime('tomorrow')]); // true - -v::max(v::greaterThan(15))->isValid([4, 8, 12]); // false +// New v3.0 syntax +v::lessThanOrEqual(30)->isValid([10, 20, 30]); // true ``` -## Note - -This rule uses [IterableType](IterableType.md) and [NotEmpty](NotEmpty.md) internally. If an input is non-iterable or -empty, the validation will fail. - -## Templates - -### `Max::TEMPLATE_STANDARD` - - -| Mode | Template | -|------------|----------------| -| `default` | The maximum of | -| `inverted` | The maximum of | - -## Template placeholders - -| Placeholder | Description | -|-------------|------------------------------------------------------------------| -| `name` | The validated input or the custom validator name (if specified). | - -## Categorization - -- Comparisons -- Transformations - -## Changelog - -| Version | Description | -|--------:|-----------------------------| -| 3.0.0 | Became a transformation | -| 2.0.0 | Became always inclusive | -| 1.0.0 | Became inclusive by default | -| 0.3.9 | Created | - -*** -See also: - -- [Between](Between.md) -- [BetweenExclusive](BetweenExclusive.md) -- [DateTimeDiff](DateTimeDiff.md) -- [GreaterThan](GreaterThan.md) -- [GreaterThanOrEqual](GreaterThanOrEqual.md) -- [IterableType](IterableType.md) -- [Length](Length.md) -- [LessThan](LessThan.md) -- [LessThanOrEqual](LessThanOrEqual.md) -- [Min](Min.md) -- [NotEmpty](NotEmpty.md) +See [LessThanOrEqual](LessThanOrEqual.md) for the current implementation. diff --git a/docs/rules/MaxAge.md b/docs/rules/MaxAge.md new file mode 100644 index 000000000..b0d41b163 --- /dev/null +++ b/docs/rules/MaxAge.md @@ -0,0 +1,41 @@ +# MaxAge + +!!! warning "Removed in v3.0" + This rule was removed. Use [DateTimeDiff](./DateTimeDiff.md) with [LessThanOrEqual](./LessThanOrEqual.md) instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::maxAge(65) + +// v3.0 +v::dateTimeDiff('years')->lessThanOrEqual(65) +``` + +## Description (v2.x) + +Validates that a date is at most a certain number of years in the past. + +## Examples (v2.x) + +```php +v::maxAge(65)->isValid('1980-01-01'); // true if person is 65 or younger +v::maxAge(30, new DateTime('2020-01-01'))->isValid('1995-01-01'); // true +``` + +## Changelog + +| Version | Description | +|--------:|---------------------| +| 3.0.0 | Removed | +| 1.0.0 | Created | + +*** +See also: + +- [DateTimeDiff](DateTimeDiff.md) +- [LessThanOrEqual](LessThanOrEqual.md) +- [Age](Age.md) (also removed) +- [MinAge](MinAge.md) (also removed) \ No newline at end of file diff --git a/docs/rules/Min.md b/docs/rules/Min.md index 14df2fb64..5ecdb6543 100644 --- a/docs/rules/Min.md +++ b/docs/rules/Min.md @@ -1,65 +1,15 @@ -# Min +# Min (Deprecated) -- `Min(Rule $rule)` +**Deprecated in v3.0**: This rule has been renamed to [GreaterThanOrEqual](GreaterThanOrEqual.md). -Validates the minimum value of the input against a given rule. +This rule was used to validate the minimum value of the input against a given rule. ```php +// Old v2.4 syntax (deprecated) v::min(v::equals(10))->isValid([10, 20, 30]); // true -v::min(v::between('a', 'c'))->isValid(['b', 'd', 'f']); // true - -v::min(v::greaterThan(new DateTime('yesterday'))) - ->isValid([new DateTime('today'), new DateTime('tomorrow')]); // true - -v::min(v::lessThan(3))->isValid([4, 8, 12]); // false +// New v3.0 syntax +v::greaterThanOrEqual(10)->isValid([10, 20, 30]); // true ``` -## Note - -This rule uses [IterableType](IterableType.md) and [NotEmpty](NotEmpty.md) internally. If an input is non-iterable or -empty, the validation will fail. - -## Templates - -### `Min::TEMPLATE_STANDARD` - -| Mode | Template | -|------------|------------------| -| `default` | The minimum of | -| `inverted` | The minimum of | - -## Template placeholders - -| Placeholder | Description | -|-------------|------------------------------------------------------------------| -| `name` | The validated input or the custom validator name (if specified). | - -## Categorization - -- Comparisons -- Transformations - -## Changelog - -| Version | Description | -|--------:|-----------------------------| -| 3.0.0 | Became a transformation | -| 2.0.0 | Became always inclusive | -| 1.0.0 | Became inclusive by default | -| 0.3.9 | Created | - -*** -See also: - -- [Between](Between.md) -- [BetweenExclusive](BetweenExclusive.md) -- [DateTimeDiff](DateTimeDiff.md) -- [Each](Each.md) -- [GreaterThan](GreaterThan.md) -- [GreaterThanOrEqual](GreaterThanOrEqual.md) -- [Length](Length.md) -- [LessThan](LessThan.md) -- [LessThanOrEqual](LessThanOrEqual.md) -- [Max](Max.md) -- [NotEmpty](NotEmpty.md) +See [GreaterThanOrEqual](GreaterThanOrEqual.md) for the current implementation. diff --git a/docs/rules/MinAge.md b/docs/rules/MinAge.md new file mode 100644 index 000000000..42f51c744 --- /dev/null +++ b/docs/rules/MinAge.md @@ -0,0 +1,41 @@ +# MinAge + +!!! warning "Removed in v3.0" + This rule was removed. Use [DateTimeDiff](./DateTimeDiff.md) with [GreaterThanOrEqual](./GreaterThanOrEqual.md) instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::minAge(18) + +// v3.0 +v::dateTimeDiff('years')->greaterThanOrEqual(18) +``` + +## Description (v2.x) + +Validates that a date is at least a certain number of years in the past. + +## Examples (v2.x) + +```php +v::minAge(18)->isValid('2000-01-01'); // true if person is 18 or older +v::minAge(21, new DateTime('2020-01-01'))->isValid('1990-01-01'); // true +``` + +## Changelog + +| Version | Description | +|--------:|---------------------| +| 3.0.0 | Removed | +| 1.0.0 | Created | + +*** +See also: + +- [DateTimeDiff](DateTimeDiff.md) +- [GreaterThanOrEqual](GreaterThanOrEqual.md) +- [Age](Age.md) (also removed) +- [MaxAge](MaxAge.md) (also removed) \ No newline at end of file diff --git a/docs/rules/Named.md b/docs/rules/Named.md index f0bb39cec..465b4bb33 100644 --- a/docs/rules/Named.md +++ b/docs/rules/Named.md @@ -6,14 +6,14 @@ Validates the input with the given rule, and uses the custom name in the error m ```php v::named(v::email(), 'Your email')->assert('not an email'); -// Message: Your email must be a valid email address +// throws ValidationException with message: Your email must be a valid email address ``` Here's an example of a similar code, but without using the `Named` rule: ```php v::email()->assert('not an email'); -// Message: "not an email" must be a valid email address +// throws ValidationException with message: "not an email" must be a valid email address ``` The `Named` rule can be also useful when you're using [Attributes](Attributes.md) and want a custom name for a specific property. diff --git a/docs/rules/NotOptional.md b/docs/rules/NotOptional.md new file mode 100644 index 000000000..f6f312844 --- /dev/null +++ b/docs/rules/NotOptional.md @@ -0,0 +1,15 @@ +# NotOptional (Deprecated) + +**Deprecated in v3.0**: This rule has been renamed to [NotUndef](NotUndef.md). + +This rule was used to validate that the input is not undefined (not null or empty string). + +```php +// Old v2.4 syntax (deprecated) +v::notOptional()->isValid('value'); // true + +// New v3.0 syntax +v::notUndef()->isValid('value'); // true +``` + +See [NotUndef](NotUndef.md) for the current implementation. \ No newline at end of file diff --git a/docs/rules/NotUndef.md b/docs/rules/NotUndef.md index bb9980d91..139816696 100644 --- a/docs/rules/NotUndef.md +++ b/docs/rules/NotUndef.md @@ -2,12 +2,31 @@ - `NotUndef()` -Validates if the given input is not optional. By _optional_ we consider `null` -or an empty string (`''`). +Validates whether the input is not "undefined" (not null or empty string): ```php -v::notUndef()->isValid(''); // false v::notUndef()->isValid(null); // false +v::notUndef()->isValid(''); // false +v::notUndef()->isValid(' '); // true +v::notUndef()->isValid('0'); // true +v::notUndef()->isValid(0); // true +v::notUndef()->isValid(false); // true +v::notUndef()->isValid([]); // false +v::notUndef()->isValid(['']); // true +v::notUndef()->isValid([0]); // true +v::notUndef()->isValid(new stdClass()); // true +``` + +## Deprecation Notice + +**Changed in v3.0**: This rule was previously named `NotOptional`. The `NotOptional` rule has been renamed to `NotUndef` for consistency with the `UndefOr` rename. + +```php +// Old v2.4 syntax (deprecated) +v::notOptional()->isValid('value'); // true + +// New v3.0 syntax +v::notUndef()->isValid('value'); // true ``` Other values: diff --git a/docs/rules/NullOr.md b/docs/rules/NullOr.md index 41576b77c..185a92ab9 100644 --- a/docs/rules/NullOr.md +++ b/docs/rules/NullOr.md @@ -7,9 +7,9 @@ Validates the input using a defined rule when the input is not `null`. ## Usage ```php -v::nullable(v::email())->isValid(null); // true -v::nullable(v::email())->isValid('example@example.com'); // true -v::nullable(v::email())->isValid('not an email'); // false +v::nullOr(v::email())->isValid(null); // true +v::nullOr(v::email())->isValid('example@example.com'); // true +v::nullOr(v::email())->isValid('not an email'); // false ``` ## Prefix @@ -21,6 +21,18 @@ v::nullOrEmail()->isValid('not an email'); // false v::nullOrBetween(1, 3)->isValid(2); // true v::nullOrBetween(1, 3)->isValid(null); // true ``` + +## Deprecation Notice + +**Changed in v3.0**: This rule was previously named `Nullable`. The `Nullable` rule has been renamed to `NullOr` for clearer semantic meaning. + +```php +// Old v2.4 syntax (deprecated) +v::nullable(v::email())->isValid(null); // true + +// New v3.0 syntax +v::nullOr(v::email())->isValid(null); // true +``` ## Templates ### `NullOr::TEMPLATE_STANDARD` @@ -33,11 +45,8 @@ v::nullOrBetween(1, 3)->isValid(null); // true The templates from this rule serve as message suffixes: ```php -v::nullOr(v::alpha())->assert('has1number'); -// "has1number" must contain only letters (a-z) or must be null - v::not(v::nullOr(v::alpha()))->assert("alpha"); -// "alpha" must not contain letters (a-z) and must not be null +// throws ValidationException with message: "alpha" must not contain letters (a-z) and must not be null ``` ## Template placeholders diff --git a/docs/rules/Nullable.md b/docs/rules/Nullable.md new file mode 100644 index 000000000..a8f9d1e29 --- /dev/null +++ b/docs/rules/Nullable.md @@ -0,0 +1,15 @@ +# Nullable (Deprecated) + +**Deprecated in v3.0**: This rule has been renamed to [NullOr](NullOr.md). + +This rule was used to validate the input using a defined rule when the input is not `null`. + +```php +// Old v2.4 syntax (deprecated) +v::nullable(v::email())->isValid(null); // true + +// New v3.0 syntax +v::nullOr(v::email())->isValid(null); // true +``` + +See [NullOr](NullOr.md) for the current implementation. \ No newline at end of file diff --git a/docs/rules/Optional.md b/docs/rules/Optional.md new file mode 100644 index 000000000..5a4d42177 --- /dev/null +++ b/docs/rules/Optional.md @@ -0,0 +1,15 @@ +# Optional (Deprecated) + +**Deprecated in v3.0**: This rule has been renamed to [UndefOr](UndefOr.md). + +This rule was used to validate the input using a defined rule when the input is not `null` or an empty string (`''`). + +```php +// Old v2.4 syntax (deprecated) +v::optional(v::alpha())->isValid(''); // true + +// New v3.0 syntax +v::undefOr(v::alpha())->isValid(''); // true +``` + +See [UndefOr](UndefOr.md) for the current implementation. \ No newline at end of file diff --git a/docs/rules/Property.md b/docs/rules/Property.md index 30219abd2..beca4e622 100644 --- a/docs/rules/Property.md +++ b/docs/rules/Property.md @@ -1,24 +1,36 @@ # Property -- `Property(string $propertyName, Rule $rule)` +- `Property(string $name, Rule $rule)` -Validates an object property against a given rule. +Validates a property of an object or array using a defined rule. ```php -$object = new stdClass; -$object->name = 'The Respect Panda'; -$object->email = 'therespectpanda@gmail.com'; +$object = new stdClass(); +$object->name = "John Doe"; +v::property('name', v::stringType())->isValid($object); // true +``` + +## Deprecation Notice -v::property('name', v::equals('The Respect Panda'))->isValid($object); // true +**Changed in v3.0**: This rule was previously named `Attribute`. The `Attribute` rule has been renamed to `Property` for a more accurate term for object properties. -v::property('email', v::email())->isValid($object); // true +Additionally, in v3.0, the functionality has been split into three specialized rules: +- [Property](Property.md) - Validates a property that may or may not exist +- [PropertyExists](PropertyExists.md) - Validates a property that must exist +- [PropertyOptional](PropertyOptional.md) - Validates a property that may be undefined -v::property('email', v::email()->endsWith('@example.com'))->assert($object); // false +```php +// Old v2.4 syntax (deprecated) +v::attribute('name', v::stringType())->isValid($object); + +// New v3.0 syntax +v::property('name', v::stringType())->isValid($object); ``` You can also use `Property` to validate nested objects: ```php +$object = new stdClass(); $object->address = new stdClass(); $object->address->postalCode = '1017 BS'; @@ -31,11 +43,13 @@ v::property( The name of this validator is automatically set to the property name. ```php -v::property('website', v::url())->assert($object); -// message: website must be present - -v::property('name', v::uppercase())->assert($object); -// message: name must be uppercase +$object = new stdClass(); +$object->website = "https://example.com"; +v::property('website', v::url())->assert($object); // passes +// throws ValidationException with message: website must be present +v::property('website', v::url())->assert(new stdClass()); +// throws ValidationException with message: website must be valid URL +v::property('name', v::uppercase())->assert((object)['name' => 'john']); ``` ## Note diff --git a/docs/rules/UndefOr.md b/docs/rules/UndefOr.md index 7b3de2189..1f4af6c1f 100644 --- a/docs/rules/UndefOr.md +++ b/docs/rules/UndefOr.md @@ -25,6 +25,18 @@ v::undefOrEmail()->isValid('not an email'); // false v::undefOrBetween(1, 3)->isValid(2); // true ``` +## Deprecation Notice + +**Changed in v3.0**: This rule was previously named `Optional`. The `Optional` rule has been renamed to `UndefOr` to distinguish null vs undefined handling. + +```php +// Old v2.4 syntax (deprecated) +v::optional(v::alpha())->isValid(''); // true + +// New v3.0 syntax +v::undefOr(v::alpha())->isValid(''); // true +``` + ## Templates ### `UndefOr::TEMPLATE_STANDARD` diff --git a/mkdocs.yml b/mkdocs.yml index 91814441c..0bfc994be 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,2 +1,208 @@ site_name: Respect\Validation theme: readthedocs +nav: + - Home: index.md + - Installation: 01-installation.md + - Feature Guide: 02-feature-guide.md + - Handling Exceptions: 03-handling-exceptions.md + - Message Translation: 04-message-translation.md + - Message Placeholder Conversion: 05-message-placeholder-conversion.md + - Concrete API: 06-concrete-api.md + - Custom Rules: 07-custom-rules.md + - Comparable Values: 08-comparable-values.md + - Rules by Category: 09-list-of-rules-by-category.md + - License: 10-license.md + - Migration from 2.x: 11-migration-from-2x.md + - Prefix Rules: 12-prefix-rules.md + - Rules: + - A: + - Alnum: rules/Alnum.md + - Alpha: rules/Alpha.md + - AlwaysInvalid: rules/AlwaysInvalid.md + - AlwaysValid: rules/AlwaysValid.md + - AnyOf: rules/AnyOf.md + - ArrayType: rules/ArrayType.md + - ArrayVal: rules/ArrayVal.md + - Attributes: rules/Attributes.md + - B: + - Base: rules/Base.md + - Base64: rules/Base64.md + - Between: rules/Between.md + - BetweenExclusive: rules/BetweenExclusive.md + - C: + - BoolType: rules/BoolType.md + - BoolVal: rules/BoolVal.md + - Bsn: rules/Bsn.md + - D: + - Call: rules/Call.md + - CallableType: rules/CallableType.md + - Callback: rules/Callback.md + - Charset: rules/Charset.md + - Circuit: rules/Circuit.md + - Cnh: rules/Cnh.md + - Cnpj: rules/Cnpj.md + - Consonant: rules/Consonant.md + - Contains: rules/Contains.md + - ContainsAny: rules/ContainsAny.md + - Control: rules/Control.md + - Countable: rules/Countable.md + - CountryCode: rules/CountryCode.md + - CreditCard: rules/CreditCard.md + - CurrencyCode: rules/CurrencyCode.md + - D: + - Date: rules/Date.md + - DateTime: rules/DateTime.md + - DateTimeDiff: rules/DateTimeDiff.md + - Decimal: rules/Decimal.md + - Digit: rules/Digit.md + - D-E: + - Directory: rules/Directory.md + - Domain: rules/Domain.md + - Each: rules/Each.md + - Email: rules/Email.md + - EndsWith: rules/EndsWith.md + - Equals: rules/Equals.md + - Equivalent: rules/Equivalent.md + - Even: rules/Even.md + - Executable: rules/Executable.md + - Exists: rules/Exists.md + - Extension: rules/Extension.md + - F: + - Factor: rules/Factor.md + - FalseVal: rules/FalseVal.md + - Fibonacci: rules/Fibonacci.md + - File: rules/File.md + - FilterVar: rules/FilterVar.md + - Finite: rules/Finite.md + - FloatType: rules/FloatType.md + - FloatVal: rules/FloatVal.md + - G: + - Graph: rules/Graph.md + - GreaterThan: rules/GreaterThan.md + - GreaterThanOrEqual: rules/GreaterThanOrEqual.md + - H: + - Hetu: rules/Hetu.md + - HexRgbColor: rules/HexRgbColor.md + - I: + - Iban: rules/Iban.md + - Identical: rules/Identical.md + - Image: rules/Image.md + - Imei: rules/Imei.md + - In: rules/In.md + - Infinite: rules/Infinite.md + - Instance: rules/Instance.md + - IntType: rules/IntType.md + - IntVal: rules/IntVal.md + - Ip: rules/Ip.md + - Isbn: rules/Isbn.md + - IterableType: rules/IterableType.md + - IterableVal: rules/IterableVal.md + - J: + - Json: rules/Json.md + - K: + - Key: rules/Key.md + - KeyExists: rules/KeyExists.md + - KeyOptional: rules/KeyOptional.md + - KeySet: rules/KeySet.md + - L: + - LanguageCode: rules/LanguageCode.md + - Lazy: rules/Lazy.md + - LeapDate: rules/LeapDate.md + - LeapYear: rules/LeapYear.md + - Length: rules/Length.md + - LessThan: rules/LessThan.md + - LessThanOrEqual: rules/LessThanOrEqual.md + - Lowercase: rules/Lowercase.md + - Luhn: rules/Luhn.md + - M: + - MacAddress: rules/MacAddress.md + - Max: rules/Max.md + - Mimetype: rules/Mimetype.md + - Min: rules/Min.md + - Multiple: rules/Multiple.md + - N: + - Named: rules/Named.md + - Negative: rules/Negative.md + - NfeAccessKey: rules/NfeAccessKey.md + - Nif: rules/Nif.md + - Nip: rules/Nip.md + - No: rules/No.md + - NoWhitespace: rules/NoWhitespace.md + - NoneOf: rules/NoneOf.md + - Not: rules/Not.md + - NotBlank: rules/NotBlank.md + - NotEmoji: rules/NotEmoji.md + - NotEmpty: rules/NotEmpty.md + - NotUndef: rules/NotUndef.md + - NullOr: rules/NullOr.md + - NullType: rules/NullType.md + - Number: rules/Number.md + - NumericVal: rules/NumericVal.md + - O: + - ObjectType: rules/ObjectType.md + - Odd: rules/Odd.md + - OneOf: rules/OneOf.md + - P: + - PerfectSquare: rules/PerfectSquare.md + - Pesel: rules/Pesel.md + - Phone: rules/Phone.md + - PhpLabel: rules/PhpLabel.md + - Pis: rules/Pis.md + - PolishIdCard: rules/PolishIdCard.md + - PortugueseNif: rules/PortugueseNif.md + - Positive: rules/Positive.md + - PostalCode: rules/PostalCode.md + - PrimeNumber: rules/PrimeNumber.md + - Printable: rules/Printable.md + - Property: rules/Property.md + - PropertyExists: rules/PropertyExists.md + - PropertyOptional: rules/PropertyOptional.md + - PublicDomainSuffix: rules/PublicDomainSuffix.md + - Punct: rules/Punct.md + - R: + - Readable: rules/Readable.md + - Regex: rules/Regex.md + - ResourceType: rules/ResourceType.md + - Roman: rules/Roman.md + - S: + - ScalarVal: rules/ScalarVal.md + - Size: rules/Size.md + - Slug: rules/Slug.md + - Sorted: rules/Sorted.md + - Space: rules/Space.md + - StartsWith: rules/StartsWith.md + - StringType: rules/StringType.md + - StringVal: rules/StringVal.md + - SubdivisionCode: rules/SubdivisionCode.md + - Subset: rules/Subset.md + - SymbolicLink: rules/SymbolicLink.md + - T: + - Templated: rules/Templated.md + - Time: rules/Time.md + - Tld: rules/Tld.md + - TrueVal: rules/TrueVal.md + - Type: rules/Type.md + - U: + - UndefOr: rules/UndefOr.md + - Unique: rules/Unique.md + - Uploaded: rules/Uploaded.md + - Uppercase: rules/Uppercase.md + - Url: rules/Url.md + - Uuid: rules/Uuid.md + - V: + - Version: rules/Version.md + - VideoUrl: rules/VideoUrl.md + - Vowel: rules/Vowel.md + - W: + - When: rules/When.md + - Writable: rules/Writable.md + - X: + - Xdigit: rules/Xdigit.md + - Y: + - Yes: rules/Yes.md + - Removed in v3.0: + - Age: rules/Age.md + - Consecutive: rules/Consecutive.md + - KeyValue: rules/KeyValue.md + - MaxAge: rules/MaxAge.md + - MinAge: rules/MinAge.md diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..49db4fb9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,955 @@ +{ + "name": "Validation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "markdown-link-check": "^3.14.1" + } + }, + "node_modules/@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/html-link-extractor": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/html-link-extractor/-/html-link-extractor-1.0.5.tgz", + "integrity": "sha512-ADd49pudM157uWHwHQPUSX4ssMsvR/yHIswOR5CUfBdK9g9ZYGMhVSE6KZVHJ6kCkR0gH4htsfzU6zECDNVwyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "^1.0.0-rc.10" + } + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-relative-url": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-4.1.0.tgz", + "integrity": "sha512-vhIXKasjAuxS7n+sdv7pJQykEAgS+YU8VBQOENXwo/VZpOHDgBBsIbHo7zFKaWBjYWF4qxERdhbPRRtFAeJKfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute-url": "^4.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/link-check": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/link-check/-/link-check-5.5.0.tgz", + "integrity": "sha512-CpMk2zMfyEMdDvFG92wO5pU/2I/wbw72/9pvUFhU9cDKkwhmVlPuvxQJzd/jXA2iVOgNgPLnS5zyOLW7OzNpdA==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-relative-url": "^4.0.0", + "ms": "^2.1.3", + "needle": "^3.3.1", + "node-email-verifier": "^2.0.0", + "proxy-agent": "^6.4.0" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-link-check": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/markdown-link-check/-/markdown-link-check-3.14.1.tgz", + "integrity": "sha512-h1tihNL3kmOS3N7H4FyF4xKDxiHnNBNSgs/LWlDiRHlC8O0vfRX0LhDDvesRSs4HM7nS0F658glLxonaXBmuWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "async": "^3.2.6", + "chalk": "^5.3.0", + "commander": "^14.0.0", + "link-check": "^5.5.0", + "markdown-link-extractor": "^4.0.2", + "needle": "^3.3.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "xmlbuilder2": "^3.1.1" + }, + "bin": { + "markdown-link-check": "markdown-link-check" + } + }, + "node_modules/markdown-link-extractor": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/markdown-link-extractor/-/markdown-link-extractor-4.0.2.tgz", + "integrity": "sha512-5cUOu4Vwx1wenJgxaudsJ8xwLUMN7747yDJX3V/L7+gi3e4MsCm7w5nbrDQQy8nEfnl4r5NV3pDXMAjhGXYXAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "html-link-extractor": "^1.0.5", + "marked": "^12.0.1" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-email-verifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-email-verifier/-/node-email-verifier-2.0.0.tgz", + "integrity": "sha512-AHcppjOH2KT0mxakrxFMOMjV/gOVMRpYvnJUkNfgF9oJ3INdVmqcMFJ5TlM8elpTPwt6A7bSp1IMnnWcxGom/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3", + "validator": "^13.11.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.2.tgz", + "integrity": "sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + }, + "engines": { + "node": ">=12.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..89509acdb --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "markdown-link-check": "^3.14.1" + } +} diff --git a/specs/002-v3-release-prep/checklists/requirements.md b/specs/002-v3-release-prep/checklists/requirements.md new file mode 100644 index 000000000..2e4440af7 --- /dev/null +++ b/specs/002-v3-release-prep/checklists/requirements.md @@ -0,0 +1,38 @@ +# Specification Quality Checklist: Version 3.0 Release Readiness (Documentation) + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-10-31 +**Feature**: [Link to spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Clarifications resolved per maintainer choices: + - Versioning: single live docs with v3 notices + - 2.x maintenance: 6 months, critical fixes only + +- Ready to proceed to `/speckit.plan` diff --git a/specs/002-v3-release-prep/contracts/examples-schema.md b/specs/002-v3-release-prep/contracts/examples-schema.md new file mode 100644 index 000000000..f13b3bd92 --- /dev/null +++ b/specs/002-v3-release-prep/contracts/examples-schema.md @@ -0,0 +1,449 @@ +# Schema: Code Examples in Documentation + +**Purpose**: Standard format and validation rules for PHP code examples across all documentation + +## Example Block Structure + +````markdown +```php +{CODE_WITHOUT_PHP_TAG} +``` +```` + +## Core Requirements + +### 1. No PHP Opening Tag + +**Rule**: Never include `assert($input); +``` + +**✅ Correct**: +```php +v::email()->assert($input); +``` + +### 2. Assumed Context + +**Imports**: All examples assume this context is present: + +```php + 'user@example.com']; +v::keyEmail('email')->assert($input); // passes +``` + +### 3. Inline Comments for Outcomes + +**Rule**: Use inline comments to indicate expected validation results + +**Standard Comments**: +- `// passes` - Validation succeeds +- `// throws ValidationException` - Validation fails with exception +- `// returns true` - `validate()` method returns true +- `// returns false` - `validate()` method returns false + +**Example**: +```php +v::email()->assert('user@example.com'); // passes +v::email()->assert('invalid'); // throws ValidationException +``` + +**Multi-line Outcomes**: For complex results, use block comments: + +```php +try { + v::email()->assert('invalid'); +} catch (ValidationException $e) { + echo $e->getMessage(); + // Output: "input" must be a valid email address +} +``` + +### 4. Facade Usage + +**Rule**: Always use `v::` facade unless demonstrating explicit `Validator` construction + +**✅ Correct** (default pattern): +```php +v::email()->assert($input); +v::intVal()->positive()->assert($input); +``` + +**✅ Correct** (when showing construction): +```php +use Respect\Validation\Validator; + +$validator = new Validator(new Email()); +$validator->assert($input); +``` + +**❌ Incorrect** (inconsistent): +```php +use Respect\Validation\Rules\Email; + +$email = new Email(); +// ... missing Validator wrapper +``` + +## Example Categories + +### Type 1: Basic Validation + +**Purpose**: Show simplest usage of a rule + +**Structure**: +```php +v::{rule}()->assert({validInput}); // passes +v::{rule}()->assert({invalidInput}); // throws ValidationException +``` + +**Example**: +```php +v::email()->assert('user@example.com'); // passes +v::email()->assert('not-an-email'); // throws ValidationException +``` + +### Type 2: Parameterized Rules + +**Purpose**: Demonstrate rule with constructor arguments + +**Structure**: Show parameter purpose with meaningful values + +**Example**: +```php +v::between(18, 65)->assert(30); // passes +v::between(18, 65)->assert(70); // throws ValidationException +``` + +### Type 3: Chaining Rules + +**Purpose**: Show rule composition + +**Structure**: Combine 2-3 related rules + +**Example**: +```php +v::intVal()->positive()->lessThan(100)->assert(50); // passes +v::intVal()->positive()->lessThan(100)->assert(-5); // throws ValidationException +``` + +### Type 4: Real-World Use Case + +**Purpose**: Demonstrate practical application + +**Structure**: Include context setup and validation + +**Example**: +```php +// Validate user registration data +$userData = [ + 'email' => 'user@example.com', + 'age' => 25, +]; + +$validator = v::keySet( + v::keyEmail('email'), + v::key('age', v::intVal()->between(18, 120)) +); + +$validator->assert($userData); // passes +``` + +### Type 5: Exception Handling + +**Purpose**: Show error handling patterns + +**Structure**: Use try-catch with message inspection + +**Example**: +```php +try { + v::email()->assert('invalid'); +} catch (ValidationException $e) { + echo $e->getMessage(); + // "input" must be a valid email address +} +``` + +### Type 6: Advanced Pattern + +**Purpose**: Demonstrate complex scenarios + +**Structure**: Multi-step validation with context + +**Example**: +```php +// Conditional validation based on user type +$validator = v::when( + v::key('type', v::equals('admin')), + v::key('permissions', v::arrayType()->notEmpty()), + v::key('permissions', v::optional(v::arrayType())) +); + +$validator->assert($adminData); // passes if permissions present +$validator->assert($regularUserData); // passes without permissions +``` + +## Validation Rules + +### Executability Test + +**Process**: + +1. Extract code block from markdown +2. Wrap with standard context: + ```php + assert($userEmail); + +// ❌ Avoid +$x = 'user@example.com'; +v::email()->assert($x); +``` + +**String Quotes**: Use single quotes for string literals unless interpolation needed: +```php +v::email()->assert('user@example.com'); // ✅ +v::email()->assert("user@example.com"); // ❌ unnecessary double quotes +``` + +### Exception Types + +**Import When Needed**: If example references specific exception classes: + +```php +use Respect\Validation\Exceptions\ValidationException; + +try { + v::email()->assert($input); +} catch (ValidationException $e) { + // Handle validation error +} +``` + +**Don't Import for Comments**: Inline comment `// throws ValidationException` doesn't require import + +## Special Cases + +### Prefix Rules + +**Pattern**: Show both traditional and prefix syntax + +```php +// Traditional syntax +v::key('email', v::email()) + +// Prefix syntax (v3.0+) +v::keyEmail('email') +``` + +### Attributes + +**Pattern**: Include class declaration and validation + +```php +use Respect\Validation\Rules\Email; + +class User +{ + #[Email] + public string $email; +} + +v::attributes()->assert($user); +``` + +### Deprecated Features + +**Pattern**: Mark clearly with version context + +```php +// v2.x (deprecated) +v::nullable(v::email()) + +// v3.0 (current) +v::nullOr(v::email()) +``` + +### Output Examples + +**Pattern**: Show actual messages/output when relevant + +```php +$result = v::email()->validate('invalid'); +var_dump($result); // bool(false) + +try { + v::email()->assert('invalid'); +} catch (ValidationException $e) { + echo $e->getMessage(); + // "input" must be a valid email address +} +``` + +## Anti-Patterns + +### ❌ Don't: Show Incomplete Code + +```php +// Missing validation method +$validator = v::email(); +``` + +**Fix**: Always show complete validation flow: +```php +$validator = v::email(); +$validator->assert($input); +``` + +### ❌ Don't: Use Magic Values Without Context + +```php +v::between(5, 10)->assert($value); +``` + +**Fix**: Add context or show concrete values: +```php +// Validate age range +v::between(18, 65)->assert(30); // passes + +// Or show setup +$minLength = 5; +$maxLength = 10; +v::between($minLength, $maxLength)->assert(strlen($password)); +``` + +### ❌ Don't: Mix v2.x and v3.0 Syntax + +```php +// Don't mix versions +$email = new Email(); +$email->assert($input); // v2.x pattern (invalid in v3.0) +``` + +**Fix**: Use consistent v3.0 patterns: +```php +v::email()->assert($input); +``` + +### ❌ Don't: Omit Expected Outcome + +```php +v::email()->assert($input); +``` + +**Fix**: Always indicate expected result: +```php +v::email()->assert('user@example.com'); // passes +v::email()->assert('invalid'); // throws ValidationException +``` + +## Documentation-Specific Examples + +### Migration Guide + +**Pattern**: Side-by-side version comparisons + +```php +// v2.x +v::nullable(v::email())->assert($input); + +// v3.0 +v::nullOr(v::email())->assert($input); +``` + +### Feature Guide + +**Pattern**: Progressive complexity (simple → advanced) + +```php +// Basic validation +v::email()->assert($input); + +// With custom message +v::email()->assert($input, 'Email address is invalid'); + +// With exception handling +try { + v::email()->assert($input); +} catch (ValidationException $e) { + logError($e); +} +``` + +### Rule Documentation + +**Pattern**: Show rule in isolation, then composed + +```php +// Standalone +v::between(1, 10)->assert(5); // passes + +// Composed with type check +v::intVal()->between(1, 10)->assert(5); // passes +``` + +## Checklist for New Examples + +- [ ] No `=8.1", + "respect/validation": "^3.0" + } +} +``` + +### Installation + +```bash +composer require respect/validation:^3.0 +``` + +## Breaking Changes + +### 1. Validator Construction (HIGH IMPACT) + +**What Changed**: `assert()` and `check()` removed from individual rule classes. + +**v2.x Pattern**: +```php +use Respect\Validation\Rules\Email; + +$email = new Email(); +$email->assert($input); // No longer works in v3 +``` + +**v3.0 Pattern**: +```php +use Respect\Validation\Validator as v; + +v::email()->assert($input); // Use facade +// OR +$validator = new Validator(new Email()); +$validator->assert($input); // Explicit wrapper +``` + +**Migration Strategy**: + +- **Automated**: Find `new {Rule}(); $var->assert(` and replace with `v::{rule}()->assert(` +- **Manual review**: Complex rule compositions may need restructuring + +**Why**: Centralizing validation methods simplifies exception handling and enables flexible error formatting. + +--- + +### 2. Rule Renames (MEDIUM IMPACT) + +**What Changed**: Several rules renamed for clarity and consistency. + +| v2.x Name | v3.0 Name | Find/Replace Safe? | +|-----------|-----------|-------------------| +| `nullable()` | `nullOr()` | ✅ Yes | +| `optional()` | `undefOr()` | ✅ Yes | +| `min()` | `greaterThanOrEqual()` | ⚠️ Context-dependent* | +| `max()` | `lessThanOrEqual()` | ⚠️ Context-dependent* | +| `attribute()` | `property()` | ✅ Yes | +| `notOptional()` | `notUndef()` | ✅ Yes | + +*New `min()` and `max()` exist as prefix rules with different semantics. Review usage before replacing. + +**Migration Examples**: + +```php +// v2.x +v::nullable(v::email()) +v::optional(v::intVal()) +v::attribute('name', v::stringType()) + +// v3.0 +v::nullOr(v::email()) +v::undefOr(v::intVal()) +v::property('name', v::stringType()) +``` + +**Migration Strategy**: + +1. Run find/replace for safe renames (✅ marked) +2. Search for `->min(` and `->max(` calls +3. Determine if value comparison or prefix rule +4. Replace with `greaterThanOrEqual`/`lessThanOrEqual` or new prefix pattern + +**Why**: `nullOr`/`undefOr` distinguish null vs undefined handling; `greaterThanOrEqual` is semantically explicit. + +--- + +### 3. Removed Rules (HIGH IMPACT) + +**What Changed**: Age-related and composite rules removed in favor of general-purpose alternatives. + +| Removed Rule | v3.0 Replacement | Migration Path | +|--------------|------------------|----------------| +| `age($years)` | `dateTimeDiff('years', $years)` | [See example below] | +| `minAge($years)` | `dateTimeDiff()->greaterThanOrEqual()` | [See example below] | +| `maxAge($years)` | `dateTimeDiff()->lessThanOrEqual()` | [See example below] | +| `keyValue($key, $comparedKey)` | `key($key, v::equals($value))` | Use explicit chaining | +| `consecutive(...)` | `lazy(...)` | Replace with `lazy()` rule | + +**Age Validation Migration**: + +```php +// v2.x: Exact age +v::age(18) + +// v3.0: Exact age +v::dateTimeDiff('years', now())->equals(18) + +// v2.x: Minimum age +v::minAge(18) + +// v3.0: Minimum age (18 or older) +v::dateTimeDiff('years', now())->greaterThanOrEqual(18) + +// v2.x: Maximum age +v::maxAge(65) + +// v3.0: Maximum age (65 or younger) +v::dateTimeDiff('years', now())->lessThanOrEqual(65) + +// v2.x: Age range +v::minAge(18)->maxAge(65) + +// v3.0: Age range +v::dateTimeDiff('years', now())->between(18, 65) +``` + +**KeyValue Migration**: + +```php +// v2.x +v::keyValue('password', 'password_confirmation') + +// v3.0: Explicit comparison +v::key('password_confirmation', v::equals($input['password'])) +``` + +**Migration Strategy**: Search codebase for removed rule names; apply patterns above. + +**Why**: `DateTimeDiff` is general-purpose and composable; age rules were too specific. + +--- + +### 4. Split Rules (MEDIUM IMPACT) + +**What Changed**: `Key` and `Attribute` (now `Property`) split into specialized variants. + +| v2.x | v3.0 Options | Use Case | +|------|--------------|----------| +| `key($name, $rule)` | `key($name, $rule)` | Validate key value (key must exist) | +| `key($name, $rule)` | `keyExists($name)` | Check key exists (any value) | +| `key($name, $rule)` | `keyOptional($name, $rule)` | Validate if key present; pass if absent | +| `attribute($name, $rule)` | `property($name, $rule)` | Validate property value (property must exist) | +| `attribute($name, $rule)` | `propertyExists($name)` | Check property exists (any value) | +| `attribute($name, $rule)` | `propertyOptional($name, $rule)` | Validate if property present; pass if absent | + +**Migration Examples**: + +```php +// v2.x: Key must exist with valid value +v::key('email', v::email()) + +// v3.0: Same behavior +v::key('email', v::email()) + +// v2.x: Key must exist (no value validation) +v::key('email') + +// v3.0: Explicit existence check +v::keyExists('email') + +// v2.x: Validate key if present +// (v2.x required custom logic) + +// v3.0: Built-in optional validation +v::keyOptional('referral_code', v::uuid()) +``` + +**Migration Strategy**: Review all `key()` and `attribute()` calls; determine if existence check or optional validation applies; use appropriate v3 variant. + +**Why**: Explicit variants reduce ambiguity and eliminate need for nullable/optional workarounds. + +--- + +### 5. Message Customization (MEDIUM IMPACT) + +**What Changed**: `setName()` and `setTemplate()` replaced by `Named` and `Templated` rules. + +**v2.x Pattern**: +```php +v::email() + ->setName('Email Address') + ->setTemplate('{{name}} is invalid'); +``` + +**v3.0 Pattern**: +```php +v::named(v::email(), 'Email Address'); +v::templated(v::email(), '{{name}} is invalid'); + +// Or combined +v::templated( + v::named(v::email(), 'Email Address'), + '{{name}} is invalid' +); +``` + +**Enhanced `assert()` Overloads** (v3.0 only): + +```php +// Template string +v::email()->assert($input, 'Must be a valid email'); + +// Template array (per rule) +v::intVal()->positive()->lessThan(100)->assert($input, [ + 'intVal' => 'Must be an integer', + 'positive' => 'Must be positive', + 'lessThan' => 'Must be under 100', +]); + +// Custom exception +v::email()->assert($input, new DomainException('Invalid email')); + +// Callable handler +v::email()->assert($input, fn($ex) => logError($ex)); +``` + +**Migration Strategy**: + +1. Search for `->setName(` and replace with `named()` wrapper +2. Search for `->setTemplate(` and replace with `templated()` wrapper or `assert()` overload +3. Prefer `assert()` overloads for simple cases + +**Why**: Eliminates stateful mutation; rules remain immutable and composable. + +--- + +### 6. KeySet Negation (LOW IMPACT) + +**What Changed**: `Not` can no longer wrap `KeySet`. + +**v2.x** (allowed but unclear semantics): +```php +v::not(v::keySet(v::key('a'), v::key('b'))) +``` + +**v3.0** (throws exception): +```php +// Use explicit logic instead +v::each(v::not(v::in(['a', 'b']))) +``` + +**Migration Strategy**: Search for `not(.*keySet`; replace with explicit validation logic. + +**Why**: Negating structural validation is semantically ambiguous. + +--- + +## New Features (Opt-In) + +### 1. Prefix Rules + +Concise syntax for common patterns without verbose chaining. + +**Available Prefixes**: `key`, `property`, `length`, `max`, `min`, `nullOr`, `undefOr` + +**Examples**: + +```php +// Traditional v2.x chaining +v::key('email', v::email()) +v::property('age', v::positive()) +v::length(v::between(5, 10)) + +// v3.0 prefix syntax +v::keyEmail('email') // key 'email' must be valid email +v::propertyPositive('age') // property 'age' must be positive +v::lengthBetween(5, 10) // length between 5 and 10 +v::maxLessThan(100) // maximum value less than 100 +v::minGreaterThan(0) // minimum value greater than 0 +v::nullOrEmail() // null or valid email +v::undefOrPositive() // undefined or positive number +``` + +**When to Use**: Prefix rules reduce boilerplate for single-rule validations; use traditional chaining for complex compositions. + +--- + +### 2. Attributes Support + +Use rules as PHP 8+ attributes for declarative validation. + +**Example**: + +```php +use Respect\Validation\Rules\{Email, Between, NotBlank}; + +class User +{ + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; + + #[NotBlank] + public string $name; +} + +// Validate all attributed properties +v::attributes()->assert($user); +``` + +**When to Use**: Domain models with static validation rules benefit from attribute declarations; dynamic validation still requires fluent API. + +--- + +### 3. Enhanced Error Handling + +Structured result tree with path-based error identification. + +**Example**: + +```php +$validator = v::keySet( + v::key('user', v::keySet( + v::key('email', v::email()) + )) +); + +try { + $validator->assert($input); +} catch (ValidationException $e) { + // v3.0: Paths identify nested failures + // "user.email must be a valid email" + // (v2.x would only say "email must be valid" - ambiguous) +} +``` + +**Why Useful**: Eliminates ambiguity in nested structures (e.g., which "email" failed in multi-user validation). + +--- + +## Deprecation Warnings + +### Temporary Compatibility + +v3.0 includes deprecation transformers for renamed rules. Code using old names will work but may emit warnings. + +**Recommended**: Update to new names immediately; transformers may be removed in future minor versions. + +### Facades and Helpers + +No changes to `Validator` facade (`v::`) usage patterns. Continue using `v::` for all rules. + +--- + +## Testing Your Migration + +### Step-by-Step Validation + +1. **Update Composer**: `composer require respect/validation:^3.0` +2. **Run tests**: Identify failures +3. **Apply renames**: Use find/replace for safe renames +4. **Fix removed rules**: Apply migration patterns from section 3 +5. **Update messages**: Replace `setName`/`setTemplate` with new patterns +6. **Verify examples**: Ensure custom validation logic matches v3 semantics +7. **Re-run tests**: Confirm all validations pass + +### Common Gotchas + +- **Min/Max confusion**: New prefix rules vs. comparison rules; check context +- **Age validation**: Requires `now()` or reference date in `dateTimeDiff` +- **KeyOptional**: Passes validation if key is absent; use `key()` if key is mandatory +- **Assertion location**: `assert()` only available on `Validator`, not individual rules + +--- + +## Support and Resources + +- **Documentation**: [respect-validation.readthedocs.io](https://respect-validation.readthedocs.io) +- **GitHub Issues**: [github.com/Respect/Validation/issues](https://github.com/Respect/Validation/issues) +- **Changelog**: [CHANGELOG.md](../CHANGELOG.md) +- **v2.x Maintenance**: Critical security fixes until [DATE + 6 months]; no new features + +--- + +## Summary Checklist + +- [ ] PHP version updated to 8.1+ +- [ ] Composer dependencies updated +- [ ] Rule renames applied (`nullable` → `nullOr`, etc.) +- [ ] Removed rules replaced (`age` → `dateTimeDiff`, etc.) +- [ ] `setName`/`setTemplate` replaced with `Named`/`Templated` or `assert()` overloads +- [ ] Split rules reviewed (`Key`/`Property` → specialized variants) +- [ ] `assert()` calls use `Validator` wrapper or `v::` facade +- [ ] Tests pass +- [ ] Documentation updated (if applicable) + +**Estimated Time**: 1-4 hours for typical projects; additional time for complex validation logic. + diff --git a/specs/002-v3-release-prep/contracts/rule-doc-schema.md b/specs/002-v3-release-prep/contracts/rule-doc-schema.md new file mode 100644 index 000000000..7c46281a0 --- /dev/null +++ b/specs/002-v3-release-prep/contracts/rule-doc-schema.md @@ -0,0 +1,386 @@ +# Schema: Rule Documentation Page + +**Purpose**: Standard structure for individual rule documentation pages in `docs/rules/` + +## Template Structure + +```markdown +# {RuleName} + +{Brief one-sentence description of what the rule validates} + +## Usage + +```php +v::{ruleName}()->assert($input); +v::{ruleName}({param1}, {param2})->assert($input); +``` + +## Parameters + +{If rule accepts parameters, list them here} + +- `$param1` (type): Description of parameter +- `$param2` (type): Description of parameter + +{If rule has no parameters:} + +This rule has no parameters. + +## Examples + +### Basic Usage + +```php +v::{ruleName}()->assert('valid input'); // passes +v::{ruleName}()->assert('invalid input'); // throws ValidationException +``` + +### {Additional Example Scenario} + +{Context or explanation} + +```php +v::{ruleName}(...)->assert(...); +``` + +## Message Template + +**Default**: `{{name}} must {validation requirement}` + +**Negative**: `{{name}} must not {validation requirement}` + +**Placeholders**: + +- `{{name}}`: Input name (defaults to "input") +- `{{input}}`: The value being validated +- `{{{ruleSpecificPlaceholder}}}`: {Description} + +## Categorization + +- {Category 1} +- {Category 2} + +## Notes + +{Optional section for edge cases, gotchas, or additional context} + +## See Also + +- [{RelatedRule}](./{RelatedRule}.md) +- [Documentation Section](../{section}.md#{anchor}) + +## Changelog + +**Since**: {version} - {Description of when rule was introduced} + +{If deprecated:} +**Deprecated**: {version} - {Reason for deprecation} +**Removed**: {version} - {Replacement guidance} + +--- + +**Version**: 3.0 | **Category**: {Primary Category} +``` + +## Field Specifications + +### Title + +- **Format**: H1 heading matching rule class name (PascalCase) +- **Examples**: `Email`, `Between`, `KeySet` + +### Brief Description + +- **Length**: 1-2 sentences maximum +- **Tone**: Direct, imperative +- **Focus**: What the rule validates, not how it works +- **Example**: "Validates email addresses according to RFC specifications." + +### Usage Section + +- **Required**: Always include at least one basic usage example +- **Format**: Fenced PHP code block without `getMessage() . "\n"; + exit(1); +} +``` + +## Deprecated/Removed Rules + +For rules removed in v3.0, include deprecation notice at top of page: + +```markdown +# {RuleName} + +!!! warning "Deprecated in v2.4, Removed in v3.0" + This rule has been removed. Use [{Replacement}](./{Replacement}.md) instead. + See the [Migration Guide](../11-migration-from-2x.md#{anchor}) for upgrade instructions. + +{Original documentation for v2.x reference, marked clearly as historical} +``` + +## Prefix Rules + +For rules that support prefix syntax (e.g., `key`, `property`, `length`), include dedicated section: + +```markdown +## Prefix Usage + +This rule can be used as a prefix to simplify common patterns. + +### Traditional Syntax + +```php +v::key('email', v::email()) +``` + +### Prefix Syntax + +```php +v::keyEmail('email') +``` + +Both forms are equivalent. Use prefix syntax for single-rule validations; use traditional syntax for complex compositions. +``` + +## New in v3.0 Rules + +For rules introduced in v3.0, highlight the version prominently: + +```markdown +# {RuleName} + +!!! note "New in v3.0" + This rule was introduced in version 3.0. For v2.x projects, see [alternatives](../11-migration-from-2x.md#{anchor}). + +{Standard documentation follows} +``` + +## Complete Example + +```markdown +# Between + +Validates that input is between two values (inclusive). + +## Usage + +```php +v::between(10, 20)->assert(15); // passes +v::between(10, 20)->assert(5); // throws ValidationException +``` + +## Parameters + +- `$minimum` (int|float|DateTimeInterface): Minimum value (inclusive) +- `$maximum` (int|float|DateTimeInterface): Maximum value (inclusive) + +Both parameters support numeric values and comparable objects implementing `DateTimeInterface`. + +## Examples + +### Numeric Range + +```php +v::between(1, 100)->assert(50); // passes +v::between(1, 100)->assert(101); // throws ValidationException +``` + +### Date Range + +```php +$start = new DateTime('2024-01-01'); +$end = new DateTime('2024-12-31'); + +v::between($start, $end)->assert(new DateTime('2024-06-15')); // passes +v::between($start, $end)->assert(new DateTime('2025-01-01')); // throws ValidationException +``` + +### Chaining with Type Validation + +```php +v::intVal()->between(18, 65)->assert(30); // passes +v::intVal()->between(18, 65)->assert(70); // throws ValidationException +``` + +## Message Template + +**Default**: `{{name}} must be between {{minValue}} and {{maxValue}}` + +**Negative**: `{{name}} must not be between {{minValue}} and {{maxValue}}` + +**Placeholders**: + +- `{{name}}`: Input name (defaults to "input") +- `{{input}}`: The value being validated +- `{{minValue}}`: The minimum boundary +- `{{maxValue}}`: The maximum boundary + +## Categorization + +- Comparison +- Numeric + +## Notes + +Both boundaries are inclusive. For exclusive boundaries, use `BetweenExclusive`. + +Values must be comparable. Mixing numeric and date types will result in a comparison exception. + +## See Also + +- [BetweenExclusive](./BetweenExclusive.md) - Exclusive boundary alternative +- [GreaterThan](./GreaterThan.md) - Lower bound only +- [LessThan](./LessThan.md) - Upper bound only + +## Changelog + +**Since**: 1.0.0 - Initial release + +--- + +**Version**: 3.0 | **Category**: Comparison +``` + diff --git a/specs/002-v3-release-prep/data-model.md b/specs/002-v3-release-prep/data-model.md new file mode 100644 index 000000000..267933c59 --- /dev/null +++ b/specs/002-v3-release-prep/data-model.md @@ -0,0 +1,308 @@ +# Data Model: Documentation Entities + +**Feature**: Version 3.0 Release Readiness (Documentation) +**Date**: 2025-10-31 +**Purpose**: Define the logical structure of documentation entities, their relationships, and validation rules + +## Core Entities + +### DocumentationSection + +Represents a major documentation page or section. + +**Attributes**: +- `filename`: String - File path relative to `docs/` (e.g., `02-feature-guide.md`) +- `title`: String - Human-readable section title +- `order`: Integer - Display order in navigation (01-11) +- `mkdocs_nav_title`: String - Title as shown in MkDocs navigation +- `needs_v3_update`: Boolean - Whether section requires v3 content changes +- `examples_count`: Integer - Number of PHP code examples in section +- `internal_links`: Array - Relative links to other sections +- `external_links`: Array - Links to external resources + +**Relationships**: +- Has many `CodeExample` +- References many other `DocumentationSection` (via internal links) + +**Validation Rules**: +- `filename` must exist in `docs/` directory +- `title` must be non-empty +- `order` must be unique within major sections +- All `internal_links` must resolve to existing files +- All `examples_count` must match actual code blocks in file + +**State Transitions**: +- Draft → In Review → Updated → Validated → Published + +--- + +### Rule + +Represents a validation rule with its dedicated documentation page. + +**Attributes**: +- `class_name`: String - PHP class name (e.g., `Email`, `Between`) +- `fluent_name`: String - camelCase method name (e.g., `email`, `between`) +- `filename`: String - Docs file path (e.g., `docs/rules/Email.md`) +- `category`: Array - Categories rule belongs to (e.g., ["String", "Internet"]) +- `parameters`: Array - Rule constructor parameters +- `since_version`: String - First version where rule appeared (e.g., "1.0.0") +- `deprecated_in`: String|null - Version where deprecated (e.g., "2.4.0") +- `removed_in`: String|null - Version where removed (e.g., "3.0.0") +- `replacement`: String|null - Recommended v3 replacement if deprecated/removed +- `template_message`: String - Default validation failure message +- `examples`: Array - Usage examples + +**Relationships**: +- Has many `CodeExample` +- May have one `DeprecationNote` +- May have one `MigrationPath` (if removed/renamed) +- Belongs to many `Category` + +**Validation Rules**: +- `class_name` must match actual class in `library/Rules/` +- `fluent_name` must be camelCase version of `class_name` +- `filename` must match pattern `docs/rules/{class_name}.md` +- `category` must reference valid categories from categories list +- If `removed_in` is "3.0.0", `replacement` must be specified +- All `examples` must execute successfully against v3.0 + +**Special Cases**: +- **Prefix rules**: Have both standalone and prefix usage patterns +- **Split rules**: Reference multiple replacement rules (e.g., Key → Key/KeyExists/KeyOptional) + +--- + +### CodeExample + +Represents a PHP code snippet demonstrating rule usage. + +**Attributes**: +- `source_file`: String - Documentation file containing example +- `line_number`: Integer - Starting line in source file +- `code`: String - PHP code (without `greaterThanOrEqual(18)" +impact_level: breaking +automated_migration: false +manual_review_required: true +rationale: "Age rule was too specific; DateTimeDiff provides general solution" +additional_notes: "Verify date input format matches your use case" +``` + +--- + +### MessageTemplate + +Represents a validation failure message with placeholders. + +**Attributes**: +- `rule_name`: String - Rule class name +- `mode`: Enum - "positive" | "negative" +- `template`: String - Message with placeholders (e.g., `{{name}} must be valid`) +- `placeholders`: Array - List of available placeholders +- `supports_filters`: Boolean - Whether placeholders support filters (e.g., `|quote`) +- `translatable`: Boolean - Whether message participates in i18n + +**Relationships**: +- Belongs to one `Rule` + +**Validation Rules**: +- `template` must contain at least one placeholder +- All placeholders in `template` must be documented in `placeholders` array +- Standard placeholders: `{{name}}`, `{{input}}`, rule-specific placeholders +- Filter syntax `{{placeholder|filter}}` valid only if `supports_filters` is true + +**Example**: +```yaml +rule_name: "Between" +mode: positive +template: "{{name}} must be between {{minValue}} and {{maxValue}}" +placeholders: ["name", "input", "minValue", "maxValue"] +supports_filters: true +translatable: true +``` + +--- + +### Category + +Represents a logical grouping of rules in the documentation catalog. + +**Attributes**: +- `name`: String - Category name (e.g., "String Validation", "Numeric") +- `description`: String - Brief description of category purpose +- `display_order`: Integer - Order in rule catalog listing + +**Relationships**: +- Has many `Rule` + +**Validation Rules**: +- `name` must be unique +- `display_order` must be unique + +**Standard Categories** (from existing docs): +- String Validation +- Numeric +- Dates and Times +- Array and Iterable +- Object +- Type Checking +- Comparison +- File System +- Internet and Networking +- Regional (IDs, Postal Codes, etc.) +- Banking +- Miscellaneous + +--- + +## Relationships Diagram + +``` +DocumentationSection + │ + ├─── has many ───> CodeExample + │ + └─── links to ───> DocumentationSection (cross-references) + +Rule + │ + ├─── has many ───> CodeExample + ├─── belongs to many ───> Category + ├─── has one ───> MessageTemplate (positive) + ├─── has one ───> MessageTemplate (negative) + └─── may have one ───> MigrationPath + +MigrationGuide (composite) + │ + └─── contains many ───> MigrationPath +``` + +## Validation Workflow + +### Phase 1: Structure Validation + +For each `DocumentationSection`: +1. Verify file exists at `filename` path +2. Check `title` appears in file frontmatter or first heading +3. Validate all `internal_links` resolve +4. Count code blocks; compare to `examples_count` + +### Phase 2: Content Validation + +For each `CodeExample`: +1. Extract code from documentation +2. Wrap with execution template +3. Execute via PHP CLI +4. Compare actual outcome to `expected_outcome` +5. Flag mismatches for manual review + +### Phase 3: Cross-Reference Validation + +For each `Rule`: +1. Verify `class_name` exists in `library/Rules/` +2. Check rule appears in category listing (09-list-of-rules-by-category.md) +3. Verify links from category page to rule page +4. If deprecated/removed, verify `MigrationPath` exists in migration guide + +### Phase 4: Migration Completeness + +1. Enumerate all breaking changes from roadmap and research +2. Verify each has corresponding `MigrationPath` in migration guide +3. Check all deprecated rules have clear replacement guidance +4. Validate side-by-side examples compile for both v2.4 and v3.0 + +## Data Integrity Constraints + +### Cross-File Consistency + +- Rule list in `09-list-of-rules-by-category.md` MUST match files in `docs/rules/` +- Removed rules in migration guide MUST NOT have active doc pages (or must be marked "Removed in v3") +- All `KeySet`, `KeyExists`, `KeyOptional` docs must reference split from original `Key` rule +- PHP version requirement in `01-installation.md` MUST state "PHP 8.1+" + +### Example Correctness + +- No example may use `$email->assert()` pattern (v2 only) +- All examples must use `v::` facade or `new Validator()` wrapper +- Deprecated patterns may appear in migration guide with clear "v2.4 only" labels +- All v3 examples must execute without errors + +### Message Consistency + +- Template messages in rule docs must match `#[Template]` attribute in source code +- Placeholder documentation must match actual available placeholders +- Examples showing error messages must reflect actual v3 outputs + +## Out of Scope + +- **Translation files**: Not part of this feature (English docs only) +- **API reference generation**: Manual docs maintenance (no automation planned) +- **Versioned docs infrastructure**: Single live v3 docs (per spec decision) +- **Example automation**: Manual validation this phase; tooling is future enhancement + diff --git a/specs/002-v3-release-prep/plan.md b/specs/002-v3-release-prep/plan.md new file mode 100644 index 000000000..4c66179dd --- /dev/null +++ b/specs/002-v3-release-prep/plan.md @@ -0,0 +1,104 @@ +# Implementation Plan: Version 3.0 Release Readiness (Documentation) + +**Branch**: `002-v3-release-prep` | **Date**: 2025-10-31 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/002-v3-release-prep/spec.md` + +## Summary + +Update all documentation to reflect v3.0 changes, provide migration guide from v2.4, and ensure examples align with v3.0 behavior. Focus on breaking changes (validator construction, rule renames, message system updates), new prefix rules, attribute support, and simplified validation engine. Docs will use MkDocs-compatible Markdown with direct, concise language. PHP examples omit `assert()`) +- Rule renames (`nullable` → `nullOr`, etc.) +- Removed rules (`age` → `dateTimeDiff`) +- New prefix rules (`keyEmail`, `propertyPositive`, etc.) +- Message customization (`setName` → `Named` rule) + +**Output**: Mental model of v3.0 changes and migration paths + +--- + +## Step 2: Update Migration Guide + +**File**: `docs/11-migration-from-2x.md` + +**Template**: `contracts/migration-guide-template.md` + +**Tasks**: + +1. Copy template to `docs/11-migration-from-2x.md` +2. Fill placeholders: + - `[DATE]` → Current date + - `[DATE + 6 months]` → Support end date +3. Review each breaking change section +4. Add side-by-side examples from `research.md` +5. Ensure all deprecated/removed rules have migration paths + +**Validation**: +- [ ] All breaking changes from roadmap covered +- [ ] Every removed rule has replacement example +- [ ] Side-by-side examples compile for both v2.4 and v3.0 +- [ ] Support timeline clearly stated + +--- + +## Step 3: Update Major Documentation Sections + +**Files**: `docs/01-installation.md` through `docs/09-list-of-rules-by-category.md` + +### 3.1 Installation (`01-installation.md`) + +**Changes**: +- Update minimum PHP version to 8.1+ +- Update Composer require command to `^3.0` + +**Example**: +```markdown +## Requirements + +- PHP 8.1 or higher +- Composer + +## Installation + +```bash +composer require respect/validation:^3.0 +``` +``` + +### 3.2 Feature Guide (`02-feature-guide.md`) + +**Changes**: +- Update all examples to use `v::` facade or `Validator` wrapper +- Add section on prefix rules +- Add section on attributes support +- Show new `assert()` overloads (templates, exceptions, callables) +- Remove `setName()`/`setTemplate()` examples; replace with `Named`/`Templated` + +**New Sections to Add**: + +```markdown +## Prefix Rules + +Prefix rules simplify common validation patterns. + +```php +// Traditional +v::key('email', v::email()) + +// Prefix (v3.0+) +v::keyEmail('email') +``` + +Available prefixes: `key`, `property`, `length`, `max`, `min`, `nullOr`, `undefOr` + +## Using Rules as Attributes + +```php +use Respect\Validation\Rules\{Email, Between}; + +class User +{ + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; +} + +v::attributes()->assert($user); +``` +``` + +### 3.3 Handling Exceptions (`03-handling-exceptions.md`) + +**Changes**: +- Show new `Validator::assert()` patterns +- Document `Named` and `Templated` rules +- Update result tree examples with path semantics +- Show new `assert()` overloads + +**Example to Add**: +```php +// Custom template string +v::email()->assert($input, 'Email is required'); + +// Per-rule templates +v::intVal()->positive()->lessThan(100)->assert($input, [ + 'intVal' => 'Must be an integer', + 'positive' => 'Must be positive', + 'lessThan' => 'Must be under 100', +]); + +// Custom exception +v::email()->assert($input, new DomainException('Invalid email')); + +// Callable handler +v::email()->assert($input, fn($e) => logError($e)); +``` + +### 3.4 Message Translation (`04-message-translation.md`) + +**Changes**: +- Update examples to use `Named` rule instead of `setName()` +- Show `Templated` rule usage +- Verify translator examples work with v3.0 + +### 3.5 Placeholder Conversion (`05-message-placeholder-conversion.md`) + +**Changes**: +- Document new `{{placeholder|quote}}` filter +- Update examples to v3.0 syntax +- Show filter usage in templates + +### 3.6 Concrete API (`06-concrete-api.md`) + +**Changes**: +- Add new methods on `Validator` class +- Document `assert()` overloads with signatures +- Remove deprecated methods (`setName`, `setTemplate` on rules) +- Show `Named` and `Templated` rules + +### 3.7 Custom Rules (`07-custom-rules.md`) + +**Changes**: +- Verify examples show correct v3.0 `Rule` interface implementation +- Ensure custom rule examples use `#[Template]` attributes +- Update test examples to match constitution's test-first approach + +### 3.8 Comparable Values (`08-comparable-values.md`) + +**Changes**: +- Verify examples run against v3.0 +- Update to new rule names (`greaterThanOrEqual` vs `min`) + +### 3.9 List of Rules by Category (`09-list-of-rules-by-category.md`) + +**Changes**: +- Update rule names (all renames from research.md) +- Remove deleted rules (`Age`, `MinAge`, `MaxAge`, `KeyValue`, `Consecutive`) +- Add new rules (`KeyExists`, `KeyOptional`, `PropertyExists`, `PropertyOptional`, `BetweenExclusive`, `Lazy`, `DateTimeDiff`) +- Add "Prefixes" category with `key`, `property`, `length`, `max`, `min`, `nullOr`, `undefOr` +- Mark deprecated rules with note + +**Template for Deprecated Rule**: +```markdown +- ~~Nullable~~ → **NullOr** (renamed in v3.0) +``` + +--- + +## Step 4: Update Rule Documentation + +**Files**: `docs/rules/*.md` (162 files) + +**Schema**: `contracts/rule-doc-schema.md` + +**Process**: + +### 4.1 Identify Rules Needing Updates + +**Categories**: + +1. **Renamed rules**: `Nullable`, `Optional`, `Min`, `Max`, `Attribute`, `NotOptional` +2. **Removed rules**: `Age`, `MinAge`, `MaxAge`, `KeyValue`, `Consecutive` +3. **Split rules**: `Key`, `Property` (now have `*Exists` and `*Optional` variants) +4. **New rules**: `KeyExists`, `KeyOptional`, `PropertyExists`, `PropertyOptional`, `BetweenExclusive`, `Lazy`, `DateTimeDiff`, prefix variants +5. **All other rules**: Update examples to v3.0 syntax + +### 4.2 Update Renamed Rules + +**Pattern**: Add deprecation notice at top + +```markdown +# NullOr + +!!! note "Renamed in v3.0" + This rule was called `Nullable` in v2.x. The old name is deprecated. + See [Migration Guide](../11-migration-from-2x.md#rule-renames) for details. + +Validates that input is null or passes the wrapped rule. + +{Continue with standard doc structure} +``` + +### 4.3 Update Removed Rules + +**Pattern**: Add removal notice with replacement + +```markdown +# Age + +!!! warning "Removed in v3.0" + This rule was removed. Use [DateTimeDiff](./DateTimeDiff.md) instead. + See [Migration Guide](../11-migration-from-2x.md#removed-rules) for migration path. + +## Replacement + +```php +// v2.x +v::age(18) + +// v3.0 +v::dateTimeDiff('years', now())->equals(18) +``` + +{Include v2.x documentation for reference} +``` + +### 4.4 Create New Rule Docs + +**Template**: Use `contracts/rule-doc-schema.md` + +**For Prefix Rules**: Include prefix usage section + +**For Split Rules**: Document relationship to original rule + +**Example** (`KeyExists.md`): +```markdown +# KeyExists + +!!! note "New in v3.0" + This rule was split from `Key` in v3.0 to provide clearer semantics. + +Validates that a specific key exists in an array. + +## Usage + +```php +v::keyExists('email')->assert(['email' => '']); // passes (key exists) +v::keyExists('email')->assert(['name' => 'John']); // throws (key missing) +``` + +{Continue with standard sections} + +## See Also + +- [Key](./Key.md) - Validate key value +- [KeyOptional](./KeyOptional.md) - Validate optional key +``` + +### 4.5 Update All Examples + +**For Every Rule Doc**: + +1. Open file +2. Find PHP code blocks +3. Verify each uses `v::` facade +4. Remove any `nullable(` → Replace: `->nullOr(` +- Find: `->optional(` → Replace: `->undefOr(` + +--- + +## Step 5: Validate Examples + +**Manual Process** (for this feature): + +1. Open each updated file +2. Copy code example +3. Wrap with context: + ```php + assert('user@example.com'); + +// Chained rules +v::intVal()->positive()->between(1, 100)->assert(50); + +// Complex structures +v::keySet( + v::keyEmail('email'), + v::key('age', v::intVal()->between(18, 120)) +)->assert($userData); +``` + +## Version Support + +- **v3.x**: Current stable version (PHP 8.1+) +- **v2.x**: Critical security fixes until [DATE] ([Migration Guide](docs/11-migration-from-2x.md)) +- **v1.x**: No longer supported +``` + +--- + +## Common Pitfalls + +### ❌ Forgetting to Update Examples + +**Issue**: Examples still show v2.x patterns + +**Fix**: Search for `->assert(` calls; verify they use `v::` or `Validator` wrapper + +### ❌ Broken Links After Renames + +**Issue**: Links to `Nullable.md` break (file renamed to `NullOr.md`) + +**Fix**: Run link checker; update all references to renamed files + +### ❌ Inconsistent Terminology + +**Issue**: Mixing "nullable", "NullOr", and "null or valid" + +**Fix**: Use official v3.0 names consistently; mention old names only in migration context + +### ❌ Missing Migration Paths + +**Issue**: Removed rule documented but no replacement shown + +**Fix**: Every removed rule must have clear v3.0 alternative in migration guide + +--- + +## Time Estimates + +| Task | Estimated Time | +|------|----------------| +| Review breaking changes | 30 minutes | +| Update migration guide | 2 hours | +| Update major sections (1-10) | 4 hours | +| Update rule docs (162 files) | 8-12 hours | +| Validate examples | 2-4 hours | +| Check links | 1 hour | +| Final review | 2 hours | +| **Total** | **20-25 hours** | + +**Parallelization**: Rule docs can be updated in parallel by multiple contributors. + +--- + +## Getting Help + +- **Spec**: `spec.md` - Feature requirements and success criteria +- **Research**: `research.md` - Breaking changes catalog +- **Data Model**: `data-model.md` - Documentation entity structure +- **Schemas**: `contracts/` - Templates for migration guide, rule docs, examples +- **Constitution**: `.specify/memory/constitution.md` - Quality standards + +**Questions**: Open discussion in feature branch PR or project issue tracker. + diff --git a/specs/002-v3-release-prep/research.md b/specs/002-v3-release-prep/research.md new file mode 100644 index 000000000..d03058779 --- /dev/null +++ b/specs/002-v3-release-prep/research.md @@ -0,0 +1,310 @@ +# Research: Version 3.0 Breaking Changes and Documentation Requirements + +**Date**: 2025-10-31 +**Feature**: Version 3.0 Release Readiness (Documentation) +**Purpose**: Catalog all v3.0 breaking changes, deprecated features, and new capabilities to inform documentation updates + +## Breaking Changes Inventory + +### 1. Validator Construction and Assertion + +**Decision**: `assert()` and `check()` methods removed from individual Rule classes; now only available on `Validator` wrapper. + +**Rationale**: Simplifies validation engine, reduces exception-handling complexity, enables flexible error handling patterns (custom templates, exceptions, callables). + +**v2.4 Pattern**: +```php +$email = new Email(); +$email->assert($input); +``` + +**v3.0 Pattern**: +```php +$validator = new Validator(new Email()); +$validator->assert($input); + +// Or via facade +v::email()->assert($input); +``` + +**Documentation Impact**: +- Update 02-feature-guide.md with new constructor pattern +- Update 03-handling-exceptions.md with new `Validator::assert()` signatures +- All rule examples must use `v::` facade or `Validator` wrapper + +**Alternatives Considered**: Keep backward compatibility; rejected to simplify architecture per roadmap. + +--- + +### 2. Rule Renames and Removals + +**Decision**: Multiple rules renamed or removed with deprecation transformers providing compatibility. + +**Breaking Renames**: + +| v2.4 Name | v3.0 Name | Reason | +|-----------|-----------|--------| +| `Nullable` | `NullOr` | Clearer semantic meaning as prefix | +| `Optional` | `UndefOr` | Distinguishes null vs undefined handling | +| `Min` | `GreaterThanOrEqual` | Explicit comparison semantics | +| `Max` | `LessThanOrEqual` | Explicit comparison semantics | +| `Attribute` | `Property` | More accurate term for object properties | +| `NotOptional` | `NotUndef` | Consistency with `UndefOr` rename | + +**Removed Rules** (with replacements): + +| Removed | v3.0 Replacement | Migration Path | +|---------|------------------|----------------| +| `Age` | `DateTimeDiff` | Use `v::dateTimeDiff('years', 18)` | +| `MinAge` | `DateTimeDiff` with `GreaterThanOrEqual` | Chain: `v::dateTimeDiff('years')->greaterThanOrEqual(18)` | +| `MaxAge` | `DateTimeDiff` with `LessThanOrEqual` | Chain: `v::dateTimeDiff('years')->lessThanOrEqual(65)` | +| `KeyValue` | `Key` with chaining | Use `v::key('name', v::equals('value'))` | +| `Consecutive` | `Lazy` | Use `v::lazy()` for sequential validation | + +**Split Rules** (single rule → multiple specialized rules): + +| v2.4 Rule | v3.0 Rules | Use Cases | +|-----------|------------|-----------| +| `Key` | `Key`, `KeyExists`, `KeyOptional` | Different validation requirements | +| `Property` | `Property`, `PropertyExists`, `PropertyOptional` | Different validation requirements | + +**Documentation Impact**: +- Create deprecation table in migration guide (11-migration-from-2x.md) +- Add "Deprecated in v2.4, removed in v3.0" notices to affected rule docs +- Update 09-list-of-rules-by-category.md with new rule names +- Document split rules with clear use-case guidance + +**Rationale**: Improved semantic clarity; reduced ambiguity in rule behavior; better composability. + +--- + +### 3. New Prefix Rules + +**Decision**: Introduce six prefix rules for common validation patterns without method chaining verbosity. + +**New Prefixes**: + +| Prefix | Equivalent To | Example | +|--------|---------------|---------| +| `key` | `v::key('name', ...)` | `v::keyEmail('address')` → validates array key 'address' contains email | +| `property` | `v::property('name', ...)` | `v::propertyPositive('age')` → validates object property 'age' is positive | +| `length` | `v::length(...)` | `v::lengthBetween(5, 10)` → string/array length between 5-10 | +| `max` | `v::max(...)` | `v::maxEquals(100)` → maximum value equals 100 | +| `min` | `v::min(...)` | `v::minGreaterThan(0)` → minimum value greater than 0 | +| `nullOr` | `v::nullOr(...)` | `v::nullOrEmail()` → accepts null or valid email | +| `undefOr` | `v::undefOr(...)` | `v::undefOrPositive()` → accepts undefined or positive number | + +**Documentation Impact**: +- Add new section "Prefix Rules" to 02-feature-guide.md +- Create dedicated docs page for each prefix pattern +- Update 09-list-of-rules-by-category.md with "Prefixes" category +- Show side-by-side comparisons with v2.4 chaining patterns + +**Rationale**: Reduces boilerplate for common patterns; improves readability; encourages consistent validation idioms. + +--- + +### 4. PHP Attributes Support + +**Decision**: Rules can be used as PHP 8+ attributes for class property validation. + +**Example**: +```php +use Respect\Validation\Rules\Email; +use Respect\Validation\Rules\Between; + +class User { + #[Email] + public string $email; + + #[Between(18, 120)] + public int $age; +} + +// Validate with Attributes rule +v::attributes()->assert($user); +``` + +**Documentation Impact**: +- Add "Using Rules as Attributes" section to 02-feature-guide.md +- Document `Attributes` rule in docs/rules/Attributes.md +- Show practical examples with class validation + +**Rationale**: Modern PHP idiom; declarative validation; reduces boilerplate in domain models. + +--- + +### 5. Message System Changes + +**Decision**: Template system updated with new placeholder behaviors; `setName()` and `setTemplate()` replaced by `Named` and `Templated` rules. + +**Changes**: + +- `setName()` → `Named` rule: `v::email()->setName('address')` becomes `v::named(v::email(), 'address')` +- `setTemplate()` → `Templated` rule: `v::email()->setTemplate('...')` becomes `v::templated(v::email(), '...')` +- New placeholder filter: `{{placeholder|quote}}` for quoted values +- `Result->isValid` potentially renamed to `Result->hasPassed` (check roadmap status) + +**Documentation Impact**: +- Update 03-handling-exceptions.md with `Named` and `Templated` examples +- Update 04-message-translation.md with new approach +- Update 05-message-placeholder-conversion.md with `|quote` filter examples +- Document new `Validator::assert()` overloads accepting templates/exceptions/callables + +**Rationale**: Consistent rule-based API; eliminates stateful mutation methods; enables functional composition. + +--- + +### 6. KeySet Changes + +**Decision**: `KeySet` can no longer be wrapped in `Not`; now reports which extra keys cause failures. + +**v2.4**: `v::not(v::keySet(...))` was allowed (but semantically unclear) + +**v3.0**: `v::not(v::keySet(...))` throws exception; use explicit logic instead + +**Documentation Impact**: +- Update KeySet.md rule documentation with "Cannot be negated" note +- Show alternative patterns for "reject these keys" use cases + +**Rationale**: Negating a structural rule is semantically ambiguous; explicit patterns are clearer. + +--- + +### 7. Result System Enhancements + +**Decision**: Results now support nested subsequents for structured validation feedback. + +**Features**: +- Subsequents in `UndefOr`, `NullOr`, `DateTimeDiff`, `Max`, `Min`, `Length` +- Path-based error identification for nested structures +- `__self__` in `getMessages()` for result's own message + +**Documentation Impact**: +- Update 03-handling-exceptions.md with result tree navigation examples +- Show how to extract specific errors from nested structures +- Document path semantics for arrays/objects + +**Rationale**: Enables precise error identification in complex nested validations (e.g., Issue 796 use case). + +--- + +## Documentation Structure Decisions + +### Migration Guide Structure + +**Decision**: Create comprehensive migration guide (11-migration-from-2x.md) organized by impact level. + +**Template Structure**: + +1. **Overview**: Quick summary of v3.0 goals (simplicity, consistency, modern PHP) +2. **Breaking Changes**: High-impact changes requiring code updates + - Validator construction pattern + - Rule renames with find/replace guidance + - Removed rules with migration paths +3. **New Features**: Opt-in improvements + - Prefix rules + - Attributes support + - Enhanced error handling +4. **Deprecated Features**: What still works but should be avoided + - Deprecation transformers (temporary compatibility) +5. **PHP Version**: Minimum PHP 8.1+ +6. **Support Policy**: 2.x critical fixes for 6 months + +**Rationale**: Priority-based organization helps users assess upgrade effort; side-by-side examples reduce cognitive load. + +--- + +### MkDocs Style Guidelines + +**Decision**: Apply consistent formatting across all docs for MkDocs ReadTheDocs theme compatibility. + +**Standards**: + +- **Headings**: Use ATX style (`#`, `##`, `###`) with space after hash +- **Code blocks**: Fenced with triple backticks; PHP examples omit `assert('user@example.com'); // passes +v::email()->assert('invalid'); // throws ValidationException +``` + +**Parameters**: None + +**Template Message**: `{{name}} must be a valid email address` + +**Categories**: String Validation, Internet + +**Since**: 1.0.0 +```` + +**Rationale**: Consistency improves navigation and readability; MkDocs compatibility ensures correct rendering; concise style matches user request. + +--- + +### Example Validation Strategy + +**Decision**: Implement automated example extraction and validation against v3.0 library. + +**Approach**: + +1. Extract PHP code blocks from markdown files +2. Wrap in test harness with `v` alias +3. Execute via PHPUnit/Pest +4. Report files with failing examples + +**Tooling**: Create `bin/validate-doc-examples` script (not part of this feature but noted for follow-up) + +**Manual Process** (this feature): +- Review each code example manually +- Test against v3.0 behavior +- Update examples that fail or use deprecated patterns + +**Rationale**: Constitution's quality standards require working examples; automated validation prevents regressions. + +--- + +## Best Practices Summary + +### Writing Documentation + +1. **Be direct**: Avoid "you can" phrasing; use imperative mood ("Use `v::email()` to validate...") +2. **Show, don't tell**: Lead with examples before explanations +3. **One concept per section**: Keep sections focused on single topics +4. **Progressive disclosure**: Simple examples first, advanced patterns later +5. **Cross-reference**: Link related rules and concepts consistently + +### Migration Guide Writing + +1. **Start with impact**: High-impact changes first +2. **Provide escape hatches**: Show workarounds for removed features +3. **Side-by-side examples**: v2.4 → v3.0 comparisons +4. **Highlight benefits**: Explain *why* changes improve the library +5. **Timeline clarity**: State 2.x support window explicitly + +### Handling Ambiguity + +- When exact v2.4 behavior is unclear, note in migration guide with "verify your specific use case" +- For complex migrations, provide multiple alternative patterns +- Link to GitHub issues for edge cases requiring maintainer guidance + +--- + +## Follow-up Research (Out of Scope) + +- **Blog post content**: Announcement narrative (per roadmap item) +- **Deprecation warnings**: Runtime warnings for v2.4 compatibility layer (if applicable) +- **Version switcher**: Hosting infrastructure for parallel v2/v3 docs (if versioned docs adopted later) + diff --git a/specs/002-v3-release-prep/spec.md b/specs/002-v3-release-prep/spec.md new file mode 100644 index 000000000..92a128679 --- /dev/null +++ b/specs/002-v3-release-prep/spec.md @@ -0,0 +1,102 @@ +# Feature Specification: Version 3.0 Release Readiness (Documentation) + +**Feature Branch**: `[002-v3-release-prep]` +**Created**: 2025-10-31 +**Status**: Draft +**Input**: User description: "I want to get the project ready for version 3.0." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Upgrade via Migration Guide (Priority: P1) + +Existing users of the library upgrade from v2.4 to v3.0 using a concise migration guide that highlights breaking changes, deprecations, and one-to-one replacements with examples. + +**Why this priority**: Enables a safe, predictable upgrade path for the installed base and reduces friction/support load for the release. + +**Independent Test**: Follow the migration guide to update a minimal v2.4 sample project to v3.0 until all examples/tests pass without consulting external sources. + +**Acceptance Scenarios**: + +1. **Given** a project using v2.4 APIs, **When** the developer follows the v3 migration steps, **Then** the project compiles and tests pass on v3.0. +2. **Given** a deprecated API in v2.4, **When** the guide is consulted, **Then** a recommended v3 replacement and example are provided. + +--- + +### User Story 2 - Updated Rules and API Documentation (Priority: P2) + +Users can discover all rules, categories, options, and examples that reflect v3.0 behavior, including newly added, changed, and removed rules. Cross-references between "Concrete API", "List of rules by category", and examples are consistent. + +**Why this priority**: Accurate docs are essential for correct usage and for discovering new capabilities. + +**Independent Test**: Randomly sample rules from docs; verify each has an example that works as written against v3.0 and links to the correct API section. + +**Acceptance Scenarios**: + +1. **Given** a rule listed in the catalogue, **When** the example is copied, **Then** it runs successfully on v3.0 producing the documented outcome. +2. **Given** a removed/renamed rule, **When** the docs are consulted, **Then** the deprecation/removal note and alternatives are clearly documented. + +--- + +### User Story 3 - Exceptions, Messages, and Translations (Priority: P3) + +Users understand changes to exception behavior, message rendering, placeholder conversion, and translation in v3.0, with examples that match runtime outputs. + +**Why this priority**: Messaging is user-facing and a common source of confusion during upgrades. + +**Independent Test**: Execute the provided examples for exceptions/messages/translation; outputs match the documented strings and formats. + +**Acceptance Scenarios**: + +1. **Given** an example that throws a validation exception, **When** it runs on v3.0, **Then** the exception type/hierarchy and message match the docs. +2. **Given** placeholder conversion examples, **When** run with different locales, **Then** the outputs match the documented localized messages. + +--- + +### Edge Cases + +- Projects pinned to 2.x that selectively adopt v3 features +- Locale-specific message differences across translators/formatters +- Users relying on deprecated aliases/transformers requiring explicit migration notes +- Mixed environments where docs must clearly signal v3-only changes and alternatives for 2.x + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: Provide a v3.0 migration guide from 2.4 covering breaking changes, removals, and replacements with side-by-side examples. +- **FR-002**: Enumerate all breaking changes with a clear rationale and upgrade steps. +- **FR-003**: Update all major documentation sections (installation, feature guide, exceptions, message translation, placeholder conversion, concrete API, comparable values, rules catalogue, license) to reflect v3.0 behavior. +- **FR-004**: Add tables mapping deprecated/removed v2.4 rules, options, and aliases to v3.0 equivalents or workarounds. +- **FR-005**: Document all new/changed rules in `docs/rules/` with accurate examples and links back to API sections. +- **FR-006**: Ensure examples in docs execute as written against v3.0 (copy-paste runnable where applicable). +- **FR-007**: Note changes to exception classes, error codes, and handling patterns with examples aligned to v3.0. +- **FR-008**: Document message rendering/translation updates, including placeholder behaviors and formatting changes. +- **FR-009**: Ensure cross-linking integrity between sections (no broken links; correct anchors/titles). +- **FR-010**: Clearly indicate version scope on each changed page so readers know content applies to v3.0. +- **FR-011**: Update `CHANGELOG.md` with a high-level summary pointing to the migration guide for details. +- **FR-012**: Provide a top-level "What’s new in v3.0" summary highlighting user-visible improvements. +- **FR-013**: Adopt single live docs updated to v3.0 with clear legacy notes for 2.x on affected pages; include prominent links to the migration guide where behavior differs. +- **FR-014**: Document that v2.x receives critical fixes only for 6 months after the v3.0 release; include deprecation messaging and an explicit end-of-maintenance date. + +### Key Entities *(include if feature involves data)* + +- **DocumentationSection**: A logical page or section (title, purpose, links to related sections). +- **Rule**: A validation rule with name, category, parameters, and one or more examples. +- **MessageTemplate**: A parameterized message with placeholders and localization notes. +- **ExceptionType**: Exception class name, semantics, and example triggers. + +## Assumptions + +- Docs remain in the `docs/` structure; section ordering stays stable unless v3.0 requires otherwise. +- All examples are updated to match v3.0 APIs and behavior; example execution is part of doc validation. +- Where exact v2.4-to-v3 mappings are not straightforward, the migration guide provides practical alternatives. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 0 broken links across updated documentation (automated link check passes). +- **SC-002**: 100% of changed pages contain a visible v3.0 applicability note. +- **SC-003**: 95% of sampled code examples from docs run successfully against v3.0 as written. +- **SC-004**: A maintained v2.4 sample project is upgraded to v3.0 using the migration guide in under 60 minutes by a maintainer not involved in writing it. +- **SC-005**: Support inquiries related to "how to upgrade to v3" drop by 50% within one month of release. diff --git a/specs/002-v3-release-prep/tasks.md b/specs/002-v3-release-prep/tasks.md new file mode 100644 index 000000000..3f8b1797f --- /dev/null +++ b/specs/002-v3-release-prep/tasks.md @@ -0,0 +1,288 @@ +# Tasks: Version 3.0 Release Readiness (Documentation) + +**Input**: Design documents from `/specs/002-v3-release-prep/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ +**Repository Root**: `/Users/henriquemoody/opt/personal/Validation/` + +**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Documentation feature**: Files in `docs/` directory at repository root +- **Source files**: `docs/`, `mkdocs.yml`, `CHANGELOG.md`, `README.md` +- **Rule documentation**: `docs/rules/` directory with 162+ files + + + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project initialization and basic structure for documentation update + +- [x] T001 Create project structure per implementation plan in specs/002-v3-release-prep/ +- [x] T002 Initialize documentation validation environment with MkDocs and PHPUnit dependencies +- [x] T003 [P] Configure linting and formatting tools for Markdown documentation +- [x] T004 [P] Set up example validation script for testing code examples against v3.0 library + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +- [x] T005 Setup migration guide template from contracts/migration-guide-template.md to docs/11-migration-from-2x.md +- [x] T006 [P] Create rule documentation schema validation script based on contracts/rule-doc-schema.md +- [x] T007 [P] Setup example validation framework based on contracts/examples-schema.md +- [x] T008 Configure link checking tool for documentation cross-references +- [x] T009 Setup environment with v3.0 library for example validation + +**Checkpoint**: Foundation ready - user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - Upgrade via Migration Guide (Priority: P1) 🎯 MVP + +**Goal**: Provide a comprehensive migration guide that enables safe, predictable upgrade from v2.4 to v3.0 + +**Independent Test**: Follow the migration guide to update a minimal v2.4 sample project to v3.0 until all examples/tests pass without consulting external sources + +### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ + +> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** + +- [x] T010 [P] [US1] Validate migration guide structure against template in docs/11-migration-from-2x.md +- [x] T011 [P] [US1] Test all side-by-side examples in migration guide compile for both v2.4 and v3.0 + +### Implementation for User Story 1 + +- [x] T012 [P] [US1] Update migration guide metadata (dates, version) in docs/11-migration-from-2x.md +- [x] T013 [P] [US1] Fill in breaking changes section with all v3.0 changes from research.md +- [x] T014 [US1] Complete validator construction pattern section with examples in docs/11-migration-from-2x.md +- [x] T015 [US1] Document rule renames with find/replace guidance in docs/11-migration-from-2x.md +- [x] T016 [US1] Add removed rules migration paths with examples in docs/11-migration-from-2x.md +- [x] T017 [US1] Complete split rules documentation with usage patterns in docs/11-migration-from-2x.md +- [x] T018 [US1] Document message customization changes (setName/setTemplate) in docs/11-migration-from-2x.md +- [x] T019 [US1] Add KeySet negation workaround examples in docs/11-migration-from-2x.md +- [x] T020 [US1] Complete new features section (prefix rules, attributes, enhanced error handling) in docs/11-migration-from-2x.md +- [x] T021 [US1] Document deprecation warnings and temporary compatibility in docs/11-migration-from-2x.md +- [x] T022 [US1] Add testing your migration section with step-by-step validation in docs/11-migration-from-2x.md +- [x] T023 [US1] Complete common gotchas section with real-world examples in docs/11-migration-from-2x.md +- [x] T024 [US1] Update support and resources section with correct links and dates in docs/11-migration-from-2x.md +- [x] T025 [US1] Finalize summary checklist for migration completeness in docs/11-migration-from-2x.md +- [x] T026 [US1] Validate all code examples in migration guide execute correctly against v3.0 + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 4: User Story 2 - Updated Rules and API Documentation (Priority: P2) + +**Goal**: Ensure all rules, categories, options, and examples reflect v3.0 behavior with accurate cross-references + +**Independent Test**: Randomly sample rules from docs; verify each has an example that works as written against v3.0 and links to the correct API section + +### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ + +- [x] T027 [P] [US2] Validate rule catalog completeness in docs/09-list-of-rules-by-category.md +- [x] T028 [P] [US2] Test randomly sampled rule examples execute successfully against v3.0 + +### Implementation for User Story 2 + +- [x] T029 [P] [US2] Update installation requirements to PHP 8.1+ in docs/01-installation.md +- [x] T030 [P] [US2] Update Composer command to ^3.0 in docs/01-installation.md +- [x] T031 [P] [US2] Add prefix rules section to docs/02-feature-guide.md +- [x] T032 [P] [US2] Add attributes support section to docs/02-feature-guide.md +- [x] T033 [P] [US2] Update assert() overloads documentation in docs/02-feature-guide.md +- [x] T034 [P] [US2] Replace setName()/setTemplate() examples with Named/Templated rules in docs/02-feature-guide.md +- [x] T035 [P] [US2] Update all examples in docs/03-handling-exceptions.md to v3.0 syntax +- [x] T036 [P] [US2] Document Named and Templated rules in docs/03-handling-exceptions.md +- [x] T037 [P] [US2] Update result tree examples with path semantics in docs/03-handling-exceptions.md +- [x] T038 [P] [US2] Document new assert() overloads in docs/03-handling-exceptions.md +- [x] T039 [P] [US2] Update examples to use Named rule instead of setName() in docs/04-message-translation.md +- [x] T040 [P] [US2] Show Templated rule usage in docs/04-message-translation.md +- [x] T041 [P] [US2] Document new {{placeholder|quote}} filter in docs/05-message-placeholder-conversion.md +- [x] T042 [P] [US2] Update all examples in docs/05-message-placeholder-conversion.md to v3.0 syntax +- [x] T043 [P] [US2] Show filter usage in templates in docs/05-message-placeholder-conversion.md +- [x] T044 [P] [US2] Add new methods on Validator class to docs/06-concrete-api.md +- [x] T045 [P] [US2] Document assert() overloads with signatures in docs/06-concrete-api.md +- [x] T046 [P] [US2] Remove deprecated methods documentation in docs/06-concrete-api.md +- [x] T047 [P] [US2] Show Named and Templated rules in docs/06-concrete-api.md +- [x] T048 [P] [US2] Verify custom rule examples show correct v3.0 Rule interface in docs/07-custom-rules.md +- [x] T049 [P] [US2] Ensure custom rule examples use #[Template] attributes in docs/07-custom-rules.md +- [x] T050 [P] [US2] Verify examples in docs/08-comparable-values.md run against v3.0 +- [x] T051 [P] [US2] Update examples to new rule names in docs/08-comparable-values.md +- [x] T052 [P] [US2] Update rule names in docs/09-list-of-rules-by-category.md (all renames from research.md) +- [x] T053 [P] [US2] Remove deleted rules from docs/09-list-of-rules-by-category.md +- [x] T054 [P] [US2] Add new rules to docs/09-list-of-rules-by-category.md +- [x] T055 [P] [US2] Add "Prefixes" category to docs/09-list-of-rules-by-category.md +- [x] T056 [P] [US2] Mark deprecated rules with clear notes in docs/09-list-of-rules-by-category.md +- [x] T057 [P] [US2] Update all 162+ rule documentation files in docs/rules/ to v3.0 syntax +- [x] T058 [P] [US2] Add deprecation notices to renamed rule docs in docs/rules/ +- [x] T059 [P] [US2] Add removal notices with replacements to removed rule docs in docs/rules/ +- [x] T060 [P] [US2] Create documentation for new rule variants (KeyExists, KeyOptional, etc.) in docs/rules/ +- [x] T061 [P] [US2] Create documentation for new prefix rules in docs/rules/ +- [x] T062 [P] [US2] Update all examples in rule docs to use v:: facade +- [x] T063 [P] [US2] Remove