Search code examples
haskellihp

How to reuse IHP classes in a IHP script?


With IHP (the haskell web framework) I created a web application. Now I want to create a IHP Script to load some external data into my database. However I'm getting a lot of import conflicts from the Prelude, but not the types I expected.

#!/usr/bin/env run-script
module Application.Script.DataLoader where

import Application.Script.Prelude      hiding (decode, pack, (.:))
import qualified Data.ByteString.Lazy  as     BL
import Data.Csv
import Data.Text                              (pack)
import qualified Data.Vector           as     V
import Control.Monad                          (mzero)

instance FromNamedRecord Product where
    parseNamedRecord r = Product def <$> r .: "title" <*> r .: "price" <*> r .: "category" <*> pure def

run :: Script
run = do
    csvData <- BL.readFile "~/tender/data/Boiler-en-kookkraan_Boiler.csv"
    case decodeByName csvData of
        Left err -> putStrLn $ pack err
        Right (_, v) -> V.forM_ v $ \ p ->
            putStrLn $ (get #title p) ++ ", " ++ show (get #price p) ++ " euro"

Where my Product schema looks like this:

CREATE TABLE products (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    title TEXT NOT NULL,
    price DOUBLE PRECISION NOT NULL,
    category TEXT NOT NULL
);

Is there a way to use the types I created as a Data object to e.g. read my csv to?

[Updated output]

Application/Script/DataLoader.hs:12:26: error:
    • Couldn't match type ‘MetaBag -> Product' a1’
                     with ‘Product' (QueryBuilder ProjectProduct)’
      Expected type: Parser Product
        Actual type: Parser (MetaBag -> Product' a1)
    • In the expression:
        Product def <$> r .: "title" <*> r .: "price" <*> r .: "category"
          <*> pure def
      In an equation for ‘parseNamedRecord’:
          parseNamedRecord r
            = Product def <$> r .: "title" <*> r .: "price" <*> r .: "category"
                <*> pure def
      In the instance declaration for ‘FromNamedRecord Product’
   |
12 |     parseNamedRecord r = Product def <$> r .: "title" <*> r .: "price" <*> r .: "category" <*> pure def
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[Solved, all credits to the help of @mpscholten]

#!/usr/bin/env run-script
module Application.Script.DataLoader where

import Application.Script.Prelude      hiding (decode, pack, (.:))
import qualified Data.ByteString.Lazy  as     BL
import Data.Csv
import Data.Text                              (pack)
import qualified Data.Vector           as     V
import Control.Monad                          (mzero)

parseProduct :: NamedRecord -> Parser Product
parseProduct r = do
  title <- r .: "title"
  price <- r .: "price"
  category <- r .: "category"

  newRecord @Product
    |> set #title title
    |> set #price price
    |> set #category category
    |> pure

run :: Script
run = do
    csvData <- BL.readFile "data/Boiler-en-kookkraan_Boiler.csv"
    case decodeByNameWithP parseProduct defaultDecodeOptions csvData of
        Left err -> putStrLn $ pack err
        Right (_, v) -> V.forM_ v $ \ p ->
            putStrLn $ (get #title p) ++ ", " ++ show (get #price p) ++ " euro"

Solution

  • Inside the FromNamedRecord instance you are missing two fields: id and meta. The id field is the first field of the record. The meta field is a hidden field used by IHP to keep track of validation errors. It's always the last field of a record.

    The easiest way to solve this is to use newRecord and write out the code in a more explicit way:

    instance FromNamedRecord Product where
        parseNamedRecord r = do
            title <- r .: "title"
            price <- r .: "price"
            category <- r .: "category"
    
            newRecord @Product
                |> set #title title
                |> set #price price
                |> set #category category
                |> pure
    

    For the error "Ambiguous occurrence ‘title’" try to use the get function instead of using the normal haskell accessor function:

    putStrLn $ (get #title p) ++ ", " ++ show (get #price p) ++ " euro"