germanic 0.2.3

Schema-validated binary data for AI agents. JSON to .grm compiler with zero-copy FlatBuffers.
Documentation
//! # Schema Validation
//!
//! Validates .grm files and JSON data against schemas.
//!
//! ## Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────────────────┐
//! │                    VALIDATION LAYERS                                        │
//! ├─────────────────────────────────────────────────────────────────────────────┤
//! │                                                                             │
//! │   Layer 1: SYNTAX                                                           │
//! │   ┌─────────────────────────────────────────┐                               │
//! │   │ • Is the JSON syntactically correct?    │                               │
//! │   │ • Does the .grm file have valid magic bytes? │                          │
//! │   └─────────────────────────────────────────┘                               │
//! │                      │                                                      │
//! │                      ▼                                                      │
//! │   Layer 2: STRUCTURE                                                        │
//! │   ┌─────────────────────────────────────────┐                               │
//! │   │ • Does JSON match the Rust struct?      │                               │
//! │   │ • Is the .grm header complete?          │                               │
//! │   └─────────────────────────────────────────┘                               │
//! │                      │                                                      │
//! │                      ▼                                                      │
//! │   Layer 3: SEMANTICS                                                        │
//! │   ┌─────────────────────────────────────────┐                               │
//! │   │ • Are all required fields filled?       │                               │
//! │   │ • Do values meet business constraints?  │                               │
//! │   └─────────────────────────────────────────┘                               │
//! │                                                                             │
//! │   FAIL-FAST: Each layer aborts immediately on error.                        │
//! │   No point in semantic checking if syntax is invalid.                       │
//! │                                                                             │
//! └─────────────────────────────────────────────────────────────────────────────┘
//! ```

use crate::error::GermanicResult;
use crate::types::{GRM_MAGIC, GrmHeader};

// ============================================================================
// .GRM VALIDATION
// ============================================================================

/// Validates a .grm file for structural correctness.
///
/// ## Checks
///
/// 1. Magic bytes present and correct
/// 2. Header complete and parsable
/// 3. Schema-ID is valid UTF-8
/// 4. Enough data for the specified payload
///
/// ## Example
///
/// ```rust,ignore
/// let bytes = std::fs::read("practice.grm")?;
/// let validation = validate_grm(&bytes)?;
/// println!("Schema-ID: {}", validation.schema_id);
/// ```
pub fn validate_grm(data: &[u8]) -> GermanicResult<GrmValidation> {
    // 1. Check minimum size
    if data.len() < 4 {
        return Ok(GrmValidation {
            valid: false,
            schema_id: None,
            error: Some("File too short for magic bytes".to_string()),
        });
    }

    // 2. Check magic bytes
    if data[0..4] != GRM_MAGIC {
        return Ok(GrmValidation {
            valid: false,
            schema_id: None,
            error: Some(format!(
                "Invalid magic bytes: {:02X?} (expected: {:02X?})",
                &data[0..4],
                &GRM_MAGIC
            )),
        });
    }

    // 3. Parse header
    match GrmHeader::from_bytes(data) {
        Ok((header, header_len)) => {
            // 4. Payload plausibility checks
            let payload = &data[header_len..];
            if payload.is_empty() {
                return Ok(GrmValidation {
                    valid: false,
                    schema_id: Some(header.schema_id),
                    error: Some("Header valid but payload is empty".to_string()),
                });
            }
            // FlatBuffer minimum: 4 bytes (root offset) + 4 bytes (vtable offset)
            if payload.len() < 8 {
                return Ok(GrmValidation {
                    valid: false,
                    schema_id: Some(header.schema_id),
                    error: Some(format!(
                        "Payload too short for valid FlatBuffer: {} bytes (minimum: 8)",
                        payload.len()
                    )),
                });
            }

            Ok(GrmValidation {
                valid: true,
                schema_id: Some(header.schema_id),
                error: None,
            })
        }
        Err(e) => Ok(GrmValidation {
            valid: false,
            schema_id: None,
            error: Some(format!("Header error: {}", e)),
        }),
    }
}

/// Result of .grm validation.
#[derive(Debug, Clone)]
pub struct GrmValidation {
    /// Is the file structurally valid?
    pub valid: bool,

    /// Extracted schema ID (if header is parsable)
    pub schema_id: Option<String>,

    /// Error message (if invalid)
    pub error: Option<String>,
}

// ============================================================================
// JSON SCHEMA VALIDATION
// ============================================================================

/// Validates JSON against a known schema.
///
/// This function is a wrapper for schema-specific validation.
/// The actual validation logic is provided by the `Validate` trait,
/// which is generated by the macro.
///
/// ## Example
///
/// ```rust,ignore
/// let json = r#"{"name": "", "bezeichnung": "Heilpraktiker"}"#;
/// let result = validate_json::<PracticeSchema>(json);
/// // → Err: "name" is empty but required
/// ```
pub fn validate_json<S>(json: &str) -> GermanicResult<S>
where
    S: serde::de::DeserializeOwned + crate::schema::Validate,
{
    // 1. Parse JSON to struct
    let schema: S = serde_json::from_str(json)?;

    // 2. Validate required fields
    schema.validate()?;

    Ok(schema)
}

// ============================================================================
// TESTS
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_grm_too_short() {
        let data = [0x47, 0x52, 0x4D]; // Only 3 bytes
        let result = validate_grm(&data).unwrap();

        assert!(!result.valid);
        assert!(result.error.unwrap().contains("too short"));
    }

    #[test]
    fn test_validate_grm_invalid_magic() {
        let data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
        let result = validate_grm(&data).unwrap();

        assert!(!result.valid);
        assert!(result.error.unwrap().contains("magic"));
    }

    #[test]
    fn test_validate_grm_empty_payload() {
        let header = GrmHeader::new("test.v1");
        let bytes = header.to_bytes().unwrap();
        let result = validate_grm(&bytes).unwrap();

        assert!(!result.valid);
        assert!(result.error.unwrap().contains("payload is empty"));
    }

    #[test]
    fn test_validate_grm_payload_too_short() {
        let header = GrmHeader::new("test.v1");
        let mut bytes = header.to_bytes().unwrap();
        bytes.extend_from_slice(&[0x00; 4]); // Only 4 bytes, need 8
        let result = validate_grm(&bytes).unwrap();

        assert!(!result.valid);
        assert!(result.error.unwrap().contains("Payload too short"));
    }

    #[test]
    fn test_validate_grm_valid() {
        let header = GrmHeader::new("test.v1");
        let mut bytes = header.to_bytes().unwrap();
        bytes.extend_from_slice(&[0x00; 16]); // Enough for a minimal FlatBuffer
        let result = validate_grm(&bytes).unwrap();

        assert!(result.valid);
        assert_eq!(result.schema_id, Some("test.v1".to_string()));
    }
}