packages feed

hakyllbars-1.0.0.0: src/Hakyllbars/Context.hs

module Hakyllbars.Context where

import Control.Monad.Except
import Control.Monad.State.Strict
import Data.Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Bifunctor
import Data.Either
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe
import Data.Proxy
import Data.Scientific (isInteger, toBoundedInteger, toBoundedRealFloat)
import qualified Data.Text as T
import qualified Data.Vector as Vector
import Hakyllbars.Ast
import Hakyllbars.Common
import Text.Parsec (SourcePos)
import Prelude hiding (lookup)

newtype Context a = Context {unContext :: ContextFunction a}

type ContextFunction a = String -> TemplateRunner a (ContextValue a)

getContext :: Identifier -> Compiler (Context a)
getContext id' = intoContext <$> getMetadata id'

itemFilePath :: Item a -> FilePath
itemFilePath = toFilePath . itemIdentifier

data TemplateState a = TemplateState
  { tplContextStack :: [(Context a, Context a)],
    tplItemStack :: [Item a],
    tplCallStack :: [String]
  }

type TemplateRunner a b = StateT (TemplateState a) Compiler b

tplItem :: TemplateRunner a (Item a)
tplItem =
  gets tplItemStack >>= \case
    [] -> tplFail "tplItem: no Item on stack"
    (item : _) -> return item

tplModifyItem :: (Item a -> TemplateRunner a (Item a)) -> TemplateRunner a ()
tplModifyItem f =
  tplItem
    >>= f
    >>= tplReplaceItem

tplReplaceItem :: Item a -> TemplateRunner a ()
tplReplaceItem item = do
  void tplPopItem
  tplPushItem item

tplPopItem :: TemplateRunner a (Item a)
tplPopItem =
  gets tplItemStack >>= \case
    [] -> error "tplPopItem: no Item on stack"
    current : previous -> do
      modify \s -> s {tplItemStack = previous}
      return current

tplPopBody :: TemplateRunner a a
tplPopBody = itemBody <$> tplPopItem

tplPushItem :: Item a -> TemplateRunner a ()
tplPushItem item = do
  stack <- gets tplItemStack
  modify \s -> s {tplItemStack = item : stack}

tplWithItem :: Item a -> TemplateRunner a b -> TemplateRunner a b
tplWithItem item f = do
  tplPushItem item
  x <- f
  void tplPopItem
  return x

tplContext :: TemplateRunner a (Context a)
tplContext =
  gets tplContextStack >>= \case
    [] -> tplFail "tplContext: no Context on stack"
    ((_, catted) : _) -> return catted

tplPushContext :: Context a -> TemplateRunner a ()
tplPushContext context = do
  stack <-
    gets tplContextStack <&> \case
      [] -> [(context, context)]
      stack@((_, cattedParent) : _) -> (context, context <> cattedParent) : stack
  modify \s -> s {tplContextStack = stack}

tplPopContext :: TemplateRunner a (Context a)
tplPopContext =
  gets tplContextStack >>= \case
    [] -> tplFail "tplPopContext: no Context on stack"
    ((current, _) : previous) -> do
      modify \s ->
        s
          { tplContextStack = previous
          }
      return current

-- | Place context within a given scope.
tplWithContext :: Context a -> TemplateRunner a b -> TemplateRunner a b
tplWithContext context f = do
  tplPushContext context
  x <- f
  void tplPopContext
  return x

-- | Get a value from the context by name and convert it.
tplGet :: (FromValue v a) => String -> TemplateRunner a v
tplGet name =
  tplContext
    >>= flip unContext name
    >>= fromValue

-- | Get a value from a specific item's context by name and convert it.
tplGetWithItemContext :: (FromValue v a) => Item a -> Context a -> String -> TemplateRunner a v
tplGetWithItemContext item context name =
  tplWithItem item $ tplWithContext context $ tplGet name

-- | Place context in global scope.
tplPut :: Context a -> TemplateRunner a ()
tplPut context = do
  stack <- fmap (second (context <>)) <$> gets tplContextStack
  modify' \s -> s {tplContextStack = stack}

-- | Perform an action within the scope of a call.
tplWithCall :: String -> TemplateRunner a b -> TemplateRunner a b
tplWithCall call f = do
  stack <- gets tplCallStack
  modify' \s -> s {tplCallStack = call : stack}
  x <- f
  modify' \s -> s {tplCallStack = stack}
  return x

