packages feed

azure-functions-worker-0.0.0.0: src/Azure/Functions/Bindings/HTTP.hs

{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE StrictData            #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
module Azure.Functions.Bindings.HTTP
( HttpRequest(..)
, HttpResponse(..)
, HttpBinding(..)
, module Azure.Functions.Bindings.Class
)
where

import           Azure.Functions.Bindings.Class
import           Azure.Functions.Internal.Lens         (toEither)
import           Data.Aeson                            ((.=))
import qualified Data.Aeson                            as Aeson
import           Data.ByteString                       (ByteString)
import qualified Data.Map                              as Map
import           Data.Map.Strict                       (Map)
import           Data.ProtoLens.Runtime.Data.ProtoLens (defMessage)
import           Data.Text                             (Text)
import qualified Data.Text                             as Text
import qualified Data.Text.Encoding                    as Text
import           GHC.Generics                          (Generic)
import           Lens.Family                           (LensLike, Phantom, to, view, (&), (.~), (^.))
import           Lens.Family.Stock                     (at)
import           Network.URI                           (URI, parseURI)
import           Proto.FunctionRpc
import           Proto.FunctionRpc_Fields

data HttpBinding  = HttpBinding

data HttpRequest = HttpRequest
  { httpRequestMethod  :: Text
  , httpRequestUrl     :: URI
  , httpRequestQuery   :: Map Text Text
  , httpRequestHeaders :: Map Text Text
  , httpRequestBody    :: Maybe ByteString
  } deriving (Show, Eq, Generic)

data HttpResponse = HttpResponse
  { httpResponseStatus  :: Int
  , httpResponseBody    :: Maybe ByteString
  , httpResponseHeaders :: Map Text Text
  } deriving (Show, Eq, Generic)

instance InMessage HttpRequest where
  type InBinding HttpRequest = HttpBinding
  fromInvocationRequest req =
    req ^. triggerMetadata . at "$request" . toEither "Unable to find $request parameter"
        >>= view (maybe'http . toEither "Unexpected payload, RpcHttp is expected")
        >>= fromRpcHttp

instance OutMessage HttpResponse where
  type OutBinding HttpResponse = HttpBinding
  toInvocationResponse resp =
    let
      td = defMessage @TypedData
              & maybe'data' .~ fmap TypedData'Bytes (httpResponseBody resp)

      ht = defMessage @RpcHttp
              & headers .~ httpResponseHeaders resp
              & body .~ td

    in defMessage @InvocationResponse
        & returnValue .~ (defMessage & http .~ ht)
        & result .~ (defMessage & status .~ StatusResult'Success)

instance ToInBinding HttpBinding where
  toInBindingJSON _ = Aeson.object
    [ "type"      .= ("httpTrigger" :: Text)
    , "direction" .= ("in"          :: Text)
    , "name"      .= ("req"         :: Text)
    ]

instance ToOutBinding HttpBinding where
  toOutBindingJSON _ = Aeson.object
    [ "type"      .= ("http"    :: Text)
    , "direction" .= ("out"     :: Text)
    , "name"      .= ("$return" :: Text)
    ]

fromRpcHttp :: RpcHttp -> Either Text HttpRequest
fromRpcHttp req = do
  uri <- req ^. url . to (Text.unpack) . to parseURI . toEither "Unable to parse URI"

  pure HttpRequest
        { httpRequestMethod   = req ^. method
        , httpRequestUrl      = uri
        , httpRequestQuery    = req ^. query
        , httpRequestHeaders  = req ^. headers
        , httpRequestBody     = (req ^. maybe'rawBody >>= fromTypedData)
        }

fromTypedData :: TypedData -> Maybe ByteString
fromTypedData td =
  td ^. maybe'data' >>= \case
    TypedData'String v -> Just (Text.encodeUtf8 v)
    TypedData'Json v   -> Just (Text.encodeUtf8 v)
    TypedData'Bytes v  -> Just v
    TypedData'Stream v -> Just v
    _                  -> Nothing