Accept range headers

TODO: response headers and proper codes
This commit is contained in:
Joe Nelson
2014-08-02 15:27:45 -07:00
parent 6fa5cc2683
commit ce361643fa
3 changed files with 51 additions and 17 deletions
+23 -8
View File
@@ -1,5 +1,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module Main where module Main where
import Control.Applicative import Control.Applicative
@@ -28,6 +30,12 @@ import qualified Data.Text as T
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Data.Ranged.Ranges (emptyRange)
import Debug.Trace
-- }}}
data AppConfig = AppConfig { data AppConfig = AppConfig {
configDbUri :: String configDbUri :: String
, configPort :: Int } , configPort :: Int }
@@ -49,26 +57,33 @@ main = do
where where
describe = progDesc "create a REST API to an existing Postgres database" describe = progDesc "create a REST API to an existing Postgres database"
traceThis :: (Show a) => a -> a
traceThis x = trace (show x) x
app :: AppConfig -> Application app :: AppConfig -> Application
app config req respond = do app config req respond = do
r <- try $ r <- try $
case path of case path of
[] -> responseLBS status200 [json] <$> (printTables ver =<< conn) [] -> responseLBS status200 [json] <$> (printTables ver =<< conn)
[table] -> responseLBS status200 [json] <$> [table] -> if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else responseLBS status200 [json] <$>
( if verb == methodOptions ( if verb == methodOptions
then printColumns ver table =<< conn then printColumns ver table =<< conn
else selectWhere (T.pack $ show ver) table qq =<< conn ) else
selectWhere (T.pack $ show ver) table qq range =<< conn )
_ -> return $ responseLBS status404 [] "" _ -> return $ responseLBS status404 [] ""
respond $ either sqlErrorHandler id r respond $ either sqlErrorHandler id r
where where
path = pathInfo req path = pathInfo req
verb = requestMethod req verb = requestMethod req
json = (hContentType, "application/json") json = (hContentType, "application/json")
conn = connectPostgreSQL $ configDbUri config conn = connectPostgreSQL $ configDbUri config
qq = queryString req qq = queryString req
ver = fromMaybe 1 $ requestedVersion (requestHeaders req) ver = fromMaybe 1 $ requestedVersion (requestHeaders req)
range = requestedRange (requestHeaders req)
requestedVersion :: RequestHeaders -> Maybe Int requestedVersion :: RequestHeaders -> Maybe Int
requestedVersion hdrs = requestedVersion hdrs =
+10 -5
View File
@@ -7,6 +7,7 @@ import Data.Maybe (fromMaybe)
import Data.List (intercalate) import Data.List (intercalate)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import qualified RangeQuery as R
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
@@ -16,20 +17,24 @@ import Database.HDBC.PostgreSQL
import Network.HTTP.Types.URI import Network.HTTP.Types.URI
selectWhere :: T.Text -> T.Text -> Query -> Connection -> IO BL.ByteString selectWhere :: T.Text -> T.Text -> Query -> Maybe R.NonnegRange -> Connection -> IO BL.ByteString
selectWhere ver table qq conn = do selectWhere ver table qq range conn = do
s <- selectSql s <- selectSql
w <- whereClause conn qq w <- whereClause conn qq
r <- quickQuery conn (BS.unpack $ s <> w) [] r <- quickQuery conn (BS.unpack $ s <> w) []
return $ case r of return $ case r of
[[json]] -> fromSql json [[json]] -> fromSql json
_ -> "" :: BL.ByteString _ -> "" :: BL.ByteString
where where
limit = fromMaybe "ALL" $ show <$> (R.limit =<< range)
offset = fromMaybe 0 (R.offset <$> range)
selectSql = pgFormat conn selectSql = pgFormat conn
"select array_to_json(array_agg(row_to_json(t)))\ "select array_to_json(array_agg(row_to_json(t)))\
\ from (select * from %I.%I) t" \ from (select * from %I.%I LIMIT %s OFFSET %s) t"
[toSql ver, toSql table] [toSql ver, toSql table, toSql limit, toSql offset]
whereClause :: Connection -> Query -> IO BS.ByteString whereClause :: Connection -> Query -> IO BS.ByteString
whereClause _ [] = return "" whereClause _ [] = return ""
@@ -41,7 +46,7 @@ whereClause conn qs =
clause = BS.intercalate " and " <$> preds clause = BS.intercalate " and " <$> preds
preds :: IO [BS.ByteString] preds :: IO [BS.ByteString]
preds = sequence $ map (wherePred conn) qs preds = mapM (wherePred conn) qs
wherePred :: Connection -> QueryItem -> IO BS.ByteString wherePred :: Connection -> QueryItem -> IO BS.ByteString
+18 -4
View File
@@ -14,15 +14,17 @@ import Text.Read (readMaybe)
import Data.Maybe (fromMaybe, listToMaybe) import Data.Maybe (fromMaybe, listToMaybe)
rangeGeq :: Int -> Range Int type NonnegRange = Range Int
rangeGeq :: Int -> NonnegRange
rangeGeq n = rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> Range Int rangeLeq :: Int -> NonnegRange
rangeLeq n = rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n) Range BoundaryBelowAll (BoundaryAbove n)
parseRange :: String -> Maybe(Range Int) parseRange :: String -> Maybe NonnegRange
parseRange range = do parseRange range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: String let rangeRegex = "^([0-9]+)-([0-9]*)$" :: String
@@ -34,5 +36,17 @@ parseRange range = do
return $ rangeIntersection lower upper return $ rangeIntersection lower upper
requestedRange :: RequestHeaders -> Maybe(Range Int) requestedRange :: RequestHeaders -> Maybe NonnegRange
requestedRange hdrs = parseRange =<< BS.unpack <$> lookup hRange hdrs requestedRange hdrs = parseRange =<< BS.unpack <$> lookup hRange hdrs
limit :: NonnegRange -> Maybe Int
limit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
offset :: NonnegRange -> Int
offset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> 0 -- should never happen