use crate::server::{
shopping_list_store::{ShoppingListItem, ShoppingListStore},
AppState,
};
use crate::util::{extract_ingredients, PARSER};
use axum::{extract::State, http::StatusCode, Json};
use cooklang::ingredient_list::IngredientList;
use serde::Deserialize;
use serde_json;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Deserialize)]
pub struct RecipeRequest {
recipe: String,
scale: Option<f64>,
}
pub async fn shopping_list(
State(state): State<Arc<AppState>>,
axum::extract::Json(payload): axum::extract::Json<Vec<RecipeRequest>>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let mut list = IngredientList::new();
let mut seen = BTreeMap::new();
for entry in payload {
let recipe_with_scale = if let Some(scale) = entry.scale {
format!("{}:{}", entry.recipe, scale)
} else {
entry.recipe
};
extract_ingredients(
&recipe_with_scale,
&mut list,
&mut seen,
&state.base_path,
PARSER.converter(),
false,
)
.map_err(|e| {
tracing::error!("Error processing recipe: {}", e);
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": e.to_string() })),
)
})?;
}
let aisle_content = if let Some(path) = &state.aisle_path {
match std::fs::read_to_string(path) {
Ok(content) => {
tracing::debug!("Loaded aisle file from: {:?}", path);
content
}
Err(e) => {
tracing::warn!("Failed to read aisle file from {:?}: {}", path, e);
String::new()
}
}
} else {
tracing::debug!("No aisle file configured");
String::new()
};
let aisle_result = cooklang::aisle::parse_lenient(&aisle_content);
if aisle_result.report().has_warnings() {
for warning in aisle_result.report().warnings() {
tracing::warn!("Aisle configuration warning: {}", warning);
}
}
let aisle = aisle_result.output().cloned().unwrap_or_default();
let pantry_conf = if let Some(path) = &state.pantry_path {
match std::fs::read_to_string(path) {
Ok(content) => {
tracing::debug!("Loaded pantry file from: {:?}", path);
let result = cooklang::pantry::parse_lenient(&content);
if result.report().has_warnings() {
for warning in result.report().warnings() {
tracing::warn!("Pantry configuration warning: {}", warning);
}
}
result.output().cloned()
}
Err(e) => {
tracing::warn!("Failed to read pantry file from {:?}: {}", path, e);
None
}
}
} else {
tracing::debug!("No pantry file configured");
None
};
list = list.use_common_names(&aisle, PARSER.converter());
let mut pantry_items = Vec::new();
if let Some(ref pantry) = pantry_conf {
for (ingredient_name, _) in list.iter() {
if let Some((_, pantry_item)) = pantry.find_ingredient(ingredient_name) {
if let Some(qty_str) = pantry_item.quantity() {
if qty_str == "unlim" || qty_str == "unlimited" {
pantry_items.push(ingredient_name.clone());
} else if let Some((value, _)) = pantry_item.parsed_quantity() {
if value > 0.0 {
pantry_items.push(ingredient_name.clone());
}
}
} else {
pantry_items.push(ingredient_name.clone());
}
}
}
}
let final_list = if let Some(ref pantry) = pantry_conf {
list.subtract_pantry(pantry, PARSER.converter())
} else {
list
};
let categories = final_list.categorize(&aisle);
let mut shopping_categories = Vec::new();
for (category, items) in categories {
let mut shopping_items = Vec::new();
for (name, qty) in items {
let item_json = serde_json::json!({
"name": name,
"quantities": qty.into_vec()
});
shopping_items.push(item_json);
}
if !shopping_items.is_empty() {
shopping_categories.push(serde_json::json!({
"category": category,
"items": shopping_items
}));
}
}
let json_value = serde_json::json!({
"categories": shopping_categories,
"pantry_items": pantry_items
});
Ok(Json(json_value))
}
pub async fn get_shopping_list_items(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<ShoppingListItem>>, (StatusCode, Json<serde_json::Value>)> {
let store = ShoppingListStore::new(&state.base_path);
let items = store.load().map_err(|e| {
tracing::error!("Failed to load shopping list: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
})?;
Ok(Json(items))
}
#[derive(Debug, Deserialize)]
pub struct AddItemRequest {
pub path: String,
pub name: String,
pub scale: f64,
}
pub async fn add_to_shopping_list(
State(state): State<Arc<AppState>>,
Json(payload): Json<AddItemRequest>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
let store = ShoppingListStore::new(&state.base_path);
let item = ShoppingListItem {
path: payload.path,
name: payload.name,
scale: payload.scale,
};
store.add(item).map_err(|e| {
tracing::error!("Failed to add to shopping list: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
})?;
Ok(StatusCode::OK)
}
#[derive(Debug, Deserialize)]
pub struct RemoveItemRequest {
pub path: String,
}
pub async fn remove_from_shopping_list(
State(state): State<Arc<AppState>>,
Json(payload): Json<RemoveItemRequest>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
let store = ShoppingListStore::new(&state.base_path);
store.remove(&payload.path).map_err(|e| {
tracing::error!("Failed to remove from shopping list: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
})?;
Ok(StatusCode::OK)
}
pub async fn clear_shopping_list(
State(state): State<Arc<AppState>>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
let store = ShoppingListStore::new(&state.base_path);
store.clear().map_err(|e| {
tracing::error!("Failed to clear shopping list: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
})?;
Ok(StatusCode::OK)
}