packages feed

shakebook-0.13.1.0: src/Shakebook/Conventions.hs

{-# LANGUAGE DeriveAnyClass       #-}
{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE UndecidableInstances #-}
{- |
   Module     : Shakebook.Conventions
   License    : MIT
   Stability  : experimental

Conventions used in Shakebook projects, common lenses, generators, and indexing wrappers over Values.
-}
module Shakebook.Conventions where

import           Composite.Aeson
import           Composite.Aeson.Path
import           Composite.Record
import qualified Composite.Record.Tuple     as C
import           Composite.TH
import           Control.Comonad.Cofree
import           Control.Comonad.Store
import           Control.Comonad.Store.Zipper
import           Data.Binary.Instances.Time ()
import           Data.Hashable.Time
import           Data.IxSet.Typed           as Ix
import           Data.Vinyl                 hiding (RElem)
import           Data.Vinyl.TypeLevel
import           Development.Shake.Plus     hiding ((:->))
import           Lucid
import           RIO
import qualified RIO.Text                   as T
import           RIO.Time
import           Shakebook.Aeson
import qualified Shakebook.Feed             as Atom
import           Shakebook.Lucid
import           Shakebook.Sitemap
import Text.Compdoc
import Control.Comonad.Zipper.Extra

withLensesAndProxies [d|
  type FId            = "id"           :-> Text
  type FUrl           = "url"          :-> Text
  |]

type Link = '[FId, FUrl]

linkJsonFormat :: JsonFormat e (Record Link)
linkJsonFormat = recordJsonFormat $ field textJsonFormat :& field textJsonFormat :& RNil

withLensesAndProxies [d|
  type FCdnImports    = "cdn-imports"  :-> HtmlFragment
  type FDescription   = "description"  :-> Text
  type FHighlighting  = "highlighting" :-> StyleFragment
  type FImage         = "image"        :-> Maybe Text
  type FItems x       = "items"        :-> [Record x]
  type FModified      = "modified"     :-> UTCTime
  type FNext          = "next"         :-> HtmlFragment
  type FPageLinks     = "page-links"   :-> HtmlFragment
  type FPageNo        = "pageno"       :-> Int
  type FPrettyDate    = "pretty-date"  :-> UTCTime
  type FPrevious      = "previous"     :-> HtmlFragment
  type FPosted        = "posted"       :-> UTCTime
  type FRecentPosts x = "recent-posts" :-> [Record x]
  type FSiteTitle     = "site-title"   :-> Text
  type FSrcPath       = "src-path"     :-> Path Rel File
  type FSocialLinks   = "social"       :-> [Record Link]
  type FSubsections x = "subsections"  :-> [Record x]
  type FTags          = "tags"         :-> [Text]
  type FTagLinks      = "tag-links"    :-> [Record Link]
  type FTeaser        = "teaser"       :-> Text
  type FTitle         = "title"        :-> Text
  type FTocHtml       = "toc"          :-> HtmlFragment
  type FTocCF         = "toc"          :-> Cofree [] (Record Link)
  |]

sbDisplayDateTimeJsonFormat :: JsonFormat e UTCTime
sbDisplayDateTimeJsonFormat = dateTimeJsonFormat defaultTimeLocale (regularDateTimeFormat "%A, %B %d, %Y" "yyyy-mm-dd" :| [])

sbShortDateJsonFormat :: JsonFormat e UTCTime
sbShortDateJsonFormat = dateTimeJsonFormat defaultTimeLocale (regularDateTimeFormat "%F" "yyyy-mm-dd" :| [])

type RawDocMeta = FModified : FTitle : FDescription : '[]

type RawPostMeta = FPosted : FImage : FTitle : FTags : '[]

type RawSingleMeta = FImage : FTitle : '[]

type RawDoc = Compdoc RawDocMeta

type RawPost = Compdoc RawPostMeta

type RawSingle = Compdoc RawSingleMeta

type Stage1PostExtras = FPrettyDate : FTagLinks : FTeaser : '[]

type Stage1Post = Stage1PostExtras ++ RawPost

type IndexPage x = FPageLinks : FTocCF : FTitle : FItems x : FPageNo : '[]

type PostIndexPage = Routed (IndexPage (Routed Stage1Post))

type FinalDoc = FTocCF : FSubsections (Routed RawDoc) : Routed RawDoc

type FinalPost = FTocCF : Routed Stage1Post

type MainPage = FRecentPosts (Routed Stage1Post) : RawSingle

type Routed x = FUrl : x

type Enrichment = FSocialLinks : FCdnImports : FHighlighting : FSiteTitle : '[]

-- | Tag indices for a `Post` for use with `IxSet`.
newtype Tag = Tag { unTag :: Text }
  deriving stock   (Show, Eq, Ord, Data, Typeable, Generic)
  deriving newtype (Hashable, Binary, NFData)

-- | Posted index for a `Post` for use with `IxSet`.
newtype Posted = Posted { unPosted :: UTCTime }
  deriving stock   (Show, Eq, Ord, Data, Typeable, Generic)
  deriving newtype (Hashable, Binary, NFData)

-- | YearMonth (yyyy, mm) index for a `Post` for use with `IxSet`.
newtype YearMonth = YearMonth { unYearMonth :: (Integer, Int) }
  deriving stock   (Show, Eq, Ord, Data, Typeable, Generic)
  deriving newtype (Hashable, Binary, NFData)

toYearMonth :: UTCTime -> YearMonth
toYearMonth = (\(a, b, _) -> YearMonth (a, b)) . toGregorian . utctDay

fromYearMonth :: YearMonth -> UTCTime
fromYearMonth (YearMonth (y,m)) = UTCTime (fromGregorian y m 1) 0

deriveTagLink :: (Tag -> Text) -> Tag -> Record Link
deriveTagLink f x = C.toSnd (f . Tag) (unTag x)

instance (Ord (Record xs), RElem FTags xs, RElem FPosted xs) => Ix.Indexable '[Tag, Posted, YearMonth] (Record xs) where
  indices = Ix.ixList (Ix.ixFun (fmap Tag . view fTags))
                      (Ix.ixFun (pure . Posted . view fPosted))
                      (Ix.ixFun (pure . toYearMonth . view fPosted))

mostRecentPosted :: Ix.IsIndexOf Posted ixs => Int -> Ix.IxSet ixs xs -> [xs]
mostRecentPosted x = take x . Ix.toDescList (Proxy @Posted)

asSitemapUrl :: (RElem FUrl xs, RElem FPosted xs) => Text -> Record xs -> SitemapUrl
asSitemapUrl baseUrl x = SitemapUrl {
   sitemapLocation = baseUrl <> view fUrl x
 , sitemapLastModified = Just (view fPosted x)
 , sitemapChangeFrequency = Nothing
 , sitemapPriority = Nothing
}

-- | Convert a Post to an Atom Entry
asAtomEntry :: (RElem FContent xs, RElem FPosted xs, RElem FUrl xs, RElem FTitle xs) => Record xs -> Atom.Entry
asAtomEntry x = (Atom.nullEntry (view fUrl x)
                  (Atom.TextString $ view fTitle x)
                  (T.pack $ formatTime defaultTimeLocale (iso8601DateFormat Nothing) $ view fPosted x)) {
                    Atom.entryContent = Just $ Atom.TextContent (view fContent x)
                  }

renderTitleLink :: (Monad m, RElem FTitle xs, RElem FUrl xs) => Record xs -> HtmlT m ()
renderTitleLink = liftA2 renderLink (view fTitle) (view fUrl)

renderDocNav :: (Monad m, RElem FTitle xs, RElem FUrl xs) => Cofree [] (Record xs) -> HtmlT m ()
renderDocNav xs = ul_ $ li_ $ renderCofree renderTitleLink xs

renderPageLinks :: (RElem FPageNo xs, RElem FUrl xs, MonadThrow m) => Int -> Zipper [] (Record xs) -> HtmlT m ()
renderPageLinks = renderZipperWithin (liftA2 renderLink (T.pack . show . view fPageNo) (view fUrl))

type BasicFields = FContent
                 : FDescription
                 : FImage
                 : FModified
                 : FPosted
                 : FSrcPath
                 : FTags
                 : FTitle
                 : FUrl
                 : '[]

basicFields :: Rec (JsonField e) BasicFields
basicFields = (field textJsonFormat                  :: JsonField e FContent)
           :& (field textJsonFormat                  :: JsonField e FDescription)
           :& (optionalField textJsonFormat          :: JsonField e FImage)
           :& (field iso8601DateTimeJsonFormat       :: JsonField e FModified)
           :& (field sbShortDateJsonFormat           :: JsonField e FPosted)
           :& (field relFileJsonFormat               :: JsonField e FSrcPath)
           :& (field (listJsonFormat textJsonFormat) :: JsonField e FTags)
           :& (field textJsonFormat                  :: JsonField e FTitle)
           :& (field textJsonFormat                  :: JsonField e FUrl)
           :& RNil

type ExtraFields = FCdnImports
                 : FHighlighting
                 : FPageLinks
                 : FPageNo
                 : FPrettyDate
                 : FSiteTitle
                 : FSocialLinks
                 : FTagLinks
                 : FTeaser
                 : FTocHtml
                 : FTocCF
                 : '[]

extraFields :: Rec (JsonField e) ExtraFields
extraFields =  (field htmlJsonFormat                    :: JsonField e FCdnImports)
            :& (field styleJsonFormat                   :: JsonField e FHighlighting)
            :& (field htmlJsonFormat                    :: JsonField e FPageLinks)
            :& (field integralJsonFormat                :: JsonField e FPageNo)
            :& (field sbDisplayDateTimeJsonFormat       :: JsonField e FPrettyDate)
            :& (field textJsonFormat                    :: JsonField e FSiteTitle)
            :& (field (listJsonFormat linkJsonFormat)   :: JsonField e FSocialLinks)
            :& (field (listJsonFormat linkJsonFormat)       :: JsonField e FTagLinks)
            :& (field textJsonFormat                        :: JsonField e FTeaser)
            :& (field htmlJsonFormat                        :: JsonField e FTocHtml)
            :& (field (cofreeListJsonFormat linkJsonFormat) :: JsonField e FTocCF)
            :& RNil

listCastElemsFormat :: (RMap a, RecordToJsonObject a, RecordFromJson a, a <: b) => JsonFormatRecord e b -> JsonFormat e [Record a]
listCastElemsFormat = listJsonFormat . recordJsonFormat . rcast

type CompositeFields = FItems (Routed Stage1Post)
                     : FSubsections (Routed RawDoc)
                     : FRecentPosts (Routed Stage1Post)
                     : '[]

compositeFields :: Rec (JsonField e) CompositeFields
compositeFields = let x = extraFields <+> basicFields
                  in (field (listCastElemsFormat x) :: JsonField e (FItems (Routed Stage1Post)))
                  :& (field (listCastElemsFormat x) :: JsonField e (FSubsections (Routed RawDoc)))
                  :& (field (listCastElemsFormat x) :: JsonField e (FRecentPosts (Routed Stage1Post)))
                  :& RNil

type StandardFields = BasicFields ++ ExtraFields ++ CompositeFields

allFields :: Rec (JsonField e) StandardFields
allFields = basicFields <+> extraFields <+> compositeFields

rawPostMetaJsonFormat :: JsonFormat e (Record RawPostMeta)
rawPostMetaJsonFormat = recordJsonFormat $ rcast basicFields

rawDocMetaJsonFormat :: JsonFormat e (Record RawDocMeta)
rawDocMetaJsonFormat = recordJsonFormat $ rcast basicFields

rawSingleMetaJsonFormat :: JsonFormat e (Record RawSingleMeta)
rawSingleMetaJsonFormat = recordJsonFormat $ rcast basicFields

enrichmentFields :: Rec (JsonField e) Enrichment
enrichmentFields = rcast allFields

mainPageJsonFields :: Rec (JsonField e) MainPage
mainPageJsonFields = rcast allFields

finalPostJsonFields :: Rec (JsonField e) FinalPost
finalPostJsonFields = rcast allFields

finalDocJsonFields :: Rec (JsonField e) FinalDoc
finalDocJsonFields = rcast allFields

postIndexPageJsonFields  :: Rec (JsonField e) PostIndexPage
postIndexPageJsonFields = rcast allFields

enrichedRecordJsonFormat :: (RMap a, RecordToJsonObject a, RecordFromJson a) => Rec (JsonField e) a -> JsonFormat e (Record (Enrichment ++ a))
enrichedRecordJsonFormat = recordJsonFormat . (enrichmentFields <+>)

type MonadSB r m = (MonadReader r m, HasLogFunc r, MonadUnliftAction m, MonadThrow m)

indexPagesBy :: (MonadThrow m, Ix.IsIndexOf a ixs) => Proxy a -> Int -> Ix.IxSet ixs (Record xs) -> m (Zipper [] (Record (FItems xs : FPageNo : '[])))
indexPagesBy k i ixset = do
  p <- paginate' i $ Ix.toDescList k ixset
  return $ p =>> \a -> extract a :*: pos a + 1 :*: RNil

createDocNav :: (RElem FTitle xs, RElem FUrl xs) => Cofree [] (Record xs) -> Cofree [] (Record Link)
createDocNav = fmap (C.fanout (view fTitle) (view fUrl))