packages feed

ema-0.8.0.0: src/Ema/Route/Generic.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}

module Ema.Route.Generic (
  -- * Generic deriving types
  GenericRoute (GenericRoute),
  HasSubRoutes,
  HasSubModels,
  WithModel,
  WithSubRoutes,
  WithSubModels,

  -- * Customizing generic deriving
  GenericRouteOpt (..),

  -- * Handy functions
  subModels,

  -- * Export these for DerivingVia coercion representations
  FileRoute (FileRoute),
  FolderRoute (FolderRoute),
) where

import Ema.Route.Class (IsRoute (..))
import Ema.Route.Generic.RGeneric
import Ema.Route.Generic.SubModel as X
import Ema.Route.Generic.SubRoute as X
import Ema.Route.Generic.Verification
import Ema.Route.Lib.File (FileRoute (FileRoute))
import Ema.Route.Lib.Folder (FolderRoute (FolderRoute))
import Ema.Route.Lib.Multi (MultiModel, MultiRoute)
import Ema.Route.Prism.Type (mapRoutePrism)
import GHC.Generics qualified as GHC
import Generics.SOP (All, I (..), NP)
import Optics.Core (ReversibleOptic (re), coercedTo, equality, review, (%))
import Prelude hiding (All, Generic)

-- | DerivingVia type to generically derive `IsRoute`
newtype GenericRoute r (opts :: [Type]) = GenericRoute r
  deriving stock (GHC.Generic)

{- | Associate the route with the given model type.

 Default: `()`
-}
data WithModel (r :: Type)

{- | Specify isomorphic types to delegate sub-route behaviour. Usually this is identical to the route product type.

    The isomorphism is specified by @Coercible@.

    The default implementation uses @FileRoute@ for terminal routes, and
    @FolderRoute@ (with constructor prefix stripped) for wrapping sub-routes types.
-}
data WithSubRoutes (subRoutes :: [Type])

{- | Specify the @Data.Generics.Product.Any.HasAny@ selector type for sub models

  Note: if the selector is a @Symbol@ you must wrap it in a @Proxy@.
-}
data WithSubModels (subModels :: [Type])

{- | Typeclass to control `GenericRoute` behaviour.

  The `FooM` type enables users to define their type optionally, whose default
  is specified in the `Foo` type family (further below).

  You can define your own options, for example:

  @
    data MySubRoutes
    instance GenericRouteOpt r MySubRoutes where
      type
        OptSubRoutesM r MySubRoutes =
          'Just (GSubRoutes (RDatatypeName r) (RConstructorNames r) (RCode r))
  @

  And use it as:

  > deriving via (GenericRoute MyRoute '[MySubRoutes])
-}
class GenericRouteOpt (r :: Type) (opt :: Type) where
  type OptModelM r opt :: Maybe Type
  type OptModelM r opt = 'Nothing
  type OptSubRoutesM r opt :: Maybe [Type]
  type OptSubRoutesM r opt = 'Nothing
  type OptSubModelsM r opt :: Maybe [Type]
  type OptSubModelsM r opt = 'Nothing

instance GenericRouteOpt r (WithModel t) where
  type OptModelM r (WithModel t) = 'Just t
instance GenericRouteOpt r (WithSubRoutes t) where
  type OptSubRoutesM r (WithSubRoutes t) = 'Just t
instance GenericRouteOpt r (WithSubModels t) where
  type OptSubModelsM r (WithSubModels t) = 'Just t

type family OptModel r (opts :: [Type]) :: Type where
  OptModel r '[] = ()
  OptModel r (opt ': opts) = FromMaybe (OptModel r opts) (OptModelM r opt)

type family OptSubRoutes r (opts :: [Type]) :: [Type] where
  OptSubRoutes r '[] = GSubRoutes (RDatatypeName r) (RConstructorNames r) (RCode r)
  OptSubRoutes r (opt ': opts) = FromMaybe (OptSubRoutes r opts) (OptSubRoutesM r opt)

type family OptSubModels r (opts :: [Type]) :: [Type] where
  OptSubModels r '[] = MultiModel (SubRoutes r)
  OptSubModels r (opt ': opts) = FromMaybe (OptSubModels r opts) (OptSubModelsM r opt)

type family FromMaybe (def :: a) (maybe :: Maybe a) :: a where
  FromMaybe def 'Nothing = def
  FromMaybe def ( 'Just a) = a

type GenericRouteOpts r opts = All (GenericRouteOpt r) opts

instance
  ( GenericRouteOpts r opts
  , RGeneric r
  , ValidSubRoutes r (OptSubRoutes r opts)
  ) =>
  HasSubRoutes (GenericRoute r opts)
  where
  type SubRoutes (GenericRoute r opts) = OptSubRoutes r opts

instance
  ( VerifyModels
      (RouteModel (GenericRoute r opts))
      (MultiModel (SubRoutes (GenericRoute r opts)))
      (OptSubModels r opts)
  , VerifyRoutes (RCode r) (SubRoutes (GenericRoute r opts))
  , GSubModels (RouteModel (GenericRoute r opts)) (MultiModel (OptSubRoutes r opts)) (OptSubModels r opts)
  , HasSubRoutes (GenericRoute r opts)
  , GenericRouteOpts r opts
  ) =>
  HasSubModels (GenericRoute r opts)
  where
  subModels m =
    gsubModels @_ @(RouteModel (GenericRoute r opts))
      @(MultiModel (SubRoutes (GenericRoute r opts)))
      @(OptSubModels r opts)
      m

instance
  ( VerifyRoutes (RCode r) (SubRoutes (GenericRoute r opts))
  , HasSubRoutes r
  , HasSubModels r
  , ValidSubRoutes r (SubRoutes r)
  , RGeneric r
  , mr ~ MultiRoute (SubRoutes r)
  , mm ~ MultiModel (SubRoutes r)
  , RouteModel r ~ OptModel r opts
  , RouteModel mr ~ NP I mm
  , IsRoute mr
  , GenericRouteOpts r opts
  ) =>
  IsRoute (GenericRoute r opts)
  where
  type RouteModel (GenericRoute r opts) = OptModel r opts
  routePrism =
    routePrism @mr
      & mapRoutePrism equality (re (subRoutesIso @r) % coercedTo) (subModels @r)
  routeUniverse m =
    GenericRoute . review subRoutesIso
      <$> routeUniverse (subModels @r m)