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 +91/−0
- chez-grater.cabal +22/−2
- src/Chez/Grater/Internal/Prelude.hs +2/−0
- src/Chez/Grater/Readable/Types.hs +39/−1
- src/Chez/Grater/Scraper/Types.hs +2/−2
+ 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