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
This commit is contained in:
2026-05-18 22:15:21 -04:00
parent 130dc5326d
commit e907a03b09
3 changed files with 189 additions and 5 deletions
+2
View File
@@ -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
+6
View File
@@ -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
+181 -5
View File
@@ -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 <https://cooklang.org/docs/spec/>.
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)