Skip to content

Commit 20a39fa

Browse files
authored
Merge pull request #46 from cooklang/simpler-model
Simpler model
2 parents 07e09d6 + 324b75f commit 20a39fa

24 files changed

Lines changed: 552 additions & 894 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
## Unreleased - ReleaseDate
44

5-
5+
- Remove generics from `Recipe`. Now recipes are scalable multiple times.
6+
- Remove `Recipe::default_scale`.
7+
- Scaling is now infallible, text values are ignored. Removed `ScaleOutcome`, if
8+
this functionality was needed, it can easily be replaced by checking if the
9+
quatity is text or not scalable.
10+
- Cookware now can have full quantities with units, not just values.
11+
- Servings value in metadata now isn't a vector
612

713
## 0.16.3 - 2025/07/28
814

915
- Fixes scaling behavior in metadata servings by @dubadub in https://github.com/cooklang/cooklang-rs/pull/43
1016
- Adds lenient aisle parsing to allow more flexible formatting by @dubadub in https://github.com/cooklang/cooklang-rs/pull/44
1117
- Softens canonical parser to reduce strictness on edge cases by @dubadub in https://github.com/cooklang/cooklang-rs/pull/45
1218

13-
1419
## 0.16.1 - 2025/05/27
1520

1621
- Adds references support into IngredientList by @dubadub in https://github.com/cooklang/cooklang-rs/pull/36

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@ use model::*;
1212
pub fn parse_recipe(input: String, scaling_factor: f64) -> CooklangRecipe {
1313
let parser = cooklang::CooklangParser::canonical();
1414

15-
let (parsed, _warnings) = parser.parse(&input).into_result().unwrap();
15+
let (mut parsed, _warnings) = parser.parse(&input).into_result().unwrap();
1616

17-
let scaled = parsed.scale(scaling_factor, parser.converter());
17+
parsed.scale(scaling_factor, parser.converter());
1818

19-
into_simple_recipe(&scaled)
19+
into_simple_recipe(&parsed)
2020
}
2121

