airtable-api (empty) → 0.1.0.0
raw patch · 7 files changed
+268/−0 lines, 7 filesdep +aesondep +airtable-apidep +basesetup-changed
Dependencies added: aeson, airtable-api, base, bytestring, hashable, lens, text, unordered-containers, wreq
Files
- LICENSE +30/−0
- README.md +3/−0
- Setup.hs +2/−0
- airtable-api.cabal +41/−0
- src/Airtable/Query.hs +59/−0
- src/Airtable/Table.hs +131/−0
- test/Spec.hs +2/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,3 @@+## The Airtable API for Haskell++Provides a high-level interface to requesting and introspecting Tables within an Airtable project.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ airtable-api.cabal view
@@ -0,0 +1,41 @@+name: airtable-api+version: 0.1.0.0+synopsis: Requesting and introspecting Tables within an Airtable project. +-- description:+homepage: https://github.com/ooblahman/airtable-api+license: BSD3+license-file: LICENSE+author: Anand Srinivasan+maintainer: Anand Srinivasan+copyright: AlphaSheets, Inc+category: Web+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Airtable.Table+ , Airtable.Query+ build-depends: base >= 4.7 && < 5+ , text+ , bytestring+ , aeson+ , wreq+ , hashable+ , lens+ , unordered-containers+ default-language: Haskell2010++test-suite airtable-api-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base+ , airtable-api+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/githubuser/airtable-api
+ src/Airtable/Query.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}++module Airtable.Query+ ( module Airtable.Table+ , AirtableOptions+ , getTable+ ) where+++import Airtable.Table++import Network.Wreq++import Control.Lens ((^.), (.~), (&))++import Data.Aeson (FromJSON, ToJSON, eitherDecode)+import Data.Monoid+import qualified Data.ByteString.Char8 as BC++-- * Configuration for Airtable requests.++data AirtableOptions = AirtableOptions {+ apiKey :: String+ , appId :: String + }++-- * Constants++-- | Base airtable API string. +base_url :: String+base_url = "https://api.airtable.com/v0/"++-- * Main API++-- | Retrieve a table from airtable.com given its name. +getTable :: (FromJSON a) => AirtableOptions -> TableName -> IO (Table a)+getTable opts tname = getTableFromUrl net_otps url + where+ net_otps = defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)] + url = base_url <> appId opts <> "/" <> tname++getTableFromUrl :: (FromJSON a) => Options -> String -> IO (Table a)+getTableFromUrl opts url = do+ resp <- getWith opts url+ getMore (fromResp resp)+ where+ getMore tbl = case tableOffset tbl of + Just offset -> do+ resp <- getWith (opts & param "offset" .~ [offset]) url+ putStrLn $ "Part " <> show offset+ getMore $ fromResp resp <> tbl+ Nothing -> + pure tbl++ fromResp r = decoder $ r ^. responseBody+ where+ decoder b = case eitherDecode b of + Left e -> error $ e <> "\nSource string: " <> show b+ Right r -> r
+ src/Airtable/Table.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Airtable.Table+ ( RecordID(..)+ , rec2str+ , IsRecord(..)+ , Table(..)+ , TableName+ , 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
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"