Link Search Menu Expand Document

Writing Custom Hooks with Haskell

Custom hooks have a type of HookProgram backend and they can be exposed to be available through YAML Spec by implementing an additional wrapper for passing arguments from YAML. Custom hooks can be used to implement assertions on data stored in the database (managed or unmanaged tables), as well as, to perform extra operations before or after table spec is executed. In contrast to custom spec programs, hooks are allowed to perform arbitrary IO, such as storing files on a disk or uploading results to cloud storage.

Napkin does not include tables accessed by hooks in the dependency graph used to create an execution plan.

Let’s implement a custom hook that we will later use as a pre-hook to check source tables are present in the database.

First, let’s create a Haskell file haskell/CustomHooks.hs in the project folder.

haskell/CustomHooks.hs

module CustomHooks where

import Control.Lens ((&), (.~))
import Data.Aeson ((.:))
import Napkin.Backends.Base

Next, we will define our hook program. It will accept a list of tables that need to be checked. It will use mapM_ to iterate over the list provided, checkTableExists will perform a check for table existence, and finally, we’ll assert the result value. We will use the failLater helper to make sure that all checks will be executed. Otherwise, the first failure would short-circuit the remaining checks.

haskell/CustomHooks.hs

checkManyTablesExistHook :: [Ref Table] -> HookProgram backend
checkManyTablesExistHook = mapM_ $ \tableName -> failLater $ do
  checkResult <- checkTableExists tableName
  assertTrue ("check if table " <> refText tableName <> " exists") checkResult

Testing hooks

Hooks can be easily tested in a REPL environment. Please refer to REPL tutorial.

In this form, we could already use our custom hook from Haskell-based spec:

spec :: Spec b ()
spec = do
  defineTable $
    tableWithQuery "sometable" someQuery
      & specPreHooks .~ [CustomHooks.checkManyTablesExistHook ["foo", "bar", "baz"]]

However, we can also expose our custom hook to be used from YAML specs. To do this, we need to implement an argument parser. To do this, we implement a simple Aeson parser that will return HookProgram backend. Note that the parser has to be wrapped with HookProgramWithArgParser newtype constructor.

checkManyTablesExist :: HookProgramWithArgParser backend
checkManyTablesExist = HookProgramWithArgParser $ \obj -> do
  tables :: [Ref Table] <- obj .: "tables"
  pure $ checkManyTablesExistHook tables

With that function implemented, we may refer to the hook in YAML-based spec:

...
tables:
  some_table:
    ...
    post_hooks:
      - CustomHooks.checkManyTablesExist:
          tables:
            - foo
            - bar
            - baz

In some cases, hooks will not accept any arguments. parserlessHook can be used to reduce boilerplate necessary:

haskell/CustomHooks.hs

veryCustomHook :: HookProgramWithArgParser backend
veryCustomHook = parserlessHook $ do
  logInfo "Runnin veryCustomHook logic"

This hook can be referenced from YAML as well:

...
tables:
  some_table:
    ...
    post_hooks:
      - CustomHooks.veryCustomHook