Skip to main content

panproto_lens_dsl/
lib.rs

1//! Declarative lens DSL for panproto.
2//!
3//! Provides a human-readable specification format for lenses, protolenses,
4//! and related optical constructs. Supports Nickel (`.ncl`), JSON, and YAML
5//! surface syntax. Nickel is the primary authoring format, providing typed
6//! contracts for validation, record merge for fragment composition, functions
7//! for parameterized templates, and imports for modularity.
8//!
9//! ## Evaluation pipeline
10//!
11//! 1. Surface syntax (Nickel/JSON/YAML) is evaluated to a normalized record
12//! 2. The record is deserialized into a [`LensDocument`]
13//! 3. The document is compiled to a `ProtolensChain` + `FieldTransform`s
14//!
15//! ## Example
16//!
17//! ```no_run
18//! use panproto_lens_dsl::{load, compile};
19//!
20//! let doc = load(std::path::Path::new("my_lens.ncl")).unwrap();
21//! let compiled = compile(&doc, "record:body", &|_| None).unwrap();
22//! // compiled.chain is a ProtolensChain ready for instantiation
23//! // compiled.field_transforms are value-level transforms
24//! ```
25
26pub mod compile;
27pub mod compose;
28pub mod document;
29pub mod error;
30pub mod eval;
31pub mod rules;
32pub mod steps;
33
34use std::path::Path;
35
36pub use compile::CompiledLens;
37pub use document::{Constraint, HintSpec, HintStringency, LensDocument, PreferencePredicate};
38pub use error::LensDslError;
39
40/// Load a lens document from a file.
41///
42/// Dispatches to the appropriate evaluator based on file extension:
43/// - `.ncl` → Nickel evaluation
44/// - `.json` → JSON deserialization
45/// - `.yaml`, `.yml` → YAML deserialization
46///
47/// # Errors
48///
49/// Returns [`LensDslError::UnsupportedExtension`] for unknown extensions,
50/// [`LensDslError::Io`] for read errors, or evaluation-specific errors.
51pub fn load(path: &Path) -> Result<LensDocument, LensDslError> {
52    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
53
54    let source = std::fs::read_to_string(path)?;
55
56    match ext {
57        "ncl" => {
58            let parent = path.parent().map(Path::to_path_buf);
59            let import_paths = parent.into_iter().collect::<Vec<_>>();
60            eval::eval_nickel(&source, &import_paths)
61        }
62        "json" => eval::eval_json(&source),
63        "yaml" | "yml" => eval::eval_yaml(&source),
64        _ => Err(LensDslError::UnsupportedExtension {
65            ext: ext.to_owned(),
66        }),
67    }
68}
69
70/// Result of loading a directory of lens documents.
71pub struct LoadDirResult {
72    /// Successfully loaded documents.
73    pub documents: Vec<LensDocument>,
74    /// Files that failed to load, with their paths and errors.
75    pub errors: Vec<(std::path::PathBuf, LensDslError)>,
76}
77
78/// Load all lens documents from a directory.
79///
80/// Scans for `.ncl`, `.json`, `.yaml`, and `.yml` files.
81/// Files that fail to parse are reported in `errors`; successfully
82/// parsed documents are returned in `documents`.
83///
84/// # Errors
85///
86/// Returns [`LensDslError::Io`] if the directory itself cannot be read.
87/// Per-file errors are returned in [`LoadDirResult::errors`].
88pub fn load_dir(dir: &Path) -> Result<LoadDirResult, LensDslError> {
89    let mut documents = Vec::new();
90    let mut errors = Vec::new();
91
92    for entry in std::fs::read_dir(dir)? {
93        let path = entry?.path();
94        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
95
96        if matches!(ext, "ncl" | "json" | "yaml" | "yml") {
97            match load(&path) {
98                Ok(doc) => documents.push(doc),
99                Err(e) => errors.push((path, e)),
100            }
101        }
102    }
103
104    Ok(LoadDirResult { documents, errors })
105}
106
107/// Compile a [`LensDocument`] to a [`CompiledLens`].
108///
109/// Convenience re-export of [`compile::compile`].
110///
111/// # Errors
112///
113/// See [`compile::compile`] for error conditions.
114pub fn compile(
115    doc: &LensDocument,
116    body_vertex: &str,
117    resolver: &dyn Fn(&str) -> Option<CompiledLens>,
118) -> Result<CompiledLens, LensDslError> {
119    compile::compile(doc, body_vertex, resolver)
120}
121
122/// Load a lens file and compile it in one step.
123///
124/// # Errors
125///
126/// Combines errors from [`load`] and [`compile()`].
127pub fn load_and_compile(path: &Path, body_vertex: &str) -> Result<CompiledLens, LensDslError> {
128    let doc = load(path)?;
129    compile::compile(&doc, body_vertex, &|_| None)
130}