#![allow(clippy::unnecessary_literal_bound)]
use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock, RwLock};
pub trait Compressor: Send + Sync {
fn name(&self) -> &str;
fn compress(&self, input: &str, budget: Option<usize>) -> String;
}
pub trait Chunker: Send + Sync {
fn name(&self) -> &str;
fn chunk(&self, input: &str) -> Vec<String>;
}
pub trait ReadMode: Send + Sync {
fn name(&self) -> &str;
fn render(&self, source: &str, path: &str) -> String;
}
pub trait RenderTransform: Send + Sync {
fn name(&self) -> &str;
fn render(&self, input: &str, hint: i32) -> String;
}
#[derive(Default)]
pub struct ExtensionRegistry {
read_modes: BTreeMap<String, Arc<dyn ReadMode>>,
compressors: BTreeMap<String, Arc<dyn Compressor>>,
chunkers: BTreeMap<String, Arc<dyn Chunker>>,
render_transforms: BTreeMap<String, Arc<dyn RenderTransform>>,
}
impl ExtensionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_builtins() -> Self {
let mut reg = Self::new();
reg.register_read_mode(Arc::new(FullReadMode));
reg.register_compressor(Arc::new(IdentityCompressor));
reg.register_compressor(Arc::new(WhitespaceCompressor));
crate::core::nc_compress::register_into(&mut reg);
reg.register_chunker(Arc::new(LineChunker::default()));
reg.register_chunker(Arc::new(ParagraphChunker));
crate::core::extractors::register_into(&mut reg);
#[cfg(feature = "wasm")]
if let Ok(dir) = std::env::var("LEAN_CTX_WASM_DIR") {
crate::core::wasm_ext::register_compressors_from_dir(&mut reg, dir);
}
reg
}
pub fn register_read_mode(&mut self, handler: Arc<dyn ReadMode>) {
self.read_modes.insert(handler.name().to_string(), handler);
}
pub fn register_compressor(&mut self, handler: Arc<dyn Compressor>) {
self.compressors.insert(handler.name().to_string(), handler);
}
pub fn register_chunker(&mut self, handler: Arc<dyn Chunker>) {
self.chunkers.insert(handler.name().to_string(), handler);
}
#[must_use]
pub fn read_mode(&self, name: &str) -> Option<Arc<dyn ReadMode>> {
self.read_modes.get(name).cloned()
}
#[must_use]
pub fn compressor(&self, name: &str) -> Option<Arc<dyn Compressor>> {
self.compressors.get(name).cloned()
}
#[must_use]
pub fn chunker(&self, name: &str) -> Option<Arc<dyn Chunker>> {
self.chunkers.get(name).cloned()
}
#[must_use]
pub fn read_mode_names(&self) -> Vec<String> {
self.read_modes.keys().cloned().collect()
}
#[must_use]
pub fn compressor_names(&self) -> Vec<String> {
self.compressors.keys().cloned().collect()
}
#[must_use]
pub fn chunker_names(&self) -> Vec<String> {
self.chunkers.keys().cloned().collect()
}
pub fn register_render_transform(&mut self, handler: Arc<dyn RenderTransform>) {
self.render_transforms
.insert(handler.name().to_string(), handler);
}
#[must_use]
pub fn render_transform(&self, name: &str) -> Option<Arc<dyn RenderTransform>> {
self.render_transforms.get(name).cloned()
}
#[must_use]
pub fn render_transform_names(&self) -> Vec<String> {
self.render_transforms.keys().cloned().collect()
}
}
pub fn global() -> &'static RwLock<ExtensionRegistry> {
static REGISTRY: OnceLock<RwLock<ExtensionRegistry>> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(ExtensionRegistry::with_builtins()))
}
struct FullReadMode;
impl ReadMode for FullReadMode {
fn name(&self) -> &str {
"full"
}
fn render(&self, source: &str, _path: &str) -> String {
source.to_string()
}
}
struct IdentityCompressor;
impl Compressor for IdentityCompressor {
fn name(&self) -> &str {
"identity"
}
fn compress(&self, input: &str, budget: Option<usize>) -> String {
truncate_to_budget(input.to_string(), budget)
}
}
struct WhitespaceCompressor;
impl Compressor for WhitespaceCompressor {
fn name(&self) -> &str {
"whitespace"
}
fn compress(&self, input: &str, budget: Option<usize>) -> String {
let mut out = String::with_capacity(input.len());
let mut blank_run = 0u32;
for line in input.lines() {
if line.trim().is_empty() {
blank_run += 1;
if blank_run > 1 {
continue;
}
out.push('\n');
} else {
blank_run = 0;
out.push_str(line.trim_end());
out.push('\n');
}
}
truncate_to_budget(out, budget)
}
}
struct LineChunker {
window: usize,
}
impl Default for LineChunker {
fn default() -> Self {
Self { window: 50 }
}
}
impl Chunker for LineChunker {
fn name(&self) -> &str {
"lines"
}
fn chunk(&self, input: &str) -> Vec<String> {
let lines: Vec<&str> = input.lines().collect();
if lines.is_empty() {
return Vec::new();
}
lines
.chunks(self.window.max(1))
.map(|w| w.join("\n"))
.collect()
}
}
struct ParagraphChunker;
impl Chunker for ParagraphChunker {
fn name(&self) -> &str {
"paragraph"
}
fn chunk(&self, input: &str) -> Vec<String> {
input
.split("\n\n")
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
}
pub(crate) fn truncate_to_budget(mut s: String, budget: Option<usize>) -> String {
if let Some(b) = budget
&& s.len() > b
{
let mut end = b;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtins_are_registered() {
let reg = ExtensionRegistry::with_builtins();
assert_eq!(reg.read_mode_names(), vec!["full"]);
assert_eq!(
reg.compressor_names(),
vec!["identity", "markdown", "prose", "whitespace"]
);
assert_eq!(
reg.chunker_names(),
vec!["csv", "eml", "html", "json", "lines", "paragraph"]
);
}
#[test]
fn whitespace_compressor_collapses_blanks() {
let reg = ExtensionRegistry::with_builtins();
let c = reg.compressor("whitespace").unwrap();
let out = c.compress("a\n\n\n\nb \n", None);
assert_eq!(out, "a\n\nb\n");
}
#[test]
fn identity_compressor_honors_budget_on_char_boundary() {
let reg = ExtensionRegistry::with_builtins();
let c = reg.compressor("identity").unwrap();
let out = c.compress("aäb", Some(2));
assert_eq!(out, "a");
}
#[test]
fn chunkers_split_as_expected() {
let reg = ExtensionRegistry::with_builtins();
let para = reg.chunker("paragraph").unwrap();
assert_eq!(
para.chunk("one\n\ntwo\n\n\nthree"),
vec!["one", "two", "three"]
);
let lines = reg.chunker("lines").unwrap();
assert_eq!(lines.chunk("a\nb\nc").len(), 1);
}
struct UpperCompressor;
impl Compressor for UpperCompressor {
fn name(&self) -> &str {
"uppercase"
}
fn compress(&self, input: &str, _budget: Option<usize>) -> String {
input.to_uppercase()
}
}
#[test]
fn extension_can_register_and_run_custom_compressor() {
let mut reg = ExtensionRegistry::with_builtins();
reg.register_compressor(Arc::new(UpperCompressor));
assert!(reg.compressor_names().contains(&"uppercase".to_string()));
let c = reg.compressor("uppercase").unwrap();
assert_eq!(c.compress("hi", None), "HI");
}
struct UpperRender;
impl RenderTransform for UpperRender {
fn name(&self) -> &str {
"upper"
}
fn render(&self, input: &str, hint: i32) -> String {
format!("{}:{}", hint, input.to_uppercase())
}
}
#[test]
fn render_transform_registers_and_resolves_with_hint() {
let mut reg = ExtensionRegistry::with_builtins();
reg.register_render_transform(Arc::new(UpperRender));
let r = reg.render_transform("upper").unwrap();
assert_eq!(r.render("hi", 1), "1:HI");
assert!(reg.render_transform_names().contains(&"upper".to_string()));
}
#[test]
fn global_registry_seeds_builtins() {
let reg = global().read().unwrap();
assert!(reg.compressor("identity").is_some());
}
}