1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use crate::util::split_recipe_name_and_scaling_factor;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use clap::{CommandFactory, Parser};
use cooklang_reports::{config::Config, render_template_with_config};
use std::{fs, path::PathBuf};
use tracing::warn;
#[derive(Parser, Debug)]
pub struct ReportArgs {
/// Path to the Jinja2 template file
///
/// The template receives recipe data including:
/// - metadata (title, author, tags, etc.)
/// - ingredients (with quantities and units)
/// - steps (parsed instructions)
/// - cookware and timers
///
/// Example template variables:
/// {{ recipe.title }}
/// {% for ingredient in recipe.ingredients %}
/// {{ ingredient.name }}: {{ ingredient.quantity }}
/// {% endfor %}
#[arg(short, long, value_hint = clap::ValueHint::FilePath)]
template: Utf8PathBuf,
/// Recipe file to process
///
/// Can include an optional scaling factor using the :N syntax
/// (e.g., "recipe.cook:2" to double the recipe). The scaling
/// will be applied to all ingredient quantities in the template.
#[arg(value_name = "RECIPE")]
recipe: String,
/// Path to the datastore directory with additional recipe data
///
/// The datastore can contain nutritional information, costs,
/// and other data that can be accessed in the template.
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
datastore: Option<Utf8PathBuf>,
/// Path to the aisle configuration file
///
/// Used for categorizing ingredients by store section.
/// The template can access ingredient aisle information.
#[arg(short, long, value_hint = clap::ValueHint::FilePath)]
aisle: Option<Utf8PathBuf>,
/// Path to the pantry configuration file
///
/// Used for filtering out pantry items from shopping lists.
/// Ingredients marked as pantry items can be accessed in templates.
#[arg(short = 'p', long, value_hint = clap::ValueHint::FilePath)]
pantry: Option<Utf8PathBuf>,
/// Base path for resolving recipe references
///
/// Defaults to the current working directory. This is used when
/// recipes reference other recipes using relative paths.
#[arg(short = 'b', long, value_hint = clap::ValueHint::DirPath)]
base_path: Option<Utf8PathBuf>,
}
pub fn run(ctx: &crate::Context, args: ReportArgs) -> Result<()> {
// Print warning about prototype feature
warn!("⚠️ The report command is a prototype feature and will change in future versions.");
// Split recipe name and scaling factor
let (recipe_name, scaling_factor) = split_recipe_name_and_scaling_factor(&args.recipe)
.map(|(name, factor)| {
let scale = factor.parse::<f64>().unwrap_or_else(|err| {
let mut cmd = crate::args::CliArgs::command();
cmd.error(
clap::error::ErrorKind::InvalidValue,
format!("Invalid scaling factor for '{name}': {err}"),
)
.exit()
});
(name, scale)
})
.unwrap_or((&args.recipe, 1.0));
// Read the recipe file
let recipe = fs::read_to_string(recipe_name)
.with_context(|| format!("Failed to read recipe file: {recipe_name}"))?;
// Read the template file
let template = fs::read_to_string(&args.template)
.with_context(|| format!("Failed to read template file: {}", args.template))?;
// Create config with scale, optional datastore, and base_path
let mut builder = Config::builder();
builder.scale(scaling_factor);
if let Some(datastore) = args.datastore {
builder.datastore_path(PathBuf::from(datastore));
}
// Use provided base_path or fall back to context
if let Some(base_path) = args.base_path {
// Convert to absolute path if relative
let abs_base_path = if base_path.is_absolute() {
PathBuf::from(base_path)
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(base_path)
};
builder.base_path(abs_base_path);
} else {
builder.base_path(PathBuf::from(ctx.base_path()));
}
// Add aisle configuration if provided
if let Some(aisle_path) = args.aisle {
// Convert to absolute path if relative
let abs_aisle_path = if aisle_path.is_absolute() {
PathBuf::from(aisle_path)
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(aisle_path)
};
builder.aisle_path(abs_aisle_path);
} else if let Some(aisle_path) = ctx.aisle() {
// Use context aisle if not explicitly provided
builder.aisle_path(PathBuf::from(aisle_path));
}
// Add pantry configuration if provided
if let Some(pantry_path) = args.pantry {
// Convert to absolute path if relative
let abs_pantry_path = if pantry_path.is_absolute() {
PathBuf::from(pantry_path)
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(pantry_path)
};
builder.pantry_path(abs_pantry_path);
} else if let Some(pantry_path) = ctx.pantry() {
// Use context pantry if not explicitly provided
builder.pantry_path(PathBuf::from(pantry_path));
}
let config = builder.build();
// Render the report
let report = match render_template_with_config(&recipe, &template, &config) {
Ok(report) => report,
Err(err) => {
// Use the enhanced error formatting from cooklang-reports
eprintln!("{}", err.format_with_source());
std::process::exit(1);
}
};
// Print the report
println!("{report}");
Ok(())
}