Add minimal Cooklang parser with first tests
- Create Roux.Parser with parseCookFile (splits paragraphs, treats each as a plain-text step, no metadata parsing yet) - Add emptyMetadata to Roux.Types for convenience - Add test dependencies: hspec, tasty-golden, tasty-hspec - Add ParserSpec with 4 tests covering single step, multiple steps, empty input, and whitespace trimming - Wire ParserSpec into test runner via tasty-hspec
This commit is contained in:
@@ -60,6 +60,9 @@ tests:
|
||||
main: Spec.hs
|
||||
source-dirs: test
|
||||
dependencies:
|
||||
- hspec
|
||||
- roux-server
|
||||
- tasty
|
||||
- tasty-golden
|
||||
- tasty-hspec
|
||||
- tasty-hunit
|
||||
|
||||
@@ -17,6 +17,7 @@ build-type: Simple
|
||||
library
|
||||
exposed-modules:
|
||||
Roux
|
||||
Roux.Parser
|
||||
Roux.Types
|
||||
other-modules:
|
||||
Paths_roux_server
|
||||
@@ -78,6 +79,7 @@ test-suite roux-server-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
other-modules:
|
||||
Roux.ParserSpec
|
||||
Paths_roux_server
|
||||
autogen-modules:
|
||||
Paths_roux_server
|
||||
@@ -94,11 +96,14 @@ test-suite roux-server-test
|
||||
, bytestring
|
||||
, containers
|
||||
, directory
|
||||
, hspec
|
||||
, http-types
|
||||
, optparse-applicative
|
||||
, parsec
|
||||
, roux-server
|
||||
, tasty
|
||||
, tasty-golden
|
||||
, tasty-hspec
|
||||
, tasty-hunit
|
||||
, text
|
||||
, time
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{- | Parser for Cooklang (.cook) recipe files.
|
||||
|
||||
Currently a minimal placeholder that splits input into paragraphs and
|
||||
treats them as plain-text steps. Proper inline parsing (ingredients,
|
||||
cookware, timers, …) and metadata support will be added incrementally.
|
||||
-}
|
||||
module Roux.Parser
|
||||
( parseCookFile
|
||||
) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Roux.Types
|
||||
|
||||
-- | Parse a complete @.cook@ file into a 'Recipe'.
|
||||
--
|
||||
-- Returns 'Left' with an error message on invalid input (currently never
|
||||
-- — the placeholder always succeeds).
|
||||
parseCookFile :: Text -> Either String Recipe
|
||||
parseCookFile input =
|
||||
let paragraphs = filter (not . T.null) (map T.strip (T.splitOn "\n\n" (T.strip input)))
|
||||
in case paragraphs of
|
||||
[] ->
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections = Section Nothing (Step [] :| []) :| []
|
||||
}
|
||||
(p : ps) ->
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections = Section Nothing (stepFromText p :| fmap stepFromText ps) :| []
|
||||
}
|
||||
|
||||
-- | Turn a paragraph of text into a single 'Step' containing nothing but
|
||||
-- 'StepText'. This is a placeholder until inline parsing is implemented.
|
||||
stepFromText :: Text -> Step
|
||||
stepFromText t = Step [StepText t]
|
||||
@@ -47,10 +47,12 @@ module Roux.Types
|
||||
|
||||
-- * Metadata
|
||||
, Metadata (..)
|
||||
, emptyMetadata
|
||||
) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.Text (Text)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -183,3 +185,23 @@ data Metadata = Metadata
|
||||
, metaCustom :: !(Map Text Text)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- | Metadata with all fields set to their default / empty values.
|
||||
emptyMetadata :: Metadata
|
||||
emptyMetadata =
|
||||
Metadata
|
||||
{ metaTitle = Nothing
|
||||
, metaServings = Nothing
|
||||
, metaAuthor = Nothing
|
||||
, metaSource = Nothing
|
||||
, metaTotalTime = Nothing
|
||||
, metaPrepTime = Nothing
|
||||
, metaCookTime = Nothing
|
||||
, metaDifficulty = Nothing
|
||||
, metaCuisine = Nothing
|
||||
, metaDiet = []
|
||||
, metaTags = []
|
||||
, metaImage = Nothing
|
||||
, metaDescription = Nothing
|
||||
, metaCustom = Map.empty
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
module Roux.ParserSpec (spec) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||
|
||||
import Roux.Parser (parseCookFile)
|
||||
import Roux.Types
|
||||
|
||||
spec :: Spec
|
||||
spec =
|
||||
describe "parseCookFile" $ do
|
||||
it "parses a single step of plain text" $ do
|
||||
let input = "Hello, world."
|
||||
expected =
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step
|
||||
{ unStep = [StepText "Hello, world."]
|
||||
}
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "parses multiple steps separated by a blank line" $ do
|
||||
let input = "Wash the potatoes.\n\nPeel and dice them."
|
||||
expected =
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step {unStep = [StepText "Wash the potatoes."]}
|
||||
:| [ Step {unStep = [StepText "Peel and dice them."]}
|
||||
]
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
|
||||
it "handles empty input by returning an empty recipe" $ do
|
||||
let expected =
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps = Step {unStep = []} :| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile "" `shouldBe` expected
|
||||
|
||||
it "trims leading and trailing whitespace from each step" $ do
|
||||
let input = "\n\n Mix well. \n\n"
|
||||
expected =
|
||||
Right Recipe
|
||||
{ recipeMetadata = emptyMetadata
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step {unStep = [StepText "Mix well."]} :| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile input `shouldBe` expected
|
||||
+7
-2
@@ -1,10 +1,15 @@
|
||||
module Main (main) where
|
||||
|
||||
import Test.Tasty (defaultMain, testGroup)
|
||||
import Test.Tasty.Hspec (testSpec)
|
||||
|
||||
import qualified Roux.ParserSpec
|
||||
|
||||
main :: IO ()
|
||||
main =
|
||||
main = do
|
||||
parserTests <- testSpec "Parser" Roux.ParserSpec.spec
|
||||
defaultMain $
|
||||
testGroup
|
||||
"roux-server"
|
||||
[ ]
|
||||
[ parserTests
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user