packages feed

servant-util-0.1.0: src/Servant/Util/Combinators/Sorting/Backend.hs

-- | Applying sorting specifications.
module Servant.Util.Combinators.Sorting.Backend
    ( SortingBackend (..)
    , fieldSort
    , SortingApp (..)
    , SortingSpecApp

    , ApplyToSortItem (..)
    , backendApplySorting
    ) where

import Universum

import GHC.TypeLits (KnownSymbol)

import Servant.Util.Combinators.Sorting.Base
import Servant.Util.Common

-- | Implementation of filtering backend.
class SortingBackend backend where

    -- | The part of object which we are filtering on,
    -- provided by server backend implementor.
    type SortedValue backend a :: *

    -- | What we require from sorted values in order to be sortable.
    type SortedValueConstraint backend a :: Constraint
    type SortedValueConstraint backend a = ()

    -- | A resulting ordering.
    type BackendOrdering backend :: *

    -- | Implement 'SortingApp' as sorting on the given field.
    backendFieldSort
        :: SortedValueConstraint backend a
        => SortedValue backend a
        -> SortingApp backend ('TyNamedParam name a)

fieldSort
    :: forall name a backend.
       (SortingBackend backend, SortedValueConstraint backend a)
    => SortedValue backend a -> SortingApp backend ('TyNamedParam name a)
fieldSort = backendFieldSort

-- | A function defining a way to apply the given 'SortingItem' (which is sorting
-- order on a single parameter).
newtype SortingApp backend param
    = SortingApp (SortingItemTagged param -> BackendOrdering backend)

{- | List of 'SortingApp' functions. Describes how to apply @SortingSpec params@
(each of possible 'SortingItem') to an SQL query.

Instance of this type can be created using 'fieldSort' function. For example:

@
sortingSpecApp :: SortingSpecApp ["course" ?: Course, "desc" ?: Text]
sortingSpecApp =
    fieldSort @"course" courseField .*.
    fieldSort @"desc" descField .*.
    HNil
@

Annotating 'fieldSort' call with parameter name is not mandatory but recommended
to prevent possible mistakes in 'fieldSort's ordering.
-}
type SortingSpecApp backend (allParams :: [TyNamedParam *]) =
    HList (SortingApp backend) allParams

-- | Lookup for appropriate 'SortingApp' in 'SortingSpecApp' and apply it to 'SortingItem'.
class ApplyToSortItem backend params where
    -- | Apply spec app to the given 'SortingItem'
    -- We return 'Maybe' here (instead of forcing presence at type-level) for convenience.
    applyToSortItem
        :: SortingSpecApp backend params
        -> SortingItem
        -> Maybe (BackendOrdering backend)

instance ApplyToSortItem backend '[] where
    applyToSortItem HNil _ = Nothing

instance (KnownSymbol name, ApplyToSortItem backend params) =>
         ApplyToSortItem backend ('TyNamedParam name p ': params) where
    applyToSortItem (SortingApp app `HCons` appRem) item = asum
        [ guard (symbolValT @name == siName item) $> app (SortingItemTagged item)
        , applyToSortItem @backend @params appRem item
        ]

-- | Apply a given 'SortingSpecApp' to a 'SortingSpec' producing a pack of
-- ordering values which define lexicographical sorting order.
backendApplySorting
    :: forall provided base allParams backend.
       ( allParams ~ AllSortingParams provided base
       , ApplyToSortItem backend allParams
       )
    => SortingSpec provided base
    -> SortingSpecApp backend allParams
    -> [BackendOrdering backend]
backendApplySorting spec app =
    ssAll spec <&> \sitem ->
        applyToSortItem @backend @allParams app sitem
           -- impossible due to invariants of 'SortingSpec'
        ?: error ("Impossible: don't know how to apply to spec item " <> show sitem)