packages feed

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

module Data.OpenApi.Compare.Formula
  ( FormulaF,
    VarRef,
    variable,
    eitherOf,
    anError,
    errors,
    calculate,
    maxFixpoint,
    mapErrors,
  )
where

import Data.Kind
import qualified Data.List.NonEmpty as NE
import Data.Monoid
import Data.OpenApi.Compare.Paths
import qualified Data.OpenApi.Compare.PathsPrefixTree as P

type VarRef = Int

-- | The type @FormulaF f r ()@ describes (modulo contents of errors) boolean
-- formulas involving variables, conjunctions, and disjunctions. These
-- operations (and the generated algebra) are monotonous. This ensures that
-- fixpoints always exist, i.e. that @x = f x@ has at least one solution.
data FormulaF (q :: k -> k -> Type) (f :: k -> Type) (r :: k) (a :: Type) where
  Result :: a -> FormulaF q f r a
  Errors ::
    !(P.PathsPrefixTree q f r) ->
    -- | invariant: never empty
    FormulaF q f r a
  Apply ::
    FormulaF q f r (b -> c) ->
    FormulaF q f r b ->
    (c -> a) ->
    -- | invariant: at least one of LHS and RHS is not 'Errors', and they are
    -- both not 'Result'
    FormulaF q f r a
  SelectFirst ::
    NE.NonEmpty (SomeFormulaF b) ->
    !(P.PathsPrefixTree q f r) ->
    (b -> a) ->
    -- | invariant: the list doesn't contain any 'Result's, 'Errors' or
    -- 'SelectFirst'
    FormulaF q f r a
  Variable :: !VarRef -> a -> FormulaF q f r a

mkApply :: FormulaF q f r (b -> c) -> FormulaF q f r b -> (c -> a) -> FormulaF q f r a
mkApply (Result f) x h = h . f <$> x
mkApply f (Result x) h = h . ($ x) <$> f
mkApply (Errors e1) (Errors e2) _ = Errors (e1 <> e2)
mkApply f x h = Apply f x h

mkSelectFirst :: [SomeFormulaF b] -> P.PathsPrefixTree q f r -> (b -> a) -> FormulaF q f r a
mkSelectFirst fs allE h = case foldMap check fs of
  (First (Just x), _) -> Result (h x)
  (First Nothing, x : xs) -> SelectFirst (x NE.:| xs) allE h
  (First Nothing, []) -> Errors allE
  where
    check (SomeFormulaF (Result x)) = (First (Just x), mempty)
    check (SomeFormulaF (Errors _)) = (mempty, mempty)
    check (SomeFormulaF (SelectFirst xs _ h')) =
      (mempty, NE.toList (fmap (fmap h') xs))
    check x = (mempty, [x])

data SomeFormulaF (a :: Type) where
  SomeFormulaF :: FormulaF q f r a -> SomeFormulaF a

anError :: AnItem q f r -> FormulaF q f r a
anError e = Errors $ P.singleton e

errors :: P.PathsPrefixTree q f r -> FormulaF q f r ()
errors t
  | P.null t = Result ()
  | otherwise = Errors t

variable :: VarRef -> FormulaF q f r ()
variable v = Variable v ()

instance Functor (FormulaF q f r) where
  fmap f (Result x) = Result (f x)
  fmap _ (Errors e) = Errors e
  fmap f (Apply g x h) = Apply g x (f . h)
  fmap f (SelectFirst xs e h) = SelectFirst xs e (f . h)
  fmap f (Variable r x) = Variable r (f x)

instance Functor SomeFormulaF where
  fmap f (SomeFormulaF x) = SomeFormulaF (fmap f x)

instance Applicative (FormulaF q f r) where
  pure = Result
  f <*> x = mkApply f x id

eitherOf :: [FormulaF q' f' r' a] -> AnItem q f r -> FormulaF q f r a
eitherOf fs allE = mkSelectFirst (map SomeFormulaF fs) (P.singleton allE) id

calculate :: FormulaF q f r a -> Either (P.PathsPrefixTree q f r) a
calculate (Result x) = Right x
calculate (Errors e) = Left e
calculate (Apply f x h) = case calculate f of
  Left e1 -> case calculate x of
    Left e2 -> Left (e1 <> e2)
    Right _ -> Left e1
  Right f' -> case calculate x of
    Left e2 -> Left e2
    Right x' -> Right (h (f' x'))
calculate (SelectFirst xs e h) = go (NE.toList xs)
  where
    go (SomeFormulaF r : rs) = case calculate r of
      Left _ -> go rs
      Right x -> Right (h x)
    go [] = Left e
calculate (Variable i _) = error $ "Unknown variable " <> show i

-- Approximate for now. Answers yes/no correctly, but the error lists aren't
-- super accurate. TODO: improve
maxFixpoint :: VarRef -> FormulaF q f r () -> FormulaF q f r ()
maxFixpoint i = go
  where
    go :: FormulaF q f r a -> FormulaF q f r a
    go (Result x) = Result x
    go (Errors e) = Errors e
    go (Apply f x h) = mkApply (go f) (go x) h
    go (SelectFirst fs e h) = mkSelectFirst (NE.toList (fmap goSF fs)) e h
    go (Variable j x) | i == j = Result x
    go v@(Variable _ _) = v
    goSF :: SomeFormulaF a -> SomeFormulaF a
    goSF (SomeFormulaF x) = SomeFormulaF (go x)

mapErrors :: (P.PathsPrefixTree q f r -> P.PathsPrefixTree q' f' r') -> FormulaF q f r a -> FormulaF q' f' r' a
mapErrors _ (Result x) = Result x
mapErrors m (Errors e) = Errors $ m e
mapErrors m (Apply f x h) = mkApply (mapErrors m f) (mapErrors m x) h
mapErrors m (SelectFirst fs e h) = mkSelectFirst (NE.toList fs) (m e) h
mapErrors _ (Variable i x) = Variable i x