BEJSON Library - Bash Validator
code
#!/bin/bash
#===============================================================================
# Library: lib-bejson_validator.sh
# Description: BEJSON validation library for Bash/Termux environments
# Author: BEJSON Core Team
# Version: 1.0.0
# Compatibility: Bash 4.0+, Termux/Android
#===============================================================================
#-------------------------------------------------------------------------------
# SAFETY & ERROR HANDLING
#-------------------------------------------------------------------------------
set -o pipefail
set -o nounset
# Error codes
readonly E_INVALID_JSON=1
readonly E_MISSING_MANDATORY_KEY=2
readonly E_INVALID_FORMAT=3
readonly E_INVALID_VERSION=4
readonly E_INVALID_RECORDS_TYPE=5
readonly E_INVALID_FIELDS=6
readonly E_INVALID_VALUES=7
readonly E_TYPE_MISMATCH=8
readonly E_RECORD_LENGTH_MISMATCH=9
readonly E_RESERVED_KEY_COLLISION=10
readonly E_INVALID_RECORD_TYPE_PARENT=11
readonly E_NULL_VIOLATION=12
readonly E_FILE_NOT_FOUND=13
readonly E_PERMISSION_DENIED=14
readonly E_ATOMIC_WRITE_FAILED=15
# Global validation state
__BEJSON_VALIDATION_ERRORS=()
__BEJSON_VALIDATION_WARNINGS=()
__BEJSON_CURRENT_FILE=""
#-------------------------------------------------------------------------------
# UTILITY FUNCTIONS
#-------------------------------------------------------------------------------
# Check if jq is available
bejson_validator_check_dependencies() {
if ! command -v jq &> /dev/null; then
echo "ERROR: 'jq' is required but not installed." >&2
echo "Please install jq: pkg install jq (Termux) or apt-get install jq" >&2
return 1
fi
return 0
}
# Reset validation state
bejson_validator_reset_state() {
__BEJSON_VALIDATION_ERRORS=()
__BEJSON_VALIDATION_WARNINGS=()
__BEJSON_CURRENT_FILE=""
}
# Add error to validation state
__bejson_validator_add_error() {
local message="$1"
local location="${2:-}"
local context="${3:-}"
local error_entry="ERROR"
[[ -n "$location" ]] && error_entry+=" | Location: $location"
error_entry+=" | Message: $message"
[[ -n "$context" ]] && error_entry+=" | Context: $context"
__BEJSON_VALIDATION_ERRORS+=("$error_entry")
}
# Add warning to validation state
__bejson_validator_add_warning() {
local message="$1"
local location="${2:-}"
local warning_entry="WARNING"
[[ -n "$location" ]] && warning_entry+=" | Location: $location"
warning_entry+=" | Message: $message"
__BEJSON_VALIDATION_WARNINGS+=("$warning_entry")
}
# Get validation errors
bejson_validator_get_errors() {
printf '%s\n' "${__BEJSON_VALIDATION_ERRORS[@]}"
}
# Get validation warnings
bejson_validator_get_warnings() {
printf '%s\n' "${__BEJSON_VALIDATION_WARNINGS[@]}"
}
# Check if validation has errors
bejson_validator_has_errors() {
[[ ${#__BEJSON_VALIDATION_ERRORS[@]} -gt 0 ]]
}
# Check if validation has warnings
bejson_validator_has_warnings() {
[[ ${#__BEJSON_VALIDATION_WARNINGS[@]} -gt 0 ]]
}
# Get error count
bejson_validator_error_count() {
echo ${#__BEJSON_VALIDATION_ERRORS[@]}
}
# Get warning count
bejson_validator_warning_count() {
echo ${#__BEJSON_VALIDATION_WARNINGS[@]}
}
#-------------------------------------------------------------------------------
# JSON SYNTAX VALIDATION
#-------------------------------------------------------------------------------
# Validate JSON syntax using jq
bejson_validator_check_json_syntax() {
local input="$1"
local is_file="${2:-false}"
if [[ "$is_file" == "true" ]]; then
if [[ ! -f "$input" ]]; then
__bejson_validator_add_error "File not found: $input" "File System"
return $E_FILE_NOT_FOUND
fi
if [[ ! -r "$input" ]]; then
__bejson_validator_add_error "Permission denied: $input" "File System"
return $E_PERMISSION_DENIED
fi
__BEJSON_CURRENT_FILE="$input"
if ! jq empty "$input" 2>/dev/null; then
local jq_error=$(jq empty "$input" 2>&1)
__bejson_validator_add_error "Invalid JSON syntax: $jq_error" "JSON Parse"
return $E_INVALID_JSON
fi
else
if ! echo "$input" | jq empty 2>/dev/null; then
local jq_error=$(echo "$input" | jq empty 2>&1)
__bejson_validator_add_error "Invalid JSON syntax: $jq_error" "JSON Parse"
return $E_INVALID_JSON
fi
fi
return 0
}
#-------------------------------------------------------------------------------
# MANDATORY KEY VALIDATION
#-------------------------------------------------------------------------------
# Check mandatory top-level keys
bejson_validator_check_mandatory_keys() {
local json_doc="$1"
local mandatory_keys=("Format" "Format_Version" "Format_Creator" "Records_Type" "Fields" "Values")
for key in "${mandatory_keys[@]}"; do
if ! echo "$json_doc" | jq -e "has(\"$key\")" &>/dev/null; then
__bejson_validator_add_error "Missing mandatory top-level key: '$key'" "Top-Level Keys"
return $E_MISSING_MANDATORY_KEY
fi
done
# Check Format value
local format_value=$(echo "$json_doc" | jq -r '.Format // empty')
if [[ "$format_value" != "BEJSON" ]]; then
__bejson_validator_add_error "Invalid 'Format' value: Expected 'BEJSON', got '${format_value:-}'" "Top-Level Keys/Format"
return $E_INVALID_FORMAT
fi
# Check Format_Version
local version_value=$(echo "$json_doc" | jq -r '.Format_Version // empty')
if [[ ! "$version_value" =~ ^(104|104a|104db)$ ]]; then
__bejson_validator_add_error "Invalid 'Format_Version': Expected '104', '104a', or '104db', got '${version_value:-}'" "Top-Level Keys/Format_Version"
return $E_INVALID_VERSION
fi
# Check Format_Creator is string
if ! echo "$json_doc" | jq -e '.Format_Creator | type == "string"' &>/dev/null; then
__bejson_validator_add_error "Invalid 'Format_Creator': Must be a string" "Top-Level Keys/Format_Creator"
return $E_INVALID_FORMAT
fi
# Check Records_Type is array
if ! echo "$json_doc" | jq -e '.Records_Type | type == "array"' &>/dev/null; then
__bejson_validator_add_error "Invalid 'Records_Type': Must be an array" "Top-Level Keys/Records_Type"
return $E_INVALID_RECORDS_TYPE
fi
# Check Fields is array
if ! echo "$json_doc" | jq -e '.Fields | type == "array"' &>/dev/null; then
__bejson_validator_add_error "Invalid 'Fields': Must be an array" "Top-Level Keys/Fields"
return $E_INVALID_FIELDS
fi
# Check Values is array
if ! echo "$json_doc" | jq -e '.Values | type == "array"' &>/dev/null; then
__bejson_validator_add_error "Invalid 'Values': Must be an array" "Top-Level Keys/Values"
return $E_INVALID_VALUES
fi
echo "$version_value"
return 0
}
#-------------------------------------------------------------------------------
# FIELDS ARRAY VALIDATION
#-------------------------------------------------------------------------------
# Validate Fields array structure
bejson_validator_check_fields_structure() {
local json_doc="$1"
local version="$2"
local fields_count=$(echo "$json_doc" | jq '.Fields | length')
if [[ $fields_count -eq 0 ]]; then
__bejson_validator_add_error "'Fields' array cannot be empty" "Fields Array"
return $E_INVALID_FIELDS
fi
local field_names=()
local i=0
while [[ $i -lt $fields_count ]]; do
local field_def=$(echo "$json_doc" | jq ".Fields[$i]")
# Check field is object
if ! echo "$field_def" | jq -e 'type == "object"' &>/dev/null; then
__bejson_validator_add_error "Field at index $i must be an object" "Fields[$i]"
return $E_INVALID_FIELDS
fi
# Check name exists and is string
if ! echo "$field_def" | jq -e 'has("name") and (.name | type == "string")' &>/dev/null; then
__bejson_validator_add_error "Field at index $i: Missing or invalid 'name' (must be string)" "Fields[$i]"
return $E_INVALID_FIELDS
fi
local field_name=$(echo "$field_def" | jq -r '.name')
# Check for duplicate names
if [[ " ${field_names[@]} " =~ " ${field_name} " ]]; then
__bejson_validator_add_error "Duplicate field name '$field_name' found in 'Fields' array" "Fields[$i]"
return $E_INVALID_FIELDS
fi
field_names+=("$field_name")
# Check type exists and is valid
if ! echo "$field_def" | jq -e 'has("type") and (.type | type == "string")' &>/dev/null; then
__bejson_validator_add_error "Field '$field_name' (index $i): Missing or invalid 'type' (must be string)" "Fields[$i]"
return $E_INVALID_FIELDS
fi
local field_type=$(echo "$field_def" | jq -r '.type')
local valid_types=("string" "integer" "number" "boolean" "array" "object")
local is_valid_type=false
for valid_type in "${valid_types[@]}"; do
if [[ "$field_type" == "$valid_type" ]]; then
is_valid_type=true
break
fi
done
if [[ "$is_valid_type" == "false" ]]; then
__bejson_validator_add_error "Field '$field_name' (index $i): Invalid type '$field_type'. Valid types: ${valid_types[*]}" "Fields[$i]"
return $E_INVALID_FIELDS
fi
# Version 104a: Only primitive types allowed
if [[ "$version" == "104a" ]]; then
if [[ "$field_type" == "array" || "$field_type" == "object" ]]; then
__bejson_validator_add_error "Field '$field_name' (index $i): Type '$field_type' not allowed in 104a. Only primitive types (string, integer, number, boolean) permitted." "Fields[$i]"
return $E_INVALID_FIELDS
fi
fi
((i++))
done
echo "$fields_count"
return 0
}
#-------------------------------------------------------------------------------
# RECORDS_TYPE VALIDATION
#-------------------------------------------------------------------------------
# Validate Records_Type based on version
bejson_validator_check_records_type() {
local json_doc="$1"
local version="$2"
local records_type_count=$(echo "$json_doc" | jq '.Records_Type | length')
case "$version" in
"104"|"104a")
if [[ $records_type_count -ne 1 ]]; then
__bejson_validator_add_error "For BEJSON $version, 'Records_Type' must contain exactly one string. Found $records_type_count entries." "Records_Type"
return $E_INVALID_RECORDS_TYPE
fi
if ! echo "$json_doc" | jq -e '.Records_Type[0] | type == "string"' &>/dev/null; then
__bejson_validator_add_error "For BEJSON $version, 'Records_Type[0]' must be a string" "Records_Type[0]"
return $E_INVALID_RECORDS_TYPE
fi
;;
"104db")
if [[ $records_type_count -lt 2 ]]; then
__bejson_validator_add_error "For BEJSON 104db, 'Records_Type' must contain two or more unique strings. Found $records_type_count entries." "Records_Type"
return $E_INVALID_RECORDS_TYPE
fi
# Check all are strings and unique
local i=0
local seen_types=()
while [[ $i -lt $records_type_count ]]; do
if ! echo "$json_doc" | jq -e ".Records_Type[$i] | type == \"string\"" &>/dev/null; then
__bejson_validator_add_error "Records_Type[$i] must be a string" "Records_Type[$i]"
return $E_INVALID_RECORDS_TYPE
fi
local type_val=$(echo "$json_doc" | jq -r ".Records_Type[$i]")
if [[ " ${seen_types[@]} " =~ " ${type_val} " ]]; then
__bejson_validator_add_error "Duplicate type '$type_val' found in 'Records_Type'" "Records_Type"
return $E_INVALID_RECORDS_TYPE
fi
seen_types+=("$type_val")
((i++))
done
;;
esac
return 0
}
#-------------------------------------------------------------------------------
# 104DB SPECIFIC VALIDATION
#-------------------------------------------------------------------------------
# Validate Record_Type_Parent for 104db
bejson_validator_check_record_type_parent() {
local json_doc="$1"
# Check first field is Record_Type_Parent
local first_field=$(echo "$json_doc" | jq '.Fields[0]')
local first_field_name=$(echo "$first_field" | jq -r '.name // empty')
local first_field_type=$(echo "$first_field" | jq -r '.type // empty')
if [[ "$first_field_name" != "Record_Type_Parent" || "$first_field_type" != "string" ]]; then
__bejson_validator_add_error "For BEJSON 104db, the first field in 'Fields' (index 0) must be '{\"name\": \"Record_Type_Parent\", \"type\": \"string\"}'. Found: '{\"name\": \"$first_field_name\", \"type\": \"$first_field_type\"}'" "Fields[0]"
return $E_INVALID_RECORD_TYPE_PARENT
fi
# Validate each record's Record_Type_Parent value
local values_count=$(echo "$json_doc" | jq '.Values | length')
local records_types=$(echo "$json_doc" | jq -r '.Records_Type[]')
local i=0
while [[ $i -lt $values_count ]]; do
local record=$(echo "$json_doc" | jq ".Values[$i]")
if ! echo "$record" | jq -e 'type == "array"' &>/dev/null; then
__bejson_validator_add_error "Values[$i] must be an array (record)" "Values[$i]"
return $E_INVALID_VALUES
fi
local record_type_parent=$(echo "$record" | jq -r '.[0] // empty')
if [[ -z "$record_type_parent" ]]; then
__bejson_validator_add_error "Record at 'Values' index $i: 'Record_Type_Parent' (first element) is missing or null" "Values[$i][0]"
return $E_INVALID_RECORD_TYPE_PARENT
fi
# Check if it matches any declared type
local is_valid_type=false
while IFS= read -r valid_type; do
if [[ "$record_type_parent" == "$valid_type" ]]; then
is_valid_type=true
break
fi
done <<< "$records_types"
if [[ "$is_valid_type" == "false" ]]; then
__bejson_validator_add_error "Record at 'Values' index $i: 'Record_Type_Parent' value '$record_type_parent' does not match any declared type in 'Records_Type'" "Values[$i][0]"
return $E_INVALID_RECORD_TYPE_PARENT
fi
((i++))
done
return 0
}
#-------------------------------------------------------------------------------
# VALUES VALIDATION
#-------------------------------------------------------------------------------
# Validate Values array structure and content
bejson_validator_check_values() {
local json_doc="$1"
local version="$2"
local fields_count="$3"
local values_count=$(echo "$json_doc" | jq '.Values | length')
if [[ $values_count -eq 0 ]]; then
return 0 # Empty Values is valid
fi
local i=0
while [[ $i -lt $values_count ]]; do
local record=$(echo "$json_doc" | jq ".Values[$i]")
# Check record is array
if ! echo "$record" | jq -e 'type == "array"' &>/dev/null; then
__bejson_validator_add_error "Values[$i] must be an array (record)" "Values[$i]"
return $E_INVALID_VALUES
fi
local record_length=$(echo "$record" | jq 'length')
# Check record length matches fields count
if [[ $record_length -ne $fields_count ]]; then
__bejson_validator_add_error "Record at 'Values' index $i has $record_length elements, but 'Fields' defines $fields_count fields. All records must have the same length." "Values[$i]"
return $E_RECORD_LENGTH_MISMATCH
fi
# Validate each field value
local j=0
while [[ $j -lt $fields_count ]]; do
local field_def=$(echo "$json_doc" | jq ".Fields[$j]")
local field_name=$(echo "$field_def" | jq -r '.name')
local field_type=$(echo "$field_def" | jq -r '.type')
local field_parent=$(echo "$field_def" | jq -r '.Record_Type_Parent // empty')
local value=$(echo "$record" | jq ".[$j]")
local value_type=$(echo "$value" | jq -r 'type')
# Special handling for 104db Record_Type_Parent property
if [[ "$version" == "104db" && -n "$field_parent" && "$j" -gt 0 ]]; then
local record_type=$(echo "$record" | jq -r '.[0]')
if [[ "$field_parent" != "$record_type" ]]; then
# Field not applicable to this record type - must be null
if [[ "$value" != "null" ]]; then
__bejson_validator_add_error "Record at 'Values' index $i (type '$record_type'), field '$field_name' (index $j): This field is defined for type '$field_parent' but not for '$record_type'. Its value must be 'null', but found '$value'." "Values[$i][$j]"
return $E_NULL_VIOLATION
fi
((j++))
continue
fi
fi
# Type validation (null is always valid)
if [[ "$value" == "null" ]]; then
((j++))
continue
fi
local type_valid=false
case "$field_type" in
"string")
[[ "$value_type" == "string" ]] && type_valid=true
;;
"integer")
if [[ "$value_type" == "number" ]]; then
# Check if it's an integer (no decimal part)
local num_val=$(echo "$value" | jq -r '.')
if [[ "$num_val" =~ ^-?[0-9]+$ ]]; then
type_valid=true
fi
fi
;;
"number")
[[ "$value_type" == "number" ]] && type_valid=true
;;
"boolean")
[[ "$value_type" == "boolean" ]] && type_valid=true
;;
"array")
[[ "$value_type" == "array" ]] && type_valid=true
;;
"object")
[[ "$value_type" == "object" ]] && type_valid=true
;;
esac
if [[ "$type_valid" == "false" ]]; then
local actual_value=$(echo "$value" | jq -r '.')
__bejson_validator_add_error "Record at 'Values' index $i, field '$field_name' (index $j): Value '$actual_value' is of type '$value_type', but 'Fields' defines type '$field_type'." "Values[$i][$j]"
return $E_TYPE_MISMATCH
fi
((j++))
done
((i++))
done
return 0
}
#-------------------------------------------------------------------------------
# CUSTOM HEADERS VALIDATION (104a)
#-------------------------------------------------------------------------------
# Check for custom top-level keys (104a only)
bejson_validator_check_custom_headers() {
local json_doc="$1"
local version="$2"
local mandatory_keys=("Format" "Format_Version" "Format_Creator" "Records_Type" "Fields" "Values")
local all_keys=$(echo "$json_doc" | jq -r 'keys[]')
local has_custom=false
while IFS= read -r key; do
local is_mandatory=false
for mandatory in "${mandatory_keys[@]}"; do
if [[ "$key" == "$mandatory" ]]; then
is_mandatory=true
break
fi
done
if [[ "$is_mandatory" == "false" ]]; then
has_custom=true
if [[ "$version" == "104" || "$version" == "104db" ]]; then
__bejson_validator_add_error "For BEJSON $version, custom top-level key '$key' is not permitted." "Top-Level Keys/$key"
return $E_RESERVED_KEY_COLLISION
fi
# 104a: Check for reserved key collision
for reserved in "${mandatory_keys[@]}"; do
if [[ "$key" == "$reserved" ]]; then
__bejson_validator_add_error "Custom top-level key '$key' collides with reserved BEJSON system key." "Top-Level Keys/$key"
return $E_RESERVED_KEY_COLLISION
fi
done
# Warning for non-PascalCase in 104a (recommendation, not error)
if [[ ! "$key" =~ ^[A-Z][a-zA-Z0-9_]*$ ]]; then
__bejson_validator_add_warning "Custom top-level key '$key' does not follow recommended PascalCase naming convention." "Top-Level Keys/$key"
fi
fi
done <<< "$all_keys"
return 0
}
#-------------------------------------------------------------------------------
# MAIN VALIDATION FUNCTIONS
#-------------------------------------------------------------------------------
# Validate a BEJSON string
bejson_validator_validate_string() {
local json_string="$1"
bejson_validator_reset_state
bejson_validator_check_dependencies || return $?
# Step 1: JSON Syntax
bejson_validator_check_json_syntax "$json_string" "false" || return $?
# Step 2: Mandatory Keys (returns version)
local version
version=$(bejson_validator_check_mandatory_keys "$json_string")
local result=$?
[[ $result -ne 0 ]] && return $result
# Step 3: Custom Headers
bejson_validator_check_custom_headers "$json_string" "$version" || return $?
# Step 4: Records_Type
bejson_validator_check_records_type "$json_string" "$version" || return $?
# Step 5: Fields Structure (returns fields_count)
local fields_count
fields_count=$(bejson_validator_check_fields_structure "$json_string" "$version")
local result=$?
[[ $result -ne 0 ]] && return $result
# Step 6: 104db specific - Record_Type_Parent
if [[ "$version" == "104db" ]]; then
bejson_validator_check_record_type_parent "$json_string" || return $?
fi
# Step 7: Values
bejson_validator_check_values "$json_string" "$version" "$fields_count" || return $?
return 0
}
# Validate a BEJSON file
bejson_validator_validate_file() {
local file_path="$1"
bejson_validator_reset_state
bejson_validator_check_dependencies || return $?
# Read file content
local file_content
if ! file_content=$(cat "$file_path" 2>/dev/null); then
__bejson_validator_add_error "Cannot read file: $file_path" "File System"
return $E_FILE_NOT_FOUND
fi
bejson_validator_validate_string "$file_content"
return $?
}
# Get detailed validation report
bejson_validator_get_report() {
local json_string="$1"
local is_file="${2:-false}"
if [[ "$is_file" == "true" ]]; then
bejson_validator_validate_file "$json_string"
else
bejson_validator_validate_string "$json_string"
fi
local exit_code=$?
echo "=== BEJSON Validation Report ==="
echo "Status: $([[ $exit_code -eq 0 ]] && echo 'VALID' || echo 'INVALID')"
echo "Exit Code: $exit_code"
echo ""
local error_count=$(bejson_validator_error_count)
local warning_count=$(bejson_validator_warning_count)
echo "Errors: $error_count"
if [[ $error_count -gt 0 ]]; then
echo "---"
bejson_validator_get_errors
fi
echo ""
echo "Warnings: $warning_count"
if [[ $warning_count -gt 0 ]]; then
echo "---"
bejson_validator_get_warnings
fi
return $exit_code
}
#-------------------------------------------------------------------------------
# EXPORT VALIDATION FUNCTIONS
#-------------------------------------------------------------------------------
export -f bejson_validator_check_dependencies
export -f bejson_validator_reset_state
export -f bejson_validator_get_errors
export -f bejson_validator_get_warnings
export -f bejson_validator_has_errors
export -f bejson_validator_has_warnings
export -f bejson_validator_error_count
export -f bejson_validator_warning_count
export -f bejson_validator_check_json_syntax
export -f bejson_validator_check_mandatory_keys
export -f bejson_validator_check_fields_structure
export -f bejson_validator_check_records_type
export -f bejson_validator_check_record_type_parent
export -f bejson_validator_check_values
export -f bejson_validator_check_custom_headers
export -f bejson_validator_validate_string
export -f bejson_validator_validate_file
export -f bejson_validator_get_report
