packages feed

compaREST-0.1.0.0: src/Data/OpenApi/Compare/Subtree.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Data.OpenApi.Compare.Subtree
  ( Steppable (..),
    Step (..),
    TraceRoot,
    Trace,
    Traced,
    Traced',
    pattern Traced,
    traced,
    retraced,
    stepTraced,
    Subtree (..),
    checkCompatibility,
    checkSubstructure,
    CompatM (..),
    CompatFormula',
    SemanticCompatFormula,
    ProdCons (..),
    orientProdCons,
    swapProdCons,
    runCompatFormula,
    issueAt,
    anItem,
    anIssue,
    invertIssueOrientation,
    invertIssueOrientationP,
    embedFormula,
    anyOfAt,
    clarifyIssue,
    structuralIssue,

    -- * Structural helpers
    structuralMaybe,
    structuralMaybeWith,
    structuralEq,
    iohmStructural,
    iohmStructuralWith,
    structuralList,

    -- * Reexports
    (>>>),
    (<<<),
    extract,
    ask,
    local,
    step,
    Typeable,
  )
where

import Control.Comonad.Env
import Control.Monad.Identity
import Control.Monad.State
import Data.Foldable
import Data.Functor.Compose
import Data.HList
import qualified Data.HashMap.Strict.InsOrd as IOHM
import Data.Hashable
import Data.Kind
import Data.OpenApi
import Data.OpenApi.Compare.Behavior
import Data.OpenApi.Compare.Formula
import Data.OpenApi.Compare.Memo
import Data.OpenApi.Compare.Paths
import qualified Data.OpenApi.Compare.PathsPrefixTree as P
import qualified Data.Set as S
import Data.Typeable

class
  (Typeable Step, Typeable a, Typeable b, Ord (Step a b), Show (Step a b)) =>
  Steppable (a :: Type) (b :: Type)
  where
  -- | How to get from an @a@ node to a @b@ node
  data Step a b :: Type

data TraceRoot

instance Steppable TraceRoot OpenApi where
  data Step TraceRoot OpenApi
    = ClientSchema
    | ServerSchema
    deriving stock (Eq, Ord, Show)

type Trace = Paths Step TraceRoot

type instance AdditionalQuiverConstraints Step _ _ = ()

type Traced' a b = Env (Trace a) b

type Traced a = Traced' a a

pattern Traced :: Trace a -> b -> Traced' a b
pattern Traced t x = EnvT t (Identity x)

