pydantic model generator for atproto lexicons
at main 79 lines 2.4 kB view raw
1//! pmgfal - pydantic model generator for atproto lexicons 2 3mod builtin; 4mod codegen; 5mod parser; 6mod types; 7 8use std::fs; 9use std::path::Path; 10 11use pyo3::prelude::*; 12use sha2::{Digest, Sha256}; 13 14/// compute a hash of all lexicon files in a directory 15#[pyfunction] 16#[pyo3(signature = (lexicon_dir, namespace_prefix=None))] 17fn hash_lexicons(lexicon_dir: &str, namespace_prefix: Option<&str>) -> PyResult<String> { 18 let lexicon_path = Path::new(lexicon_dir); 19 20 let mut hasher = Sha256::new(); 21 22 // include version in hash so cache invalidates on upgrades 23 hasher.update(env!("CARGO_PKG_VERSION").as_bytes()); 24 25 // include prefix in hash 26 if let Some(prefix) = namespace_prefix { 27 hasher.update(prefix.as_bytes()); 28 } 29 30 // collect and sort json files for deterministic hashing 31 let mut json_files: Vec<_> = walkdir::WalkDir::new(lexicon_path) 32 .into_iter() 33 .filter_map(|e| e.ok()) 34 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json")) 35 .collect(); 36 37 json_files.sort_by(|a, b| a.path().cmp(b.path())); 38 39 for entry in json_files { 40 let path = entry.path(); 41 if let Some(name) = path.file_name() { 42 hasher.update(name.as_encoded_bytes()); 43 } 44 if let Ok(content) = fs::read(path) { 45 hasher.update(&content); 46 } 47 } 48 49 let result = hasher.finalize(); 50 Ok(hex::encode(&result[..8])) // 16 hex chars 51} 52 53/// generate pydantic models from lexicon files 54#[pyfunction] 55#[pyo3(signature = (lexicon_dir, output_dir, namespace_prefix=None))] 56fn generate( 57 lexicon_dir: &str, 58 output_dir: &str, 59 namespace_prefix: Option<&str>, 60) -> PyResult<Vec<String>> { 61 let lexicon_path = Path::new(lexicon_dir); 62 let output_path = Path::new(output_dir); 63 64 let docs = parser::parse_lexicons(lexicon_path) 65 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?; 66 67 let files = codegen::generate_models(&docs, output_path, namespace_prefix) 68 .map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string()))?; 69 70 Ok(files) 71} 72 73#[pymodule] 74fn _pmgfal(m: &Bound<'_, PyModule>) -> PyResult<()> { 75 m.add_function(wrap_pyfunction!(generate, m)?)?; 76 m.add_function(wrap_pyfunction!(hash_lexicons, m)?)?; 77 m.add("__version__", env!("CARGO_PKG_VERSION"))?; 78 Ok(()) 79}