2022-03-08 03:42:06 +03:00
|
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
|
|
|
|
module Hasura.RQL.DDL.Webhook.Transform.Body
|
|
|
|
( -- * Body Transformations
|
|
|
|
Body (..),
|
|
|
|
TransformFn (..),
|
|
|
|
|
|
|
|
-- ** Body Transformation Action
|
|
|
|
BodyTransformAction (..),
|
|
|
|
foldFormEncoded,
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
import Data.Aeson (FromJSON, ToJSON)
|
|
|
|
import Data.Aeson qualified as J
|
|
|
|
import Data.ByteString qualified as B
|
|
|
|
import Data.ByteString.Lazy qualified as BL
|
|
|
|
import Data.HashMap.Internal.Strict qualified as M
|
|
|
|
import Data.List qualified as L
|
|
|
|
import Data.Text qualified as T
|
|
|
|
import Data.Text.Encoding qualified as TE
|
2022-03-11 02:22:54 +03:00
|
|
|
import Data.Validation (Validation)
|
2022-03-08 03:42:06 +03:00
|
|
|
import Data.Validation qualified as V
|
|
|
|
import Hasura.Incremental (Cacheable)
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Class
|
|
|
|
( RequestTransformCtx (..),
|
|
|
|
Template (..),
|
2022-03-11 02:22:54 +03:00
|
|
|
TemplatingEngine,
|
2022-03-08 03:42:06 +03:00
|
|
|
Transform (..),
|
|
|
|
TransformErrorBundle (..),
|
|
|
|
UnescapedTemplate,
|
2022-03-11 02:22:54 +03:00
|
|
|
runRequestTemplateTransform,
|
|
|
|
runUnescapedRequestTemplateTransform',
|
|
|
|
validateRequestTemplateTransform',
|
|
|
|
validateRequestUnescapedTemplateTransform',
|
2022-03-08 03:42:06 +03:00
|
|
|
)
|
|
|
|
import Network.URI.Extended qualified as URI
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
-- | The actual JSON body we are performing a transformation on.
|
|
|
|
data Body = JSONBody (Maybe J.Value) | RawBody BL.ByteString
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
instance Transform Body where
|
|
|
|
-- TODO(jkachmar): Document.
|
|
|
|
newtype TransformFn Body
|
|
|
|
= BodyTransform BodyTransformAction
|
|
|
|
deriving stock (Eq, Generic, Show)
|
|
|
|
deriving newtype (FromJSON, ToJSON)
|
|
|
|
deriving anyclass (Cacheable, NFData)
|
|
|
|
|
|
|
|
transform ::
|
|
|
|
MonadError TransformErrorBundle m =>
|
|
|
|
TransformFn Body ->
|
|
|
|
RequestTransformCtx ->
|
|
|
|
Body ->
|
|
|
|
m Body
|
|
|
|
transform (BodyTransform transformation) context _originalBody = do
|
|
|
|
case transformation of
|
|
|
|
RemoveBody -> pure $ JSONBody Nothing
|
|
|
|
ModifyBody template -> do
|
2022-03-11 02:22:54 +03:00
|
|
|
result <- liftEither $ runRequestTemplateTransform template context
|
2022-03-08 03:42:06 +03:00
|
|
|
pure . JSONBody . Just $ result
|
|
|
|
FormUrlEncoded formTemplates -> do
|
2022-03-11 02:22:54 +03:00
|
|
|
result <- liftEither . V.toEither $ traverse (runUnescapedRequestTemplateTransform' context) formTemplates
|
2022-03-08 03:42:06 +03:00
|
|
|
pure $ RawBody $ foldFormEncoded result
|
|
|
|
|
2022-03-11 02:22:54 +03:00
|
|
|
validate ::
|
|
|
|
TemplatingEngine ->
|
|
|
|
TransformFn Body ->
|
|
|
|
Validation TransformErrorBundle ()
|
|
|
|
validate engine = \case
|
|
|
|
BodyTransform (RemoveBody) -> pure ()
|
|
|
|
BodyTransform (ModifyBody template) -> validateRequestTemplateTransform' engine template
|
|
|
|
BodyTransform (FormUrlEncoded kv) -> traverse_ (validateRequestUnescapedTemplateTransform' engine) kv
|
|
|
|
|
2022-03-08 03:42:06 +03:00
|
|
|
-- | Helper function for URI escaping 'T.Text' values.
|
|
|
|
escapeURIText :: T.Text -> T.Text
|
|
|
|
escapeURIText =
|
|
|
|
T.pack . URI.escapeURIString URI.isUnescapedInURIComponent . T.unpack
|
|
|
|
|
|
|
|
-- | Helper function for URI escaping 'B.ByteString' values
|
|
|
|
escapeURIBS :: B.ByteString -> B.ByteString
|
|
|
|
escapeURIBS =
|
|
|
|
TE.encodeUtf8 . T.pack . URI.escapeURIString URI.isUnescapedInURIComponent . T.unpack . TE.decodeUtf8
|
|
|
|
|
|
|
|
-- | Fold a 'M.HashMap Text B.ByteString' into a 'BL.ByteString'
|
|
|
|
-- encoded `x-www-form-urlencoded` request body.
|
|
|
|
foldFormEncoded :: M.HashMap Text B.ByteString -> BL.ByteString
|
|
|
|
foldFormEncoded = fold . L.intersperse "&" . M.foldMapWithKey @[BL.ByteString] \k v -> [BL.fromStrict $ TE.encodeUtf8 (escapeURIText k) <> "=" <> escapeURIBS v]
|
|
|
|
|
|
|
|
-- | The defunctionalized transformation function on 'Body'.
|
|
|
|
--
|
|
|
|
-- Our transformation function can either remove the body or modify it
|
|
|
|
-- with a 'Template'.
|
|
|
|
data BodyTransformAction
|
|
|
|
= RemoveBody
|
|
|
|
| ModifyBody Template
|
|
|
|
| FormUrlEncoded (M.HashMap T.Text UnescapedTemplate)
|
|
|
|
deriving stock (Eq, Generic, Show)
|
|
|
|
deriving anyclass (Cacheable, NFData)
|
|
|
|
|
|
|
|
instance FromJSON BodyTransformAction where
|
|
|
|
parseJSON = J.withObject "BodyTransform" \o -> do
|
|
|
|
action <- o J..: "action"
|
|
|
|
case (action :: T.Text) of
|
|
|
|
"remove" -> pure RemoveBody
|
|
|
|
"transform" -> do
|
|
|
|
template <- o J..: "template"
|
|
|
|
pure $ ModifyBody template
|
|
|
|
"x_www_form_urlencoded" -> do
|
|
|
|
formTemplates <- o J..: "form_template"
|
|
|
|
pure $ FormUrlEncoded formTemplates
|
|
|
|
_ -> fail "invalid transform action"
|
|
|
|
|
|
|
|
instance ToJSON BodyTransformAction where
|
|
|
|
toJSON = \case
|
|
|
|
RemoveBody -> J.object ["action" J..= ("remove" :: T.Text)]
|
|
|
|
ModifyBody a ->
|
|
|
|
J.object
|
|
|
|
[ "action" J..= ("transform" :: T.Text),
|
|
|
|
"template" J..= J.toJSON a
|
|
|
|
]
|
|
|
|
FormUrlEncoded formTemplates ->
|
|
|
|
J.object
|
|
|
|
[ "action" J..= ("x_www_form_urlencoded" :: T.Text),
|
|
|
|
"form_template" J..= J.toJSON formTemplates
|
|
|
|
]
|