packages feed

chez-grater 0.1.0 → 0.1.1

raw patch · 5 files changed

+156/−5 lines, 5 filesdep +aeson-prettydep +optparse-applicativedep ~bytestringnew-component:exe:chez-graterPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson-pretty, optparse-applicative

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

- Chez.Grater.Scraper.Types: data ScrapedIngredient
- Chez.Grater.Scraper.Types: data ScrapedStep
+ Chez.Grater.Internal.Prelude: toList :: Foldable t => t a -> [a]
+ Chez.Grater.Internal.Prelude: traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()
+ Chez.Grater.Readable.Types: ReadableIngredient :: IngredientName -> ReadableQuantity -> Maybe ReadableUnit -> ReadableIngredient
+ Chez.Grater.Readable.Types: [readableIngredientName] :: ReadableIngredient -> IngredientName
+ Chez.Grater.Readable.Types: [readableIngredientQuantity] :: ReadableIngredient -> ReadableQuantity
+ Chez.Grater.Readable.Types: [readableIngredientUnit] :: ReadableIngredient -> Maybe ReadableUnit
+ Chez.Grater.Readable.Types: data ReadableIngredient
+ Chez.Grater.Readable.Types: instance Data.Aeson.Types.FromJSON.FromJSON Chez.Grater.Readable.Types.ReadableIngredient
+ Chez.Grater.Readable.Types: instance Data.Aeson.Types.ToJSON.ToJSON Chez.Grater.Readable.Types.ReadableIngredient
+ Chez.Grater.Readable.Types: instance GHC.Classes.Eq Chez.Grater.Readable.Types.ReadableIngredient
+ Chez.Grater.Readable.Types: instance GHC.Classes.Ord Chez.Grater.Readable.Types.ReadableIngredient
+ Chez.Grater.Readable.Types: instance GHC.Show.Show Chez.Grater.Readable.Types.ReadableIngredient
+ Chez.Grater.Readable.Types: mkReadableIngredient :: Ingredient -> ReadableIngredient
+ Chez.Grater.Readable.Types: showReadableIngredient :: ReadableIngredient -> Text
+ Chez.Grater.Readable.Types: showReadableQuantity :: ReadableQuantity -> Maybe Text
+ Chez.Grater.Readable.Types: showReadableUnit :: ReadableUnit -> Text
+ Chez.Grater.Scraper.Types: [unScrapedIngredient] :: ScrapedIngredient -> Text
+ Chez.Grater.Scraper.Types: [unScrapedStep] :: ScrapedStep -> Text
+ Chez.Grater.Scraper.Types: newtype ScrapedIngredient
+ Chez.Grater.Scraper.Types: newtype ScrapedStep

Files

