diff --git a/docs/plans/2026-05-19-schema-org-jsonld.md b/docs/plans/2026-05-19-schema-org-jsonld.md new file mode 100644 index 0000000..8337410 --- /dev/null +++ b/docs/plans/2026-05-19-schema-org-jsonld.md @@ -0,0 +1,679 @@ +# Schema.org JSON-LD Recipe Types Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Model schema.org Recipe JSON-LD as Haskell types with `FromJSON` instances and a conversion function to `Data.CookLang.Recipe`. + +**Architecture:** New `Roux.SchemaOrg` module with three sections: types + FromJSON, ISO 8601 duration parsing, conversion to Cooklang Recipe. The NYT-specific `nytToCooklang` in `Roux.NYTimes` remains unchanged. + +**Tech Stack:** Haskell (aeson, text), existing test data HTML files + +--- + +### Task 1: Data types and `FromJSON` instances + +**Files:** +- Create: `src/Roux/SchemaOrg.hs` +- Test: `test/Roux/SchemaOrgSpec.hs` + +- [ ] **Step 1: Write failing test — verify JSON from test data parses** + +Create the test file first: + +```haskell +module Roux.SchemaOrgSpec (spec) where + +import Data.Aeson (Value) +import qualified Data.Aeson as A +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (decodeUtf8) +import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy) + +import Roux.SchemaOrg + +spec :: Spec +spec = describe "SchemaOrg" $ do + describe "parseSchemaOrgRecipe" $ do + it "parses fried-rice.html embedded JSON-LD" $ do + html <- readFile "test-data/fried-rice.html" + let decoded = decodeUtf8 <$> tryDecode (T.pack html) + decoded `shouldSatisfy` isJust + let recipe = parseSchemaOrgRecipe (fromJust decoded) + recipe `shouldSatisfy` isJust + let Just r = recipe + soName r `shouldBe` "Fried Rice" + soRecipeYield r `shouldBe` Just "4 to 6 servings" + soTotalTime r `shouldBe` Just "PT20M" + soRecipeCuisine r `shouldBe` Just "asian" + length (soRecipeIngredient r) `shouldBe` 14 + length (soRecipeInstructions r) `shouldBe` 4 + + it "parses carrot-risotto.html embedded JSON-LD" $ do + html <- readFile "test-data/carrot-risotto.html" + let decoded = decodeUtf8 <$> tryDecode (T.pack html) + decoded `shouldSatisfy` isJust + let recipe = parseSchemaOrgRecipe (fromJust decoded) + recipe `shouldSatisfy` isJust + let Just r = recipe + soName r `shouldBe` "Carrot Risotto" +``` + +Add helpers at top of the test file: + +```haskell +import Data.Maybe (fromJust, isJust) + +-- | Extract the first schema.org Recipe JSON-LD from HTML. +tryDecode :: Text -> Maybe A.Value +tryDecode html = + case T.splitOn openTag html of + (_before : rest : _) -> + case T.splitOn "" rest of + (jsonContent : _) -> A.decodeStrict (TE.encodeUtf8 (T.strip jsonContent)) + _ -> Nothing + _ -> Nothing + where + openTag = "