packages feed

jordan-servant-0.1.0.0: lib/Jordan/Servant/Query/Render.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

module Jordan.Servant.Query.Render where

import Data.Bifunctor
import qualified Data.ByteString as BS
import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Builder.Scientific
import Data.ByteString.Lazy (toStrict)
import Data.Functor.Contravariant
import Data.Functor.Contravariant.Divisible
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Void
import Jordan.ToJSON.Builder
import Jordan.ToJSON.Class
import Network.HTTP.Types.URI

newtype QueryRender a = QueryRender {runQueryRender :: a -> Query}
  deriving (Semigroup, Monoid) via (a -> Query)

instance Contravariant QueryRender where
  contramap f (QueryRender a) = QueryRender $ a . f

instance Divisible QueryRender where
  conquer = QueryRender mempty
  divide div (QueryRender renderB) (QueryRender renderC) = QueryRender $ \a ->
    let (b, c) = div a in renderB b <> renderC c

instance Selectable QueryRender where
  giveUp f = QueryRender $ absurd . f
  select sel renderL renderR =
    QueryRender $
      either
        (runQueryRender renderL)
        (runQueryRender renderR)
        . sel

escapeBracketComponent :: T.Text -> BS.ByteString
escapeBracketComponent text = urlEncode False encoded
  where
    encoded = encodeUtf8 text
    backsEscaped = BS.intercalate "\\" $ BS.split 92 encoded
    firstEscaped = case BS.stripPrefix "[" backsEscaped of
      Nothing -> encoded
      Just bs -> "\\[" <> bs
    endsEscaped = BS.intercalate "]]" $ BS.split 93 firstEscaped

escapeRawComponent :: T.Text -> BS.ByteString
escapeRawComponent text = urlEncode False encoded
  where
    encoded = encodeUtf8 text

addBracked :: T.Text -> BS.ByteString -> BS.ByteString
addBracked key v =
  "[" <> escapeBracketComponent key <> "]" <> v

addArray :: BS.ByteString -> BS.ByteString
addArray v =
  "[]" <> v

instance JSONObjectSerializer QueryRender where
  serializeFieldWith name = \(QueryRender f) -> QueryRender $ \other ->
    map (first $ addBracked name) $ f other
  serializeJust name qr = QueryRender $ \case
    Nothing -> []
    Just a -> map (first $ addBracked name) $ runQueryRender qr a

instance JSONTupleSerializer QueryRender where
  serializeItemWith = \(QueryRender f) -> QueryRender $ \other ->
    map (first addArray) $ f other

instance JSONSerializer QueryRender where
  serializeObject = \x -> x
  serializeTuple = \x -> x
  serializeTextConstant t = QueryRender $ const [(mempty, Just (encodeUtf8 t))]
  serializeArray =
    QueryRender $
      foldMap $ fmap (first addArray) . runQueryRender toJSON
  serializeNumber = QueryRender $ \num ->
    [(mempty, Just $ toStrict $ toLazyByteString $ scientificBuilder num)]
  serializeNull = QueryRender $ const [(mempty, Nothing)]
  serializeText = QueryRender $ \t ->
    [(mempty, Just (encodeUtf8 t))]
  serializeBool = QueryRender $ \b ->
    pure
      (mempty, if b then Just "t" else Just "f")
  serializeDictionary = \(QueryRender renderItem) -> QueryRender $
    foldMap $ \(key, v) ->
      first (addBracked key) <$> renderItem v

--- | Render a query with a given base key.
renderQueryAtKeyWith ::
  -- | Query renderer to use.
  (forall jsonSerializer. (JSONSerializer jsonSerializer) => jsonSerializer a) ->
  -- | Base key
  T.Text ->
  -- | Value to serialize
  a ->
  -- | Query
  Query
renderQueryAtKeyWith (QueryRender k) key =
  fmap (first (escapeRawComponent key <>)) . k

-- | Render a query at a given key, using the 'ToJSON' instance, which is what you want most of the time.
renderQueryAtKey :: (ToJSON a) => T.Text -> a -> Query
renderQueryAtKey = renderQueryAtKeyWith toJSON