pydantic model generator for atproto lexicons
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}