diff options
Diffstat (limited to 'validators')
| -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 | 234 | ||||
| -rw-r--r-- | validators/powershell/README.md | 46 | ||||
| -rw-r--r-- | validators/powershell/Validate-GitInfo.ps1 | 248 |
6 files changed, 797 insertions, 0 deletions
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/validators/nodejs/validate.js b/validators/nodejs/validate.js new file mode 100644 index 0000000..8c230c4 --- /dev/null +++ b/validators/nodejs/validate.js @@ -0,0 +1,234 @@ +#!/usr/bin/env node + +/** + * CLI validator for .gitinfo files + * Usage: node validate.js [file] + * node validate.js # validates .gitinfo in current directory + * node validate.js path/to/.gitinfo + */ + +const fs = require("fs"); +const path = require("path"); + +// 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 /* *\/) + */ +function stripJsonComments(jsonc) { + let result = ""; + let i = 0; + let inString = false; + let stringChar = null; + + while (i < jsonc.length) { + const char = jsonc[i]; + const next = jsonc[i + 1]; + + // Track string state + if (!inString && (char === '"' || char === "'")) { + inString = true; + stringChar = char; + result += char; + i++; + continue; + } + + if (inString) { + if (char === "\\" && i + 1 < jsonc.length) { + // Escape sequence + result += char + jsonc[i + 1]; + i += 2; + continue; + } + if (char === stringChar) { + inString = false; + stringChar = null; + } + result += char; + i++; + continue; + } + + // Single-line comment + if (char === "/" && next === "/") { + while (i < jsonc.length && jsonc[i] !== "\n") { + i++; + } + continue; + } + + // Multi-line comment + if (char === "/" && next === "*") { + i += 2; + while ( + i < jsonc.length - 1 && + !(jsonc[i] === "*" && jsonc[i + 1] === "/") + ) { + i++; + } + i += 2; + continue; + } + + result += char; + i++; + } + + return result; +} + +/** + * Simple JSON Schema validator (subset of draft 2020-12) + */ +function validateSchema(data, schema, path = "") { + const errors = []; + + if (schema.type === "object") { + if (typeof data !== "object" || data === null || Array.isArray(data)) { + errors.push(`${path || "root"}: expected object`); + return errors; + } + + // Check additionalProperties + if (schema.additionalProperties === false && schema.properties) { + const allowed = new Set(Object.keys(schema.properties)); + for (const key of Object.keys(data)) { + if (!allowed.has(key)) { + errors.push(`${path || "root"}: unknown property "${key}"`); + } + } + } + + // Validate each property + if (schema.properties) { + for (const [key, propSchema] of Object.entries(schema.properties)) { + if (data[key] !== undefined) { + errors.push( + ...validateSchema(data[key], propSchema, `${path}.${key}`), + ); + } + } + } + } else if (schema.type === "array") { + if (!Array.isArray(data)) { + errors.push(`${path}: expected array`); + return errors; + } + + if (schema.items) { + if (Array.isArray(schema.items)) { + // Tuple validation + for (let i = 0; i < data.length; i++) { + const itemSchema = schema.items[i] || {}; + errors.push(...validateSchema(data[i], itemSchema, `${path}[${i}]`)); + } + if (schema.minItems && data.length < schema.minItems) { + errors.push(`${path}: expected at least ${schema.minItems} items`); + } + if (schema.maxItems && data.length > schema.maxItems) { + errors.push(`${path}: expected at most ${schema.maxItems} items`); + } + } else { + // Array of same type + for (let i = 0; i < data.length; i++) { + errors.push( + ...validateSchema(data[i], schema.items, `${path}[${i}]`), + ); + } + } + } + } else if (schema.type === "string") { + if (typeof data !== "string") { + errors.push(`${path}: expected string`); + return errors; + } + + if (schema.minLength && data.length < schema.minLength) { + errors.push(`${path}: string too short (min ${schema.minLength})`); + } + + if (schema.format === "uri") { + try { + new URL(data); + } catch { + errors.push(`${path}: invalid URI "${data}"`); + } + } + + if (schema.format === "email") { + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) { + errors.push(`${path}: invalid email "${data}"`); + } + } + + if (schema.pattern) { + if (!new RegExp(schema.pattern).test(data)) { + errors.push(`${path}: does not match pattern ${schema.pattern}`); + } + } + } + + return errors; +} + +function main() { + const args = process.argv.slice(2); + const filePath = args[0] || ".gitinfo"; + + // Check if file exists + if (!fs.existsSync(filePath)) { + console.error(`Error: File not found: ${filePath}`); + process.exit(1); + } + + // Check if schema exists + if (!fs.existsSync(SCHEMA_PATH)) { + console.error(`Error: Schema not found: ${SCHEMA_PATH}`); + process.exit(1); + } + + // Read and parse schema + let schema; + try { + schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, "utf-8")); + } catch (err) { + console.error(`Error parsing schema: ${err.message}`); + process.exit(1); + } + + // Read and parse .gitinfo file + let content; + try { + content = fs.readFileSync(filePath, "utf-8"); + } catch (err) { + console.error(`Error reading file: ${err.message}`); + process.exit(1); + } + + let data; + try { + const stripped = stripJsonComments(content); + data = JSON.parse(stripped); + } catch (err) { + console.error(`Error parsing JSONC: ${err.message}`); + process.exit(1); + } + + // Validate against schema + const errors = validateSchema(data, schema); + + if (errors.length > 0) { + console.error(`Validation failed for ${filePath}:`); + for (const error of errors) { + console.error(` - ${error}`); + } + process.exit(1); + } + + console.log(`✓ ${filePath} is valid`); + process.exit(0); +} + +main(); 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 |