{-# COMPLETE Traced #-}

traced :: Trace a -> a -> Traced a
traced = env

retraced :: (Trace a -> Trace a') -> Traced' a b -> Traced' a' b
retraced f (Traced a b) = Traced (f a) b

stepTraced :: Steppable a a' => Step a a' -> Traced' a b -> Traced' a' b
stepTraced s = retraced (>>> step s)

data ProdCons a = ProdCons
  { producer :: a
  , consumer :: a
  }
  deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)

orientProdCons :: Orientation -> ProdCons x -> ProdCons x
orientProdCons Forward x = x
orientProdCons Backward (ProdCons p c) = ProdCons c p

swapProdCons ::
  SwapEnvRoles xs =>
  (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a) ->
  (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a)
swapProdCons f e (ProdCons p c) =
  invertIssueOrientation $
    f (swapEnvRoles e) (ProdCons c p)
{-# INLINE swapProdCons #-}

type family IsProdCons (x :: Type) :: Bool where
  IsProdCons (ProdCons _) = 'True
  IsProdCons _ = 'False

type SwapEnvElementRoles x = SwapEnvElementRoles' x (IsProdCons x)

class IsProdCons x ~ f => SwapEnvElementRoles' (x :: Type) f where
  swapEnvElementRoles :: x -> x

instance SwapEnvElementRoles' (ProdCons x) 'True where
  swapEnvElementRoles (ProdCons p c) = ProdCons c p

instance IsProdCons x ~ 'False => SwapEnvElementRoles' x 'False where
  swapEnvElementRoles = id

class SwapEnvRoles xs where
  swapEnvRoles :: HList xs -> HList xs

instance SwapEnvRoles '[] where
  swapEnvRoles = id

instance (SwapEnvElementRoles x, SwapEnvRoles xs) => SwapEnvRoles (x ': xs) where
  swapEnvRoles (HCons x xs) = HCons (swapEnvElementRoles x) (swapEnvRoles xs)

instance Applicative ProdCons where
  pure x = ProdCons x x
  ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)

newtype CompatM a = CompatM
  { unCompatM ::
      StateT (MemoState VarRef) Identity a
  }
  deriving newtype
    ( Functor
    , Applicative
    , Monad
    , MonadState (MemoState VarRef)
    )

type CompatFormula' q f r = Compose CompatM (FormulaF q f r)

type SemanticCompatFormula = CompatFormula' Behave AnIssue 'APILevel

type StructuralCompatFormula = CompatFormula' VoidQuiver Proxy ()

data VoidQuiver a b

deriving stock instance Eq (VoidQuiver a b)

type instance AdditionalQuiverConstraints VoidQuiver _ _ = ()

deriving stock instance Ord (VoidQuiver a b)

deriving stock instance Show (VoidQuiver a b)

class (Typeable t, Issuable (SubtreeLevel t)) => Subtree (t :: Type) where
  type CheckEnv t :: [Type]
  type SubtreeLevel t :: BehaviorLevel

  checkStructuralCompatibility ::
    HList (CheckEnv t) ->
    ProdCons (Traced t) ->
    StructuralCompatFormula ()

  checkSemanticCompatibility ::
    HList (CheckEnv t) ->
    Behavior (SubtreeLevel t) ->
    ProdCons (Traced t) ->
    SemanticCompatFormula ()

{-# WARNING checkStructuralCompatibility "You should not be calling this directly. Use 'checkSubstructure'" #-}

{-# WARNING checkSemanticCompatibility "You should not be calling this directly. Use 'checkCompatibility'" #-}

checkCompatibility ::
  forall t xs.
  (ReassembleHList xs (CheckEnv t), Subtree t) =>
  Behavior (SubtreeLevel t) ->
  HList xs ->
  ProdCons (Traced t) ->
  SemanticCompatFormula ()
checkCompatibility bhv e = memo bhv SemanticMemoKey $ \pc ->
  case runCompatFormula $ checkSubstructure e pc of
    Left _ -> checkSemanticCompatibility (reassemble e) bhv pc
    Right () -> pure ()
{-# INLINE checkCompatibility #-}

checkSubstructure ::
  (ReassembleHList xs (CheckEnv t), Subtree t) =>
  HList xs ->
  ProdCons (Traced t) ->
  StructuralCompatFormula ()
checkSubstructure e = memo Root StructuralMemoKey $ checkStructuralCompatibility (reassemble e)
{-# INLINE checkSubstructure #-}

structuralMaybe ::
  (Subtree a, ReassembleHList xs (CheckEnv a)) =>
  HList xs ->
  ProdCons (Maybe (Traced a)) ->
  StructuralCompatFormula ()
structuralMaybe e = structuralMaybeWith (checkSubstructure e)
{-# INLINE structuralMaybe #-}

structuralMaybeWith ::
  (ProdCons a -> StructuralCompatFormula ()) ->
  ProdCons (Maybe a) ->
  StructuralCompatFormula ()
structuralMaybeWith f (ProdCons (Just a) (Just b)) = f $ ProdCons a b
structuralMaybeWith _ (ProdCons Nothing Nothing) = pure ()
structuralMaybeWith _ _ = structuralIssue
{-# INLINE structuralMaybeWith #-}

structuralList ::
  (Subtree a, ReassembleHList xs (CheckEnv a)) =>
  HList xs ->
  ProdCons [Traced a] ->
  StructuralCompatFormula ()
structuralList _ (ProdCons [] []) = pure ()
structuralList e (ProdCons (a : aa) (b : bb)) = do
  checkSubstructure e $ ProdCons a b
  structuralList e $ ProdCons aa bb
  pure ()
structuralList _ _ = structuralIssue
{-# INLINE structuralList #-}

structuralEq :: (Eq a, Comonad w) => ProdCons (w a) -> StructuralCompatFormula ()
structuralEq (ProdCons a b) = if extract a == extract b then pure () else structuralIssue
{-# INLINE structuralEq #-}

iohmStructural ::
  (ReassembleHList (k ': xs) (CheckEnv v), Ord k, Subtree v, Hashable k, Typeable k, Show k) =>
  HList xs ->
  ProdCons (Traced (IOHM.InsOrdHashMap k v)) ->
  StructuralCompatFormula ()
iohmStructural e =
  iohmStructuralWith (\k -> checkSubstructure (k `HCons` e))
{-# INLINE iohmStructural #-}

instance (Typeable k, Typeable v, Ord k, Show k) => Steppable (IOHM.InsOrdHashMap k v) v where
  data Step (IOHM.InsOrdHashMap k v) v = InsOrdHashMapKeyStep k
    deriving stock (Eq, Ord, Show)

iohmStructuralWith ::
  (Ord k, Hashable k, Typeable k, Typeable v, Show k) =>
  (k -> ProdCons (Traced v) -> StructuralCompatFormula ()) ->
  ProdCons (Traced (IOHM.InsOrdHashMap k v)) ->
  StructuralCompatFormula ()
iohmStructuralWith f pc = do
  let ProdCons pEKeys cEKeys = S.fromList . IOHM.keys . extract <$> pc
  if pEKeys == cEKeys
    then
      for_
        pEKeys
        ( \eKey ->
            f eKey $ stepTraced (InsOrdHashMapKeyStep eKey) . fmap (IOHM.lookupDefault (error "impossible") eKey) <$> pc
        )
    else structuralIssue
{-# INLINE iohmStructuralWith #-}

runCompatFormula ::
  CompatFormula' q f r a ->
  Either (P.PathsPrefixTree q f r) a
runCompatFormula (Compose f) =
  calculate . runIdentity . runMemo 0 . unCompatM $ f
{-# INLINE runCompatFormula #-}

embedFormula :: Paths q r l -> CompatFormula' q f l a -> CompatFormula' q f r a
embedFormula bhv (Compose x) = Compose $ mapErrors (P.embed bhv) <$> x

issueAt :: Issuable l => Paths q r l -> Issue l -> CompatFormula' q AnIssue r a
issueAt xs issue = Compose $ pure $ anError $ AnItem xs $ anIssue issue
{-# INLINE issueAt #-}

anIssue :: Issuable l => Issue l -> AnIssue l
anIssue = AnIssue Forward
{-# INLINE anIssue #-}

anItem :: AnItem q AnIssue r -> CompatFormula' q AnIssue r a
anItem = Compose . pure . anError
{-# INLINE anItem #-}

invertIssueOrientation :: CompatFormula' q AnIssue r a -> CompatFormula' q AnIssue r a
invertIssueOrientation (Compose x) =
  Compose $ mapErrors invertIssueOrientationP <$> x

invertIssueOrientationP :: P.PathsPrefixTree q AnIssue r -> P.PathsPrefixTree q AnIssue r
invertIssueOrientationP = P.map (\(AnIssue ori i) -> AnIssue (toggleOrientation ori) i)

structuralIssue :: StructuralCompatFormula a
structuralIssue = Compose $ pure $ anError $ AnItem Root Proxy

anyOfAt ::
  Issuable l =>
  Paths q r l ->
  Issue l ->
  [CompatFormula' q AnIssue r a] ->
  CompatFormula' q AnIssue r a
anyOfAt _ _ [x] = x
anyOfAt xs issue fs =
  Compose $ (`eitherOf` AnItem xs (anIssue issue)) <$> traverse getCompose fs

-- | If the given formula contains any issues, add another issue on top. Otherwise succeed.
clarifyIssue ::
  AnItem q AnIssue r ->
  CompatFormula' q AnIssue r a ->
  CompatFormula' q AnIssue r a
clarifyIssue item f =
  Compose ((`eitherOf` item) <$> pure <$> getCompose f) *> f

fixpointKnot ::
  MonadState (MemoState VarRef) m =>
  KnotTier (FormulaF q f r ()) VarRef m
fixpointKnot =
  KnotTier
    { onKnotFound = modifyMemoNonce succ
    , onKnotUsed = \i -> pure $ variable i
    , tieKnot = \i x -> pure $ maxFixpoint i x
    }

memo ::
  (Typeable (l :: k), Typeable q, Typeable f, Typeable k, Typeable a) =>
  Paths q r l ->
  MemoKey ->
  (ProdCons (Traced a) -> CompatFormula' q f r ()) ->
  (ProdCons (Traced a) -> CompatFormula' q f r ())
memo bhv k f pc = Compose $ do
  formula' <-
    memoWithKnot
      fixpointKnot
      ( do
          formula <- getCompose $ f pc
          pure $ mapErrors (P.takeSubtree bhv) formula
      )
      (k, ask <$> pc)
  pure $ mapErrors (P.embed bhv) formula'

data MemoKey = SemanticMemoKey | StructuralMemoKey
  deriving stock (Eq, Ord)