use std::path::Path;
use std::sync::RwLock;
use dashmap::DashMap;
use tower_lsp::lsp_types::Url;
use crate::document::Document;
#[derive(Debug, Clone)]
pub struct AisleIngredient {
pub name: String,
pub common_name: String,
pub category: String,
}
#[derive(Debug, Default)]
pub struct AisleConfig {
pub ingredients: Vec<AisleIngredient>,
}
impl AisleConfig {
pub fn parse(content: &str) -> Option<Self> {
let result = cooklang::aisle::parse_lenient(content);
let (aisle_conf, warnings) = result.into_result().ok()?;
for warning in warnings.iter() {
tracing::warn!("aisle.conf warning: {}", warning);
}
let mut ingredients = Vec::new();
for category in &aisle_conf.categories {
for ingredient in &category.ingredients {
if let Some(common_name) = ingredient.names.first() {
for name in &ingredient.names {
ingredients.push(AisleIngredient {
name: name.to_string(),
common_name: common_name.to_string(),
category: category.name.to_string(),
});
}
}
}
}
Some(AisleConfig { ingredients })
}
pub fn load_from_workspace(workspace_path: &Path) -> Option<Self> {
let config_path = workspace_path.join("config").join("aisle.conf");
if config_path.exists() {
if let Ok(content) = std::fs::read_to_string(&config_path) {
tracing::info!("Loading aisle.conf from {:?}", config_path);
return Self::parse(&content);
}
}
let root_path = workspace_path.join("aisle.conf");
if root_path.exists() {
if let Ok(content) = std::fs::read_to_string(&root_path) {
tracing::info!("Loading aisle.conf from {:?}", root_path);
return Self::parse(&content);
}
}
None
}
}
pub struct ServerState {
pub documents: DashMap<Url, Document>,
pub aisle_config: RwLock<Option<AisleConfig>>,
}
impl ServerState {
pub fn new() -> Self {
Self {
documents: DashMap::new(),
aisle_config: RwLock::new(None),
}
}
pub fn load_aisle_config(&self, workspace_path: &Path) {
if let Some(config) = AisleConfig::load_from_workspace(workspace_path) {
let count = config.ingredients.len();
if let Ok(mut guard) = self.aisle_config.write() {
*guard = Some(config);
tracing::info!("Loaded {} ingredients from aisle.conf", count);
}
}
}
pub fn get_aisle_ingredients(&self) -> Vec<AisleIngredient> {
if let Ok(guard) = self.aisle_config.read() {
if let Some(ref config) = *guard {
return config.ingredients.clone();
}
}
Vec::new()
}
pub fn open_document(&self, uri: Url, version: i32, content: String) {
let doc = Document::new(uri.clone(), version, content);
self.documents.insert(uri, doc);
}
pub fn update_document(&self, uri: &Url, version: i32, content: String) {
if let Some(mut doc) = self.documents.get_mut(uri) {
doc.update(version, content);
}
}
pub fn close_document(&self, uri: &Url) {
self.documents.remove(uri);
}
pub fn get_document(&self, uri: &Url) -> Option<dashmap::mapref::one::Ref<'_, Url, Document>> {
self.documents.get(uri)
}
}
impl Default for ServerState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aisle_config_parse() {
let content = r#"
[produce]
potatoes
carrots
onions|yellow onion|white onion
[dairy]
milk
butter
cheese|cheddar|parmesan
"#;
let config = AisleConfig::parse(content).unwrap();
assert!(!config.ingredients.is_empty());
let potatoes = config
.ingredients
.iter()
.find(|i| i.name == "potatoes")
.unwrap();
assert_eq!(potatoes.category, "produce");
assert_eq!(potatoes.common_name, "potatoes");
let yellow_onion = config
.ingredients
.iter()
.find(|i| i.name == "yellow onion")
.unwrap();
assert_eq!(yellow_onion.category, "produce");
assert_eq!(yellow_onion.common_name, "onions");
let cheddar = config
.ingredients
.iter()
.find(|i| i.name == "cheddar")
.unwrap();
assert_eq!(cheddar.category, "dairy");
assert_eq!(cheddar.common_name, "cheese");
}
#[test]
fn test_aisle_config_lenient_parsing() {
let content = r#"
orphan ingredient before any category
[produce]
apple
banana
apple
[dairy]
milk
[produce]
carrot
"#;
let config = AisleConfig::parse(content).unwrap();
assert!(!config.ingredients.is_empty());
let apple = config.ingredients.iter().find(|i| i.name == "apple");
assert!(apple.is_some());
assert_eq!(apple.unwrap().category, "produce");
let banana = config.ingredients.iter().find(|i| i.name == "banana");
assert!(banana.is_some());
let milk = config.ingredients.iter().find(|i| i.name == "milk");
assert!(milk.is_some());
assert_eq!(milk.unwrap().category, "dairy");
let apple_count = config.ingredients.iter().filter(|i| i.name == "apple").count();
assert_eq!(apple_count, 1);
}
}