From b620e6f8b3f143065449e34e26cefa6d8668bcbe Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Sun, 9 Feb 2020 20:43:30 +1100 Subject: [PATCH 01/13] Remove web-routes and boomerang --- package.yaml | 5 ----- stack.yaml | 4 ---- stack.yaml.lock | 28 ---------------------------- 3 files changed, 37 deletions(-) diff --git a/package.yaml b/package.yaml index 399a138..0716a6d 100644 --- a/package.yaml +++ b/package.yaml @@ -29,7 +29,6 @@ library: dependencies: - aeson - authenticate-oauth - - boomerang - directory - filepath - http-conduit @@ -46,10 +45,6 @@ library: - transformers - twitter-types - unix - - web-routes - - web-routes-boomerang - - web-routes-happstack - - web-routes-th executables: multiblog: main: Main.hs diff --git a/stack.yaml b/stack.yaml index ff829f4..929b471 100644 --- a/stack.yaml +++ b/stack.yaml @@ -4,14 +4,10 @@ packages: ghc-options: '$everything': -threaded extra-deps: - - boomerang-1.4.5.5 - derive-2.6.4 - twitter-conduit-0.3.0 - twitter-types-0.7.2.2 - twitter-types-lens-0.7.2 - - web-routes-boomerang-0.28.4.2 - - web-routes-happstack-0.23.11 - - web-routes-th-0.22.6.3 flags: pandoc: embed_data_files: true diff --git a/stack.yaml.lock b/stack.yaml.lock index 4945754..f399fd5 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -4,13 +4,6 @@ # https://docs.haskellstack.org/en/stable/lock_files packages: -- completed: - hackage: boomerang-1.4.5.5@sha256:a6ea7b0cdc82e7d9d8c66bd63047da13453e20acd5e299e92fa8d286f8663041,1951 - pantry-tree: - size: 791 - sha256: 71b50b11f18cc0e27d040802a8d811caee9066a4bf40dd7e8b44cc57c8133fee - original: - hackage: boomerang-1.4.5.5 - completed: hackage: derive-2.6.4@sha256:f697cbe62828b5d777ea148814978940bd8707cb2ad4adff9415fc6139a7c05d,3184 pantry-tree: @@ -39,27 +32,6 @@ packages: sha256: 2b4a738a9ca4f6c98bc82f39b92e09f18e298f524f7121e568582e8f0f4dfe2c original: hackage: twitter-types-lens-0.7.2 -- completed: - hackage: web-routes-boomerang-0.28.4.2@sha256:5885435f8527a056d820630e26f76567d4106c8e13fe67a0bc191149dc9f4242,1060 - pantry-tree: - size: 228 - sha256: bac758a18c07d100c5a259e4e9c49eea3412c0d8d3b96fa1660a8b07dd97d30f - original: - hackage: web-routes-boomerang-0.28.4.2 -- completed: - hackage: web-routes-happstack-0.23.11@sha256:c61a497a13810c15e3b3a17db4fef05357cbf31a4a56bd73cdfbb9f9ae521b10,1044 - pantry-tree: - size: 228 - sha256: 37f74334eadaf5380f76f9f42e0da6ff678ab5961f7e72b35bbe28638e069523 - original: - hackage: web-routes-happstack-0.23.11 -- completed: - hackage: web-routes-th-0.22.6.3@sha256:9a71de3770de13445bf116b16a5468d12e3dac0b91b8d3546088333a96bd235d,1533 - pantry-tree: - size: 267 - sha256: d2d7656d3cc35177ae51689dbe849d5c48d339339ba97018921d858f0a2bca0b - original: - hackage: web-routes-th-0.22.6.3 snapshots: - completed: size: 508835 From 9e52248f254b872d7c90d3cdd6dc9fc430ee10a6 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Sun, 9 Feb 2020 21:22:03 +1100 Subject: [PATCH 02/13] Manual routeURL and parseURL --- package.yaml | 2 +- src/App.hs | 54 ++++++----------------- src/CrossPost.hs | 9 ++-- src/Routes.hs | 96 +++-------------------------------------- src/Views.hs | 62 +++++++++++--------------- src/Views/Export.hs | 12 ++---- src/Views/Feed.hs | 12 +++--- testsuite/TestRoutes.hs | 26 +++++++---- 8 files changed, 73 insertions(+), 200 deletions(-) diff --git a/package.yaml b/package.yaml index 0716a6d..27bbc70 100644 --- a/package.yaml +++ b/package.yaml @@ -20,6 +20,7 @@ dependencies: - pandoc - pandoc-types - text +- time - twitter-conduit - xml-conduit - xml-types @@ -41,7 +42,6 @@ library: - shakespeare - split - tagsoup - - time - transformers - twitter-types - unix diff --git a/src/App.hs b/src/App.hs index c94960e..c358f70 100644 --- a/src/App.hs +++ b/src/App.hs @@ -22,10 +22,6 @@ import System.Directory import System.Environment import System.FilePath -import Web.Routes -import Web.Routes.Boomerang (boomerangSiteRouteT) -import Web.Routes.Happstack - import Cache import Import import Models @@ -39,7 +35,7 @@ import Views.Feed type App = StateT AppCache (ReaderT AppData IO) -type AppPart a = RouteT Sitemap (ServerPartT App) a +type AppPart a = ServerPartT App a loadApp :: String -- ^ directory to load from @@ -87,33 +83,19 @@ site :: ServerPartT App Response site = do address <- lift $ asks appAddress appDir <- lift $ asks appDirectory - let routedSite = boomerangSiteRouteT handler sitemap let staticDir = appDir "static" let staticSite = serveDirectory DisableBrowsing ["index.html"] staticDir - implSite address "" routedSite `mplus` staticSite - --- Run an action in application routing context -runRoute :: RouteT Sitemap m a -> m a -runRoute act - -- Supply a known good URL (root) to run the site, - -- producing the result of the given action - = - let (Right res) = runSite "" (boomerangSiteRouteT (const act) sitemap) [] - in res - -parseRoute :: T.Text -> Either String Sitemap -parseRoute = - fmap runIdentity . runSite "" (boomerangSiteRouteT pure sitemap) . segments - where - segments = decodePathInfo . T.encodeUtf8 + let mainSite = do + method GET + uriRest $ \uri -> case parseURL (T.pack uri) of + Nothing -> mzero + Just route -> handler route + mainSite `mplus` staticSite handler :: Sitemap -> AppPart Response handler route = case route of Index -> index - Yearly y -> yearlyIndex y - Monthly y m -> monthlyIndex y m - Daily d -> dailyIndex d ArticleView d s -> article d s MetaView s f -> meta s f Feed lang -> feedIndex lang @@ -124,15 +106,6 @@ handler route = index :: AppPart Response index = articleList $ const True -yearlyIndex :: Integer -> AppPart Response -yearlyIndex = articleList . byYear - -monthlyIndex :: Integer -> Int -> AppPart Response -monthlyIndex year month = articleList $ byYearMonth year month - -dailyIndex :: Day -> AppPart Response -dailyIndex = articleList . byDate - -- Find the most relevant language preference in a request -- Includes: explicit GET parameter, cookie and Accept-Language header languageHeaderM :: AppPart LanguagePreference @@ -169,12 +142,11 @@ articleList articleFilter = do meta :: Text -> Maybe PageFormat -> AppPart Response meta slug format' = do - let format = fromMaybe Html format' language <- languageHeaderM m <- askMeta slug - case format of - Html -> metaDisplay language m >>= okResponse - _ -> metaExport format language m >>= okResponse + case format' of + Nothing -> metaDisplay language m >>= okResponse + Just format -> metaExport format language m >>= okResponse feedIndex :: Language -> AppPart Response feedIndex language = do @@ -183,10 +155,10 @@ feedIndex language = do feedDisplay language sorted >>= okResponse siteScript :: AppPart Response -siteScript = renderSiteScript >>= okResponse +siteScript = okResponse renderSiteScript printStylesheet :: AppPart Response -printStylesheet = renderPrintStylesheet >>= okResponse +printStylesheet = okResponse renderPrintStylesheet codeStylesheet :: AppPart Response -codeStylesheet = renderCodeStylesheet >>= okResponse +codeStylesheet = okResponse renderCodeStylesheet diff --git a/src/CrossPost.hs b/src/CrossPost.hs index ce83f92..cc0caeb 100644 --- a/src/CrossPost.hs +++ b/src/CrossPost.hs @@ -21,7 +21,6 @@ import qualified Data.Text as T import Network.HTTP.Conduit (newManager, tlsManagerSettings) -import Web.Routes import Web.Twitter.Conduit (OAuth, TWInfo(..), call, twCredential, twOAuth) import Web.Twitter.Conduit.Api import Web.Twitter.Conduit.Parameters hiding (map) @@ -40,11 +39,11 @@ import Views crossPost :: App () crossPost = do liftIO $ putStrLn "Cross-posting new articles..." - runRoute crossPostTwitter + crossPostTwitter liftIO $ putStrLn "All new articles cross-posted." crossPostTwitter :: - (MonadRoute m, URL m ~ Sitemap, MonadIO m, MonadReader AppData m) => m () + (MonadIO m, MonadReader AppData m) => m () crossPostTwitter = do mgr <- liftIO $ newManager tlsManagerSettings address <- asks appAddress @@ -68,7 +67,7 @@ crossPostTwitter = do let lpref = singleLanguage credLang forM unposted $ \art -> do let title = langTitle lpref art - articleLink <- linkTo art + let articleLink = linkTo art let content = title <> " " <> address <> articleLink doCall $ update content return () @@ -84,7 +83,7 @@ twitterArticleLinks statuses = do -- Filter out the ones for the article route let articleLinks = [ (date, slug) - | Right (ArticleView date slug) <- map parseRoute urls + | Just (ArticleView date slug) <- map parseURL urls ] -- Get the matched articles let articleFilter art = any (\(d, s) -> byDateSlug d s art) articleLinks diff --git a/src/Routes.hs b/src/Routes.hs index f0282fb..7b00a25 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -12,26 +12,18 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time -import Text.Boomerang.TH (makeBoomerangs) - -import Web.Routes.Boomerang - import Types.Language data PageFormat - = Html - | Pdf + = Pdf | Docx deriving (Eq, Ord, Show) --- TODO: use Boomerang for these formatToStr :: PageFormat -> Text -formatToStr Html = "pdf" formatToStr Pdf = "pdf" formatToStr Docx = "docx" strToFormat :: Text -> Maybe PageFormat -strToFormat "html" = Just Html strToFormat "pdf" = Just Pdf strToFormat "docx" = Just Docx strToFormat _ = Nothing @@ -40,10 +32,6 @@ type MaybeFormat = Maybe PageFormat data Sitemap = Index - | Yearly Integer - | Monthly Integer - Int - | Daily Day | ArticleView Day Text | MetaView Text @@ -54,87 +42,13 @@ data Sitemap | CodeStylesheet deriving (Eq, Ord, Show) -makeBoomerangs ''Sitemap - -rDay :: Boomerang TextsError [Text] r (Day :- r) -rDay = xpure mkDay parseDay . (integer int int) - where - mkDay (y :- m :- d :- x) = fromGregorian y m d :- x - parseDay (day :- x) = - let (y, m, d) = toGregorian day - in Just $ y :- m :- d :- x - --- TODO: This will error on strings which are not language codes -rLanguage :: Boomerang TextsError [Text] r (Language :- r) -rLanguage = xpure mkLang parseLang . anyString - where - mkLang (str :- x) = - let Just lang = parseLanguageM str - in lang :- x - parseLang (lang :- x) = Just $ showLanguage lang :- x - -rString :: Boomerang e tok i (Text :- o) -> Boomerang e tok i (String :- o) -rString = xmaph T.unpack (Just . T.pack) - -anyString :: Boomerang TextsError [Text] o (String :- o) -anyString = rString anyText +routeURL :: Sitemap -> Text +routeURL = undefined -rExtension :: - Boomerang e tok i (Text :- o) - -> Boomerang e tok i (Text :- Maybe Text :- o) -rExtension = xmap splitExt' (Just . joinExt') - where - splitExt' :: Text :- o -> Text :- Maybe Text :- o - splitExt' (seg :- o) = - let (seg', ext) = splitExt seg - in seg' :- ext :- o - joinExt' :: Text :- Maybe Text :- o -> Text :- o - joinExt' (seg :- ext :- o) = joinExt seg ext :- o - --- Swap the top 2 components in a Boomerang -xflip :: Boomerang e tok i (a :- b :- o) -> Boomerang e tok i (b :- a :- o) -xflip = xmap pflip (Just . pflip) - where - pflip (a :- b :- o) = b :- a :- o - --- Apply a transformation to the second topmost component of a Boomerang -xmaph2 :: - (b -> c) - -> (c -> Maybe b) - -> Boomerang e tok i (a :- b :- o) - -> Boomerang e tok i (a :- c :- o) -xmaph2 f g = xflip . xmaph f g . xflip - --- Split a path component into basename and extension -splitExt :: Text -> (Text, Maybe Text) -splitExt segment = - case T.splitOn "." segment of - [] -> ("", Nothing) - [s] -> (s, Nothing) - ss -> (T.intercalate "." $ init ss, Just $ last ss) +parseURL :: Text -> Maybe Sitemap +parseURL = undefined -- Join a basename and extension together joinExt :: Text -> Maybe Text -> Text joinExt segment Nothing = segment joinExt segment (Just ext) = segment <> "." <> ext - --- Convert the second topmost component into a MaybeFormat -xFormat :: - Boomerang e tok i (Text :- Maybe Text :- o) - -> Boomerang e tok i (Text :- MaybeFormat :- o) -xFormat = xmaph2 (strToFormat =<<) (Just . fmap formatToStr) - -sitemap :: Boomerang TextsError [Text] r (Sitemap :- r) -sitemap = - mconcat - [ rIndex - , rYearly . integer - , rMonthly . integer int - , rDaily . rDay - , rFeed . "feed" rLanguage - , rSiteScript . "site.js" - , rPrintStylesheet . "print.css" - , rCodeStylesheet . "code.css" - , rArticleView . rDay anyText - , rMetaView . xFormat (rExtension anyText) - ] diff --git a/src/Views.hs b/src/Views.hs index 93e09ed..de8e0c9 100644 --- a/src/Views.hs +++ b/src/Views.hs @@ -29,8 +29,6 @@ import Text.Pandoc hiding (Meta, Reader) import Text.Pandoc.Highlighting import Text.Pandoc.Walk -import Web.Routes - import Models import Render import Routes @@ -49,8 +47,6 @@ instance Linkable Article where instance Linkable Meta where link m = MetaView (mtSlug m) Nothing -type AppRoute m = (MonadRoute m, URL m ~ Sitemap) - data PageContent = PageContent { pcTitle :: Maybe Text , pcLayout :: Layout @@ -88,10 +84,8 @@ paginate size page allItems = Paginated prev items next convRender :: (url -> [(a, Maybe b)] -> c) -> url -> [(a, b)] -> c convRender maybeF url params = maybeF url $ map (A.second Just) params -render :: MonadRoute m => HtmlUrl (URL m) -> m Markup -render html = do - route <- fmap convRender askRouteFn - return $ html route +render :: HtmlUrl Sitemap -> Markup +render html = html perhapsRoute askLangStringFn :: MonadReader AppData m => LanguagePreference -> m (Text -> Text) @@ -115,15 +109,14 @@ linkTitle lang (MetaLink slug) = do pure $ langTitle lang meta linkDestination :: - (AppRoute m, MonadReader AppData m, MonadPlus m) => Link -> m Text + (MonadReader AppData m, MonadPlus m) => Link -> m Text linkDestination (ExternalLink url _) = pure url linkDestination (MetaLink slug) = do meta <- askMeta slug - route <- askRouteFn - pure $ route (link meta) [] + pure $ perhapsRoute (link meta) [] template :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> PageContent -> m Markup @@ -139,22 +132,22 @@ template lang page = do analyticsIDs <- asks appAnalytics let analytics = $(hamletFile "templates/analytics.hamlet") case pcLayout page of - BaseLayout -> render $(hamletFile "templates/base.hamlet") + BaseLayout -> pure $ render $(hamletFile "templates/base.hamlet") PresentationLayout -> - render $(hamletFile "templates/base_presentation.hamlet") + pure $ render $(hamletFile "templates/base_presentation.hamlet") articleListDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Paginated Article -> m Markup articleListDisplay lang articles = do - articlesContent <- mapM (linkedContent lang) (pageItems articles) + let articlesContent = map (linkedContent lang) (pageItems articles) langString <- askLangStringFn lang template lang $ def {pcContent = $(hamletFile "templates/list.hamlet")} articleDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Article -> m Markup @@ -166,7 +159,7 @@ articleDisplay lang article = } metaDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Meta -> m Markup @@ -185,20 +178,16 @@ metaDisplay lang meta = $(hamletFile "templates/meta_presentation.hamlet") -- Generate a link to some content -linkTo :: (Linkable a, AppRoute m) => a -> m Text -linkTo a = do - routeFn <- askRouteFn - pure $ routeFn (link a) [] +linkTo :: (Linkable a) => a -> Text +linkTo a = perhapsRoute (link a) [] -- Modify the content to have a link to itself and have no anchors linkedContent :: - (HasContent a, Linkable a, AppRoute m) + (HasContent a, Linkable a) => LanguagePreference -> a - -> m Pandoc -linkedContent lang a = do - target <- linkTo a - pure $ linkedHeader target $ langContent lang a + -> Pandoc +linkedContent lang a = linkedHeader (linkTo a) $ langContent lang a -- Modify the first header to be a link to a given place -- and remove all anchors from headers @@ -220,18 +209,17 @@ linkedHeader target doc = evalState (walkM linkHeader doc) True return $ Header n ("", [], []) text' linkHeader x = return x -renderSiteScript :: MonadRoute m => m JavaScript -renderSiteScript = do - route <- fmap convRender askRouteFn - return $ JavaScript $ renderJavascriptUrl route $(juliusFile "templates/site.julius") +perhapsRoute :: Render Sitemap +perhapsRoute r _ = routeURL r + +renderSiteScript :: JavaScript +renderSiteScript = JavaScript $ renderJavascriptUrl perhapsRoute $(juliusFile "templates/site.julius") -renderPrintStylesheet :: MonadRoute m => m Stylesheet -renderPrintStylesheet = do - route <- fmap convRender askRouteFn - return $ Stylesheet $ renderCssUrl route $(luciusFile "templates/print.lucius") +renderPrintStylesheet :: Stylesheet +renderPrintStylesheet = Stylesheet $ renderCssUrl perhapsRoute $(luciusFile "templates/print.lucius") -renderCodeStylesheet :: MonadRoute m => m Stylesheet -renderCodeStylesheet = pure $ Stylesheet $ TL.pack $ styleToCss highlightingStyle +renderCodeStylesheet :: Stylesheet +renderCodeStylesheet = Stylesheet $ TL.pack $ styleToCss highlightingStyle -- | Presentation can only render the pipe tables. Disable the other kinds presentationOptions :: WriterOptions diff --git a/src/Views/Export.hs b/src/Views/Export.hs index 8e7371c..a0c78f4 100644 --- a/src/Views/Export.hs +++ b/src/Views/Export.hs @@ -31,8 +31,6 @@ import Text.StringLike import Text.Pandoc hiding (Meta) -import Web.Routes - import Cache import Models import Render @@ -43,9 +41,7 @@ import Views -- Export a meta into one of the supported formats metaExport :: - ( MonadRoute m - , URL m ~ Sitemap - , MonadReader AppData m + ( MonadReader AppData m , MonadState AppCache m , MonadIO m ) @@ -56,7 +52,6 @@ metaExport :: -- Pandoc uses TeX to render PDFs, which requires a lot of packages for Unicode -- support, etc. Use wkhtmltopdf instead metaExport Pdf lang meta = pdfExport lang meta -metaExport Html _ _ = error "HTML is not an export format" metaExport Docx lang meta = do let content = langContent lang meta pure $ @@ -68,12 +63,11 @@ metaExportFileName format meta = Text.intercalate "." [metaName, fileExtension f where metaName = mtExportSlug meta fileExtension Docx = "docx" - fileExtension Html = error "HTML is not an export format" fileExtension Pdf = "pdf" -- Export a PDF using wkhtmltopdf pdfExport :: - (MonadRoute m, URL m ~ Sitemap, MonadState AppCache m, MonadIO m) + (MonadState AppCache m, MonadIO m) => LanguagePreference -> Meta -> m (Export LB.ByteString) @@ -82,7 +76,7 @@ pdfExport lang meta = withCacheM (bestLanguage lang, mtSlug meta) $ do let content = runPandocPure' $ writeHtml $ langContent lang meta let title = langTitle lang meta - html <- render $(hamletFile "templates/pdf-export.hamlet") + let html = render $(hamletFile "templates/pdf-export.hamlet") let htmlText = TextRenderer.renderMarkup html let fixedHtmlText = fixupHtml htmlText wkhtmltopdf $ encodeUtf8 fixedHtmlText diff --git a/src/Views/Feed.hs b/src/Views/Feed.hs index 45645b1..115ed7f 100644 --- a/src/Views/Feed.hs +++ b/src/Views/Feed.hs @@ -39,8 +39,6 @@ import Text.Blaze.Renderer.Text (renderMarkup) import qualified Text.XML as C -import Web.Routes - import Models import Routes import Types.Content @@ -60,13 +58,13 @@ atomDate :: UTCTime -> Date atomDate = (<> "T00:00:00Z") . Text.pack . showGregorian . utctDay articleEntry :: - (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m) + MonadReader AppData m => Language -> Article -> m Entry articleEntry lang article = do let lpref = singleLanguage lang - articleLink <- linkTo article + let articleLink = linkTo article authorName <- askLangString lpref "authorName" let entry = nullEntry @@ -84,18 +82,18 @@ articleEntry lang article = do } feedDisplay :: - (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m) + MonadReader AppData m => Language -> [Article] -> m Response feedDisplay lang articles = do siteName <- askLangString (singleLanguage lang) "siteName" -- TODO: Web.Routes generate a link without the trailing slash - home <- (<> "/") <$> linkTo Index + let home = linkTo Index <> "/" let lastUpdated = arAuthored $ head articles let blankFeed = nullFeed home (TextString siteName) (atomDate lastUpdated) entries <- mapM (articleEntry lang) articles - selfAddress <- linkTo $ Routes.Feed lang + let selfAddress = linkTo $ Routes.Feed lang let selfLink = (nullLink selfAddress) {linkRel = Just $ Left "self"} let feed = blankFeed {feedEntries = entries, feedLinks = [selfLink]} pure $ toResponse $ AtomFeed feed diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 1e21c7b..444306f 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -3,18 +3,26 @@ module TestRoutes where +import Data.LanguageCodes +import Data.Time.Calendar + import Routes import Test.Framework import Test.QuickCheck.Instances () -test_splitExt = do - assertEqual ("segment", Nothing) (splitExt "segment") - assertEqual ("segment", Just "ext") (splitExt "segment.ext") - assertEqual - ("segment.more.dots", Just "ext") - (splitExt "segment.more.dots.ext") +test_routeURL_parseURL = do + assertEqual (routeURL Index) "" + assertEqual (parseURL "/") $ Just Index + let testArticle = ArticleView (fromGregorian 2020 01 01) "test" + assertEqual (routeURL testArticle) "/2020-01-01/test" + assertEqual (parseURL "/2020/01/01/test") $ Just testArticle + assertEqual (parseURL "/2020-01-01/test/") $ Just testArticle + assertEqual (routeURL $ MetaView "meta" Nothing) "/meta" + assertEqual (parseURL "/meta/") $ Just $ MetaView "meta" Nothing + assertEqual (routeURL $ MetaView "meta" (Just Pdf)) "/meta.pdf" + assertEqual (routeURL $ Feed EN) "/feed/en" -prop_splitExt_joinExt seg = - let (seg', ext) = splitExt seg - in joinExt seg' ext == seg +prop_routeURL_parseURL route = + let url = routeURL route + in parseURL url == Just route From f3270adc9c01fcdbe9572bd749b39101b26f5acf Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Sun, 9 Feb 2020 21:28:56 +1100 Subject: [PATCH 03/13] Add Arbitrary instances --- package.yaml | 1 + src/Routes.hs | 9 +++++---- testsuite/TestRoutes.hs | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/package.yaml b/package.yaml index 27bbc70..21e7dc7 100644 --- a/package.yaml +++ b/package.yaml @@ -67,6 +67,7 @@ tests: - HTF - HUnit - derive + - generic-arbitrary - QuickCheck - quickcheck-instances stability: Experimental diff --git a/src/Routes.hs b/src/Routes.hs index 7b00a25..4dbf11f 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -1,6 +1,5 @@ +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeOperators #-} module Routes where @@ -12,12 +11,14 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time +import GHC.Generics + import Types.Language data PageFormat = Pdf | Docx - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic) formatToStr :: PageFormat -> Text formatToStr Pdf = "pdf" @@ -40,7 +41,7 @@ data Sitemap | SiteScript | PrintStylesheet | CodeStylesheet - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic) routeURL :: Sitemap -> Text routeURL = undefined diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 444306f..9b09896 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -4,12 +4,20 @@ module TestRoutes where import Data.LanguageCodes +import Data.LanguageCodes.Arbitrary () import Data.Time.Calendar import Routes import Test.Framework import Test.QuickCheck.Instances () +import Test.QuickCheck.Arbitrary.Generic + +instance Arbitrary PageFormat where + arbitrary = genericArbitrary + +instance Arbitrary Sitemap where + arbitrary = genericArbitrary test_routeURL_parseURL = do assertEqual (routeURL Index) "" From 7db7d42fbbd58e9f850edf2bb15ae7d9623ec9b9 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Mon, 10 Feb 2020 08:02:28 +1100 Subject: [PATCH 04/13] Implement URL parsing --- package.yaml | 1 + src/Routes.hs | 52 +++++++++++++++++++++++++++++++++-- src/Types/Language.hs | 44 +++++++++++++++-------------- testsuite/Integration/Base.hs | 2 +- testsuite/TestRoutes.hs | 26 +++++++++++------- 5 files changed, 91 insertions(+), 34 deletions(-) diff --git a/package.yaml b/package.yaml index 21e7dc7..54659a3 100644 --- a/package.yaml +++ b/package.yaml @@ -32,6 +32,7 @@ library: - authenticate-oauth - directory - filepath + - here - http-conduit - lens - lifted-base diff --git a/src/Routes.hs b/src/Routes.hs index 4dbf11f..9b209e7 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} module Routes where @@ -7,12 +8,17 @@ import Prelude hiding ((.)) import Control.Category (Category((.))) +import Data.String.Here (i) + +import Data.Char (isDigit) import Data.Text (Text) -import qualified Data.Text as T +import qualified Data.Text as Text import Data.Time import GHC.Generics +import Text.Read + import Types.Language data PageFormat @@ -44,10 +50,50 @@ data Sitemap deriving (Eq, Ord, Show, Generic) routeURL :: Sitemap -> Text -routeURL = undefined +routeURL Index = "" +routeURL (ArticleView date text) = let (year, month, day) = toGregorian date in [i|/${pad 2 year}-${pad 2 month}-${pad 2 day}/${text}|] +routeURL (MetaView text Nothing) = [i|/${text}|] +routeURL (MetaView text (Just format)) = [i|/${text}.${formatToStr format}|] +routeURL (Feed language) = [i|/feed/${showLanguage language}|] +routeURL SiteScript = "/assets/site.js" +routeURL PrintStylesheet = "/assets/print.css" +routeURL CodeStylesheet = "/assets/code.css" + +pad :: Show a => Int -> a -> Text +pad len str = let printed = Text.pack (show str) + padding = Text.replicate (len - Text.length printed) "0" + in padding <> printed + +splitNonEmpty :: Text -> Text -> [Text] +splitNonEmpty splitter = filter (not . Text.null) . Text.splitOn splitter + +readText :: Read a => Text -> Maybe a +readText = readMaybe . Text.unpack parseURL :: Text -> Maybe Sitemap -parseURL = undefined +parseURL = parseSegments . splitNonEmpty "/" + where + parseSegments [] = Just Index + parseSegments [metaText] = case splitNonEmpty "." metaText of + [text] -> Just $ MetaView text Nothing + [text, formatStr] -> do + format <- strToFormat formatStr + pure $ MetaView text (Just format) + _ -> Nothing + parseSegments [date, articleText] | dateLike date = parseArticle (splitNonEmpty "-" date) articleText + parseSegments [year, month, day, articleText] = parseArticle [year, month, day] articleText + parseSegments ["assets", "site.js"] = Just SiteScript + parseSegments ["assets", "print.css"] = Just PrintStylesheet + parseSegments ["assets", "code.css"] = Just CodeStylesheet + parseSegments _ = Nothing + dateLike = Text.all $ \c -> isDigit c || c == '-' + parseArticle [yearStr, monthStr, dayStr] text = do + year <- readText yearStr + month <- readText monthStr + day <- readText dayStr + date <- fromGregorianValid year month day + pure $ ArticleView date text + parseArticle _ _ = Nothing -- Join a basename and extension together joinExt :: Text -> Maybe Text -> Text diff --git a/src/Types/Language.hs b/src/Types/Language.hs index 69ab13d..43a0a25 100644 --- a/src/Types/Language.hs +++ b/src/Types/Language.hs @@ -4,6 +4,7 @@ Language-related types. {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Types.Language where @@ -16,18 +17,20 @@ import Data.Function import Data.LanguageCodes import Data.List import Data.List.Split -import qualified Data.Map as M +import Data.Map (Map) +import qualified Data.Map as Map import Data.Maybe -import qualified Data.Text as T +import Data.Text (Text) +import qualified Data.Text as Text import Utils type Language = ISO639_1 -type LanguageMap = M.Map Language +type LanguageMap = Map Language -mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> M.Map k1 a -> m (M.Map k2 a) -mapKeysM kfunc = fmap M.fromList . traverse kvfunc . M.toList +mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> Map k1 a -> m (Map k2 a) +mapKeysM kfunc = fmap Map.fromList . traverse kvfunc . Map.toList where kvfunc (k, v) = (,) <$> kfunc k <*> pure v @@ -36,7 +39,7 @@ instance A.FromJSON ISO639_1 where parseJSON _ = mzero instance A.FromJSONKey ISO639_1 where - fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM . T.unpack + fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM . Text.unpack -- | Newtype for parsing either a single value or a map of values newtype LanguageChoices a = @@ -46,7 +49,7 @@ instance A.FromJSON v => A.FromJSON (LanguageChoices v) where parseJSON v@(A.Object _) = LanguageChoices <$> (A.parseJSON v >>= mapKeysM parseLanguageM) parseJSON v@(A.String _) = - LanguageChoices . M.singleton defaultLanguage <$> A.parseJSON v + LanguageChoices . Map.singleton defaultLanguage <$> A.parseJSON v parseJSON _ = mzero newtype LanguagePreference = LanguagePreference @@ -57,26 +60,26 @@ defaultLanguage :: Language defaultLanguage = EN singleLanguage :: Language -> LanguagePreference -singleLanguage lang = LanguagePreference $ M.singleton lang 1 +singleLanguage lang = LanguagePreference $ Map.singleton lang 1 bestLanguage :: LanguagePreference -> Language bestLanguage = fromMaybe defaultLanguage . listToMaybe . fmap fst . - sortBy (reverseCompare `on` snd) . M.toList . unLanguagePreference + sortBy (reverseCompare `on` snd) . Map.toList . unLanguagePreference rankLanguage :: Language -> LanguagePreference -> Float -rankLanguage lang = fromMaybe 0 . M.lookup lang . unLanguagePreference +rankLanguage lang = fromMaybe 0 . Map.lookup lang . unLanguagePreference matchLanguage :: LanguagePreference -> LanguageMap a -> Maybe a matchLanguage = matchLanguageFunc (const 1) matchLanguageFunc :: (a -> Float) -> LanguagePreference -> LanguageMap a -> Maybe a -matchLanguageFunc quality pref values = fst <$> M.maxView ranked +matchLanguageFunc quality pref values = fst <$> Map.maxView ranked where - ranked = M.fromList $ M.elems $ M.mapWithKey rank values + ranked = Map.fromList $ Map.elems $ Map.mapWithKey rank values rank lang value = (rankLanguage lang pref * quality value, value) parseLanguage :: MonadError String m => String -> m Language @@ -93,19 +96,19 @@ parseLanguageM [c1, c2] = parseLanguageM (c1:c2:'-':_) = parseLanguageM [c1, c2] parseLanguageM _ = mzero -showLanguage :: Language -> String -showLanguage = (\(a, b) -> [a, b]) . toChars +showLanguage :: Language -> Text +showLanguage = Text.pack . (\(a, b) -> [a, b]) . toChars -iso3166 :: Language -> String +iso3166 :: Language -> Text iso3166 EN = "gb" iso3166 ZH = "cn" iso3166 x = showLanguage x -- TODO: Parsec or library languageHeader :: Maybe String -> LanguagePreference -languageHeader Nothing = LanguagePreference $ M.singleton defaultLanguage 1 +languageHeader Nothing = LanguagePreference $ Map.singleton defaultLanguage 1 languageHeader (Just str) = - LanguagePreference $ M.fromList $ mapMaybe parsePref $ splitOn "," str + LanguagePreference $ Map.fromList $ mapMaybe parsePref $ splitOn "," str where parsePref pref = case splitOn ";q=" pref of @@ -116,8 +119,9 @@ languageHeader (Just str) = instance Show LanguagePreference where show = - intercalate "," . - map (uncurry showPref) . M.toList . unLanguagePreference + Text.unpack . + Text.intercalate "," . + map (uncurry showPref) . Map.toList . unLanguagePreference where showPref lang 1 = showLanguage lang - showPref lang qvalue = showLanguage lang ++ ";q=" ++ show qvalue + showPref lang qvalue = showLanguage lang <> ";q=" <> Text.pack (show qvalue) diff --git a/testsuite/Integration/Base.hs b/testsuite/Integration/Base.hs index 13ebdee..6af85ec 100644 --- a/testsuite/Integration/Base.hs +++ b/testsuite/Integration/Base.hs @@ -184,7 +184,7 @@ withLang1 = withLang . singleLanguage withLangCookie :: Language -> TestRequest -> TestRequest withLangCookie lang req = req - {trCookies = M.insert "lang" (T.pack $ showLanguage lang) (trCookies req)} + {trCookies = M.insert "lang" (showLanguage lang) (trCookies req)} -- Extract contents from a response responseContent :: Response -> IO LB.ByteString diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 9b09896..57761a3 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -19,17 +19,23 @@ instance Arbitrary PageFormat where instance Arbitrary Sitemap where arbitrary = genericArbitrary -test_routeURL_parseURL = do - assertEqual (routeURL Index) "" - assertEqual (parseURL "/") $ Just Index +test_index_URL = do + assertEqual "" (routeURL Index) + assertEqual (Just Index) $ parseURL "/" + +test_article_URL = do let testArticle = ArticleView (fromGregorian 2020 01 01) "test" - assertEqual (routeURL testArticle) "/2020-01-01/test" - assertEqual (parseURL "/2020/01/01/test") $ Just testArticle - assertEqual (parseURL "/2020-01-01/test/") $ Just testArticle - assertEqual (routeURL $ MetaView "meta" Nothing) "/meta" - assertEqual (parseURL "/meta/") $ Just $ MetaView "meta" Nothing - assertEqual (routeURL $ MetaView "meta" (Just Pdf)) "/meta.pdf" - assertEqual (routeURL $ Feed EN) "/feed/en" + assertEqual "/2020-01-01/test" (routeURL testArticle) + assertEqual (Just testArticle) (parseURL "/2020/01/01/test") + assertEqual (Just testArticle) (parseURL "/2020-01-01/test/") + +test_meta_URL = do + assertEqual "/meta" (routeURL $ MetaView "meta" Nothing) + assertEqual (Just $ MetaView "meta" Nothing) (parseURL "/meta/") + assertEqual "/meta.pdf" (routeURL $ MetaView "meta" (Just Pdf)) + +test_feed_URL = do + assertEqual "/feed/en" (routeURL $ Feed EN) prop_routeURL_parseURL route = let url = routeURL route From 2bb45ba4e5541dfe49c7e08b9764963ece874881 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 18:06:49 +1100 Subject: [PATCH 05/13] Fix URL parsing --- src/Import.hs | 2 +- src/Routes.hs | 1 + src/Types/Language.hs | 28 +++++++++++++++------------- testsuite/TestLanguage.hs | 1 + testsuite/TestRoutes.hs | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Import.hs b/src/Import.hs index 5298740..fd633e6 100644 --- a/src/Import.hs +++ b/src/Import.hs @@ -94,7 +94,7 @@ parseContent dir = do case M.lookup (T.pack $ tail ext) readers of Nothing -> pure Nothing Just reader -> do - lang <- parseLanguage fileName + lang <- parseLanguage (T.pack fileName) case reader (T.decodeUtf8 $ sfContent file) of Left err -> throwError $ show err Right res -> pure (Just (lang, res)) diff --git a/src/Routes.hs b/src/Routes.hs index 9b209e7..8827d28 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -85,6 +85,7 @@ parseURL = parseSegments . splitNonEmpty "/" parseSegments ["assets", "site.js"] = Just SiteScript parseSegments ["assets", "print.css"] = Just PrintStylesheet parseSegments ["assets", "code.css"] = Just CodeStylesheet + parseSegments ["feed", lang] = Feed <$> parseLanguageM lang parseSegments _ = Nothing dateLike = Text.all $ \c -> isDigit c || c == '-' parseArticle [yearStr, monthStr, dayStr] text = do diff --git a/src/Types/Language.hs b/src/Types/Language.hs index 43a0a25..5eee9fc 100644 --- a/src/Types/Language.hs +++ b/src/Types/Language.hs @@ -39,7 +39,7 @@ instance A.FromJSON ISO639_1 where parseJSON _ = mzero instance A.FromJSONKey ISO639_1 where - fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM . Text.unpack + fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM -- | Newtype for parsing either a single value or a map of values newtype LanguageChoices a = @@ -82,19 +82,21 @@ matchLanguageFunc quality pref values = fst <$> Map.maxView ranked ranked = Map.fromList $ Map.elems $ Map.mapWithKey rank values rank lang value = (rankLanguage lang pref * quality value, value) -parseLanguage :: MonadError String m => String -> m Language +parseLanguage :: MonadError String m => Text -> m Language parseLanguage langStr = case parseLanguageM langStr :: Maybe Language of (Just lang) -> pure lang - Nothing -> throwError $ langStr ++ " is not a valid language code." - -parseLanguageM :: MonadPlus m => String -> m Language -parseLanguageM [c1, c2] = - case fromChars c1 c2 of - Just lang -> return lang - Nothing -> mzero -parseLanguageM (c1:c2:'-':_) = parseLanguageM [c1, c2] -parseLanguageM _ = mzero + Nothing -> throwError $ Text.unpack $ langStr <> " is not a valid language code." + +parseLanguageM :: MonadPlus m => Text -> m Language +parseLanguageM = parseLanguageStr . Text.unpack + where + parseLanguageStr [c1, c2] = + case fromChars c1 c2 of + Just lang -> return lang + Nothing -> mzero + parseLanguageStr (c1:c2:'-':_) = parseLanguageStr [c1, c2] + parseLanguageStr _ = mzero showLanguage :: Language -> Text showLanguage = Text.pack . (\(a, b) -> [a, b]) . toChars @@ -112,8 +114,8 @@ languageHeader (Just str) = where parsePref pref = case splitOn ";q=" pref of - [lang] -> pairWith 1 <$> parseLanguageM lang - [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM lang + [lang] -> pairWith 1 <$> parseLanguageM (Text.pack lang) + [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM (Text.pack lang) _ -> Nothing pairWith y x = (x, y) diff --git a/testsuite/TestLanguage.hs b/testsuite/TestLanguage.hs index 743236d..395bf2c 100644 --- a/testsuite/TestLanguage.hs +++ b/testsuite/TestLanguage.hs @@ -1,5 +1,6 @@ {-# OPTIONS_GHC -F -pgmF htfpp #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE OverloadedStrings #-} module TestLanguage where diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 57761a3..8be315f 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -5,19 +5,34 @@ module TestRoutes where import Data.LanguageCodes import Data.LanguageCodes.Arbitrary () + +import Data.Text (Text) +import qualified Data.Text as Text + import Data.Time.Calendar import Routes import Test.Framework +import Test.QuickCheck import Test.QuickCheck.Instances () import Test.QuickCheck.Arbitrary.Generic instance Arbitrary PageFormat where arbitrary = genericArbitrary +arbitraryName :: Gen Text +arbitraryName = Text.pack <$> listOf (elements ['a'..'z']) + instance Arbitrary Sitemap where - arbitrary = genericArbitrary + arbitrary = oneof [ pure Index + , ArticleView <$> arbitrary <*> arbitraryName + , MetaView <$> arbitraryName <*> arbitrary + , Feed <$> arbitrary + , pure SiteScript + , pure PrintStylesheet + , pure CodeStylesheet + ] test_index_URL = do assertEqual "" (routeURL Index) From 6f6441de6d53b2d134c847ca317c84c1d0d8f9cf Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 18:27:29 +1100 Subject: [PATCH 06/13] Remove derive dependency --- package.yaml | 1 - src/Types/Content.hs | 11 +++++--- src/Types/Services.hs | 6 ++--- stack.yaml | 1 - stack.yaml.lock | 7 ----- testsuite/Data/LanguageCodes/Arbitrary.hs | 11 ++++---- testsuite/TestModels.hs | 33 ++++++++++++++--------- testsuite/TestRoutes.hs | 2 +- 8 files changed, 38 insertions(+), 34 deletions(-) diff --git a/package.yaml b/package.yaml index 54659a3..b272820 100644 --- a/package.yaml +++ b/package.yaml @@ -67,7 +67,6 @@ tests: - multiblog - HTF - HUnit - - derive - generic-arbitrary - QuickCheck - quickcheck-instances diff --git a/src/Types/Content.hs b/src/Types/Content.hs index 975ba2e..ba1dd68 100644 --- a/src/Types/Content.hs +++ b/src/Types/Content.hs @@ -1,6 +1,7 @@ {-| Types for the blog content - articles and metas. -} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Types.Content where @@ -15,6 +16,8 @@ import Data.Text (Text) import Data.Time import Data.Yaml +import GHC.Generics (Generic) + import Text.Blaze.Html (Html) import Text.Pandoc hiding (Meta) @@ -40,7 +43,7 @@ data Article = Article { arSlug :: Text , arContent :: LanguageContent , arAuthored :: UTCTime - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) instance Ord Article where a `compare` b = (arAuthored a, arSlug a) `compare` (arAuthored b, arSlug b) @@ -55,7 +58,7 @@ instance HasSlug Article where data Layout = BaseLayout | PresentationLayout - deriving (Eq, Show) + deriving (Eq, Show, Generic) instance FromJSON Layout where parseJSON (String v) @@ -69,7 +72,7 @@ data Meta = Meta , mtLayout :: Layout , mtExportSlugOverride :: Maybe Text , mtContent :: LanguageContent - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) mtExportSlug :: Meta -> Text mtExportSlug meta = fromMaybe (mtSlug meta) (mtExportSlugOverride meta) @@ -85,7 +88,7 @@ data Link = MetaLink { lnName :: Text } | ExternalLink { lnUrl :: Text , lnText :: LanguageString } - deriving (Eq, Show) + deriving (Eq, Show, Generic) instance FromJSON Link where parseJSON (Object v) = diff --git a/src/Types/Services.hs b/src/Types/Services.hs index 0a4be29..68d704e 100644 --- a/src/Types/Services.hs +++ b/src/Types/Services.hs @@ -49,7 +49,7 @@ instance FromJSON AppServices where newtype AppAuth = AppAuthTwitter TwitterAuth - deriving (Eq, Show) + deriving (Eq, Show, Generic) class ToJSON a => ServiceAuth a where toAppAuth :: a -> AppAuth @@ -59,7 +59,7 @@ class ToJSON a => ServiceAuth a where data TwitterAuth = TwitterAuth { taToken :: BS.ByteString , taSecret :: BS.ByteString - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) instance ServiceAuth TwitterAuth where toAppAuth = AppAuthTwitter @@ -98,7 +98,7 @@ instance ToJSON AppAuth where data CrossPost = CrossPost { cpLanguage :: Language , cpServiceDetails :: AppAuth - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) type AppCrossPost = [CrossPost] diff --git a/stack.yaml b/stack.yaml index 929b471..986860c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -4,7 +4,6 @@ packages: ghc-options: '$everything': -threaded extra-deps: - - derive-2.6.4 - twitter-conduit-0.3.0 - twitter-types-0.7.2.2 - twitter-types-lens-0.7.2 diff --git a/stack.yaml.lock b/stack.yaml.lock index f399fd5..f242b91 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -4,13 +4,6 @@ # https://docs.haskellstack.org/en/stable/lock_files packages: -- completed: - hackage: derive-2.6.4@sha256:f697cbe62828b5d777ea148814978940bd8707cb2ad4adff9415fc6139a7c05d,3184 - pantry-tree: - size: 3967 - sha256: cb33c5025c4629ea5377dd0fc521700cac79f8a88a5d062cfbb0cd7f9f37bcb5 - original: - hackage: derive-2.6.4 - completed: hackage: twitter-conduit-0.3.0@sha256:67b68511fc19563ff4159ae37188ac3db7dbc6603d81f11c6f1c2dd8ec1f5bd8,4318 pantry-tree: diff --git a/testsuite/Data/LanguageCodes/Arbitrary.hs b/testsuite/Data/LanguageCodes/Arbitrary.hs index aa9f470..3af5935 100644 --- a/testsuite/Data/LanguageCodes/Arbitrary.hs +++ b/testsuite/Data/LanguageCodes/Arbitrary.hs @@ -1,13 +1,14 @@ -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.LanguageCodes.Arbitrary where -import Prelude hiding (LT) +import Data.LanguageCodes (ISO639_1(..), fromChars) -import Data.DeriveTH -import Data.LanguageCodes (ISO639_1(..)) +import Data.Maybe import Test.QuickCheck -derive makeArbitrary ''ISO639_1 +instance Arbitrary ISO639_1 where + arbitrary = elements $ catMaybes $ fromChars <$> letter <*> letter + where + letter = ['a'..'z'] diff --git a/testsuite/TestModels.hs b/testsuite/TestModels.hs index 98e80dd..a73ea4d 100644 --- a/testsuite/TestModels.hs +++ b/testsuite/TestModels.hs @@ -6,8 +6,6 @@ module TestModels where -import Data.DeriveTH - import Data.LanguageCodes.Arbitrary () import qualified Data.Map as M @@ -22,17 +20,23 @@ import Types.Content import Types.Services import Test.Framework +import Test.QuickCheck.Arbitrary.Generic import Test.QuickCheck.Instances () -derive makeArbitrary ''Article +instance Arbitrary Article where + arbitrary = genericArbitrary -derive makeArbitrary ''Layout +instance Arbitrary Layout where + arbitrary = genericArbitrary -derive makeArbitrary ''Meta +instance Arbitrary Meta where + arbitrary = genericArbitrary -derive makeArbitrary ''Link +instance Arbitrary Link where + arbitrary = genericArbitrary -derive makeArbitrary ''Analytics +instance Arbitrary Analytics where + arbitrary = genericArbitrary instance Arbitrary TW.OAuth where arbitrary = do @@ -44,15 +48,20 @@ instance Arbitrary TW.OAuth where , TW.oauthConsumerSecret = secret } -derive makeArbitrary ''AppServices +instance Arbitrary AppServices where + arbitrary = genericArbitrary -derive makeArbitrary ''TwitterAuth +instance Arbitrary TwitterAuth where + arbitrary = genericArbitrary -derive makeArbitrary ''AppAuth +instance Arbitrary AppAuth where + arbitrary = genericArbitrary -derive makeArbitrary ''CrossPost +instance Arbitrary CrossPost where + arbitrary = genericArbitrary -derive makeArbitrary ''AppData +instance Arbitrary AppData where + arbitrary = genericArbitrary fall :: [a] -> (a -> Bool) -> Bool fall = flip all diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 8be315f..b2cfef7 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -15,8 +15,8 @@ import Routes import Test.Framework import Test.QuickCheck -import Test.QuickCheck.Instances () import Test.QuickCheck.Arbitrary.Generic +import Test.QuickCheck.Instances () instance Arbitrary PageFormat where arbitrary = genericArbitrary From 7203d8da81a1d09b50da4a9a0792f1aa795576fe Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:02:50 +1100 Subject: [PATCH 07/13] Fix routing with query parameters --- src/Routes.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Routes.hs b/src/Routes.hs index 8827d28..35f81e6 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -15,6 +15,8 @@ import Data.Text (Text) import qualified Data.Text as Text import Data.Time +import Debug.Trace + import GHC.Generics import Text.Read @@ -71,7 +73,7 @@ readText :: Read a => Text -> Maybe a readText = readMaybe . Text.unpack parseURL :: Text -> Maybe Sitemap -parseURL = parseSegments . splitNonEmpty "/" +parseURL = parseSegments . splitNonEmpty "/" . dropQueryParams where parseSegments [] = Just Index parseSegments [metaText] = case splitNonEmpty "." metaText of @@ -95,6 +97,7 @@ parseURL = parseSegments . splitNonEmpty "/" date <- fromGregorianValid year month day pure $ ArticleView date text parseArticle _ _ = Nothing + dropQueryParams = Text.takeWhile (/= '?') -- Join a basename and extension together joinExt :: Text -> Maybe Text -> Text From c1ce5b3889c28beb62b35e22aa1970ba939467c9 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:15:35 +1100 Subject: [PATCH 08/13] Fix tests for asset locations --- testsuite/Integration/TestHome.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testsuite/Integration/TestHome.hs b/testsuite/Integration/TestHome.hs index 3fcbfa2..38e4a26 100644 --- a/testsuite/Integration/TestHome.hs +++ b/testsuite/Integration/TestHome.hs @@ -53,10 +53,10 @@ test_home_next_page = do test_css :: IO () test_css = do - css <- makeRequestText $ simpleRequest "/code.css" + css <- makeRequestText $ simpleRequest "/assets/code.css" assertTextContains "color" css test_js :: IO () test_js = do - js <- makeRequestText $ simpleRequest "/site.js" + js <- makeRequestText $ simpleRequest "/assets/site.js" assertTextContains "function" js From 14ec1259f4cc4bcb492cfcac65547f90bd13aca8 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:33:37 +1100 Subject: [PATCH 09/13] Add monadic context back to routes --- src/App.hs | 4 ++-- src/CrossPost.hs | 2 +- src/Views.hs | 43 +++++++++++++++++++++++-------------------- src/Views/Export.hs | 4 ++-- src/Views/Feed.hs | 6 +++--- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/App.hs b/src/App.hs index c358f70..66cc85f 100644 --- a/src/App.hs +++ b/src/App.hs @@ -155,10 +155,10 @@ feedIndex language = do feedDisplay language sorted >>= okResponse siteScript :: AppPart Response -siteScript = okResponse renderSiteScript +siteScript = renderSiteScript >>= okResponse printStylesheet :: AppPart Response -printStylesheet = okResponse renderPrintStylesheet +printStylesheet = renderPrintStylesheet >>= okResponse codeStylesheet :: AppPart Response codeStylesheet = okResponse renderCodeStylesheet diff --git a/src/CrossPost.hs b/src/CrossPost.hs index cc0caeb..b7cee35 100644 --- a/src/CrossPost.hs +++ b/src/CrossPost.hs @@ -67,7 +67,7 @@ crossPostTwitter = do let lpref = singleLanguage credLang forM unposted $ \art -> do let title = langTitle lpref art - let articleLink = linkTo art + articleLink <- linkTo art let content = title <> " " <> address <> articleLink doCall $ update content return () diff --git a/src/Views.hs b/src/Views.hs index de8e0c9..d2a1487 100644 --- a/src/Views.hs +++ b/src/Views.hs @@ -81,11 +81,10 @@ paginate size page allItems = Paginated prev items next | null rest = Nothing | otherwise = Just (page + 1) -convRender :: (url -> [(a, Maybe b)] -> c) -> url -> [(a, b)] -> c -convRender maybeF url params = maybeF url $ map (A.second Just) params - -render :: HtmlUrl Sitemap -> Markup -render html = html perhapsRoute +render :: MonadReader AppData m => HtmlUrl Sitemap -> m Markup +render html = do + r <- routeURLParams + pure $ html r askLangStringFn :: MonadReader AppData m => LanguagePreference -> m (Text -> Text) @@ -113,7 +112,7 @@ linkDestination :: linkDestination (ExternalLink url _) = pure url linkDestination (MetaLink slug) = do meta <- askMeta slug - pure $ perhapsRoute (link meta) [] + linkTo meta template :: (MonadReader AppData m, MonadPlus m) @@ -132,9 +131,9 @@ template lang page = do analyticsIDs <- asks appAnalytics let analytics = $(hamletFile "templates/analytics.hamlet") case pcLayout page of - BaseLayout -> pure $ render $(hamletFile "templates/base.hamlet") + BaseLayout -> render $(hamletFile "templates/base.hamlet") PresentationLayout -> - pure $ render $(hamletFile "templates/base_presentation.hamlet") + render $(hamletFile "templates/base_presentation.hamlet") articleListDisplay :: (MonadReader AppData m, MonadPlus m) @@ -142,7 +141,7 @@ articleListDisplay :: -> Paginated Article -> m Markup articleListDisplay lang articles = do - let articlesContent = map (linkedContent lang) (pageItems articles) + articlesContent <- mapM (linkedContent lang) (pageItems articles) langString <- askLangStringFn lang template lang $ def {pcContent = $(hamletFile "templates/list.hamlet")} @@ -178,16 +177,20 @@ metaDisplay lang meta = $(hamletFile "templates/meta_presentation.hamlet") -- Generate a link to some content -linkTo :: (Linkable a) => a -> Text -linkTo a = perhapsRoute (link a) [] +linkTo :: (MonadReader AppData m, Linkable a) => a -> m Text +linkTo a = do + r <- routeURLParams + pure $ r (link a) [] -- Modify the content to have a link to itself and have no anchors linkedContent :: - (HasContent a, Linkable a) + (MonadReader AppData m, HasContent a, Linkable a) => LanguagePreference -> a - -> Pandoc -linkedContent lang a = linkedHeader (linkTo a) $ langContent lang a + -> m Pandoc +linkedContent lang a = do + lnk <- linkTo a + pure $ linkedHeader lnk $ langContent lang a -- Modify the first header to be a link to a given place -- and remove all anchors from headers @@ -209,14 +212,14 @@ linkedHeader target doc = evalState (walkM linkHeader doc) True return $ Header n ("", [], []) text' linkHeader x = return x -perhapsRoute :: Render Sitemap -perhapsRoute r _ = routeURL r +routeURLParams :: MonadReader AppData m => m (Render Sitemap) +routeURLParams = pure $ \r _ -> routeURL r -renderSiteScript :: JavaScript -renderSiteScript = JavaScript $ renderJavascriptUrl perhapsRoute $(juliusFile "templates/site.julius") +renderSiteScript :: MonadReader AppData m => m JavaScript +renderSiteScript = routeURLParams >>= \r -> pure $ JavaScript $ renderJavascriptUrl r $(juliusFile "templates/site.julius") -renderPrintStylesheet :: Stylesheet -renderPrintStylesheet = Stylesheet $ renderCssUrl perhapsRoute $(luciusFile "templates/print.lucius") +renderPrintStylesheet :: MonadReader AppData m => m Stylesheet +renderPrintStylesheet = routeURLParams >>= \r -> pure $ Stylesheet $ renderCssUrl r $(luciusFile "templates/print.lucius") renderCodeStylesheet :: Stylesheet renderCodeStylesheet = Stylesheet $ TL.pack $ styleToCss highlightingStyle diff --git a/src/Views/Export.hs b/src/Views/Export.hs index a0c78f4..9c98ecc 100644 --- a/src/Views/Export.hs +++ b/src/Views/Export.hs @@ -67,7 +67,7 @@ metaExportFileName format meta = Text.intercalate "." [metaName, fileExtension f -- Export a PDF using wkhtmltopdf pdfExport :: - (MonadState AppCache m, MonadIO m) + (MonadReader AppData m, MonadState AppCache m, MonadIO m) => LanguagePreference -> Meta -> m (Export LB.ByteString) @@ -76,7 +76,7 @@ pdfExport lang meta = withCacheM (bestLanguage lang, mtSlug meta) $ do let content = runPandocPure' $ writeHtml $ langContent lang meta let title = langTitle lang meta - let html = render $(hamletFile "templates/pdf-export.hamlet") + html <- render $(hamletFile "templates/pdf-export.hamlet") let htmlText = TextRenderer.renderMarkup html let fixedHtmlText = fixupHtml htmlText wkhtmltopdf $ encodeUtf8 fixedHtmlText diff --git a/src/Views/Feed.hs b/src/Views/Feed.hs index 115ed7f..296b0ee 100644 --- a/src/Views/Feed.hs +++ b/src/Views/Feed.hs @@ -64,7 +64,7 @@ articleEntry :: -> m Entry articleEntry lang article = do let lpref = singleLanguage lang - let articleLink = linkTo article + articleLink <- linkTo article authorName <- askLangString lpref "authorName" let entry = nullEntry @@ -89,11 +89,11 @@ feedDisplay :: feedDisplay lang articles = do siteName <- askLangString (singleLanguage lang) "siteName" -- TODO: Web.Routes generate a link without the trailing slash - let home = linkTo Index <> "/" + home <- (<> "/") <$> linkTo Index let lastUpdated = arAuthored $ head articles let blankFeed = nullFeed home (TextString siteName) (atomDate lastUpdated) entries <- mapM (articleEntry lang) articles - let selfAddress = linkTo $ Routes.Feed lang + selfAddress <- linkTo $ Routes.Feed lang let selfLink = (nullLink selfAddress) {linkRel = Just $ Left "self"} let feed = blankFeed {feedEntries = entries, feedLinks = [selfLink]} pure $ toResponse $ AtomFeed feed From 88bb30d88d65b67c352872917e8d7b42810f6f33 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:36:37 +1100 Subject: [PATCH 10/13] Prepend site address to links --- src/Views.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Views.hs b/src/Views.hs index d2a1487..364cbff 100644 --- a/src/Views.hs +++ b/src/Views.hs @@ -213,7 +213,9 @@ linkedHeader target doc = evalState (walkM linkHeader doc) True linkHeader x = return x routeURLParams :: MonadReader AppData m => m (Render Sitemap) -routeURLParams = pure $ \r _ -> routeURL r +routeURLParams = do + address <- asks appAddress + pure $ \r _ -> address <> routeURL r renderSiteScript :: MonadReader AppData m => m JavaScript renderSiteScript = routeURLParams >>= \r -> pure $ JavaScript $ renderJavascriptUrl r $(juliusFile "templates/site.julius") From adc8c0142656574810225e755c9e93e8785ebf85 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:37:00 +1100 Subject: [PATCH 11/13] Remove obsolete test --- testsuite/Integration/TestMeta.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/testsuite/Integration/TestMeta.hs b/testsuite/Integration/TestMeta.hs index 42873f7..8a9abb2 100644 --- a/testsuite/Integration/TestMeta.hs +++ b/testsuite/Integration/TestMeta.hs @@ -17,11 +17,6 @@ test_meta = do meta <- makeRequestText $ simpleRequest "/meta" assertTextContains "

