packages feed

mig-0.2.0.1: src/Mig/Core/Api/NormalForm/TreeApi.hs

{-| Normal form where on handler search API is
traversed in tree like facion without retraversal of the paths.
-}
module Mig.Core.Api.NormalForm.TreeApi (
  TreeApi (..),
  CaptureCase (..),
  getPath,
  toTreeApi,
) where

import Data.List qualified as List
import Data.List.Extra qualified as List
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (isNothing, mapMaybe)
import Data.Text (Text)
import Data.Text qualified as Text

import Mig.Core.Api (Api (..), Path (..), PathItem (..))

type CaptureMap = Map Text Text

{-| This form of API encodes path switch points as Map's so
it does not retraverse the routes and can find the right
branch on switch. In the plain api it tries the routes one by one
until it finds the right one.
-}
data TreeApi a
  = WithStaticPath [Text] (TreeApi a)
  | WithCapturePath [Text] (TreeApi a)
  | SwitchApi (Maybe a) (Map Text (TreeApi a)) (Maybe (CaptureCase a))
  deriving (Eq, Show, Functor)

-- | Capture case alternative
data CaptureCase a = CaptureCase
  { name :: Text
  , api :: TreeApi a
  }
  deriving (Eq, Show, Functor)

-- | Get a route by path, also extracts capture map
getPath :: [Text] -> TreeApi a -> Maybe (a, CaptureMap)
getPath mainPath = go mempty (filter (not . Text.null) mainPath)
  where
    go :: CaptureMap -> [Text] -> TreeApi a -> Maybe (a, CaptureMap)
    go !captures !path !api =
      case path of
        [] ->
          case api of
            SwitchApi (Just result) _ _ -> Just (result, captures)
            _ -> Nothing
        headPath : tailPath ->
          case api of
            WithStaticPath static subApi -> onStaticPath captures (headPath : tailPath) static subApi
            WithCapturePath names subApi -> onCapturePath captures (headPath : tailPath) names subApi
            SwitchApi _ alternatives mCapture -> onSwitch captures headPath tailPath alternatives mCapture

    onStaticPath captures pathQuery staticPath subApi = do
      rest <- checkPrefix staticPath pathQuery
      go captures rest subApi

    onCapturePath captures pathQuery names subApi = do
      (nextCaptures, nextPath) <- accumCapture captures names pathQuery
      go nextCaptures nextPath subApi

    onSwitch captures headPath tailPath alternatives mCapture =
      case Map.lookup headPath alternatives of
        Just subApi -> go captures tailPath subApi
        Nothing -> do
          captureCase <- mCapture
          go (Map.insert captureCase.name headPath captures) tailPath captureCase.api

checkPrefix :: (Eq a) => [a] -> [a] -> Maybe [a]
checkPrefix (a : as) (b : bs)
  | a == b = checkPrefix as bs
  | otherwise = Nothing
checkPrefix [] b = Just b
checkPrefix _ _ = Nothing

accumCapture :: CaptureMap -> [Text] -> [Text] -> Maybe (CaptureMap, [Text])
accumCapture !captures !names !path =
  case names of
    [] -> Just (captures, path)
    name : rest ->
      case path of
        pathHead : pathTail -> accumCapture (Map.insert name pathHead captures) rest pathTail
        [] -> Nothing

-------------------------------------------------------------------------------------

-- | Converts api to tree normal form
toTreeApi :: Api a -> TreeApi a
toTreeApi =
  joinPaths . \case
    Empty -> SwitchApi Nothing mempty Nothing
    WithPath path subApi -> case fromPathPrefix path of
      Nothing -> toTreeApi subApi
      Just prefix -> case prefix of
        StaticPrefix ps rest -> WithStaticPath ps (toTreeApi $ WithPath rest subApi)
        CapturePrefix ps rest -> WithCapturePath ps (toTreeApi $ WithPath rest subApi)
    HandleRoute a -> SwitchApi (Just a) mempty Nothing
    Append a b -> fromAlts $ orderAppends (collectAppends a <> collectAppends b)

