use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum RegistryError {
#[error("Schema not found: {0}")]
SchemaNotFound(String),
#[error("Load failed: {0}")]
LoadFailed(String),
#[error("Invalid schema: {0}")]
InvalidSchema(String),
#[error("Registration failed: {0}")]
RegistrationFailed(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Parse error: {0}")]
ParseError(String),
}
#[derive(Debug, Clone)]
pub struct RegistryStats {
pub total_schemas: usize,
pub roman_scripts: usize,
pub brahmic_scripts: usize,
pub implicit_a_scripts: usize,
pub cached_schemas: usize,
pub total_mappings: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaMetadata {
pub name: String,
pub script_type: String,
pub has_implicit_a: bool,
pub description: Option<String>,
pub aliases: Option<Vec<String>>,
}
impl Default for SchemaMetadata {
fn default() -> Self {
Self {
name: String::new(),
script_type: "roman".to_string(),
has_implicit_a: false,
description: None,
aliases: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaMapping {
pub vowels: Option<FxHashMap<String, String>>,
pub consonants: Option<FxHashMap<String, String>>,
pub vowel_signs: Option<FxHashMap<String, String>>,
pub marks: Option<FxHashMap<String, String>>,
pub digits: Option<FxHashMap<String, String>>,
pub sanskrit_extensions: Option<FxHashMap<String, String>>,
pub special: Option<FxHashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodegenConfig {
pub mapping_type: Option<String>,
pub processor_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaFile {
pub metadata: SchemaMetadata,
pub target: Option<String>, pub mappings: SchemaMapping,
pub codegen: Option<CodegenConfig>,
}
#[derive(Debug, Clone)]
pub struct Schema {
pub name: String,
pub script_type: String,
pub target: String,
pub mappings: FxHashMap<String, String>,
pub metadata: SchemaMetadata,
}
impl Schema {
pub fn new(name: String, script_type: String) -> Self {
Self {
name: name.clone(),
script_type: script_type.clone(),
target: if script_type == "roman" {
"iso15919".to_string()
} else {
"devanagari".to_string()
},
mappings: FxHashMap::default(),
metadata: SchemaMetadata {
name,
script_type,
has_implicit_a: false,
description: None,
aliases: None,
},
}
}
pub fn from_schema_file(schema_file: SchemaFile) -> Result<Self, RegistryError> {
let mut flattened_mappings = FxHashMap::default();
if let Some(vowels) = &schema_file.mappings.vowels {
flattened_mappings.extend(vowels.clone());
}
if let Some(consonants) = &schema_file.mappings.consonants {
flattened_mappings.extend(consonants.clone());
}
if let Some(vowel_signs) = &schema_file.mappings.vowel_signs {
flattened_mappings.extend(vowel_signs.clone());
}
if let Some(marks) = &schema_file.mappings.marks {
flattened_mappings.extend(marks.clone());
}
if let Some(digits) = &schema_file.mappings.digits {
flattened_mappings.extend(digits.clone());
}
if let Some(sanskrit_extensions) = &schema_file.mappings.sanskrit_extensions {
flattened_mappings.extend(sanskrit_extensions.clone());
}
if let Some(special) = &schema_file.mappings.special {
flattened_mappings.extend(special.clone());
}
let target = schema_file.target.unwrap_or_else(|| {
if schema_file.metadata.script_type == "roman" {
"iso15919".to_string()
} else {
"devanagari".to_string()
}
});
Ok(Self {
name: schema_file.metadata.name.clone(),
script_type: schema_file.metadata.script_type.clone(),
target,
mappings: flattened_mappings,
metadata: schema_file.metadata,
})
}
}
pub trait SchemaRegistryTrait {
fn get_schema(&self, script_name: &str) -> Option<&Schema>;
fn register_schema(&mut self, name: String, schema: Schema) -> Result<(), RegistryError>;
fn add_schema(&mut self, name: String, schema: Schema) -> Result<(), RegistryError>;
fn load_schema(&mut self, schema_path: &str) -> Result<(), RegistryError>;
fn load_schema_from_string(
&mut self,
yaml_content: &str,
schema_name: &str,
) -> Result<(), RegistryError>;
fn list_schemas(&self) -> Vec<&str>;
fn list_schemas_owned(&self) -> Vec<String>;
fn validate_schema(&self, schema: &Schema) -> Result<(), RegistryError>;
fn remove_schema(&mut self, script_name: &str) -> bool;
fn clear(&mut self);
fn schema_count(&self) -> usize;
fn has_schema(&self, script_name: &str) -> bool;
fn get_schema_metadata(&self, script_name: &str) -> Option<&SchemaMetadata>;
fn get_registry_stats(&self) -> RegistryStats;
}
#[derive(Clone)]
pub struct SchemaRegistry {
schemas: FxHashMap<String, Schema>,
schema_cache: FxHashMap<String, SchemaFile>,
}
impl SchemaRegistry {
pub fn new() -> Self {
let mut registry = Self {
schemas: FxHashMap::default(),
schema_cache: FxHashMap::default(),
};
registry.register_builtin_schemas();
registry
}
fn register_builtin_schemas(&mut self) {
let devanagari_schema = Schema::new("devanagari".to_string(), "brahmic".to_string());
let iso_schema = Schema::new("iso15919".to_string(), "roman".to_string());
let _ = self.register_schema("devanagari".to_string(), devanagari_schema);
let _ = self.register_schema("iso15919".to_string(), iso_schema);
}
fn load_schema_from_file(&mut self, path: &Path) -> Result<Schema, RegistryError> {
let contents = fs::read_to_string(path)
.map_err(|e| RegistryError::IoError(format!("Failed to read file: {e}")))?;
let schema_file: SchemaFile = serde_yaml::from_str(&contents)
.map_err(|e| RegistryError::ParseError(format!("Failed to parse YAML: {e}")))?;
self.schema_cache
.insert(schema_file.metadata.name.clone(), schema_file.clone());
Schema::from_schema_file(schema_file)
}
pub fn load_schemas_from_directory(&mut self, dir_path: &str) -> Result<usize, RegistryError> {
let dir = Path::new(dir_path);
if !dir.is_dir() {
return Err(RegistryError::LoadFailed(format!(
"Not a directory: {dir_path}"
)));
}
let mut loaded_count = 0;
for entry in fs::read_dir(dir)
.map_err(|e| RegistryError::IoError(format!("Failed to read directory: {e}")))?
{
let entry = entry.map_err(|e| {
RegistryError::IoError(format!("Failed to read directory entry: {e}"))
})?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "yaml" || ext == "yml" {
match self.load_schema(path.to_str().unwrap_or("")) {
Ok(_) => loaded_count += 1,
Err(e) => {
eprintln!("Warning: Failed to load schema from {path:?}: {e}");
}
}
}
}
} else if path.is_dir() {
if let Ok(count) = self.load_schemas_from_directory(path.to_str().unwrap_or("")) {
loaded_count += count;
}
}
}
Ok(loaded_count)
}
pub fn get_schemas_by_type(&self, script_type: &str) -> Vec<&Schema> {
self.schemas
.values()
.filter(|schema| schema.script_type == script_type)
.collect()
}
pub fn get_implicit_a_schemas(&self) -> Vec<&Schema> {
self.schemas
.values()
.filter(|schema| schema.metadata.has_implicit_a)
.collect()
}
pub fn find_schema_by_alias(&self, alias: &str) -> Option<&Schema> {
self.schemas.values().find(|schema| {
schema
.metadata
.aliases
.as_ref()
.map(|aliases| aliases.contains(&alias.to_string()))
.unwrap_or(false)
})
}
pub fn is_empty(&self) -> bool {
self.schemas.len() <= 2 }
pub fn export_summary(&self) -> String {
let stats = self.get_registry_stats();
format!(
"Registry Summary:\n\
- Total schemas: {}\n\
- Roman scripts: {}\n\
- Brahmic scripts: {}\n\
- Schemas with implicit 'a': {}\n\
- Cached schemas: {}\n\
- Total mappings: {}\n\
- Schema names: [{}]",
stats.total_schemas,
stats.roman_scripts,
stats.brahmic_scripts,
stats.implicit_a_scripts,
stats.cached_schemas,
stats.total_mappings,
self.list_schemas().join(", ")
)
}
}
impl SchemaRegistryTrait for SchemaRegistry {
fn get_schema(&self, script_name: &str) -> Option<&Schema> {
if let Some(schema) = self.schemas.get(script_name) {
return Some(schema);
}
self.find_schema_by_alias(script_name)
}
fn register_schema(&mut self, name: String, schema: Schema) -> Result<(), RegistryError> {
self.validate_schema(&schema)?;
self.schemas.insert(name, schema);
Ok(())
}
fn add_schema(&mut self, name: String, schema: Schema) -> Result<(), RegistryError> {
self.register_schema(name, schema)
}
fn load_schema(&mut self, schema_path: &str) -> Result<(), RegistryError> {
let path = Path::new(schema_path);
if !path.exists() {
return Err(RegistryError::LoadFailed(format!(
"Schema file not found: {schema_path}"
)));
}
let schema = self.load_schema_from_file(path)?;
let name = schema.name.clone();
self.register_schema(name, schema)
}
fn list_schemas(&self) -> Vec<&str> {
let mut schemas: Vec<&str> = self.schemas.keys().map(|s| s.as_str()).collect();
schemas.sort();
schemas
}
fn validate_schema(&self, schema: &Schema) -> Result<(), RegistryError> {
if schema.name.is_empty() {
return Err(RegistryError::InvalidSchema(
"Schema name cannot be empty".to_string(),
));
}
if schema.script_type.is_empty() {
return Err(RegistryError::InvalidSchema(
"Script type cannot be empty".to_string(),
));
}
if !["roman", "brahmic"].contains(&schema.script_type.as_str()) {
return Err(RegistryError::InvalidSchema(
"Invalid script type".to_string(),
));
}
Ok(())
}
fn load_schema_from_string(
&mut self,
yaml_content: &str,
schema_name: &str,
) -> Result<(), RegistryError> {
let schema_file: SchemaFile = serde_yaml::from_str(yaml_content)
.map_err(|e| RegistryError::ParseError(format!("Failed to parse YAML: {e}")))?;
let mut schema = Schema::from_schema_file(schema_file)?;
if !schema_name.is_empty() {
schema.name = schema_name.to_string();
}
let name = schema.name.clone();
self.register_schema(name, schema)
}
fn list_schemas_owned(&self) -> Vec<String> {
let mut schemas: Vec<String> = self.schemas.keys().cloned().collect();
schemas.sort();
schemas
}
fn remove_schema(&mut self, script_name: &str) -> bool {
self.schemas.remove(script_name).is_some()
}
fn clear(&mut self) {
self.schemas.clear();
self.schema_cache.clear();
}
fn schema_count(&self) -> usize {
self.schemas.len()
}
fn has_schema(&self, script_name: &str) -> bool {
self.schemas.contains_key(script_name) || self.find_schema_by_alias(script_name).is_some()
}
fn get_schema_metadata(&self, script_name: &str) -> Option<&SchemaMetadata> {
self.get_schema(script_name).map(|schema| &schema.metadata)
}
fn get_registry_stats(&self) -> RegistryStats {
let total_schemas = self.schemas.len();
let roman_scripts = self
.schemas
.values()
.filter(|schema| schema.script_type == "roman")
.count();
let brahmic_scripts = self
.schemas
.values()
.filter(|schema| schema.script_type == "brahmic")
.count();
let implicit_a_scripts = self
.schemas
.values()
.filter(|schema| schema.metadata.has_implicit_a)
.count();
let cached_schemas = self.schema_cache.len();
let total_mappings = self
.schemas
.values()
.map(|schema| schema.mappings.len())
.sum();
RegistryStats {
total_schemas,
roman_scripts,
brahmic_scripts,
implicit_a_scripts,
cached_schemas,
total_mappings,
}
}
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self::new()
}
}
mod error_tests;
#[cfg(test)]
mod validation_tests;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_registry_creation() {
let registry = SchemaRegistry::new();
let schemas = registry.list_schemas();
assert!(schemas.contains(&"devanagari"));
assert!(schemas.contains(&"iso15919"));
}
#[test]
fn test_schema_registration() {
let mut registry = SchemaRegistry::new();
let test_schema = Schema {
name: "test".to_string(),
script_type: "roman".to_string(),
target: "iso15919".to_string(),
mappings: FxHashMap::default(),
metadata: SchemaMetadata {
name: "test".to_string(),
script_type: "roman".to_string(),
has_implicit_a: false,
description: None,
aliases: None,
},
};
assert!(registry
.register_schema("test".to_string(), test_schema)
.is_ok());
assert!(registry.get_schema("test").is_some());
}
#[test]
fn test_schema_validation() {
let registry = SchemaRegistry::new();
let invalid_schema = Schema {
name: "".to_string(),
script_type: "roman".to_string(),
target: "iso15919".to_string(),
mappings: FxHashMap::default(),
metadata: SchemaMetadata::default(),
};
assert!(registry.validate_schema(&invalid_schema).is_err());
let invalid_script_type_schema = Schema {
name: "test".to_string(),
script_type: "invalid".to_string(),
target: "iso15919".to_string(),
mappings: FxHashMap::default(),
metadata: SchemaMetadata::default(),
};
assert!(registry
.validate_schema(&invalid_script_type_schema)
.is_err());
}
#[test]
fn test_load_schema_from_yaml() {
let mut registry = SchemaRegistry::new();
let test_path = "schemas/test/sample_schema.yaml";
if std::path::Path::new(test_path).exists() {
let result = registry.load_schema(test_path);
assert!(result.is_ok());
let schema = registry.get_schema("sample");
assert!(schema.is_some());
let schema = schema.unwrap();
assert_eq!(schema.name, "sample");
assert_eq!(schema.script_type, "roman");
assert_eq!(schema.target, "devanagari");
assert!(!schema.metadata.has_implicit_a);
}
}
#[test]
fn test_list_schemas_sorted() {
let mut registry = SchemaRegistry::new();
let schema1 = Schema::new("zulu".to_string(), "brahmic".to_string());
let schema2 = Schema::new("arabic".to_string(), "roman".to_string());
registry
.register_schema("zulu".to_string(), schema1)
.unwrap();
registry
.register_schema("arabic".to_string(), schema2)
.unwrap();
let schemas = registry.list_schemas();
assert_eq!(schemas[0], "arabic");
assert_eq!(schemas[1], "devanagari");
assert_eq!(schemas[2], "iso15919");
assert_eq!(schemas[3], "zulu");
}
#[test]
fn test_load_schemas_from_directory() {
let mut registry = SchemaRegistry::new();
let test_dir = "schemas";
if std::path::Path::new(test_dir).exists() {
let result = registry.load_schemas_from_directory(test_dir);
assert!(result.is_ok());
let count = result.unwrap();
assert!(count > 0);
assert!(registry.get_schema("sample").is_some());
}
}
#[test]
fn test_schema_metadata() {
let mut registry = SchemaRegistry::new();
let mut schema = Schema::new("test_meta".to_string(), "brahmic".to_string());
schema.metadata.has_implicit_a = true;
schema.metadata.description = Some("Test description".to_string());
schema.metadata.aliases = Some(vec!["test_alias".to_string()]);
registry
.register_schema("test_meta".to_string(), schema)
.unwrap();
let retrieved = registry.get_schema("test_meta").unwrap();
assert!(retrieved.metadata.has_implicit_a);
assert_eq!(
retrieved.metadata.description,
Some("Test description".to_string())
);
assert_eq!(
retrieved.metadata.aliases,
Some(vec!["test_alias".to_string()])
);
}
#[test]
fn test_new_interface_methods() {
let mut registry = SchemaRegistry::new();
let initial_count = registry.schema_count();
assert_eq!(initial_count, 2);
let test_schema = Schema::new("test_interface".to_string(), "roman".to_string());
registry
.register_schema("test_interface".to_string(), test_schema)
.unwrap();
assert!(registry.has_schema("test_interface"));
assert!(!registry.has_schema("nonexistent"));
assert_eq!(registry.schema_count(), 3);
let metadata = registry.get_schema_metadata("test_interface");
assert!(metadata.is_some());
assert_eq!(metadata.unwrap().name, "test_interface");
let stats = registry.get_registry_stats();
assert_eq!(stats.total_schemas, 3);
assert!(stats.roman_scripts >= 1);
let roman_schemas = registry.get_schemas_by_type("roman");
assert!(!roman_schemas.is_empty());
let summary = registry.export_summary();
assert!(summary.contains("Registry Summary"));
assert!(summary.contains("test_interface"));
assert!(!registry.is_empty());
}
#[test]
fn test_schema_caching() {
let mut registry = SchemaRegistry::new();
let test_path = "schemas/test/sample_schema.yaml";
if std::path::Path::new(test_path).exists() {
registry.load_schema(test_path).unwrap();
assert!(registry.schema_cache.contains_key("sample"));
let cached = registry.schema_cache.get("sample").unwrap();
assert_eq!(cached.metadata.name, "sample");
assert_eq!(cached.metadata.script_type, "roman");
}
}
}