Phase 2: Ingredient, cookware, and timer parsing
- Add ingredient parser: @name{quantity%unit}(preparation)
- Handles single-word names without braces
- Handles multi-word names with braces
- Handles quantity (with unit) and preparation text
- Add cookware parser: #name or #multi word name{}
- Uses shared pNameWithBraces helper
- Add timer parser: ~name{duration%unit} or ~{duration%unit}
- Separate named/unnamed timer branches for correct brace handling
- Add quantity parsing: {2}, {1%kg}, {1/2%tbsp}, {=1%tsp}
- Handles simple integers, fractions (1/2), compound (1,1/2)
- Extract shared pNameWithQuantity and pNameWithBraces helpers
- Update EasyPancakes tests to verify ingredient/cookware/timer parsing
- All 17 tests passing
This commit is contained in:
+150
-10
@@ -2,7 +2,7 @@ module Roux.ParserSpec (spec) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, shouldBe)
|
||||
import Test.Hspec (Spec, describe, expectationFailure, it, shouldBe)
|
||||
|
||||
import Roux.Parser (parseCookFile)
|
||||
import Roux.Types
|
||||
@@ -147,6 +147,118 @@ spec = do
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
describe "Ingredients" $ do
|
||||
it "parses a simple ingredient with quantity" $ do
|
||||
let input = "@eggs{3}"
|
||||
expected =
|
||||
Right
|
||||
Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionBody =
|
||||
SecStep
|
||||
( Step
|
||||
[ StepIngredient
|
||||
(Ingredient "eggs" (Just (Quantity 3 Nothing False)) Nothing)
|
||||
]
|
||||
)
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "parses an ingredient with quantity and unit" $ do
|
||||
let input = "@flour{125%g}"
|
||||
expected =
|
||||
Right
|
||||
Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionBody =
|
||||
SecStep
|
||||
( Step
|
||||
[ StepIngredient
|
||||
(Ingredient "flour" (Just (Quantity 125 (Just "g") False)) Nothing)
|
||||
]
|
||||
)
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "parses an ingredient without quantity (single word)" $ do
|
||||
let input = "drizzle of @oil"
|
||||
expected =
|
||||
Right
|
||||
Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionBody =
|
||||
SecStep
|
||||
( Step
|
||||
[ StepText "drizzle of "
|
||||
, StepIngredient
|
||||
(Ingredient "oil" Nothing Nothing)
|
||||
]
|
||||
)
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "parses a multi-word ingredient with empty braces" $ do
|
||||
let input = "@sea salt{}"
|
||||
expected =
|
||||
Right
|
||||
Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionBody =
|
||||
SecStep
|
||||
( Step
|
||||
[ StepIngredient
|
||||
(Ingredient "sea salt" Nothing Nothing)
|
||||
]
|
||||
)
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "parses a multi-word ingredient with quantity and unit" $ do
|
||||
let input = "@sea salt{1%pinch}"
|
||||
expected =
|
||||
Right
|
||||
Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionBody =
|
||||
SecStep
|
||||
( Step
|
||||
[ StepIngredient
|
||||
(Ingredient "sea salt" (Just (Quantity 1 (Just "pinch") False)) Nothing)
|
||||
]
|
||||
)
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
describe "EasyPancakes example" $ do
|
||||
let pancakeInput =
|
||||
"-- TODO add source\n"
|
||||
@@ -180,18 +292,46 @@ spec = do
|
||||
(SecComment t : _) -> t `shouldBe` "TODO add source"
|
||||
other -> expectationFailure ("expected SecComment first, got " <> show other)
|
||||
|
||||
it "remaining 6 items are text-only steps" $ do
|
||||
it "first step parses ingredients correctly" $ do
|
||||
case parseCookFile pancakeInput of
|
||||
Left err -> expectationFailure ("parse failed: " <> err)
|
||||
Right recipe -> do
|
||||
let body = NE.toList (sectionBody (NE.head (recipeSections recipe)))
|
||||
case body of
|
||||
(_comment : steps) -> mapM_ expectTextStep steps
|
||||
[] -> expectationFailure "empty body"
|
||||
(_ : SecStep (Step items) : _) -> do
|
||||
-- "Crack the @eggs{3} into a blender, then add the @flour{125%g}, @milk{250%ml} and @sea salt{1%pinch}, and blitz until smooth."
|
||||
let expected =
|
||||
[ StepText "Crack the "
|
||||
, StepIngredient (Ingredient "eggs" (Just (Quantity 3 Nothing False)) Nothing)
|
||||
, StepText " into a blender, then add the "
|
||||
, StepIngredient (Ingredient "flour" (Just (Quantity 125 (Just "g") False)) Nothing)
|
||||
, StepText ", "
|
||||
, StepIngredient (Ingredient "milk" (Just (Quantity 250 (Just "ml") False)) Nothing)
|
||||
, StepText " and "
|
||||
, StepIngredient (Ingredient "sea salt" (Just (Quantity 1 (Just "pinch") False)) Nothing)
|
||||
, StepText ", and blitz until smooth."
|
||||
]
|
||||
items `shouldBe` expected
|
||||
_ ->
|
||||
expectationFailure
|
||||
("expected SecStep as second body item, got body=" <> show body)
|
||||
|
||||
-- | Check that a body item is a 'SecStep' containing a single 'StepText'.
|
||||
expectTextStep :: SectionBodyItem -> Expectation
|
||||
expectTextStep (SecStep (Step [StepText _])) = pure ()
|
||||
expectTextStep other =
|
||||
expectationFailure
|
||||
("expected SecStep with single StepText, got " <> show other)
|
||||
it "second step parses cookware and timer" $ do
|
||||
case parseCookFile pancakeInput of
|
||||
Left err -> expectationFailure ("parse failed: " <> err)
|
||||
Right recipe -> do
|
||||
let body = NE.toList (sectionBody (NE.head (recipeSections recipe)))
|
||||
case body of
|
||||
(_ : _ : SecStep (Step items) : _) -> do
|
||||
-- "Pour into a #bowl and leave to stand for ~{15%minutes}."
|
||||
let expected =
|
||||
[ StepText "Pour into a "
|
||||
, StepCookware (Cookware "bowl")
|
||||
, StepText " and leave to stand for "
|
||||
, StepTimer (Timer Nothing (Duration 15 (Just "minutes")))
|
||||
, StepText "."
|
||||
]
|
||||
items `shouldBe` expected
|
||||
_ ->
|
||||
expectationFailure
|
||||
("expected SecStep as third body item, got body=" <> show body)
|
||||
|
||||
Reference in New Issue
Block a user