From e907a03b093d1e34357b63c5cfa8abb05341bcaf Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Mon, 18 May 2026 22:15:21 -0400 Subject: [PATCH] Define Cooklang data model and add DerivingStrategies + containers deps - Add full Cooklang (.cook) recipe data model to Roux.Types: Recipe, Section, Step, StepItem (with StepText/StepIngredient/ StepCookware/StepTimer/StepRecipeRef/StepNote/StepComment/StepBreak), Ingredient, Cookware, Timer, RecipeRef, Quantity, Duration, Metadata - Enable DerivingStrategies extension project-wide - Add containers to build dependencies --- package.yaml | 2 + roux-server.cabal | 6 ++ src/Roux/Types.hs | 186 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 189 insertions(+), 5 deletions(-) diff --git a/package.yaml b/package.yaml index 6254488..d627f55 100644 --- a/package.yaml +++ b/package.yaml @@ -9,6 +9,7 @@ copyright: 2026 James Brechtel license: BSD-3-Clause default-extensions: + - DerivingStrategies - OverloadedStrings ghc-options: @@ -28,6 +29,7 @@ dependencies: - blaze-html - blaze-markup - bytestring + - containers - directory - http-types - optparse-applicative diff --git a/roux-server.cabal b/roux-server.cabal index 82517ef..8607071 100644 --- a/roux-server.cabal +++ b/roux-server.cabal @@ -25,6 +25,7 @@ library hs-source-dirs: src default-extensions: + DerivingStrategies OverloadedStrings ghc-options: -Wall -Werror -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints build-depends: @@ -32,6 +33,7 @@ library , blaze-html , blaze-markup , bytestring + , containers , directory , http-types , optparse-applicative @@ -51,6 +53,7 @@ executable roux-server hs-source-dirs: app default-extensions: + DerivingStrategies OverloadedStrings ghc-options: -Wall -Werror -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N build-depends: @@ -58,6 +61,7 @@ executable roux-server , blaze-html , blaze-markup , bytestring + , containers , directory , http-types , optparse-applicative @@ -80,6 +84,7 @@ test-suite roux-server-test hs-source-dirs: test default-extensions: + DerivingStrategies OverloadedStrings ghc-options: -Wall -Werror -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints build-depends: @@ -87,6 +92,7 @@ test-suite roux-server-test , blaze-html , blaze-markup , bytestring + , containers , directory , http-types , optparse-applicative diff --git a/src/Roux/Types.hs b/src/Roux/Types.hs index f696219..38e9d20 100644 --- a/src/Roux/Types.hs +++ b/src/Roux/Types.hs @@ -1,9 +1,185 @@ -{- | Core types for Roux. +{- | Core types for Roux. Models the structure of a parsed Cooklang (.cook) +recipe file as specified at . + +Hierarchy: + +@ +Recipe + Metadata — YAML front matter (title, tags, servings, …) + NonEmpty Section — at least one section per recipe + +Section + Maybe Text — optional section name ("Dough", "Filling") + NonEmpty Step — at least one step per section + +Step + [StepItem] — sequence of inline elements + +StepItem + StepText — plain text + StepIngredient — \@name{amount%unit}(preparation) + StepCookware — \#name{} + StepTimer — \~name{duration%unit} + StepRecipeRef — \@.\/path\/to\/recipe{amount} + StepNote — \> note / commentary + StepComment — [- comment -] + StepBreak — \\ at end of line +@ -} module Roux.Types - ( -- * Types - RecipeDir (..) + ( -- * Recipe + Recipe (..) + + -- * Sections and steps + , Section (..) + , Step (..) + + -- * Inline elements + , StepItem (..) + , Ingredient (..) + , Cookware (..) + , Timer (..) + , RecipeRef (..) + + -- * Quantities and durations + , Quantity (..) + , Duration (..) + + -- * Metadata + , Metadata (..) ) where --- | Path to the directory containing Cooklang recipe files. -newtype RecipeDir = RecipeDir FilePath +import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import Data.Text (Text) + +-- --------------------------------------------------------------------------- +-- Top-level +-- --------------------------------------------------------------------------- + +-- | A fully parsed Cooklang recipe file. +data Recipe = Recipe + { recipeMetadata :: !Metadata + , recipeSections :: !(NonEmpty Section) + } + deriving stock (Eq, Show) + +-- --------------------------------------------------------------------------- +-- Sections and steps +-- --------------------------------------------------------------------------- + +-- | A named or unnamed section within a recipe. Each section contains one +-- or more cooking steps. +data Section = Section + { sectionName :: !(Maybe Text) + , sectionSteps :: !(NonEmpty Step) + } + deriving stock (Eq, Show) + +-- | A single cooking step — one paragraph of text in the Cooklang file, +-- separated from other steps by a blank line. +newtype Step = Step + { unStep :: [StepItem] + } + deriving stock (Eq, Show) + +-- --------------------------------------------------------------------------- +-- Inline elements +-- --------------------------------------------------------------------------- + +-- | An element that appears within a step. +data StepItem + = StepText !Text + | StepIngredient !Ingredient + | StepCookware !Cookware + | StepTimer !Timer + | StepRecipeRef !RecipeRef + | StepNote !Text + | StepComment !Text + | StepBreak + deriving stock (Eq, Show) + +-- | An ingredient reference. +-- +-- @\@salt\@ground black pepper{}\@potato{2}\@bacon strips{1%kg}@ +data Ingredient = Ingredient + { ingName :: !Text + , ingQuantity :: !(Maybe Quantity) + , ingPreparation :: !(Maybe Text) + } + deriving stock (Eq, Show) + +-- | A piece of cookware. +-- +-- @\#pot\#potato masher{}@ +newtype Cookware = Cookware + { cwName :: Text + } + deriving stock (Eq, Show) + +-- | A timer. +-- +-- @\{25%minutes\}\~eggs{3%minutes}@ +data Timer = Timer + { timerName :: !(Maybe Text) + , timerDuration :: !Duration + } + deriving stock (Eq, Show) + +-- | A reference to another recipe, signalled by a path starting with @.\/@. +-- +-- @\@.\/sauces\/Hollandaise{150%g}@ +data RecipeRef = RecipeRef + { refPath :: !FilePath + , refQuantity :: !(Maybe Quantity) + } + deriving stock (Eq, Show) + +-- --------------------------------------------------------------------------- +-- Quantities and durations +-- --------------------------------------------------------------------------- + +-- | An ingredient quantity with optional unit and scaling lock. +-- +-- @{2}\{1%kg}\{1\/2%tbsp}\{=1%tsp}@ +data Quantity = Quantity + { quantityAmount :: !Rational -- ^ e.g. @1\/2@, @3@, @3\/4@ + , quantityUnit :: !(Maybe Text) -- ^ e.g. @\"kg\"@, @\"tbsp\"@ + , quantityFixed :: !Bool -- ^ @{=1%tsp}@ — locked from scaling + } + deriving stock (Eq, Show) + +-- | A timer duration — structurally similar to 'Quantity' but semantically +-- distinct. Timers are not scaled with serving size. +data Duration = Duration + { durationAmount :: !Rational -- ^ e.g. @25@, @3@ + , durationUnit :: !(Maybe Text) -- ^ e.g. @\"minutes\"@, @\"hours\"@ + } + deriving stock (Eq, Show) + +-- --------------------------------------------------------------------------- +-- Metadata +-- --------------------------------------------------------------------------- + +-- | Metadata parsed from YAML front matter (the @---@ … @---@ block at the +-- top of a @.cook@ file). +-- +-- Canonical metadata keys are lifted to dedicated fields; everything else +-- lives in 'metaCustom'. +data Metadata = Metadata + { metaTitle :: !(Maybe Text) + , metaServings :: !(Maybe (Int, Maybe Text)) + , metaAuthor :: !(Maybe Text) + , metaSource :: !(Maybe Text) + , metaTotalTime :: !(Maybe Duration) + , metaPrepTime :: !(Maybe Duration) + , metaCookTime :: !(Maybe Duration) + , metaDifficulty :: !(Maybe Text) + , metaCuisine :: !(Maybe Text) + , metaDiet :: ![Text] + , metaTags :: ![Text] + , metaImage :: !(Maybe Text) + , metaDescription :: !(Maybe Text) + , metaCustom :: !(Map Text Text) + } + deriving stock (Eq, Show)