{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Airtable.Table ( -- * RecordID RecordID(..) , rec2str -- * IsRecord class , IsRecord(..) -- * Table , Table(..) , TableName -- * Table methods , toList , exists , select , selectMaybe , selectAll , selectAllKeys , selectWhere , selectKeyWhere , deleteWhere ) where import GHC.Generics import GHC.Stack import Control.Applicative ((<|>)) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as Map import Data.Aeson import Data.Aeson.Types import Data.Text (Text) import qualified Data.Text as T import Data.Monoid import Data.Hashable import Data.Foldable (foldlM) -- * RecordID -- | Airtable's record ID for use in indexing records newtype RecordID = RecordID Text deriving (FromJSON, Show, Eq, Generic, Ord) instance Hashable RecordID rec2str :: RecordID -> String rec2str (RecordID rec) = T.unpack rec -- * IsRecord class -- | A convenience typeclass for selecting records using RecordID-like keys. class IsRecord a where toRec :: a -> RecordID instance IsRecord RecordID where toRec = id instance IsRecord String where toRec = RecordID . T.pack -- * Table -- | Airtable's table type data Table a = Table { tableRecords :: Map.HashMap RecordID a , tableOffset :: Maybe Text } deriving ( Show ) -- | Synonym used in querying tables from the API. type TableName = String instance (FromJSON a) => FromJSON (Table a) where parseJSON (Object v) = do recs <- v .: "records" :: Parser [Value] parsedRecs <- foldlM parseRec Map.empty recs offset <- v .:? "offset" return $ Table parsedRecs offset where parseRec tbl (Object v) = do recId <- v .: "id" obj <- v .: "fields" return $ Map.insert recId obj tbl <|> error ("could not decode: " <> show v) instance Monoid (Table a) where mempty = Table mempty Nothing mappend (Table t1 o) (Table t2 _) = Table (mappend t1 t2) o -- * Table methods -- | Convert a 'Table' to a list of key-record pairs. toList :: Table a -> [(RecordID, a)] toList = Map.toList . tableRecords -- | Check if a record exists at the given key in a table. exists :: (IsRecord r) => Table a -> r -> Bool exists tbl rec = Map.member (toRec rec) (tableRecords tbl) -- | Unsafely lookup a record using its RecordID. select :: (HasCallStack, IsRecord r, Show a) => Table a -> r -> a select tbl rec = tableRecords tbl `lookup` toRec rec where lookup mp k = case Map.lookup k mp of Just v -> v Nothing -> error $ "lookup failed in map: " <> show k -- | Safely lookup a record using its RecordID. selectMaybe :: (IsRecord r, Show a) => Table a -> r -> Maybe a selectMaybe tbl rec = toRec rec `Map.lookup` tableRecords tbl -- | Read all records. selectAll :: Table a -> [a] selectAll = map snd . toList -- | Read all RecordID's. selectAllKeys :: Table a -> [RecordID] selectAllKeys = map fst . toList -- | Select all records satisfying a condition. selectWhere :: Table a -> (RecordID -> a -> Bool) -> [a] selectWhere tbl f = map snd $ filter (uncurry f) (toList tbl) -- | Select all RecordID's satisfying a condition. selectKeyWhere :: Table a -> (RecordID -> a -> Bool) -> [RecordID] selectKeyWhere tbl f = map fst $ filter (uncurry f) (toList tbl) -- | Delete all Records satisfying a condition. deleteWhere :: Table a -> (RecordID -> a -> Bool) -> Table a deleteWhere (Table recs off) f = Table (Map.filterWithKey (\k v -> not $ f k v) recs) off