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 "Running veryCustomHook logic"
This hook can be referenced from YAML as well:
...
tables:
some_table:
...
post_hooks:
- CustomHooks.veryCustomHook