Test Meta

" meta -test_meta_html :: IO () -test_meta_html = do - meta <- makeRequestText $ simpleRequest "/meta.html" - assertTextContains "

Test Meta

" meta - test_meta_pdf :: IO () test_meta_pdf = do meta_pdf <- makeRequest $ simpleRequest "/meta.pdf" From 1356906c3ec0ae0b4d0ab49f7fe28e0d952d29ef Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 21:39:17 +1100 Subject: [PATCH 12/13] Disallow empty article/meta names in tests --- testsuite/TestRoutes.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index b2cfef7..b163bf0 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -22,7 +22,7 @@ instance Arbitrary PageFormat where arbitrary = genericArbitrary arbitraryName :: Gen Text -arbitraryName = Text.pack <$> listOf (elements ['a'..'z']) +arbitraryName = Text.pack <$> listOf1 (elements ['a'..'z']) instance Arbitrary Sitemap where arbitrary = oneof [ pure Index From 033914b5e29571b05494f55bd878dc7cad05125b Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 11 Feb 2020 22:12:19 +1100 Subject: [PATCH 13/13] hlint --- src/Types/Language.hs | 2 +- src/Views.hs | 4 +--- testsuite/TestModels.hs | 2 -- testsuite/TestRoutes.hs | 3 +-- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Types/Language.hs b/src/Types/Language.hs index 5eee9fc..2f3c34f 100644 --- a/src/Types/Language.hs +++ b/src/Types/Language.hs @@ -39,7 +39,7 @@ instance A.FromJSON ISO639_1 where parseJSON _ = mzero instance A.FromJSONKey ISO639_1 where - fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM + fromJSONKey = A.FromJSONKeyTextParser parseLanguageM -- | Newtype for parsing either a single value or a map of values newtype LanguageChoices a = diff --git a/src/Views.hs b/src/Views.hs index 364cbff..5960b60 100644 --- a/src/Views.hs +++ b/src/Views.hs @@ -82,9 +82,7 @@ paginate size page allItems = Paginated prev items next | otherwise = Just (page + 1) render :: MonadReader AppData m => HtmlUrl Sitemap -> m Markup -render html = do - r <- routeURLParams - pure $ html r +render html = html <$> routeURLParams askLangStringFn :: MonadReader AppData m => LanguagePreference -> m (Text -> Text) diff --git a/testsuite/TestModels.hs b/testsuite/TestModels.hs index a73ea4d..59e8d97 100644 --- a/testsuite/TestModels.hs +++ b/testsuite/TestModels.hs @@ -1,8 +1,6 @@ {-# OPTIONS_GHC -F -pgmF htfpp #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-missing-signatures #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} module TestModels where diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index b163bf0..ddc5cd5 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -49,8 +49,7 @@ test_meta_URL = do assertEqual (Just $ MetaView "meta" Nothing) (parseURL "/meta/") assertEqual "/meta.pdf" (routeURL $ MetaView "meta" (Just Pdf)) -test_feed_URL = do - assertEqual "/feed/en" (routeURL $ Feed EN) +test_feed_URL = assertEqual "/feed/en" (routeURL $ Feed EN) prop_routeURL_parseURL route = let url = routeURL route