Files
attune/crates/common/src/schema.rs

320 lines
9.0 KiB
Rust

//! 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<Self> {
// 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());
}
}