@@ -200,12 +200,8 @@ impl Metadata {
200200 }
201201
202202 /// Servings the recipe is made for
203- ///
204- /// This returns a list of servings to support scaling. See
205- /// [`CooklangValueExt::as_servings`] for the expected format.
206- pub fn servings ( & self ) -> Option < Vec < u32 > > {
207- self . get ( StdKey :: Servings )
208- . and_then ( CooklangValueExt :: as_servings)
203+ pub fn servings ( & self ) -> Option < Servings > {
204+ self . get ( StdKey :: Servings ) . and_then ( |v| v. as_servings ( ) )
209205 }
210206
211207 /// Recipe locale
@@ -271,35 +267,6 @@ pub trait CooklangValueExt: private::Sealed {
271267 /// Duplicates and empty entries removed.
272268 fn as_tags ( & self ) -> Option < Vec < Cow < str > > > ;
273269
274- /// Pipe ('|') separated string or YAML sequence of numbers
275- ///
276- /// This extracts only the number at the beginning, but the entry can have
277- /// extra text, like the unit. For example:
278- /// ```yaml
279- /// servings: 5 cups worth
280- /// ```
281- /// will return `vec![5]`.
282- ///
283- /// These values will be used for scaling the recipe. If you want to display
284- /// the full text alongside the values, you can do something like:
285- ///
286- /// ```no_run
287- /// # use cooklang::metadata::*;
288- /// # fn f() -> Option<()> {
289- /// # let metadata = Metadata::default();
290- /// let nums = metadata.get("servings")?.as_servings()?;
291- /// let texts = metadata.get("servings")?.as_string_list("|")?;
292- ///
293- /// for (num, text) in nums.iter().zip(texts.iter()) {
294- /// println!("{num} - '{text}'")
295- /// }
296- /// # Some(())
297- /// # }
298- /// ```
299- ///
300- /// Duplicates not allowed, will return `None`.
301- fn as_servings ( & self ) -> Option < Vec < u32 > > ;
302-
303270 /// String separated by `sep` or YAML sequence of strings and/or numbers
304271 ///
305272 /// This only checks types and convert numbers to strings if neccesary.
@@ -345,17 +312,18 @@ pub trait CooklangValueExt: private::Sealed {
345312
346313 /// String or number as a string
347314 fn as_str_like ( & self ) -> Option < Cow < str > > ;
315+
316+ /// Get servings information
317+ ///
318+ /// Can be a number or a string that parses to [`Servings`]
319+ fn as_servings ( & self ) -> Option < Servings > ;
348320}
349321
350322impl CooklangValueExt for serde_yaml:: Value {
351323 fn as_tags ( & self ) -> Option < Vec < Cow < str > > > {
352324 value_as_tags ( self ) . ok ( )
353325 }
354326
355- fn as_servings ( & self ) -> Option < Vec < u32 > > {
356- value_as_servings ( self ) . ok ( )
357- }
358-
359327 fn as_string_list < ' a > ( & ' a self , sep : & str ) -> Option < Vec < Cow < ' a , str > > > {
360328 if let Some ( s) = self . as_str ( ) {
361329 let v = s. split ( sep) . map ( |e| e. into ( ) ) . collect ( ) ;
@@ -415,6 +383,25 @@ impl CooklangValueExt for serde_yaml::Value {
415383 None
416384 }
417385 }
386+
387+ fn as_servings ( & self ) -> Option < Servings > {
388+ // Try as number first
389+ if let Some ( n) = self . as_u32 ( ) {
390+ return Some ( Servings :: Number ( n) ) ;
391+ }
392+
393+ // Return as text if it's a string
394+ if let Some ( s) = self . as_str ( ) {
395+ // Try to parse as number
396+ if let Ok ( n) = s. parse :: < u32 > ( ) {
397+ Some ( Servings :: Number ( n) )
398+ } else {
399+ Some ( Servings :: Text ( s. to_string ( ) ) )
400+ }
401+ } else {
402+ None
403+ }
404+ }
418405}
419406
420407fn value_as_tags ( val : & serde_yaml:: Value ) -> Result < Vec < Cow < str > > , MetadataError > {
@@ -441,54 +428,6 @@ fn value_as_tags(val: &serde_yaml::Value) -> Result<Vec<Cow<str>>, MetadataError
441428 Ok ( tags)
442429}
443430
444- fn value_as_servings ( val : & serde_yaml:: Value ) -> Result < Vec < u32 > , MetadataError > {
445- fn extract_value ( s : & str ) -> Result < u32 , std:: num:: ParseIntError > {
446- let idx = s
447- . find ( |c : char | !c. is_ascii_alphanumeric ( ) )
448- . unwrap_or ( s. len ( ) ) ;
449- let n_str = & s[ ..idx] ;
450- n_str. parse ( )
451- }
452-
453- let servings: Vec < u32 > = if let Some ( n) = val. as_u32 ( ) {
454- vec ! [ n]
455- } else if let Some ( s) = val. as_str ( ) {
456- s. split ( '|' )
457- . map ( str:: trim)
458- . map ( extract_value)
459- . collect :: < Result < Vec < _ > , _ > > ( ) ?
460- } else if let Some ( seq) = val. as_sequence ( ) {
461- let mut v = Vec :: with_capacity ( seq. len ( ) ) ;
462- for e in seq {
463- if let Some ( n) = e. as_u32 ( ) {
464- v. push ( n) ;
465- } else if let Some ( s) = e. as_str ( ) {
466- v. push ( extract_value ( s) ?) ;
467- } else {
468- return Err ( MetadataError :: BadSequenceType {
469- expected : MetaType :: Number ,
470- got : MetaType :: from ( e) ,
471- } ) ;
472- }
473- }
474- v
475- } else {
476- return Err ( MetadataError :: expect_type ( MetaType :: Sequence , val) ) ;
477- } ;
478-
479- let l = servings. len ( ) ;
480- let dedup_l = {
481- let mut temp = servings. clone ( ) ;
482- temp. sort_unstable ( ) ;
483- temp. dedup ( ) ;
484- temp. len ( )
485- } ;
486- if l != dedup_l {
487- return Err ( MetadataError :: DuplicateServings { servings } ) ;
488- }
489- Ok ( servings)
490- }
491-
492431fn value_as_minutes ( val : & serde_yaml:: Value , converter : & Converter ) -> Result < u32 , MetadataError > {
493432 if let Some ( s) = val. as_str ( ) {
494433 let t = parse_time ( s, converter) ?;
@@ -554,7 +493,9 @@ pub(crate) fn check_std_entry(
554493) -> Result < ( ) , MetadataError > {
555494 match key {
556495 StdKey :: Servings => {
557- value_as_servings ( value) ?;
496+ value
497+ . as_u32 ( )
498+ . ok_or ( MetadataError :: expect_type ( MetaType :: Number , value) ) ?;
558499 }
559500 StdKey :: Tags => {
560501 value_as_tags ( value) ?;
@@ -589,6 +530,43 @@ pub(crate) fn check_std_entry(
589530 Ok ( ( ) )
590531}
591532
533+ /// Servings information that can be numeric or a string
534+ #[ derive( Serialize , Deserialize , Debug , PartialEq , Eq , Clone ) ]
535+ #[ serde( untagged) ]
536+ pub enum Servings {
537+ /// Numeric servings count
538+ Number ( u32 ) ,
539+ /// String servings (when it can't be parsed as a number)
540+ Text ( String ) ,
541+ }
542+
543+ impl Servings {
544+ /// Get the numeric value if available
545+ pub fn as_number ( & self ) -> Option < u32 > {
546+ match self {
547+ Servings :: Number ( n) => Some ( * n) ,
548+ Servings :: Text ( _) => None ,
549+ }
550+ }
551+
552+ /// Get as text
553+ pub fn as_text ( & self ) -> Option < & str > {
554+ match self {
555+ Servings :: Number ( _) => None ,
556+ Servings :: Text ( s) => Some ( s) ,
557+ }
558+ }
559+ }
560+
561+ impl std:: fmt:: Display for Servings {
562+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
563+ match self {
564+ Servings :: Number ( n) => write ! ( f, "{}" , n) ,
565+ Servings :: Text ( s) => write ! ( f, "{}" , s) ,
566+ }
567+ }
568+ }
569+
592570/// Combination of name and URL.
593571///
594572/// At least one of the fields is [`Some`].
@@ -845,8 +823,6 @@ pub(crate) enum MetadataError {
845823 BadMapping ,
846824 #[ error( transparent) ]
847825 ParseIntError ( #[ from] std:: num:: ParseIntError ) ,
848- #[ error( "Duplicate servings: {servings:?}" ) ]
849- DuplicateServings { servings : Vec < u32 > } ,
850826 #[ error( transparent) ]
851827 ParseTimeError ( #[ from] ParseTimeError ) ,
852828 #[ error( "Invalid locale: {0}" ) ]
0 commit comments