21a4cea083
- Add Roux.NYTimes module with:
- extractNextData: finds <script id="__NEXT_DATA__"> tag in HTML
and parses its JSON content via aeson
- parseNYTRecipe: maps NYTimes recipe JSON structure into a
typed intermediate representation (NYTRecipe, NYTIngredientGroup,
NYTIngredientItem, NYTStepGroup, NYTStepItem)
- Supports all NYTimes Cooking data: title, URL, total time, yield,
topnote, ingredient groups (with section names), step groups
- Add aeson to dependencies
- Add 4 tests: extraction + parsing for both fried-rice and
carrot-risotto example HTML files
- Wire NYTimesSpec into test runner
77 lines
3.7 KiB
Haskell
77 lines
3.7 KiB
Haskell
module Roux.NYTimesSpec (spec) where
|
|
|
|
import qualified Data.Text.IO as TIO
|
|
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
|
|
|
|
import Roux.NYTimes
|
|
|
|
spec :: Spec
|
|
spec = do
|
|
describe "extractNextData" $ do
|
|
it "extracts JSON from fried-rice.html" $ do
|
|
html <- TIO.readFile "test-data/fried-rice.html"
|
|
let result = extractNextData html
|
|
result `shouldSatisfy` isJust
|
|
|
|
it "extracts JSON from carrot-risotto.html" $ do
|
|
html <- TIO.readFile "test-data/carrot-risotto.html"
|
|
let result = extractNextData html
|
|
result `shouldSatisfy` isJust
|
|
|
|
describe "parseNYTRecipe" $ do
|
|
it "parses fried-rice.html recipe correctly" $ do
|
|
html <- TIO.readFile "test-data/fried-rice.html"
|
|
let result = do
|
|
val <- extractNextData html
|
|
parseNYTRecipe val
|
|
case result of
|
|
Nothing -> error "failed to parse fried-rice recipe"
|
|
Just recipe -> do
|
|
nytTitle recipe `shouldBe` "Fried Rice"
|
|
nytUrl recipe `shouldBe` Just "https://cooking.nytimes.com/recipes/12177-fried-rice"
|
|
nytTotalTime recipe `shouldBe` Just "20 minutes"
|
|
nytRecipeYield recipe `shouldBe` Just "4 to 6 servings"
|
|
nytTopnote recipe `shouldSatisfy` isJust
|
|
length (nytIngredientGroups recipe) `shouldBe` 1
|
|
case nytIngredientGroups recipe of
|
|
[group] -> do
|
|
length (nytIngredients group) `shouldBe` 14
|
|
case nytIngredients group of
|
|
(firstIng : _) -> do
|
|
nytIngredientText firstIng `shouldBe` "tablespoons neutral oil, like canola or grapeseed"
|
|
nytIngredientQuantity firstIng `shouldBe` "3"
|
|
[] -> error "empty ingredient group"
|
|
_ -> error "expected 1 ingredient group"
|
|
case nytStepGroups recipe of
|
|
[stepGroup] -> do
|
|
length (nytSteps stepGroup) `shouldBe` 4
|
|
case nytSteps stepGroup of
|
|
(firstStep : _) -> nytStepNumber firstStep `shouldBe` 1
|
|
[] -> error "empty step group"
|
|
_ -> error "expected 1 step group"
|
|
|
|
it "parses carrot-risotto.html recipe correctly" $ do
|
|
html <- TIO.readFile "test-data/carrot-risotto.html"
|
|
let result = do
|
|
val <- extractNextData html
|
|
parseNYTRecipe val
|
|
case result of
|
|
Nothing -> error "failed to parse carrot-risotto recipe"
|
|
Just recipe -> do
|
|
nytTitle recipe `shouldBe` "Carrot Risotto With Chile Crisp"
|
|
nytUrl recipe `shouldBe` Just "https://cooking.nytimes.com/recipes/1024086-carrot-risotto-with-chile-crisp"
|
|
nytTotalTime recipe `shouldBe` Just "30 minutes"
|
|
nytRecipeYield recipe `shouldBe` Just "4 servings"
|
|
length (nytIngredientGroups recipe) `shouldBe` 1
|
|
case nytIngredientGroups recipe of
|
|
[group] -> length (nytIngredients group) `shouldBe` 11
|
|
_ -> error "expected 1 ingredient group"
|
|
case nytStepGroups recipe of
|
|
[stepGroup] -> length (nytSteps stepGroup) `shouldBe` 7
|
|
_ -> error "expected 1 step group"
|
|
|
|
-- | Check if a Maybe value is Just.
|
|
isJust :: Maybe a -> Bool
|
|
isJust (Just _) = True
|
|
isJust Nothing = False
|