use once_cell::sync::Lazy;
use pyo3::prelude::*;
use std::collections::HashMap;
use crate::Shlesha;
static GLOBAL_TRANSLITERATOR: Lazy<Shlesha> = Lazy::new(Shlesha::new);
#[pyclass(unsendable)]
pub struct PyShlesha {
inner: Shlesha,
}
#[pyclass]
#[derive(Clone)]
pub struct PyTransliterationMetadata {
#[pyo3(get)]
source_script: String,
#[pyo3(get)]
target_script: String,
#[pyo3(get)]
used_extensions: String,
#[pyo3(get)]
unknown_tokens: Vec<PyUnknownToken>,
}
#[pyclass]
#[derive(Clone)]
pub struct PyUnknownToken {
#[pyo3(get)]
script: String,
#[pyo3(get)]
token: String,
#[pyo3(get)]
position: usize,
#[pyo3(get)]
unicode: String,
#[pyo3(get)]
is_extension: bool,
}
#[pyclass]
pub struct PyTransliterationResult {
#[pyo3(get)]
output: String,
#[pyo3(get)]
metadata: Option<PyTransliterationMetadata>,
}
#[pymethods]
impl PyShlesha {
#[new]
fn new() -> Self {
Self {
inner: Shlesha::new(),
}
}
fn transliterate(&self, text: &str, from_script: &str, to_script: &str) -> PyResult<String> {
self.inner
.transliterate(text, from_script, to_script)
.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Transliteration failed: {e}"
))
})
}
fn transliterate_with_metadata(
&self,
text: &str,
from_script: &str,
to_script: &str,
) -> PyResult<PyTransliterationResult> {
let result = self
.inner
.transliterate_with_metadata(text, from_script, to_script)
.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Transliteration failed: {e}"
))
})?;
let py_metadata = result.metadata.map(|metadata| {
let unknown_tokens = metadata
.unknown_tokens
.into_iter()
.map(|token| PyUnknownToken {
script: token.script,
token: token.token.to_string(),
position: token.position,
unicode: token.unicode,
is_extension: token.is_extension,
})
.collect();
PyTransliterationMetadata {
source_script: metadata.source_script,
target_script: metadata.target_script,
used_extensions: metadata.used_extensions.to_string(),
unknown_tokens,
}
});
Ok(PyTransliterationResult {
output: result.output,
metadata: py_metadata,
})
}
fn list_supported_scripts(&self) -> Vec<String> {
self.inner
.list_supported_scripts()
.into_iter()
.map(|s| s.to_string())
.collect()
}
fn supports_script(&self, script: &str) -> bool {
self.inner.supports_script(script)
}
fn load_schema_from_file(&mut self, file_path: &str) -> PyResult<()> {
self.inner.load_schema_from_file(file_path).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Schema loading failed: {e}"))
})
}
fn load_schema_from_string(&mut self, yaml_content: &str, schema_name: &str) -> PyResult<()> {
self.inner
.load_schema_from_string(yaml_content, schema_name)
.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Schema loading failed: {e}"
))
})
}
fn get_schema_info(&self, py: Python<'_>, script_name: &str) -> PyResult<Option<PyObject>> {
Ok(self.inner.get_schema_info(script_name).map(|info| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("name", info.name).unwrap();
dict.set_item("description", info.description).unwrap();
dict.set_item("script_type", info.script_type).unwrap();
dict.set_item("is_runtime_loaded", info.is_runtime_loaded)
.unwrap();
dict.set_item("mapping_count", info.mapping_count).unwrap();
dict.into()
}))
}
fn remove_schema(&mut self, script_name: &str) -> bool {
self.inner.remove_schema(script_name)
}
fn clear_runtime_schemas(&mut self) {
self.inner.clear_runtime_schemas()
}
fn get_script_info(&self) -> HashMap<String, String> {
let mut info = HashMap::new();
for script in self.inner.list_supported_scripts() {
let description = match script.as_str() {
"iast" => "IAST (International Alphabet of Sanskrit Transliteration)",
"itrans" => "ITRANS (ASCII transliteration)",
"slp1" => "SLP1 (Sanskrit Library Phonetic scheme)",
"harvard_kyoto" | "hk" => "Harvard-Kyoto (ASCII-based academic standard)",
"velthuis" => "Velthuis (TeX-based notation)",
"wx" => "WX (Computational notation)",
"devanagari" | "deva" => "Devanagari script (देवनागरी)",
"bengali" | "bn" => "Bengali script (বাংলা)",
"tamil" | "ta" => "Tamil script (தமிழ்)",
"telugu" | "te" => "Telugu script (తెలుగు)",
"gujarati" | "gu" => "Gujarati script (ગુજરાતી)",
"kannada" | "kn" => "Kannada script (ಕನ್ನಡ)",
"malayalam" | "ml" => "Malayalam script (മലയാളം)",
"odia" | "od" | "oriya" => "Odia script (ଓଡ଼ିଆ)",
"iso15919" | "iso" | "iso_15919" => "ISO-15919 (International standard)",
_ => "Unknown script type",
};
info.insert(script.to_string(), description.to_string());
}
info
}
fn __repr__(&self) -> String {
let scripts = self.inner.list_supported_scripts();
format!("Shlesha(supported_scripts={})", scripts.len())
}
fn __str__(&self) -> String {
format!(
"Shlesha transliterator with {} supported scripts",
self.inner.list_supported_scripts().len()
)
}
fn benchmark_processor(
&self,
text: &str,
processor_type: &str,
mappings: HashMap<String, String>,
) -> PyResult<String> {
use crate::modules::script_converter::processors::{
FastMappingBuilder, RomanScriptProcessor,
};
use rustc_hash::FxHashMap;
let fx_mappings: FxHashMap<&str, &str> = mappings
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let result = match processor_type {
"fx_hashmap" => RomanScriptProcessor::process_with_fx_hashmap(text, &fx_mappings),
"aho_corasick" => {
let mappings_vec: Vec<(&str, &str)> = mappings
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let (ac, replacements) =
FastMappingBuilder::build_aho_corasick_mapping(&mappings_vec).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to build Aho-Corasick automaton: {e}"
))
})?;
RomanScriptProcessor::process_with_aho_corasick(text, &ac, &replacements)
}
"fast_lookup" => {
let mappings_vec: Vec<(&str, &str)> = mappings
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let (mapping, by_first_char) =
FastMappingBuilder::build_optimized_mapping(&mappings_vec);
RomanScriptProcessor::process_with_fast_lookup(text, &mapping, &by_first_char)
}
_ => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Unknown processor type: {processor_type}. Use 'fx_hashmap', 'aho_corasick', or 'fast_lookup'")
));
}
};
result.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Processor failed: {e}"))
})
}
}
#[pymethods]
impl PyTransliterationResult {
fn __repr__(&self) -> String {
match &self.metadata {
Some(metadata) => format!(
"TransliterationResult(output='{}', unknown_tokens={})",
self.output,
metadata.unknown_tokens.len()
),
None => format!("TransliterationResult(output='{}')", self.output),
}
}
}
#[pymethods]
impl PyTransliterationMetadata {
fn __repr__(&self) -> String {
format!(
"TransliterationMetadata(source='{}', target='{}', unknown_tokens={})",
self.source_script,
self.target_script,
self.unknown_tokens.len()
)
}
}
#[pymethods]
impl PyUnknownToken {
fn __repr__(&self) -> String {
format!(
"UnknownToken(script='{}', token='{}', position={})",
self.script, self.token, self.position
)
}
}
#[pyfunction]
fn create_transliterator() -> PyShlesha {
PyShlesha::new()
}
#[pyfunction]
fn transliterate(text: &str, from_script: &str, to_script: &str) -> PyResult<String> {
GLOBAL_TRANSLITERATOR
.transliterate(text, from_script, to_script)
.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Transliteration failed: {e}"
))
})
}
#[pyfunction]
fn get_supported_scripts() -> Vec<String> {
let transliterator = Shlesha::new();
transliterator
.list_supported_scripts()
.into_iter()
.map(|s| s.to_string())
.collect()
}
pub fn configure_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyShlesha>()?;
m.add_class::<PyTransliterationResult>()?;
m.add_class::<PyTransliterationMetadata>()?;
m.add_class::<PyUnknownToken>()?;
m.add_function(wrap_pyfunction!(create_transliterator, m)?)?;
m.add_function(wrap_pyfunction!(transliterate, m)?)?;
m.add_function(wrap_pyfunction!(get_supported_scripts, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("__author__", "Shlesha Contributors")?;
m.add(
"__description__",
"High-performance extensible transliteration library",
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_python_basic_transliteration() {
let transliterator = PyShlesha::new();
let result = transliterator
.transliterate("अ", "devanagari", "iast")
.unwrap();
assert_eq!(result, "a");
}
#[test]
fn test_python_metadata_collection() {
let transliterator = PyShlesha::new();
let result = transliterator
.transliterate_with_metadata("धर्मkr", "devanagari", "iast")
.unwrap();
assert!(result.output.contains("dharma"));
assert!(result.metadata.is_some());
let metadata = result.metadata.unwrap();
assert_eq!(metadata.source_script, "devanagari");
assert_eq!(metadata.target_script, "iast");
assert!(!metadata.unknown_tokens.is_empty());
}
#[test]
fn test_python_script_support() {
let transliterator = PyShlesha::new();
assert!(transliterator.supports_script("devanagari"));
assert!(transliterator.supports_script("iast"));
assert!(!transliterator.supports_script("nonexistent"));
let scripts = transliterator.list_supported_scripts();
assert!(!scripts.is_empty());
assert!(scripts.iter().any(|s| s == "devanagari"));
}
#[test]
fn test_convenience_functions() {
let result = transliterate("अ", "devanagari", "iast").unwrap();
assert_eq!(result, "a");
let scripts = get_supported_scripts();
assert!(!scripts.is_empty());
assert!(scripts.iter().any(|s| s == "devanagari"));
}
}