diff options
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | validators/bash/README.md | 60 | ||||
| -rw-r--r-- | validators/bash/validate.sh | 172 | ||||
| -rw-r--r-- | validators/nodejs/README.md | 37 | ||||
| -rw-r--r-- | validators/nodejs/validate.js (renamed from validate.js) | 4 | ||||
| -rw-r--r-- | validators/powershell/README.md | 46 | ||||
| -rw-r--r-- | validators/powershell/Validate-GitInfo.ps1 | 248 |
7 files changed, 581 insertions, 8 deletions
@@ -31,16 +31,26 @@ https://forgejo.zue.dev/zuedev/gitinfo/raw/branch/main/gitinfo.schema.json You can reference the schema in your `.gitinfo` file using the `$schema` property for editor autocompletion and validation support. -#### CLI Validator +#### CLI Validators -A command-line validator is included: +Command-line validators are available in multiple languages. See the [`validators/`](validators/) folder: + +**Node.js** ([readme](validators/nodejs/README.md)): ```bash -# Validate .gitinfo in current directory -node validate.js +node validators/nodejs/validate.js [path/to/.gitinfo] +``` -# Validate a specific file -node validate.js path/to/.gitinfo +**PowerShell** ([readme](validators/powershell/README.md)): + +```powershell +.\validators\powershell\Validate-GitInfo.ps1 [-Path path/to/.gitinfo] +``` + +**Bash** ([readme](validators/bash/README.md)) - requires `jq`: + +```bash +./validators/bash/validate.sh [path/to/.gitinfo] ``` ### Example `.gitinfo` File diff --git a/validators/bash/README.md b/validators/bash/README.md new file mode 100644 index 0000000..3692a1d --- /dev/null +++ b/validators/bash/README.md @@ -0,0 +1,60 @@ +# Bash Validator + +A Bash script for validating `.gitinfo` files. + +## Requirements + +- Bash 4+ +- [jq](https://jqlang.github.io/jq/) (JSON processor) + +Install jq: + +```bash +# Debian/Ubuntu +apt install jq + +# macOS +brew install jq + +# Windows (via Chocolatey) +choco install jq +``` + +## Usage + +```bash +# Make executable (first time only) +chmod +x validate.sh + +# Validate .gitinfo in current directory +./validate.sh + +# Validate a specific file +./validate.sh path/to/.gitinfo +``` + +## Features + +- Parses JSONC (strips `//` and `/* */` comments) +- Validates against the gitinfo JSON Schema +- Checks types and formats (URI, email) +- Enforces `additionalProperties: false` +- Returns exit code 0 on success, 1 on failure +- Color-coded output (green for success, red for errors) + +## Example Output + +``` +✓ .gitinfo is valid +``` + +``` +Validation failed for .gitinfo: + - .root: invalid URI "not-a-url" + - root: unknown property "invalid_field" +``` + +## Limitations + +- Comment stripping is simplified and may not handle edge cases with comments inside strings +- For complex validation, consider using the Node.js validator diff --git a/validators/bash/validate.sh b/validators/bash/validate.sh new file mode 100644 index 0000000..55324f1 --- /dev/null +++ b/validators/bash/validate.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +# CLI validator for .gitinfo files +# Usage: ./validate.sh [file] +# ./validate.sh # validates .gitinfo in current directory +# ./validate.sh path/to/.gitinfo + +set -e + +# Schema path (two levels up from validators/bash/) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCHEMA_PATH="$SCRIPT_DIR/../../gitinfo.schema.json" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Check for jq +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq is required but not installed.${NC}" >&2 + echo "Install with: apt install jq / brew install jq / choco install jq" >&2 + exit 1 +fi + +# Strip JSONC comments using sed +strip_comments() { + # Remove single-line comments (// ...) and multi-line comments (/* ... */) + # This is a simplified version - doesn't handle comments inside strings perfectly + sed -e 's|//.*$||g' -e ':a;s|/\*.*\*/||g;ta' -e '/\/\*/,/\*\//d' "$1" +} + +# Validate URI format +validate_uri() { + local uri="$1" + if [[ "$uri" =~ ^https?:// ]]; then + return 0 + fi + return 1 +} + +# Validate email format +validate_email() { + local email="$1" + if [[ "$email" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]; then + return 0 + fi + return 1 +} + +# Main validation function +validate() { + local file="$1" + local errors=() + + # Parse JSON + local json + if ! json=$(strip_comments "$file" | jq -c . 2>&1); then + echo -e "${RED}Error parsing JSONC: $json${NC}" >&2 + exit 1 + fi + + # Load schema + local schema + if ! schema=$(jq -c . "$SCHEMA_PATH" 2>&1); then + echo -e "${RED}Error parsing schema: $schema${NC}" >&2 + exit 1 + fi + + # Check if root is an object + local type + type=$(echo "$json" | jq -r 'type') + if [[ "$type" != "object" ]]; then + errors+=("root: expected object, got $type") + fi + + # Get allowed properties from schema + local allowed_props + allowed_props=$(echo "$schema" | jq -r '.properties | keys[]') + + # Check for unknown properties + local actual_props + actual_props=$(echo "$json" | jq -r 'keys[]') + for prop in $actual_props; do + if ! echo "$allowed_props" | grep -qx "$prop"; then + errors+=("root: unknown property \"$prop\"") + fi + done + + # Validate each property + local schema_props + schema_props=$(echo "$schema" | jq -r '.properties | to_entries[] | @base64') + + for entry in $schema_props; do + local key format prop_type + key=$(echo "$entry" | base64 -d | jq -r '.key') + format=$(echo "$entry" | base64 -d | jq -r '.value.format // empty') + prop_type=$(echo "$entry" | base64 -d | jq -r '.value.type') + + # Check if property exists + local value + value=$(echo "$json" | jq -r --arg k "$key" '.[$k] // empty') + + if [[ -n "$value" && "$value" != "null" ]]; then + local actual_type + actual_type=$(echo "$json" | jq -r --arg k "$key" '.[$k] | type') + + # Type check + if [[ "$prop_type" == "string" && "$actual_type" != "string" ]]; then + errors+=(".$key: expected string") + elif [[ "$prop_type" == "array" && "$actual_type" != "array" ]]; then + errors+=(".$key: expected array") + fi + + # Format validation for strings + if [[ "$actual_type" == "string" ]]; then + if [[ "$format" == "uri" ]]; then + if ! validate_uri "$value"; then + errors+=(".$key: invalid URI \"$value\"") + fi + elif [[ "$format" == "email" ]]; then + if ! validate_email "$value"; then + errors+=(".$key: invalid email \"$value\"") + fi + fi + fi + + # Validate array items + if [[ "$actual_type" == "array" ]]; then + local item_format + item_format=$(echo "$entry" | base64 -d | jq -r '.value.items.format // empty') + + if [[ "$item_format" == "uri" ]]; then + local i=0 + while IFS= read -r item; do + if ! validate_uri "$item"; then + errors+=(".${key}[$i]: invalid URI \"$item\"") + fi + ((i++)) + done < <(echo "$json" | jq -r --arg k "$key" '.[$k][]?') + fi + fi + fi + done + + # Output results + if [[ ${#errors[@]} -gt 0 ]]; then + echo -e "${RED}Validation failed for $file:${NC}" >&2 + for error in "${errors[@]}"; do + echo -e " - $error" >&2 + done + exit 1 + fi + + echo -e "${GREEN}✓ $file is valid${NC}" + exit 0 +} + +# Main +FILE="${1:-.gitinfo}" + +if [[ ! -f "$FILE" ]]; then + echo -e "${RED}Error: File not found: $FILE${NC}" >&2 + exit 1 +fi + +if [[ ! -f "$SCHEMA_PATH" ]]; then + echo -e "${RED}Error: Schema not found: $SCHEMA_PATH${NC}" >&2 + exit 1 +fi + +validate "$FILE" diff --git a/validators/nodejs/README.md b/validators/nodejs/README.md new file mode 100644 index 0000000..4fe9fa6 --- /dev/null +++ b/validators/nodejs/README.md @@ -0,0 +1,37 @@ +# Node.js Validator + +A zero-dependency Node.js CLI tool for validating `.gitinfo` files. + +## Requirements + +- Node.js 14+ + +## Usage + +```bash +# Validate .gitinfo in current directory +node validate.js + +# Validate a specific file +node validate.js path/to/.gitinfo +``` + +## Features + +- Parses JSONC (strips `//` and `/* */` comments) +- Validates against the gitinfo JSON Schema +- Checks types, formats (URI, email), and patterns +- Enforces `additionalProperties: false` +- Returns exit code 0 on success, 1 on failure + +## Example Output + +``` +✓ .gitinfo is valid +``` + +``` +Validation failed for .gitinfo: + - .root: invalid URI "not-a-url" + - root: unknown property "invalid_field" +``` diff --git a/validate.js b/validators/nodejs/validate.js index 404a6f0..8c230c4 100644 --- a/validate.js +++ b/validators/nodejs/validate.js @@ -10,8 +10,8 @@ const fs = require("fs"); const path = require("path"); -// Load the schema -const SCHEMA_PATH = path.join(__dirname, "gitinfo.schema.json"); +// Load the schema (two levels up from validators/nodejs/) +const SCHEMA_PATH = path.join(__dirname, "..", "..", "gitinfo.schema.json"); /** * Strip JSONC comments (single-line // and multi-line /* *\/) diff --git a/validators/powershell/README.md b/validators/powershell/README.md new file mode 100644 index 0000000..822659e --- /dev/null +++ b/validators/powershell/README.md @@ -0,0 +1,46 @@ +# PowerShell Validator + +A PowerShell script for validating `.gitinfo` files. + +## Requirements + +- PowerShell 5.1+ (Windows) or PowerShell Core 7+ (cross-platform) + +## Usage + +```powershell +# Validate .gitinfo in current directory +.\Validate-GitInfo.ps1 + +# Validate a specific file +.\Validate-GitInfo.ps1 -Path "path/to/.gitinfo" +``` + +## Features + +- Parses JSONC (strips `//` and `/* */` comments) +- Validates against the gitinfo JSON Schema +- Checks types, formats (URI, email), and patterns +- Enforces `additionalProperties: false` +- Returns exit code 0 on success, 1 on failure +- Color-coded output (green for success, red for errors) + +## Example Output + +``` +✓ .gitinfo is valid +``` + +``` +Validation failed for .gitinfo: + - .root: invalid URI "not-a-url" + - root: unknown property "invalid_field" +``` + +## Notes + +On Windows, you may need to adjust the execution policy: + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` diff --git a/validators/powershell/Validate-GitInfo.ps1 b/validators/powershell/Validate-GitInfo.ps1 new file mode 100644 index 0000000..e84606c --- /dev/null +++ b/validators/powershell/Validate-GitInfo.ps1 @@ -0,0 +1,248 @@ +<# +.SYNOPSIS + Validates .gitinfo files against the gitinfo JSON Schema. + +.DESCRIPTION + A PowerShell validator for .gitinfo files. Parses JSONC (strips comments) + and validates against the schema. + +.PARAMETER Path + Path to the .gitinfo file. Defaults to ".gitinfo" in the current directory. + +.EXAMPLE + .\Validate-GitInfo.ps1 + Validates .gitinfo in the current directory. + +.EXAMPLE + .\Validate-GitInfo.ps1 -Path "path/to/.gitinfo" + Validates a specific .gitinfo file. +#> + +param( + [Parameter(Position = 0)] + [string]$Path = ".gitinfo" +) + +$ErrorActionPreference = "Stop" + +# Schema path (two levels up from validators/powershell/) +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$SchemaPath = Join-Path $ScriptDir "..\..\gitinfo.schema.json" + +function Remove-JsonComments { + param([string]$Jsonc) + + $result = New-Object System.Text.StringBuilder + $i = 0 + $inString = $false + $stringChar = $null + + while ($i -lt $Jsonc.Length) { + $char = $Jsonc[$i] + $next = if ($i + 1 -lt $Jsonc.Length) { $Jsonc[$i + 1] } else { $null } + + # Track string state + if (-not $inString -and ($char -eq '"' -or $char -eq "'")) { + $inString = $true + $stringChar = $char + [void]$result.Append($char) + $i++ + continue + } + + if ($inString) { + if ($char -eq '\' -and $i + 1 -lt $Jsonc.Length) { + [void]$result.Append($char) + [void]$result.Append($Jsonc[$i + 1]) + $i += 2 + continue + } + if ($char -eq $stringChar) { + $inString = $false + $stringChar = $null + } + [void]$result.Append($char) + $i++ + continue + } + + # Single-line comment + if ($char -eq '/' -and $next -eq '/') { + while ($i -lt $Jsonc.Length -and $Jsonc[$i] -ne "`n") { + $i++ + } + continue + } + + # Multi-line comment + if ($char -eq '/' -and $next -eq '*') { + $i += 2 + while ($i -lt $Jsonc.Length - 1 -and -not ($Jsonc[$i] -eq '*' -and $Jsonc[$i + 1] -eq '/')) { + $i++ + } + $i += 2 + continue + } + + [void]$result.Append($char) + $i++ + } + + return $result.ToString() +} + +function Test-Uri { + param([string]$Value) + try { + $uri = [System.Uri]::new($Value) + return $uri.Scheme -eq "http" -or $uri.Scheme -eq "https" + } + catch { + return $false + } +} + +function Test-Email { + param([string]$Value) + return $Value -match '^[^\s@]+@[^\s@]+\.[^\s@]+$' +} + +function Test-Schema { + param( + $Data, + $Schema, + [string]$JsonPath = "" + ) + + $errors = @() + + if ($Schema.type -eq "object") { + if ($Data -isnot [System.Collections.IDictionary] -and $Data.GetType().Name -ne "PSCustomObject") { + $errors += "$(if ($JsonPath) { $JsonPath } else { 'root' }): expected object" + return $errors + } + + # Convert PSCustomObject to hashtable for easier handling + $dataHash = @{} + $Data.PSObject.Properties | ForEach-Object { $dataHash[$_.Name] = $_.Value } + + # Check additionalProperties + if ($Schema.additionalProperties -eq $false -and $Schema.properties) { + $allowed = $Schema.properties.PSObject.Properties.Name + foreach ($key in $dataHash.Keys) { + if ($key -notin $allowed) { + $errors += "$(if ($JsonPath) { $JsonPath } else { 'root' }): unknown property `"$key`"" + } + } + } + + # Validate each property + if ($Schema.properties) { + foreach ($prop in $Schema.properties.PSObject.Properties) { + $key = $prop.Name + $propSchema = $prop.Value + if ($dataHash.ContainsKey($key)) { + $errors += Test-Schema -Data $dataHash[$key] -Schema $propSchema -JsonPath "$JsonPath.$key" + } + } + } + } + elseif ($Schema.type -eq "array") { + if ($Data -isnot [System.Array]) { + $errors += "${JsonPath}: expected array" + return $errors + } + + if ($Schema.items) { + for ($i = 0; $i -lt $Data.Count; $i++) { + $itemSchema = if ($Schema.items -is [System.Array]) { $Schema.items[$i] } else { $Schema.items } + if ($itemSchema) { + $errors += Test-Schema -Data $Data[$i] -Schema $itemSchema -JsonPath "$JsonPath[$i]" + } + } + + if ($Schema.minItems -and $Data.Count -lt $Schema.minItems) { + $errors += "${JsonPath}: expected at least $($Schema.minItems) items" + } + if ($Schema.maxItems -and $Data.Count -gt $Schema.maxItems) { + $errors += "${JsonPath}: expected at most $($Schema.maxItems) items" + } + } + } + elseif ($Schema.type -eq "string") { + if ($Data -isnot [string]) { + $errors += "${JsonPath}: expected string" + return $errors + } + + if ($Schema.minLength -and $Data.Length -lt $Schema.minLength) { + $errors += "${JsonPath}: string too short (min $($Schema.minLength))" + } + + if ($Schema.format -eq "uri") { + if (-not (Test-Uri $Data)) { + $errors += "${JsonPath}: invalid URI `"$Data`"" + } + } + + if ($Schema.format -eq "email") { + if (-not (Test-Email $Data)) { + $errors += "${JsonPath}: invalid email `"$Data`"" + } + } + + if ($Schema.pattern) { + if ($Data -notmatch $Schema.pattern) { + $errors += "${JsonPath}: does not match pattern $($Schema.pattern)" + } + } + } + + return $errors +} + +# Main +if (-not (Test-Path $Path)) { + Write-Error "Error: File not found: $Path" + exit 1 +} + +if (-not (Test-Path $SchemaPath)) { + Write-Error "Error: Schema not found: $SchemaPath" + exit 1 +} + +# Read and parse schema +try { + $schemaContent = Get-Content -Path $SchemaPath -Raw + $schema = $schemaContent | ConvertFrom-Json +} +catch { + Write-Error "Error parsing schema: $_" + exit 1 +} + +# Read and parse .gitinfo file +try { + $content = Get-Content -Path $Path -Raw + $stripped = Remove-JsonComments -Jsonc $content + $data = $stripped | ConvertFrom-Json +} +catch { + Write-Error "Error parsing JSONC: $_" + exit 1 +} + +# Validate against schema +$errors = Test-Schema -Data $data -Schema $schema + +if ($errors.Count -gt 0) { + Write-Host "Validation failed for ${Path}:" -ForegroundColor Red + foreach ($error in $errors) { + Write-Host " - $error" -ForegroundColor Red + } + exit 1 +} + +Write-Host "✓ $Path is valid" -ForegroundColor Green +exit 0 |