joinPaths :: TreeApi a -> TreeApi a
joinPaths = \case
  SwitchApi mRoute alts mCapture -> SwitchApi mRoute (fmap joinPaths alts) (fmap joinCapturePaths mCapture)
  WithStaticPath pathA (WithStaticPath pathB subApi) -> joinPaths (WithStaticPath (pathA ++ pathB) subApi)
  WithCapturePath namesA (WithCapturePath namesB subApi) -> joinPaths (WithCapturePath (namesA ++ namesB) subApi)
  WithStaticPath path subApi -> WithStaticPath path (joinPaths subApi)
  WithCapturePath names subApi -> WithCapturePath names (joinPaths subApi)
  where
    joinCapturePaths x = x{api = joinPaths x.api}

data Alts a = Alts
  { appends :: [(Text, Api a)]
  , capture :: Maybe (Text, Api a)
  , route :: Maybe a
  }

data AppendItem a
  = StaticAppend Text (Api a)
  | RouteAppend a
  | CaptureAppend Text (Api a)

collectAppends :: Api a -> [AppendItem a]
collectAppends = \case
  Empty -> []
  HandleRoute a -> [RouteAppend a]
  Append a b -> collectAppends a <> collectAppends b
  WithPath (Path items) subApi -> case items of
    [] -> collectAppends subApi
    StaticPath item : [] -> pure $ StaticAppend item subApi
    StaticPath item : rest -> pure $ StaticAppend item (WithPath (Path rest) subApi)
    CapturePath item : [] -> pure $ CaptureAppend item subApi
    CapturePath item : rest -> pure $ CaptureAppend item (WithPath (Path rest) subApi)

orderAppends :: [AppendItem a] -> Alts a
orderAppends items =
  Alts
    { appends = mapMaybe toAppend items
    , capture = List.firstJust toCapture items
    , route = List.firstJust toRoute items
    }
  where
    toAppend = \case
      StaticAppend name api -> Just (name, api)
      _ -> Nothing

    toCapture = \case
      CaptureAppend name api -> Just (name, api)
      _ -> Nothing

    toRoute = \case
      RouteAppend route -> Just route
      _ -> Nothing

fromAlts :: Alts a -> TreeApi a
fromAlts alts =
  case getStaticSingleton of
    Just (path, subApi) -> WithStaticPath [path] (toTreeApi subApi)
    Nothing ->
      case getCaptureSingleton of
        Just (names, subApi) -> WithCapturePath [names] (toTreeApi subApi)
        Nothing -> SwitchApi alts.route (fmap toTreeApi $ Map.fromList alts.appends) (fmap toCaptureCase alts.capture)
  where
    toCaptureCase (name, api) = CaptureCase name (toTreeApi api)

    getStaticSingleton =
      case alts.appends of
        [(path, subApi)] | isNothing alts.route && isNothing alts.capture -> Just (path, subApi)
        _ -> Nothing

    getCaptureSingleton =
      case alts.capture of
        Just (name, subApi) | isNothing alts.route && null alts.appends -> Just (name, subApi)
        _ -> Nothing

data PathPrefix
  = StaticPrefix [Text] Path
  | CapturePrefix [Text] Path

fromPathPrefix :: Path -> Maybe PathPrefix
fromPathPrefix (Path items) = case items of
  [] -> Nothing
  StaticPath item : rest -> Just (accumStatics [item] rest)
  CapturePath item : rest -> Just (accumCaptures [item] rest)
  where
    accumStatics res rest =
      case rest of
        StaticPath item : nextRest -> accumStatics (item : res) nextRest
        _ -> StaticPrefix (List.reverse res) (Path rest)

    accumCaptures res rest =
      case rest of
        CapturePath item : nextRest -> accumCaptures (item : res) nextRest
        _ -> CapturePrefix (List.reverse res) (Path rest)