tplWithPos :: (x -> SourcePos) -> (x -> TemplateRunner a b) -> x -> TemplateRunner a b
tplWithPos getPos f x = tplWithCall (show $ getPos x) $ f x

-- | Perform an action within the scope of a field call.
tplWithField :: String -> TemplateRunner a b -> TemplateRunner a b
tplWithField field' f = do
  file <- itemFilePath <$> tplItem
  tplWithCall ("field " ++ show field' ++ " in " ++ file) f

-- | Fail with an error message and trace.
tplFail :: String -> TemplateRunner a b
tplFail = fail <=< tplTraced

-- | Fail with a no-result message and trace.
tplTried :: String -> TemplateRunner a b
tplTried = lift . noResult <=< tplTraced

-- | Return the current call stack, with the most recent call first.
tplTrace :: TemplateRunner a [String]
tplTrace = gets tplCallStack

-- | Get a formatted trace message with the most recent call first.
tplTraced :: String -> TemplateRunner a String
tplTraced message = do
  trace <- tplTrace
  return $ message ++ ", trace from most recent:\n" ++ intercalate "\n\t" trace ++ "\n"

-- | Apply @f@ to an item if @key@ is requested.
field :: (IntoValue v a) => String -> (Item a -> TemplateRunner a v) -> Context a
field key f = Context f'
  where
    f' k =
      tplWithField key $
        if k == key
          then do
            i <- tplItem
            intoValue <$> f i
          else do
            tplTried $ "key " ++ show k ++ " did not match field " ++ show key

-- | Reports missing field.
missingField :: Context a
missingField = Context f
  where
    f key = tplTried $ "missing " ++ show key

-- | Const-valued field, returns the same @val@ per @key@.
constField :: (IntoValue v a) => String -> v -> Context a
constField key val = field key f
  where
    constResult = return val
    f _ = tplWithCall key constResult

-- | Creates a field containing a list of items.
itemsField :: String -> Context a -> [Item a] -> Context a
itemsField key context items = constField key (context, items)

-- | Mapping of function @g@ after context @f@.
mapField :: (FromValue v a, IntoValue w a) => (v -> w) -> Context a -> Context a
mapField g (Context f) = Context h
  where
    h k = tplWithCall ("mapField of " ++ show k) do
      fmap (intoValue . g) $ fromValue =<< f k

-- | Binding of function @g@ after context @f@.
bindField :: (FromValue v a, IntoValue w a) => (v -> TemplateRunner a w) -> Context a -> Context a
bindField g (Context f) = Context h
  where
    h k = do
      tplWithCall ("bindField of " ++ show k) do
        fmap intoValue $ g =<< fromValue =<< f k

-- | Alternation of context @g@ after context @f@.
composeField :: Context a -> Context a -> Context a
composeField (Context g) (Context f) = Context h
  where
    h name = f name `catchError` (\_ -> g name)

-- | Lookup of @val@ by @key@ into provided @HashMap@.
hashMapField :: (IntoValue v a) => HashMap String v -> Context a
hashMapField m = Context f
  where
    m' = intoValue <$> m
    f k = tplWithCall "hashMap" $ maybe (tried k) return (HashMap.lookup k m')
    tried k = tplTried $ "tried " ++ show k ++ " from hashmap of keys " ++ show (HashMap.keys m')

forItemField :: (IntoValue v a) => String -> [Identifier] -> (Item a -> TemplateRunner a v) -> Context a
forItemField key ids f = field key f'
  where
    f' item
      | itemIdentifier item `elem` ids = f item
      | otherwise = tplTried $ show key ++ " for items " ++ show (toFilePath <$> ids)

functionField :: (FromValue v a, IntoValue w a) => String -> (v -> TemplateRunner a w) -> Context a
functionField = constField

functionField2 :: (FromValue v a, FromValue x a, IntoValue w a) => String -> (v -> x -> TemplateRunner a w) -> Context a
functionField2 = constField

functionField3 :: (FromValue v a, FromValue x a, FromValue y a, IntoValue w a) => String -> (v -> x -> y -> TemplateRunner a w) -> Context a
functionField3 = constField

functionField4 :: (FromValue v a, FromValue x a, FromValue y a, FromValue z a, IntoValue w a) => String -> (v -> x -> y -> z -> TemplateRunner a w) -> Context a
functionField4 = constField

instance Semigroup (Context a) where
  (<>) = flip composeField

instance Monoid (Context a) where
  mempty = missingField

class IntoContext v a where
  intoContext :: v -> Context a

instance IntoContext (Context a) a where
  intoContext = id

instance (IntoValue v a) => IntoContext (HashMap String v) a where
  intoContext = hashMapField

instance (IntoValue v a) => IntoContext [(String, v)] a where
  intoContext = intoContext . HashMap.fromList

instance IntoContext Object a where
  intoContext = ic . fmap (bimap Key.toString intoValue) . KeyMap.toList
    where
      ic :: [(String, ContextValue a)] -> Context a
      ic = intoContext

-- | ContextValues can hold certain types of data within a context.
data ContextValue a
  = EmptyValue
  | UndefinedValue String (Item a) [String] [String]
  | ContextValue (Context a)
  | ListValue [ContextValue a]
  | BoolValue Bool
  | StringValue String
  | DoubleValue Double
  | IntValue Int
  | FunctionValue (ContextValue a -> TemplateRunner a (ContextValue a))
  | BlockValue Block
  | ItemValue (Item a)
  | ThunkValue (TemplateRunner a (ContextValue a))
  | PairValue (ContextValue a, ContextValue a)

type FunctionValue v w a = v -> TemplateRunner a w

type FunctionValue2 v x w a = v -> FunctionValue x w a

type FunctionValue3 v x y w a = v -> FunctionValue2 x y w a

type FunctionValue4 v x y z w a = v -> FunctionValue3 x y z w a

instance Show (ContextValue a) where
  show = \case
    EmptyValue -> "EmptyValue"
    UndefinedValue name _ _ _ -> "UndefinedValue " ++ show name
    ContextValue {} -> "ContextValue"
    ListValue values -> "ListValue " ++ show values
    BoolValue value -> "BoolValue " ++ show value
    StringValue value -> "StringValue " ++ show value
    DoubleValue value -> "DoubleValue " ++ show value
    IntValue value -> "IntValue " ++ show value
    FunctionValue {} -> "FunctionValue"
    BlockValue {} -> "BlockValue"
    ItemValue item -> "ItemValue " ++ show (itemFilePath item)
    ThunkValue {} -> "ThunkValue"
    PairValue (x, y) -> "PairValue (" ++ show x ++ ", " ++ show y ++ ")"

itemValue :: Context a -> Item a -> ContextValue a
itemValue context item = intoValue (context, [item])

class IntoValue' (flag :: Bool) v a where
  intoValue' :: Proxy flag -> v -> ContextValue a

-- "Specialize" List
type family FString a :: Bool where
  FString Char = 'True
  FString _ = 'False

-- | Inject a concrete type @v@ into a @ContextValue a@.
class IntoValue v a where
  intoValue :: v -> ContextValue a

instance (FString v ~ flag, IntoValue' flag [v] a) => IntoValue [v] a where
  intoValue = intoValue' (Proxy :: Proxy flag)

instance (IntoValue v a) => IntoValue' 'False [v] a where
  intoValue' _ = ListValue . (intoValue <$>)

instance IntoValue' 'True String a where
  intoValue' _ = StringValue

instance IntoValue Block a where
  intoValue = BlockValue

instance IntoValue (ContextValue a) a where
  intoValue = id

instance IntoValue (Context a) a where
  intoValue = ContextValue

instance IntoValue Value a where
  intoValue = \case
    Object o -> ContextValue $ intoContext o
    Array a -> ListValue $ Vector.toList $ Vector.map intoValue a
    String t -> StringValue $ T.unpack t
    Number n
      | isInteger n -> IntValue $ fromJust $ toBoundedInteger n
      | otherwise -> DoubleValue $ fromRight 0.0 $ toBoundedRealFloat n
    Bool b -> BoolValue b
    Null -> EmptyValue

instance IntoValue () a where
  intoValue () = EmptyValue

instance IntoValue Bool a where
  intoValue = BoolValue

instance IntoValue Double a where
  intoValue = DoubleValue

instance IntoValue Int a where
  intoValue = IntValue

instance (IntoValue v a) => IntoValue (Maybe v) a where
  intoValue (Just v) = intoValue v
  intoValue Nothing = EmptyValue

instance (FromValue v a, IntoValue w a) => IntoValue (FunctionValue v w a) a where
  intoValue f = FunctionValue f'
    where
      f' cv = do
        v <- fromValue cv
        intoValue <$> f v

instance (FromValue v a, FromValue x a, IntoValue w a) => IntoValue (FunctionValue2 v x w a) a where
  intoValue f = FunctionValue f'
    where
      f' cv =
        intoValue . f <$> fromValue cv

instance (FromValue v a, FromValue x a, FromValue y a, IntoValue w a) => IntoValue (FunctionValue3 v x y w a) a where
  intoValue f = FunctionValue f'
    where
      f' cv =
        intoValue . f <$> fromValue cv

instance (FromValue v a, FromValue x a, FromValue y a, FromValue z a, IntoValue w a) => IntoValue (FunctionValue4 v x y z w a) a where
  intoValue f = FunctionValue f'
    where
      f' cv =
        intoValue . f <$> fromValue cv

instance IntoValue (TemplateRunner a (ContextValue a)) a where
  intoValue = ThunkValue

instance (IntoValue v a, IntoValue x a) => IntoValue (v, x) a where
  intoValue (v, x) = PairValue (intoValue v, intoValue x)

instance IntoValue (Item a) a where
  intoValue = ItemValue

-- | Extract a concrete value of type @v@ from a @ContextValue a@.
class FromValue v a where
  fromValue :: ContextValue a -> TemplateRunner a v

class FromValue' (flag :: Bool) v a where
  fromValue' :: Proxy flag -> ContextValue a -> TemplateRunner a v

instance (FString v ~ flag, FromValue' flag [v] a) => FromValue [v] a where
  fromValue = fromValue' (Proxy :: Proxy flag)

instance (FromValue v a) => FromValue' 'False [v] a where
  fromValue' flag = \case
    ListValue xs -> sequence $ fromValue <$> xs
    ThunkValue fx -> fromValue' flag =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as List"

instance FromValue' 'True String a where
  fromValue' flag = \case
    StringValue x -> return x
    EmptyValue -> return ""
    ThunkValue fx -> fromValue' flag =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as String"

instance FromValue (Context a) a where
  fromValue = \case
    ContextValue c -> return c
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Context"

instance FromValue (ContextValue a) a where
  fromValue = return

instance FromValue Bool a where
  fromValue = \case
    BoolValue x -> return x
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Bool"

instance FromValue Double a where
  fromValue = \case
    DoubleValue x -> return x
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Double"

instance FromValue Int a where
  fromValue = \case
    IntValue x -> return x
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Int"

instance FromValue (Item a) a where
  fromValue = \case
    ItemValue item -> return item
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Item"

instance FromValue Block a where
  fromValue = \case
    BlockValue block -> return block
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Block"

instance (IntoValue v a, FromValue w a) => FromValue (FunctionValue v w a) a where
  fromValue cv = case cv of
    FunctionValue f -> return f'
      where
        f' v = fromValue =<< f (intoValue v)
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Function"

instance (IntoValue v a, IntoValue x a, FromValue w a) => FromValue (FunctionValue2 v x w a) a where
  fromValue cv = case cv of
    FunctionValue f -> return f'
      where
        f' v x = do
          g <- fromValue =<< f (intoValue v)
          g x
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Function2"

instance (IntoValue v a, IntoValue x a, IntoValue y a, FromValue w a) => FromValue (FunctionValue3 v x y w a) a where
  fromValue = \case
    FunctionValue f -> return f'
      where
        f' v x y = do
          g <- fromValue =<< f (intoValue v)
          h <- g x
          h y
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Function3"

instance (IntoValue v a, IntoValue x a, IntoValue y a, IntoValue z a, FromValue w a) => FromValue (FunctionValue4 v x y z w a) a where
  fromValue = \case
    FunctionValue f -> return f'
      where
        f' v x y z = do
          g <- fromValue =<< f (intoValue v)
          h <- g x
          i <- h y
          i z
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Function4"

instance (FromValue v a, FromValue x a) => FromValue (v, x) a where
  fromValue = \case
    PairValue (a, b) -> (,) <$> fromValue a <*> fromValue b
    ThunkValue fx -> fromValue =<< fx
    x -> tplFail $ "Tried to get " ++ show x ++ " as Pair"