+ app/main.hs view
@@ -0,0 +1,91 @@+import Chez.Grater.Internal.Prelude++import Chez.Grater (scrapeAndParseUrl, scrapeUrl)+import Chez.Grater.Manager (createManager)+import Chez.Grater.Parser (mkIngredients)+import Chez.Grater.Readable.Types (mkReadableIngredient, showReadableIngredient)+import Chez.Grater.Scraper.Site (allScrapers)+import Chez.Grater.Scraper.Types (ScrapedIngredient(..), ScrapedRecipeName(..), ScrapedStep(..))+import Chez.Grater.Types (Ingredient(..), RecipeName(..), Step(..))+import Data.Aeson ((.=), object)+import Data.Aeson.Encode.Pretty (encodePretty)+import Network.URI (parseURI)+import Options.Applicative ((<**>))+import qualified Data.ByteString.Lazy.Char8 as LC8+import qualified Data.Text as Text+import qualified Options.Applicative as Opt++data Output = OutputText | OutputJson+  deriving (Eq)++instance Show Output where+  show = \case+    OutputText -> "text"+    OutputJson -> "json"++data Opts = Opts+  { optsUrl :: String+  , optsNoParse :: Bool+  , optsOutput :: Output+  }++parseArgs :: IO Opts+parseArgs = Opt.execParser (Opt.info (parser <**> Opt.helper) (Opt.progDesc "Scrape a recipe from a URL"))+  where+    parser = Opts+      <$> Opt.strArgument+        ( Opt.metavar "URL"+        )+      <*> Opt.switch+        ( Opt.long "no-parse"+            <> Opt.help "Don't parse the recipe"+        )+      <*> Opt.option readOutput+        ( Opt.short 'o'+            <> Opt.long "output"+            <> Opt.value OutputText+            <> Opt.showDefault+            <> Opt.help "Output format for the recipe (text or json)"+        )+    readOutput = Opt.maybeReader $ \case+      "text" -> Just OutputText+      "json" -> Just OutputJson+      _ -> Nothing++showRecipe :: RecipeName -> [Ingredient] -> [Step] -> IO ()+showRecipe (RecipeName name) ingredients steps = do+  let width = length (Text.unpack name)+  putStrLn $ Text.unpack name+  putStrLn $ replicate width '-'+  traverse_ putStrLn $ fmap (Text.unpack . showReadableIngredient . mkReadableIngredient) ingredients+  putStrLn $ replicate width '-'+  traverse_ putStrLn $ fmap (("- " <>) . Text.unpack . unStep) steps++jsonRecipe :: RecipeName -> [Ingredient] -> [Step] -> IO ()+jsonRecipe name ingredients steps =+  putStrLn . LC8.unpack . encodePretty . object $+    [ "name" .= name+    , "ingredients" .= fmap mkReadableIngredient ingredients+    , "steps" .= fmap unStep steps+    ]++renderRecipe :: Output -> RecipeName -> [Ingredient] -> [Step] -> IO ()+renderRecipe = \case+  OutputText -> showRecipe+  OutputJson -> jsonRecipe++main :: IO ()+main = do+  Opts {..} <- parseArgs+  manager <- createManager+  url <- maybe (fail "Invalid URL") pure $ parseURI optsUrl+  case optsNoParse of+    True -> do+      (name, ingredients, steps, _) <- scrapeUrl allScrapers manager url+      renderRecipe optsOutput+        (RecipeName . unScrapedRecipeName $ name)+        (concatMap (mkIngredients . unScrapedIngredient) ingredients)+        (fmap (Step . unScrapedStep) steps)+    False -> do+      (name, ingredients, steps, _) <- scrapeAndParseUrl allScrapers manager url+      renderRecipe optsOutput name ingredients steps
chez-grater.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name:          chez-grater-version:       0.1.0+version:       0.1.1 author:        Dan Fithian maintainer:    Dan Fithian copyright:     2022 Dan Fithian@@ -55,6 +55,9 @@ common ghc-options   ghc-options: -Wall -Wunused-packages -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog +common ghc-exe-options+  ghc-options: -Wall -Wunused-packages -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog -O2+ library   import: options, ghc-options   exposed-modules:@@ -94,6 +97,23 @@     , text < 1.3     , unordered-containers < 0.3 +executable chez-grater+  import: options, ghc-exe-options+  main-is: main.hs+  other-modules:+      Paths_chez_grater+  hs-source-dirs:+      app+  build-depends:+      aeson < 1.6+    , aeson-pretty < 0.9+    , base < 5.0+    , bytestring < 0.12+    , network-uri < 2.7+    , optparse-applicative < 0.18+    , text < 1.3+    , chez-grater+ test-suite tests   import: options, ghc-options   type: exitcode-stdio-1.0@@ -108,7 +128,7 @@   build-depends:       attoparsec < 0.14     , base < 5.0-    , bytestring < 0.11+    , bytestring < 0.12     , case-insensitive < 1.3     , containers < 0.7     , file-embed < 0.1
src/Chez/Grater/Internal/Prelude.hs view
@@ -5,6 +5,7 @@   , module Control.Exception   , module Control.Monad   , module Data.CaseInsensitive+  , module Data.Foldable   , module Data.HashMap.Strict   , module Data.List   , module Data.Map.Strict@@ -21,6 +22,7 @@ import Control.Monad (replicateM, void) import Data.CaseInsensitive (CI) import Data.Containers.ListUtils (nubOrd)+import Data.Foldable (toList, traverse_) import Data.HashMap.Strict (HashMap) import Data.List (find, groupBy, sortBy, sortOn) import Data.Map.Strict (Map)
src/Chez/Grater/Readable/Types.hs view
@@ -5,9 +5,10 @@  import Chez.Grater.Internal.CI.Orphans () import Chez.Grater.Internal.Json (jsonOptions)-import Chez.Grater.Types (Quantity(..), Unit(..))+import Chez.Grater.Types (Ingredient(..), IngredientName(..), Quantity(..), Unit(..)) import Data.Aeson (FromJSON, ToJSON) import Data.Aeson.TH (deriveJSON)+import qualified Data.CaseInsensitive as CI  data ReadableFraction = ReadableFraction   { readableFractionNumerator   :: Int@@ -24,8 +25,16 @@ newtype ReadableUnit = ReadableUnit { unReadableUnit :: CI Text }   deriving (Eq, Ord, Show, FromJSON, ToJSON) +data ReadableIngredient = ReadableIngredient+  { readableIngredientName     :: IngredientName+  , readableIngredientQuantity :: ReadableQuantity+  , readableIngredientUnit     :: Maybe ReadableUnit+  }+  deriving (Eq, Ord, Show)+ deriveJSON (jsonOptions "readableFraction") ''ReadableFraction deriveJSON (jsonOptions "readableQuantity") ''ReadableQuantity+deriveJSON (jsonOptions "readableIngredient") ''ReadableIngredient  mkReadableQuantity :: Quantity -> ReadableQuantity mkReadableQuantity q = case splitQuantity q of@@ -65,7 +74,36 @@           True -> Just (round q2, 0.0)           False -> let w = truncate q2 in Just (w, q2 - fromIntegral w) +showReadableQuantity :: ReadableQuantity -> Maybe Text+showReadableQuantity ReadableQuantity {..} =+  case (readableQuantityWhole, readableQuantityFraction) of+    (Nothing, Nothing) -> Nothing+    (Just w, Nothing) -> Just $ tshow w+    (Nothing, Just ReadableFraction {..}) -> Just $ tshow readableFractionNumerator <> "/" <> tshow readableFractionDenominator+    (Just w, Just ReadableFraction {..}) -> Just $ tshow w <> " " <> tshow readableFractionNumerator <> "/" <> tshow readableFractionDenominator+ mkReadableUnit :: Unit -> Maybe ReadableUnit mkReadableUnit = \case   Unit x -> Just (ReadableUnit x)   UnitMissing -> Nothing++showReadableUnit :: ReadableUnit -> Text+showReadableUnit = CI.original . unReadableUnit++mkReadableIngredient :: Ingredient -> ReadableIngredient+mkReadableIngredient Ingredient {..} =+  ReadableIngredient+    { readableIngredientName = ingredientName+    , readableIngredientQuantity = mkReadableQuantity ingredientQuantity+    , readableIngredientUnit = mkReadableUnit ingredientUnit+    }++showReadableIngredient :: ReadableIngredient -> Text+showReadableIngredient ReadableIngredient {..} =+  case (showReadableQuantity readableIngredientQuantity, showReadableUnit <$> readableIngredientUnit) of+    (Nothing, Nothing) -> showIngredientName readableIngredientName+    (Just q, Nothing) -> q <> " " <> showIngredientName readableIngredientName+    (Nothing, Just u) -> u <> " " <> showIngredientName readableIngredientName+    (Just q, Just u) -> q <> " " <> u <> " " <> showIngredientName readableIngredientName+  where+    showIngredientName = CI.original . unIngredientName
src/Chez/Grater/Scraper/Types.hs view
@@ -35,10 +35,10 @@   deriving (Eq, Show, Generic)  -- |Unparsed ingredient.-data ScrapedIngredient = ScrapedIngredient Text+newtype ScrapedIngredient = ScrapedIngredient { unScrapedIngredient :: Text }   deriving (Eq, Ord, Show) -data ScrapedStep = ScrapedStep Text+newtype ScrapedStep = ScrapedStep { unScrapedStep :: Text }   deriving (Eq, Ord, Show)  data IngredientScraper = IngredientScraper