2222
#[uniffi::export]
2323
pub fn parse_metadata(input: String, scaling_factor: f64) -> CooklangMetadata {
2424
let mut metadata = CooklangMetadata::new();
2525
let parser = cooklang::CooklangParser::canonical();
2626

27-
let (parsed, _warnings) = parser.parse(&input).into_result().unwrap();
27+
let (mut parsed, _warnings) = parser.parse(&input).into_result().unwrap();
2828

29-
let scaled = parsed.scale(scaling_factor, parser.converter());
29+
parsed.scale(scaling_factor, parser.converter());
3030

3131
// converting IndexMap into HashMap
32-
let _ = &(scaled.metadata.map).iter().for_each(|(key, value)| {
32+
let _ = &(parsed.metadata.map).iter().for_each(|(key, value)| {
3333
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
3434
metadata.insert(key.to_string(), value.to_string());
3535
}

bindings/src/model.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
use std::collections::HashMap;
22

33
use cooklang::model::Item as OriginalItem;
4-
use cooklang::quantity::{
5-
Quantity as OriginalQuantity, Value as OriginalValue
6-
};
7-
use cooklang::ScaledRecipe as OriginalRecipe;
4+
use cooklang::quantity::{Quantity as OriginalQuantity, Value as OriginalValue};
5+
use cooklang::Recipe as OriginalRecipe;
86

97
#[derive(uniffi::Record, Debug)]
108
pub struct CooklangRecipe {
@@ -174,7 +172,7 @@ trait Amountable {
174172
fn extract_amount(&self) -> Amount;
175173
}
176174

177-
impl Amountable for OriginalQuantity<OriginalValue> {
175+
impl Amountable for OriginalQuantity {
178176
fn extract_amount(&self) -> Amount {
179177
let quantity = extract_value(self.value());
180178

@@ -419,8 +417,8 @@ pub(crate) fn into_simple_recipe(recipe: &OriginalRecipe) -> CooklangRecipe {
419417
}
420418
}
421419

422-
impl From<&cooklang::Ingredient<OriginalValue>> for Ingredient {
423-
fn from(ingredient: &cooklang::Ingredient<OriginalValue>) -> Self {
420+
impl From<&cooklang::Ingredient> for Ingredient {
421+
fn from(ingredient: &cooklang::Ingredient) -> Self {
424422
Ingredient {
425423
name: ingredient.name.clone(),
426424
amount: ingredient.quantity.as_ref().map(|q| q.extract_amount()),
@@ -429,17 +427,17 @@ impl From<&cooklang::Ingredient<OriginalValue>> for Ingredient {
429427
}
430428
}
431429

432-
impl From<&cooklang::Cookware<OriginalValue>> for Cookware {
433-
fn from(cookware: &cooklang::Cookware<OriginalValue>) -> Self {
430+
impl From<&cooklang::Cookware> for Cookware {
431+
fn from(cookware: &cooklang::Cookware) -> Self {
434432
Cookware {
435433
name: cookware.name.clone(),
436434
amount: cookware.quantity.as_ref().map(|q| q.extract_amount()),
437435
}
438436
}
439437
}
440438

441-
impl From<&cooklang::Timer<OriginalValue>> for Timer {
442-
fn from(timer: &cooklang::Timer<OriginalValue>) -> Self {
439+
impl From<&cooklang::Timer> for Timer {
440+
fn from(timer: &cooklang::Timer) -> Self {
443441
Timer {
444442
name: Some(timer.name.clone().unwrap_or_default()),
445443
amount: timer.quantity.as_ref().map(|q| q.extract_amount()),

fuzz/fuzz_targets/fuzz_parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use libfuzzer_sys::fuzz_target;
44

5-
use cooklang::{CooklangParser, Extensions, Converter};
5+
use cooklang::{Converter, CooklangParser, Extensions};
66

77
fuzz_target!(|contents: &str| {
88
let parser = CooklangParser::new(Extensions::all(), Converter::default());

src/aisle.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ pub fn parse(input: &str) -> Result<AisleConf, AisleConfError> {
275275
pub fn parse_lenient(input: &str) -> PassResult<AisleConf> {
276276
let mut report = SourceReport::empty();
277277

278-
let conf = parse_core(input, true, Some(&mut report)).expect("lenient parsing should never fail");
278+
let conf =
279+
parse_core(input, true, Some(&mut report)).expect("lenient parsing should never fail");
279280
PassResult::new(Some(conf), report)
280281
}
281282

src/analysis/event_consumer.rs

Lines changed: 61 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::parser::{
99
self, BlockKind, Event, IntermediateData, IntermediateRefMode, IntermediateTargetKind,
1010
Modifiers,
1111
};
12-
use crate::quantity::{Quantity, QuantityValue, ScalableValue, Value};
12+
use crate::quantity::{Quantity, Value};
1313
use crate::span::Span;
1414
use crate::text::Text;
1515
use crate::{model::*, Extensions, ParseOptions};
@@ -61,14 +61,13 @@ pub fn parse_events<'i, 'c>(
6161
converter,
6262
parse_options,
6363

64-
content: ScalableRecipe {
64+
content: Recipe {
6565
metadata: Default::default(),
6666
sections: Default::default(),
6767
ingredients: Default::default(),
6868
cookware: Default::default(),
6969
timers: Default::default(),
7070
inline_quantities: Default::default(),
71-
data: crate::scale::Servings(None),
7271
},
7372
current_section: Section::default(),
7473

@@ -90,7 +89,7 @@ struct RecipeCollector<'i, 'c> {
9089
converter: &'c Converter,
9190
parse_options: ParseOptions<'c>,
9291

93-
content: ScalableRecipe,
92+
content: Recipe,
9493
current_section: Section,
9594

9695
define_mode: DefineMode,
@@ -265,22 +264,18 @@ impl<'i> RecipeCollector<'i, '_> {
265264
continue;
266265
}
267266
if let Some(sk) = key.as_str().and_then(|s| StdKey::from_str(s).ok()) {
268-
match check_std_entry(sk, value, self.converter) {
269-
Ok(Some(servings)) => self.content.data = servings,
270-
Ok(None) => {}
271-
Err(err) => {
272-
let mut diag = warning!(format!(
273-
"Unsupported value for key: '{}'",
274-
key.as_str().unwrap()
275-
))
276-
.set_source(err);
277-
if let Some(key_s) = key.as_str() {
278-
if let Some(pos) = yaml_find_key_position(&yaml_str, key_s) {
279-
diag.add_label(label!(Span::pos(yaml_text.span().start() + pos)));
280-
}
267+
if let Err(err) = check_std_entry(sk, value, self.converter) {
268+
let mut diag = warning!(format!(
269+
"Unsupported value for key: '{}'",
270+
key.as_str().unwrap()
271+
))
272+
.set_source(err);
273+
if let Some(key_s) = key.as_str() {
274+
if let Some(pos) = yaml_find_key_position(&yaml_str, key_s) {
275+
diag.add_label(label!(Span::pos(yaml_text.span().start() + pos)));
281276
}
282-
self.ctx.warn(diag);
283277
}
278+
self.ctx.warn(diag);
284279
}
285280
}
286281
}
@@ -364,9 +359,7 @@ impl<'i> RecipeCollector<'i, '_> {
364359
format!("Unknown config metadata key: {key_t}"),
365360
label!(key.span())
366361
)
367-
.hint(
368-
"Possible config keys are '[mode]' and '[duplicate]''",
369-
),
362+
.hint("Possible config keys are '[mode]' and '[duplicate]''"),
370363
);
371364
if self.old_style_metadata {
372365
self.content.metadata.map.insert(
@@ -413,21 +406,17 @@ impl<'i> RecipeCollector<'i, '_> {
413406
self.converter,
414407
);
415408

416-
match check_result {
417-
Ok(Some(servings)) => self.content.data = servings,
418-
Ok(None) => {}
419-
Err(err) => {
420-
self.ctx.warn(
421-
warning!(
422-
format!("Unsupported value for key: '{}'", key.text_trimmed()),
423-
label!(value.span(), "this value"),
424-
)
425-
.label(label!(key.span(), "this key does not support"))
426-
.hint("It will be a regular metadata entry")
427-
.set_source(err),
428-
);
429-
return;
430-
}
409+
if let Err(err) = check_result {
410+
self.ctx.warn(
411+
warning!(
412+
format!("Unsupported value for key: '{}'", key.text_trimmed()),
413+
label!(value.span(), "this value"),
414+
)
415+
.label(label!(key.span(), "this key does not support"))
416+
.hint("It will be a regular metadata entry")
417+
.set_source(err),
418+
);
419+
return;
431420
}
432421

433422
// store it's location if it was inserted
@@ -896,7 +885,7 @@ impl<'i> RecipeCollector<'i, '_> {
896885
let mut new_cw = Cookware {
897886
name: cookware.name.text_trimmed().into_owned(),
898887
alias: cookware.alias.map(|t| t.text_trimmed().into_owned()),
899-
quantity: cookware.quantity.map(|q| self.value(q.into_inner(), false)),
888+
quantity: cookware.quantity.clone().map(|q| self.quantity(q, false)),
900889
note: cookware.note.map(|n| n.text_trimmed().into_owned()),
901890
modifiers: cookware.modifiers.into_inner(),
902891
relation: ComponentRelation::Definition {
@@ -940,8 +929,8 @@ impl<'i> RecipeCollector<'i, '_> {
940929
if let Some((ref_q, def_q)) =
941930
&new_cw.quantity.as_ref().zip(definition.quantity.as_ref())
942931
{
943-
let ref_is_text = ref_q.is_text();
944-
let def_is_text = def_q.is_text();
932+
let ref_is_text = ref_q.value().is_text();
933+
let def_is_text = def_q.value().is_text();
945934

946935
if ref_is_text != def_is_text {
947936
let ref_q_loc = located_cookware.quantity.as_ref().unwrap().span();
@@ -1020,22 +1009,27 @@ impl<'i> RecipeCollector<'i, '_> {
10201009
&mut self,
10211010
quantity: Located<parser::Quantity<'i>>,
10221011
is_ingredient: bool,
1023-
) -> Quantity<ScalableValue> {
1012+
) -> Quantity {
10241013
let parser::Quantity { value, unit, .. } = quantity.into_inner();
1025-
Quantity::new(
1026-
self.value(value, is_ingredient),
1027-
unit.map(|t| t.text_trimmed().into_owned()),
1028-
)
1014+
let (value, scalable) = self.value(value, is_ingredient);
1015+
Quantity {
1016+
value,
1017+
unit: unit.map(|t| t.text_trimmed().into_owned()),
1018+
scalable,
1019+
}
10291020
}
10301021

1031-
fn value(&mut self, value: parser::QuantityValue, is_ingredient: bool) -> ScalableValue {
1032-
let parser::QuantityValue { value, scaling_lock } = value;
1022+
fn value(&mut self, value: parser::QuantityValue, is_ingredient: bool) -> (Value, bool) {
1023+
let parser::QuantityValue {
1024+
value,
1025+
scaling_lock,
1026+
} = value;
10331027
let has_scaling_lock = scaling_lock.is_some();
10341028
let is_text = value.is_text();
10351029

1036-
// For ingredients without text values and without scaling lock, use Linear
1030+
// For ingredients without text values and without scaling lock, enable scaling
10371031
if is_ingredient && !is_text && !has_scaling_lock {
1038-
return ScalableValue::Linear(value.into_inner());
1032+
return (value.into_inner(), true);
10391033
}
10401034

10411035
// Warn if scaling lock is used unnecessarily (on non-ingredients or text values)
@@ -1054,8 +1048,8 @@ impl<'i> RecipeCollector<'i, '_> {
10541048
self.ctx.warn(warning);
10551049
}
10561050

1057-
// Everything else uses Fixed
1058-
ScalableValue::Fixed(value.into_inner())
1051+
// Everything else doesn't scale
1052+
(value.into_inner(), false)
10591053
}
10601054

10611055
fn resolve_reference<C: RefComponent>(
@@ -1229,10 +1223,10 @@ trait RefComponent: Sized {
12291223
fn set_reference(&mut self, references_to: usize);
12301224
fn set_referenced_from(all: &mut [Self], references_to: usize);
12311225

1232-
fn all(content: &ScalableRecipe) -> &[Self];
1226+
fn all(content: &Recipe) -> &[Self];
12331227
}
12341228

1235-
impl RefComponent for Ingredient<ScalableValue> {
1229+
impl RefComponent for Ingredient {
12361230
#[inline]
12371231
fn name(&self) -> &str {
12381232
&self.name
@@ -1275,12 +1269,12 @@ impl RefComponent for Ingredient<ScalableValue> {
12751269
}
12761270

12771271
#[inline]
1278-
fn all(content: &ScalableRecipe) -> &[Self] {
1272+
fn all(content: &Recipe) -> &[Self] {
12791273
&content.ingredients
12801274
}
12811275
}
12821276

1283-
impl RefComponent for Cookware<ScalableValue> {
1277+
impl RefComponent for Cookware {
12841278
#[inline]
12851279
fn name(&self) -> &str {
12861280
&self.name
@@ -1322,15 +1316,15 @@ impl RefComponent for Cookware<ScalableValue> {
13221316
}
13231317

13241318
#[inline]
1325-
fn all(content: &ScalableRecipe) -> &[Self] {
1319+
fn all(content: &Recipe) -> &[Self] {
13261320
&content.cookware
13271321
}
13281322
}
13291323

13301324
fn find_inline_quantity<'a>(
13311325
text: &'a str,
13321326
converter: &Converter,
1333-
) -> Option<(&'a str, Quantity<Value>, &'a str)> {
1327+
) -> Option<(&'a str, Quantity, &'a str)> {
13341328
let mut i = 0;
13351329

13361330
fn eat_word<'a>(text: &'a str, i: &mut usize) -> Option<&'a str> {
@@ -1496,13 +1490,17 @@ fn yaml_find_key_position(text: &str, key: &str) -> Option<usize> {
14961490
}
14971491

14981492
fn parse_reference(name: &str) -> Option<RecipeReference> {
1499-
if name.starts_with("./") || name.starts_with("../") || name.starts_with(".\\") || name.starts_with("..\\") {
1493+
if name.starts_with("./")
1494+
|| name.starts_with("../")
1495+
|| name.starts_with(".\\")
1496+
|| name.starts_with("..\\")
1497+
{
15001498
let path = name.replace('\\', "/");
15011499
let mut components: Vec<String> = path.split('/').map(String::from).skip(1).collect();
15021500
let file_stem = components.pop().unwrap();
15031501
Some(RecipeReference {
15041502
components,
1505-
name: file_stem.into()
1503+
name: file_stem.into(),
15061504
})
15071505
} else {
15081506
None
@@ -1553,7 +1551,11 @@ mod tests {
15531551
assert_eq!(
15541552
parse_reference("./recipes/italian/pasta/spaghetti"),
15551553
Some(RecipeReference {
1556-
components: vec!["recipes".to_string(), "italian".to_string(), "pasta".to_string()],
1554+
components: vec![
1555+
"recipes".to_string(),
1556+
"italian".to_string(),
1557+
"pasta".to_string()
1558+
],
15571559
name: "spaghetti".into()
15581560
})
15591561
);

0 commit comments

Comments
 (0)