Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,28 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};

#[cfg(feature = "self-update")]
use crate::update;
use crate::{doctor, import, lsp, pantry, recipe, report, search, seed, server, shopping_list};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum ParserExtensions {
AdvancedUnits,
All,
Compat,
ComponentAlias,
InlineQuantities,
IntermediatePreparations,
Modes,
Modifiers,
#[default]
None,
RangeValues,
TimerRequiresTime,
}

#[derive(Parser, Debug)]
#[command(
author,
Expand All @@ -46,6 +62,21 @@ pub struct CliArgs {
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
pub verbosity: u8,

/// Configure parser extension
///
/// `all` and `none` are mutually exclusive AND overwrite all other extensions
///
/// `compat` enables all extensions except `timer-requires-time`
#[arg(
name = "EXTENSION",
short = 'e',
long = "extension",
value_enum,
global = true,
default_value = "none"
)]
pub extensions: Vec<ParserExtensions>,

#[command(subcommand)]
pub command: Command,
}
Expand Down
40 changes: 27 additions & 13 deletions src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ fn run_pantry(ctx: &Context, args: PantryArgs) -> Result<()> {

// Walk through the tree to find and process all recipes
fn process_recipes(
ctx: &Context,
tree: &cooklang_find::RecipeTree,
all_ingredients: &mut BTreeSet<String>,
pantry_ingredients: &mut BTreeSet<String>,
Expand All @@ -221,7 +222,7 @@ fn run_pantry(ctx: &Context, args: PantryArgs) -> Result<()> {
*recipe_count += 1;

// Parse the recipe
let recipe = match parse_recipe_from_entry(entry, 1.0) {
let recipe = match parse_recipe_from_entry(ctx, entry, 1.0) {
Ok(r) => r,
Err(e) => {
let name = entry.name().as_deref().unwrap_or("unknown");
Expand Down Expand Up @@ -252,9 +253,13 @@ fn run_pantry(ctx: &Context, args: PantryArgs) -> Result<()> {
}
}

// Recursively check children
for subtree in tree.children.values() {
// Recursively check children (sorted for deterministic output)
let mut keys: Vec<_> = tree.children.keys().collect();
keys.sort();
for key in keys {
let subtree = tree.children.get(key).unwrap();
process_recipes(
ctx,
subtree,
all_ingredients,
pantry_ingredients,
Expand All @@ -265,6 +270,7 @@ fn run_pantry(ctx: &Context, args: PantryArgs) -> Result<()> {
}

process_recipes(
ctx,
&tree,
&mut all_ingredients,
&mut pantry_ingredients,
Expand Down Expand Up @@ -337,6 +343,7 @@ fn run_aisle(ctx: &Context, args: AisleArgs) -> Result<()> {

// Walk through the tree to find and process all recipes
fn process_recipes(
ctx: &Context,
tree: &cooklang_find::RecipeTree,
all_ingredients: &mut BTreeSet<String>,
recipe_count: &mut usize,
Expand All @@ -346,7 +353,7 @@ fn run_aisle(ctx: &Context, args: AisleArgs) -> Result<()> {
*recipe_count += 1;

// Parse the recipe
let recipe = match parse_recipe_from_entry(entry, 1.0) {
let recipe = match parse_recipe_from_entry(ctx, entry, 1.0) {
Ok(r) => r,
Err(e) => {
let name = entry.name().as_deref().unwrap_or("unknown");
Expand All @@ -369,13 +376,16 @@ fn run_aisle(ctx: &Context, args: AisleArgs) -> Result<()> {
}
}

// Recursively check children
for subtree in tree.children.values() {
process_recipes(subtree, all_ingredients, recipe_count);
// Recursively check children (sorted for deterministic output)
let mut keys: Vec<_> = tree.children.keys().collect();
keys.sort();
for key in keys {
let subtree = tree.children.get(key).unwrap();
process_recipes(ctx, subtree, all_ingredients, recipe_count);
}
}

process_recipes(&tree, &mut all_ingredients, &mut recipe_count);
process_recipes(ctx, &tree, &mut all_ingredients, &mut recipe_count);

println!(
"Scanned {} recipes, found {} unique ingredients",
Expand Down Expand Up @@ -439,6 +449,7 @@ fn run_validate(ctx: &Context, args: ValidateArgs) -> Result<()> {

// Validate recipes and collect references
fn validate_recipes(
ctx: &Context,
tree: &cooklang_find::RecipeTree,
base_path: &Utf8PathBuf,
stats: &mut (usize, usize, usize, usize, usize),
Expand All @@ -462,7 +473,7 @@ fn run_validate(ctx: &Context, args: ValidateArgs) -> Result<()> {
match fs::read_to_string(&recipe_path) {
Ok(content) => {
// Parse with our configured parser to get all errors and warnings
let parsed = crate::util::PARSER.parse(&content);
let parsed = ctx.parser().parse(&content);

let errors: Vec<_> = parsed.report().errors().collect();
let warnings: Vec<_> = parsed.report().warnings().collect();
Expand Down Expand Up @@ -518,9 +529,12 @@ fn run_validate(ctx: &Context, args: ValidateArgs) -> Result<()> {
}
}

// Recursively check children
for subtree in tree.children.values() {
validate_recipes(subtree, base_path, stats, recipe_refs);
// Recursively check children (sorted for deterministic output)
let mut keys: Vec<_> = tree.children.keys().collect();
keys.sort();
for key in keys {
let subtree = tree.children.get(key).unwrap();
validate_recipes(ctx, subtree, base_path, stats, recipe_refs);
}
}

Expand All @@ -531,7 +545,7 @@ fn run_validate(ctx: &Context, args: ValidateArgs) -> Result<()> {
total_errors,
total_warnings,
);
validate_recipes(&tree, base_path, &mut stats, &mut recipe_references);
validate_recipes(ctx, &tree, base_path, &mut stats, &mut recipe_references);
(
total_recipes,
recipes_with_errors,
Expand Down
140 changes: 121 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Re-export modules for testing
use anyhow::{Context as _, Result};
use std::sync::OnceLock;

use anyhow::{bail, Context as _, Result};
use args::{CliArgs, Command, ParserExtensions};
use camino::{Utf8Path, Utf8PathBuf};
use cooklang::{Converter, CooklangParser, Extensions};
use util::resolve_to_absolute_path;

// Commands - make them available as public modules
pub mod doctor;
Expand All @@ -20,44 +24,142 @@ pub mod update;
pub mod args;
pub mod util;

// Context struct for testing - matches the one in main.rs
const LOCAL_CONFIG_DIR: &str = "config";
const APP_NAME: &str = "cook";
const UTF8_PATH_PANIC: &str = "cook only supports UTF-8 paths.";
const AUTO_AISLE: &str = "aisle.conf";
const AUTO_PANTRY: &str = "pantry.conf";

pub struct Context {
base_path: Utf8PathBuf,
parser: OnceLock<CooklangParser>,
}

impl Context {
pub fn new(base_path: Utf8PathBuf) -> Self {
Self { base_path }
Self {
base_path,
parser: OnceLock::new(),
}
}

pub fn parser(&self) -> &CooklangParser {
self.parser.get().expect("parser was not configured")
}

pub fn aisle(&self) -> Option<Utf8PathBuf> {
let local_config = self.base_path.join("config").join("aisle.conf");
if local_config.is_file() {
Some(local_config)
} else {
None
}
let auto = self.base_path.join(LOCAL_CONFIG_DIR).join(AUTO_AISLE);

tracing::trace!("checking auto aisle file: {auto}");

auto.is_file().then_some(auto).or_else(|| {
let global = global_file_path(AUTO_AISLE).ok()?;
tracing::trace!("checking global auto aisle file: {global}");
global.is_file().then_some(global)
})
}

pub fn pantry(&self) -> Option<Utf8PathBuf> {
let local_config = self.base_path.join("config").join("pantry.conf");
if local_config.is_file() {
Some(local_config)
} else {
None
}
let auto = self.base_path.join(LOCAL_CONFIG_DIR).join(AUTO_PANTRY);

tracing::trace!("checking auto pantry file: {auto}");

auto.is_file().then_some(auto).or_else(|| {
let global = global_file_path(AUTO_PANTRY).ok()?;
tracing::trace!("checking global auto pantry file: {global}");
global.is_file().then_some(global)
})
}

pub fn base_path(&self) -> &Utf8PathBuf {
&self.base_path
}
}

const APP_NAME: &str = "cook";
const UTF8_PATH_PANIC: &str = "cook only supports UTF-8 paths.";
fn configure_parser(args: &CliArgs) -> CooklangParser {
if args
.extensions
.iter()
.any(|e| matches!(e, ParserExtensions::None))
{
return CooklangParser::new(Extensions::empty(), Converter::default());
}
if args
.extensions
.iter()
.any(|e| matches!(e, ParserExtensions::All))
{
return CooklangParser::new(Extensions::all(), Converter::default());
}
let mut extensions = Extensions::empty();

for ext in &args.extensions {
match ext {
ParserExtensions::Compat => extensions |= Extensions::COMPAT,
ParserExtensions::Modifiers => extensions |= Extensions::COMPONENT_MODIFIERS,
ParserExtensions::ComponentAlias => extensions |= Extensions::COMPONENT_ALIAS,
ParserExtensions::AdvancedUnits => extensions |= Extensions::ADVANCED_UNITS,
ParserExtensions::Modes => extensions |= Extensions::MODES,
ParserExtensions::InlineQuantities => extensions |= Extensions::INLINE_QUANTITIES,
ParserExtensions::RangeValues => extensions |= Extensions::RANGE_VALUES,
ParserExtensions::TimerRequiresTime => extensions |= Extensions::TIMER_REQUIRES_TIME,
ParserExtensions::IntermediatePreparations => {
extensions |= Extensions::INTERMEDIATE_PREPARATIONS;
}
ParserExtensions::All | ParserExtensions::None => {}
}
}

CooklangParser::new(extensions, Converter::default())
}

pub fn configure_context(args: &CliArgs) -> Result<Context> {
let base_path = match args.command {
Command::Server(ref server_args) => server_args
.get_base_path()
.unwrap_or_else(|| Utf8PathBuf::from(".")),
Command::ShoppingList(ref shopping_list_args) => shopping_list_args
.get_base_path()
.unwrap_or_else(|| Utf8PathBuf::from(".")),
_ => Utf8PathBuf::from("."),
};

let absolute_base_path = resolve_to_absolute_path(&base_path)?;

if !absolute_base_path.is_dir() {
bail!("Base path is not a directory: {}", absolute_base_path);
}

let parser = OnceLock::new();
parser
.set(configure_parser(args))
.expect("failed to set parser");

Ok(Context {
base_path: absolute_base_path,
parser,
})
}

pub fn configure_logging(verbosity: u8) {
let env_filter = match verbosity {
0 => "warn,cook=warn", // Default: warnings and errors only
1 => "info,cook=info", // -v: info level
2 => "debug,cook=debug", // -vv: debug level
_ => "trace,cook=trace", // -vvv or more: trace level
};

tracing_subscriber::fmt()
.with_env_filter(env_filter)
.without_time()
.with_target(false)
.compact()
.with_writer(std::io::stderr)
.init();
}

/// Resolve a global configuration file path (e.g. `~/.config/cook/{name}`).
pub fn global_file_path(name: &str) -> Result<Utf8PathBuf> {
fn global_file_path(name: &str) -> Result<Utf8PathBuf> {
let dirs = directories::ProjectDirs::from("", "", APP_NAME)
.context("Could not determine home directory path")?;
let config = Utf8Path::from_path(dirs.config_dir()).expect(UTF8_PATH_PANIC);
Expand Down
Loading
Loading