//! Database schema utilities //! //! This module provides utilities for working with database schemas, //! including query builders and schema validation. use serde_json::Value as JsonValue; use crate::error::{Error, Result}; /// Database schema name pub const SCHEMA_NAME: &str = "attune"; /// Table identifiers #[derive(Debug, Clone, Copy)] pub enum Table { Pack, Runtime, Worker, Trigger, Sensor, Action, Rule, Event, Enforcement, Execution, Inquiry, Identity, PermissionSet, PermissionAssignment, Policy, Key, Notification, Artifact, } impl Table { /// Get the table name as a string pub fn as_str(&self) -> &'static str { match self { Self::Pack => "pack", Self::Runtime => "runtime", Self::Worker => "worker", Self::Trigger => "trigger", Self::Sensor => "sensor", Self::Action => "action", Self::Rule => "rule", Self::Event => "event", Self::Enforcement => "enforcement", Self::Execution => "execution", Self::Inquiry => "inquiry", Self::Identity => "identity", Self::PermissionSet => "permission_set", Self::PermissionAssignment => "permission_assignment", Self::Policy => "policy", Self::Key => "key", Self::Notification => "notification", Self::Artifact => "artifact", } } } /// Common column identifiers #[derive(Debug, Clone, Copy)] pub enum Column { Id, Ref, Pack, PackRef, Label, Description, Version, Name, Status, Created, Updated, Enabled, Config, Meta, Tags, RuntimeType, WorkerType, Entrypoint, Runtime, RuntimeRef, Trigger, TriggerRef, Action, ActionRef, Rule, RuleRef, ParamSchema, OutSchema, ConfSchema, Payload, Response, ResponseSchema, Result, Execution, Enforcement, Executor, Prompt, AssignedTo, TimeoutAt, RespondedAt, Login, DisplayName, Attributes, Owner, OwnerType, Encrypted, Value, Channel, Entity, EntityType, Activity, State, Content, } /// JSON Schema validator pub struct SchemaValidator { schema: JsonValue, } impl SchemaValidator { /// Create a new schema validator pub fn new(schema: JsonValue) -> Result { // Validate that the schema itself is valid JSON Schema if !schema.is_object() { return Err(Error::schema_validation("Schema must be a JSON object")); } Ok(Self { schema }) } /// Validate data against the schema pub fn validate(&self, data: &JsonValue) -> Result<()> { // Use jsonschema crate for validation let compiled = jsonschema::validator_for(&self.schema) .map_err(|e| Error::schema_validation(format!("Failed to compile schema: {}", e)))?; if let Err(error) = compiled.validate(data) { return Err(Error::schema_validation(format!( "Validation failed: {}", error ))); } Ok(()) } /// Get the underlying schema pub fn schema(&self) -> &JsonValue { &self.schema } } /// Reference format validator pub struct RefValidator; impl RefValidator { /// Validate pack.component format (e.g., "core.webhook") pub fn validate_component_ref(ref_str: &str) -> Result<()> { let parts: Vec<&str> = ref_str.split('.').collect(); if parts.len() != 2 { return Err(Error::validation(format!( "Invalid component reference format: '{}'. Expected 'pack.component'", ref_str ))); } Self::validate_identifier(parts[0])?; Self::validate_identifier(parts[1])?; Ok(()) } /// Validate pack.name format (e.g., "core.python", "core.shell") pub fn validate_runtime_ref(ref_str: &str) -> Result<()> { let parts: Vec<&str> = ref_str.split('.').collect(); if parts.len() != 2 { return Err(Error::validation(format!( "Invalid runtime reference format: '{}'. Expected 'pack.name' (e.g., 'core.python')", ref_str ))); } Self::validate_identifier(parts[0])?; Self::validate_identifier(parts[1])?; Ok(()) } /// Validate pack reference format (simple identifier) pub fn validate_pack_ref(ref_str: &str) -> Result<()> { Self::validate_identifier(ref_str) } /// Validate identifier (lowercase alphanumeric with hyphens/underscores) fn validate_identifier(identifier: &str) -> Result<()> { if identifier.is_empty() { return Err(Error::validation("Identifier cannot be empty")); } // Must start with lowercase letter if !identifier.chars().next().unwrap().is_ascii_lowercase() { return Err(Error::validation(format!( "Identifier '{}' must start with a lowercase letter", identifier ))); } // Must contain only lowercase alphanumeric, hyphens, or underscores for ch in identifier.chars() { if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() && ch != '-' && ch != '_' { return Err(Error::validation(format!( "Identifier '{}' contains invalid character '{}'. Only lowercase letters, digits, hyphens, and underscores are allowed", identifier, ch ))); } } Ok(()) } } /// Build a qualified table name with schema pub fn qualified_table(table: Table) -> String { format!("{}.{}", SCHEMA_NAME, table.as_str()) } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_table_as_str() { assert_eq!(Table::Pack.as_str(), "pack"); assert_eq!(Table::Action.as_str(), "action"); assert_eq!(Table::Execution.as_str(), "execution"); } #[test] fn test_qualified_table() { assert_eq!(qualified_table(Table::Pack), "attune.pack"); assert_eq!(qualified_table(Table::Action), "attune.action"); } #[test] fn test_ref_validator_component() { assert!(RefValidator::validate_component_ref("core.webhook").is_ok()); assert!(RefValidator::validate_component_ref("my-pack.my-action").is_ok()); assert!(RefValidator::validate_component_ref("pack_name.component_name").is_ok()); // Invalid formats assert!(RefValidator::validate_component_ref("nopack").is_err()); assert!(RefValidator::validate_component_ref("too.many.parts").is_err()); assert!(RefValidator::validate_component_ref("Capital.name").is_err()); assert!(RefValidator::validate_component_ref("pack.Name").is_err()); } #[test] fn test_ref_validator_runtime() { assert!(RefValidator::validate_runtime_ref("core.python").is_ok()); assert!(RefValidator::validate_runtime_ref("core.shell").is_ok()); assert!(RefValidator::validate_runtime_ref("mypack.nodejs").is_ok()); assert!(RefValidator::validate_runtime_ref("core.builtin").is_ok()); // Invalid formats assert!(RefValidator::validate_runtime_ref("core.action.webhook").is_err()); // 3-part no longer valid assert!(RefValidator::validate_runtime_ref("python").is_err()); // missing pack assert!(RefValidator::validate_runtime_ref("Core.python").is_err()); // uppercase } #[test] fn test_ref_validator_pack() { assert!(RefValidator::validate_pack_ref("core").is_ok()); assert!(RefValidator::validate_pack_ref("my-pack").is_ok()); assert!(RefValidator::validate_pack_ref("pack_name").is_ok()); // Invalid formats assert!(RefValidator::validate_pack_ref("").is_err()); assert!(RefValidator::validate_pack_ref("Core").is_err()); assert!(RefValidator::validate_pack_ref("pack.name").is_err()); // dots are not allowed in pack refs assert!(RefValidator::validate_pack_ref("pack name").is_err()); } #[test] fn test_schema_validator() { let schema = json!({ "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "number"} }, "required": ["name"] }); let validator = SchemaValidator::new(schema).unwrap(); // Valid data let valid_data = json!({"name": "John", "age": 30}); assert!(validator.validate(&valid_data).is_ok()); // Missing required field let invalid_data = json!({"age": 30}); assert!(validator.validate(&invalid_data).is_err()); // Wrong type let invalid_data = json!({"name": "John", "age": "thirty"}); assert!(validator.validate(&invalid_data).is_err()); } #[test] fn test_schema_validator_invalid_schema() { let invalid_schema = json!("not an object"); assert!(SchemaValidator::new(invalid_schema).is_err()); } }