feat: add schemaOrgToCooklang conversion function

This commit is contained in:
2026-05-19 22:50:48 -04:00
parent 1addddd8fc
commit 3e55f0a7d2
2 changed files with 102 additions and 8 deletions
+64 -4
View File
@@ -17,7 +17,8 @@ import Data.Aeson (FromJSON (..), (.!=), (.:), (.:?))
import qualified Data.Aeson as A
import qualified Data.Aeson.KeyMap as KM
import Data.Aeson.Types (parseMaybe)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.List.NonEmpty (NonEmpty ((:|)), fromList)
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
@@ -213,12 +214,71 @@ parseSchemaOrgRecipe val = do
_ -> Nothing
-- ---------------------------------------------------------------------------
-- Stubs for future tasks
-- Conversion to Cooklang Recipe
-- ---------------------------------------------------------------------------
-- | Convert a SchemaOrgRecipe to a Cooklang Recipe. (Stub — will be implemented in Task 3)
-- | Convert a SchemaOrgRecipe to a Cooklang Recipe.
schemaOrgToCooklang :: SchemaOrgRecipe -> Either String Recipe
schemaOrgToCooklang _ = Left "Not yet implemented"
schemaOrgToCooklang r =
Right
Recipe
{ recipeMetadata = buildMetadata r
, recipeSections = buildSections r
}
buildMetadata :: SchemaOrgRecipe -> Metadata
buildMetadata r =
emptyMetadata
{ metaTitle = Just (soName r)
, metaDescription = soDescription r
, metaSource = soUrl r
, metaTotalTime = soTotalTime r >>= parseISODuration
, metaPrepTime = soPrepTime r >>= parseISODuration
, metaCookTime = soCookTime r >>= parseISODuration
, metaServings = parseServings =<< soRecipeYield r
, metaAuthor = soPersonName <$> soAuthor r
, metaCourse = soRecipeCategory r
, metaCuisine = soRecipeCuisine r
, metaTags = soKeywords r
, metaImage = case soImage r of
(img : _) -> Just (soImageObjectUrl img)
[] -> Nothing
}
buildSections :: SchemaOrgRecipe -> NonEmpty Section
buildSections r =
let ingSection = buildIngredientSection (soRecipeIngredient r)
methodSection = buildMethodSection (soRecipeInstructions r)
sections = catMaybes [Just ingSection, methodSection]
in case sections of
(s : ss) -> s :| ss
[] -> Section Nothing (SecStep (Step []) :| []) :| []
buildIngredientSection :: [Text] -> Section
buildIngredientSection ings
| null ings = Section Nothing (SecStep (Step []) :| [])
| otherwise =
let items = concatMap (\t -> [StepText (", "), StepText t]) ings
step = Step items
in Section (Just "Ingredients") (SecStep step :| [])
buildMethodSection :: [SchemaOrgHowToStep] -> Maybe Section
buildMethodSection [] = Nothing
buildMethodSection steps =
let stepItems = map (\s -> SecStep (Step [StepText (sohsText s)])) steps
in Just (Section (Just "Method") (fromList stepItems))
-- | Parse a servings string like "4 to 6 servings".
parseServings :: Text -> Maybe (Int, Maybe Text)
parseServings t =
case T.words t of
(numStr : rest) -> do
n <- readMaybe (T.unpack numStr)
let unit = case rest of
(_ : _) -> Just (T.intercalate " " rest)
[] -> Nothing
Just (n, unit)
_ -> Nothing
{- | Parse an ISO 8601 duration string into a Cooklang Duration.
Supports formats like PT20M, PT1H30M, P1DT2H, P1D.