packages feed

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 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"