gdritter repos new-inf-blog / master src / Inf / Types.hs
master

Tree @master (Download .tar.gz)

Types.hs @masterraw · history · blame

{-# LANGUAGE OverloadedStrings #-}

module Inf.Types where

import qualified Control.Exception as Exn
import qualified Data.Char as Char
import qualified Data.Text as T
import qualified Data.Time as Time
import qualified Database.SQLite.Simple as SQL
import qualified Database.SQLite.Simple.FromField as SQL
import qualified Database.SQLite.Simple.ToField as SQL

newtype PostId = PostId { fromPostId :: Int } deriving (Eq, Show)

instance SQL.FromField PostId where
  fromField f = PostId `fmap` SQL.fromField f

instance SQL.ToField PostId where
  toField = SQL.toField . fromPostId

newtype User = User { userName :: T.Text } deriving (Eq, Show)

instance SQL.FromField User where
  fromField f = User `fmap` SQL.fromField f

instance SQL.ToField User where
  toField = SQL.toField . userName

-- | A 'PostRef' is just the information needed to identify a post
-- internally, including when it was posted and its title and slug
data PostRef = PostRef
  { prYear  :: !Int
  , prMonth :: !Int
  , prSlug  :: !T.Text
  , prName  :: !T.Text
  , prDate  :: !Time.UTCTime
  } deriving Show

-- | A 'Post' is all the information needed to display a post in full,
-- including the raw Markdown of the post itself
data Post = Post
  { postId       :: !PostId
  , postDate     :: !Time.UTCTime
  , postTitle    :: !T.Text
  , postSlug     :: !T.Text
  , postContents :: !T.Text
  , postAuthor   :: !User
  , postNext     :: !(Maybe PostRef)
  , postPrev     :: !(Maybe PostRef)
  } deriving Show

-- | A 'RawPost' is the information needed to create a new post, and
-- thus may not have a post ID (as it might not have been submitted
-- yet!)
data RawPost = RawPost
  { rpId       :: !(Maybe PostId)
  , rpTitle    :: !T.Text
  , rpAuthor   :: !User
  , rpContents :: !T.Text
  } deriving Show

-- | A blank post for populating an editing field
emptyRawPost :: RawPost
emptyRawPost = RawPost
  { rpId       = Nothing
  , rpTitle    = ""
  , rpAuthor   = User { userName = "" }
  , rpContents = ""
  }

data Page = Page
  { pageName :: !T.Text
  , pageText :: !T.Text
  } deriving Show

instance SQL.FromRow Page where
  fromRow = uncurry Page <$> SQL.fromRow

data URL = URL
  { urlRelative :: !T.Text
  , urlAbsolute :: !T.Text
  } deriving Show


mkURL :: [T.Text] -> URL
mkURL path =
  let pathJoined = T.intercalate "/" path
      root       = "https://blog.infinitenegativeutility.com"
  in URL { urlRelative = T.cons '/' pathJoined
         , urlAbsolute = root `T.append` T.cons '/' pathJoined
         }


postDateT :: Post -> T.Text
postDateT Post { postDate = date } =
  T.pack (Time.formatTime
           Time.defaultTimeLocale
           Time.rfc822DateFormat
           date)


postURL :: Post -> T.Text
postURL Post { postDate = date, postSlug = slug } = do
  let (year, month, _) = Time.toGregorian (Time.utctDay date) in
    T.concat [ "/", T.pack (show year)
             , "/", T.pack (show month)
             , "/", slug
             ]


postRefDateT :: PostRef -> T.Text
postRefDateT PostRef { prDate = date } =
  T.pack (Time.formatTime
           Time.defaultTimeLocale
           "%Y-%m-%d"
           date)


postRefURL :: PostRef -> T.Text
postRefURL PostRef { prYear = year, prMonth = month, prSlug = slug } =
  T.concat [ "/", T.pack (show year)
           , "/", T.pack (show month)
           , "/", slug
           ]


slugify :: T.Text -> T.Text
slugify = T.map conv
  where
    conv c
      | Char.isAlphaNum c = Char.toLower c
      | otherwise = '-'


postToRawPost :: Post -> RawPost
postToRawPost p = RawPost
  { rpId       = Just (postId p)
  , rpTitle    = postTitle p
  , rpContents = postContents p
  , rpAuthor   = postAuthor p
  }


data DBException
  = MissingPost !PostId
  | MissingPage !T.Text
  | NoPostFound !T.Text !T.Text !T.Text
  | NonUniqueResult
  | BadUserError
  | NoSuchPost
  deriving (Eq, Show)

instance Exn.Exception DBException where

data Unimplemented = Unimplemented String
  deriving (Eq, Show)