201 lines
6.7 KiB
Haskell
201 lines
6.7 KiB
Haskell
-- |
|
|
-- Module: PostgREST.Request.Preferences
|
|
-- Description: Track client preferences to be employed when processing requests
|
|
--
|
|
-- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
|
|
--
|
|
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
|
--
|
|
module PostgREST.Request.Preferences
|
|
( Preferences(..)
|
|
, PreferCount(..)
|
|
, PreferParameters(..)
|
|
, PreferRepresentation(..)
|
|
, PreferResolution(..)
|
|
, PreferTransaction(..)
|
|
, fromHeaders
|
|
, ToAppliedHeader(..)
|
|
) where
|
|
|
|
import qualified Data.ByteString.Char8 as BS
|
|
import qualified Data.Map as Map
|
|
import qualified Network.HTTP.Types.Header as HTTP
|
|
|
|
import Protolude
|
|
|
|
|
|
-- $setup
|
|
-- Setup for doctests
|
|
-- >>> import Text.Pretty.Simple (pPrint)
|
|
-- >>> deriving instance Show PreferResolution
|
|
-- >>> deriving instance Show PreferRepresentation
|
|
-- >>> deriving instance Show PreferParameters
|
|
-- >>> deriving instance Show PreferCount
|
|
-- >>> deriving instance Show PreferTransaction
|
|
-- >>> deriving instance Show Preferences
|
|
|
|
-- | Preferences recognized by the application.
|
|
data Preferences
|
|
= Preferences
|
|
{ preferResolution :: Maybe PreferResolution
|
|
, preferRepresentation :: Maybe PreferRepresentation
|
|
, preferParameters :: Maybe PreferParameters
|
|
, preferCount :: Maybe PreferCount
|
|
, preferTransaction :: Maybe PreferTransaction
|
|
}
|
|
|
|
-- |
|
|
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
|
|
--
|
|
-- One header with comma-separated values can be used to set multiple preferences:
|
|
--
|
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
|
-- Preferences
|
|
-- { preferResolution = Just IgnoreDuplicates
|
|
-- , preferRepresentation = Nothing
|
|
-- , preferParameters = Nothing
|
|
-- , preferCount = Just ExactCount
|
|
-- , preferTransaction = Nothing
|
|
-- }
|
|
--
|
|
-- Multiple headers can also be used:
|
|
--
|
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact")]
|
|
-- Preferences
|
|
-- { preferResolution = Just IgnoreDuplicates
|
|
-- , preferRepresentation = Nothing
|
|
-- , preferParameters = Nothing
|
|
-- , preferCount = Just ExactCount
|
|
-- , preferTransaction = Nothing
|
|
-- }
|
|
--
|
|
-- If a preference is set more than once, only the first is used:
|
|
--
|
|
-- >>> preferTransaction $ fromHeaders [("Prefer", "tx=commit, tx=rollback")]
|
|
-- Just Commit
|
|
--
|
|
-- This is also the case across multiple headers:
|
|
--
|
|
-- >>> :{
|
|
-- preferResolution . fromHeaders $
|
|
-- [ ("Prefer", "resolution=ignore-duplicates")
|
|
-- , ("Prefer", "resolution=merge-duplicates")
|
|
-- ]
|
|
-- :}
|
|
-- Just IgnoreDuplicates
|
|
--
|
|
-- Preferences not recognized by the application are ignored:
|
|
--
|
|
-- >>> preferResolution $ fromHeaders [("Prefer", "resolution=foo")]
|
|
-- Nothing
|
|
--
|
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
|
--
|
|
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=minimal")]
|
|
-- Preferences
|
|
-- { preferResolution = Nothing
|
|
-- , preferRepresentation = Just None
|
|
-- , preferParameters = Nothing
|
|
-- , preferCount = Just ExactCount
|
|
-- , preferTransaction = Just Commit
|
|
-- }
|
|
--
|
|
fromHeaders :: [HTTP.Header] -> Preferences
|
|
fromHeaders headers =
|
|
Preferences
|
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
|
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
|
, preferTransaction = parsePrefs [Commit, Rollback]
|
|
}
|
|
where
|
|
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
|
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
|
|
|
|
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
|
|
parsePrefs vals =
|
|
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
|
|
|
|
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
|
|
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
|
|
|
-- |
|
|
-- Convert a preference into the value that we look for in the 'Prefer' headers.
|
|
--
|
|
-- >>> toHeaderValue MergeDuplicates
|
|
-- "resolution=merge-duplicates"
|
|
--
|
|
class ToHeaderValue a where
|
|
toHeaderValue :: a -> ByteString
|
|
|
|
-- |
|
|
-- Header to indicate that a preference has been applied.
|
|
--
|
|
-- >>> toAppliedHeader MergeDuplicates
|
|
-- ("Preference-Applied","resolution=merge-duplicates")
|
|
--
|
|
class ToHeaderValue a => ToAppliedHeader a where
|
|
toAppliedHeader :: a -> HTTP.Header
|
|
toAppliedHeader x = (HTTP.hPreferenceApplied, toHeaderValue x)
|
|
|
|
-- | How to handle duplicate values.
|
|
data PreferResolution
|
|
= MergeDuplicates
|
|
| IgnoreDuplicates
|
|
|
|
instance ToHeaderValue PreferResolution where
|
|
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
|
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
|
|
|
instance ToAppliedHeader PreferResolution
|
|
|
|
-- |
|
|
-- How to return the mutated data.
|
|
--
|
|
-- From https://tools.ietf.org/html/rfc7240#section-4.2
|
|
data PreferRepresentation
|
|
= Full -- ^ Return the body plus the Location header(in case of POST).
|
|
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
|
| None -- ^ Return nothing from the mutated data.
|
|
deriving Eq
|
|
|
|
instance ToHeaderValue PreferRepresentation where
|
|
toHeaderValue Full = "return=representation"
|
|
toHeaderValue None = "return=minimal"
|
|
toHeaderValue HeadersOnly = "return=headers-only"
|
|
|
|
-- | How to pass parameters to stored procedures.
|
|
data PreferParameters
|
|
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
|
|
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure.
|
|
deriving Eq
|
|
|
|
instance ToHeaderValue PreferParameters where
|
|
toHeaderValue SingleObject = "params=single-object"
|
|
toHeaderValue MultipleObjects = "params=multiple-objects"
|
|
|
|
-- | How to determine the count of (expected) results
|
|
data PreferCount
|
|
= ExactCount -- ^ Exact count (slower).
|
|
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
|
| EstimatedCount -- ^ Use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
|
deriving Eq
|
|
|
|
instance ToHeaderValue PreferCount where
|
|
toHeaderValue ExactCount = "count=exact"
|
|
toHeaderValue PlannedCount = "count=planned"
|
|
toHeaderValue EstimatedCount = "count=estimated"
|
|
|
|
-- | Whether to commit or roll back transactions.
|
|
data PreferTransaction
|
|
= Commit -- ^ Commit transaction - the default.
|
|
| Rollback -- ^ Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
|
deriving Eq
|
|
|
|
instance ToHeaderValue PreferTransaction where
|
|
toHeaderValue Commit = "tx=commit"
|
|
toHeaderValue Rollback = "tx=rollback"
|
|
|
|
instance ToAppliedHeader PreferTransaction
|