Include location header in post response

This commit is contained in:
Joe Nelson
2014-08-25 11:31:27 -07:00
parent a48474b21a
commit 780c9d7c0f
5 changed files with 45 additions and 33 deletions
+6 -1
View File
@@ -14,9 +14,11 @@ library
, HDBC, HDBC-postgresql , HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2 , warp, wai >= 3.0.1 && < 3.0.2
, http-types, scientific, time , http-types, scientific, time
, HTTP, convertible
, bytestring, aeson, network , bytestring, aeson, network
, text, optparse-applicative , text, optparse-applicative
, unordered-containers , unordered-containers
, containers
, regex-base , regex-base
, http-media, regex-tdfa , http-media, regex-tdfa
, Ranged-sets , Ranged-sets
@@ -34,9 +36,11 @@ executable dbapi
build-depends: base >=4.6 && <5 build-depends: base >=4.6 && <5
, HDBC, HDBC-postgresql , HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2 , warp, wai >= 3.0.1 && < 3.0.2
, HTTP, convertible
, http-types, scientific, time , http-types, scientific, time
, bytestring, aeson, network , bytestring, aeson, network
, text, optparse-applicative , text, optparse-applicative
, containers
, unordered-containers , unordered-containers
, regex-base , regex-base
, http-media, regex-tdfa , http-media, regex-tdfa
@@ -57,7 +61,8 @@ Test-Suite spec
, hspec-wai , hspec-wai
, HDBC, HDBC-postgresql , HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2 , warp, wai >= 3.0.1 && < 3.0.2
, wai-extra , HTTP, convertible
, wai-extra, containers
, http-types, scientific, time , http-types, scientific, time
, bytestring, aeson, network , bytestring, aeson, network
, text, optparse-applicative , text, optparse-applicative
+21 -9
View File
@@ -12,7 +12,10 @@ import Options.Applicative hiding (columns)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import Data.Map (intersection, fromList, toList)
import Data.Convertible.Base (convert)
import Network.HTTP.Base (urlEncodeVars)
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
@@ -23,10 +26,12 @@ import qualified Data.ByteString.Char8 as BS
import Database.HDBC.PostgreSQL (Connection) import Database.HDBC.PostgreSQL (Connection)
import Database.HDBC.Types (SqlError, seErrorMsg) import Database.HDBC.Types (SqlError, seErrorMsg)
import PgStructure (printTables, printColumns) import Database.HDBC.SqlValue (SqlValue(..))
import PgStructure (printTables, printColumns, primaryKeyColumns)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Data.Text (pack, unpack) import Data.Text (pack, unpack)
import Data.Text.Encoding (encodeUtf8)
import PgQuery import PgQuery
import RangeQuery import RangeQuery
@@ -50,26 +55,33 @@ jsonBodyAction req handler = do
Right body -> handler body Right body -> handler body
jsonBody :: Request -> IO (Either String SqlRow) jsonBody :: Request -> IO (Either String SqlRow)
jsonBody = (fmap JSON.eitherDecode) . strictRequestBody jsonBody = fmap JSON.eitherDecode . strictRequestBody
app :: Connection -> Application app :: Connection -> Application
app conn req respond = do app conn req respond = do
r <- try $ r <- try $
case (path, verb) of case (path, verb) of
([], _) -> ([], _) ->
responseLBS status200 [jsonContentType] <$> (printTables ver conn) responseLBS status200 [jsonContentType] <$> printTables ver conn
([table], "OPTIONS") -> ([table], "OPTIONS") ->
responseLBS status200 [jsonContentType] <$> ( responseLBS status200 [jsonContentType] <$>
printColumns ver (unpack table) conn) printColumns ver (unpack table) conn
([table], "GET") -> ([table], "GET") ->
if range == Just emptyRange if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error" then return $ responseLBS status416 [] "HTTP Range error"
else respondWithRangedResult <$> else respondWithRangedResult <$>
(getRows (show ver) (unpack table) qq range conn) getRows (show ver) (unpack table) qq range conn
([table], "POST") -> ([table], "POST") ->
jsonBodyAction req (\row -> jsonBodyAction req (\row -> do
responseLBS status201 [jsonContentType] <$> ( allvals <- insert ver table row conn
insert ver table row conn)) keys <- primaryKeyColumns ver (unpack table) conn
let keyvals = allvals `intersection` fromList (zip keys $ repeat SqlNull)
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList keyvals
return $ responseLBS status201
[ jsonContentType
, (hLocation, "/" <> encodeUtf8 table <> "?" <> BS.pack params)
] ""
)
(_, _) -> (_, _) ->
return $ responseLBS status404 [] "" return $ responseLBS status404 [] ""
+8 -10
View File
@@ -9,8 +9,7 @@ import Data.Functor ( (<$>) )
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.List (intersperse, intercalate) import Data.List (intersperse, intercalate)
import Data.Monoid ((<>), mconcat) import Data.Monoid ((<>), mconcat)
import Data.HashMap.Strict (fromList) import qualified Data.Map as M
import qualified Data.Aeson as JSON
import qualified RangeQuery as R import qualified RangeQuery as R
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
@@ -66,8 +65,8 @@ wherePred (column, predicate) =
("%I " <> op <> "%L", map toSql [column, value]) ("%I " <> op <> "%L", map toSql [column, value])
where where
opCode:rest = BS.split ':' $ fromMaybe ":" predicate opCode:rest = BS.split '.' $ fromMaybe "." predicate
value = BS.intercalate ":" rest value = BS.intercalate "." rest
op = case opCode of op = case opCode of
"eq" -> "=" "eq" -> "="
"gt" -> ">" "gt" -> ">"
@@ -104,15 +103,14 @@ jsonArrayRows :: QuotedSql -> QuotedSql
jsonArrayRows q = jsonArrayRows q =
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", []) ("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
insert :: Int -> Text -> SqlRow -> Connection -> IO BL.ByteString insert :: Int -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
insert schema table row conn = do insert schema table row conn = do
query <- populateSql conn ("insert into %I.%I ("++colIds++")", map toSql $ (pack . show $ schema):table:cols) query <- populateSql conn ("insert into %I.%I ("++colIds++")",
map toSql $ (pack . show $ schema):table:cols)
stmt <- prepare conn (query ++ " values ("++phs++") returning *") stmt <- prepare conn (query ++ " values ("++phs++") returning *")
_ <- execute stmt values _ <- execute stmt values
keys <- getColumnNames stmt Just m <- fetchRowMap stmt
Just vals <- fetchRow stmt return m
let rowMap = fromList $ zip keys vals
return $ JSON.encode rowMap
where where
(cols, values) = unzip . getRow $ row (cols, values) = unzip . getRow $ row
colIds = intercalate ", " $ map (const "%I") cols colIds = intercalate ", " $ map (const "%I") cols
+7 -10
View File
@@ -8,11 +8,9 @@ import Test.Hspec.Wai.JSON
import SpecHelper import SpecHelper
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Test (SResponse(..))
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Data.Aeson ((.:)) import Data.Aeson ((.:))
import Data.Maybe (fromJust)
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>), (<*>))
import Control.Monad (mzero) import Control.Monad (mzero)
@@ -102,12 +100,11 @@ spec = around appWithFixture $ do
{ "non_nullable_string":"not null"} |] { "non_nullable_string":"not null"} |]
`shouldRespondWith` 201 `shouldRespondWith` 201
it "responds with the created row" $ do it "links to the created resource" $ do
r <- post "/auto_incrementing_pk" [json| post "/auto_incrementing_pk" [json|
{ "non_nullable_string":"not null"} |] { "non_nullable_string":"not null"} |]
let row = fromJust (JSON.decode $ simpleBody r :: Maybe IncPK) `shouldRespondWith` ResponseMatcher {
liftIO $ do matchBody = Nothing,
incStr row `shouldBe` "not null" matchStatus = 201,
incNullableStr row `shouldBe` Nothing matchHeaders = [("Location", "/auto_incrementing_pk?id=eq.2")]
-- Add assertions to check timestamp and id }
-- OR: change behavior to return no body at all