git.iko.soy/typeable/compaREST
on master help sign in
Add Trace (#17)

* Add Trace * Add fold and filter * Some tricky instances * Add Subtree * Add memoization and use a concrete monad

commitc951e3eb89902b0aaf5a86f35272c5d36d933a36 authormniip <mniip@users.noreply.github.com> dateThu 11 Mar 2021, 14:34 (6 years ago) parent1660766
5 files changed+423−1
openapi-diff.cabal+8−1 src/OpenAPI/Checker/Memo.hs+70 src/OpenAPI/Checker/Subtree.hs+111 src/OpenAPI/Checker/Trace.hs+101 src/OpenAPI/Checker/TracePrefixTree.hs+133
openapi-diff.cabal+8−1
@@ -43,8 +43,11 @@ common common-options
4343 , yaml
4444 , attoparsec
4545 , optparse-applicative
46+ , typerep-map
47+ , tagged
4648
47− default-extensions: ConstraintKinds
49+ default-extensions: BangPatterns
50+ , ConstraintKinds
4851 , DataKinds
4952 , DeriveAnyClass
5053 , DeriveFoldable
@@ -83,8 +86,12 @@ library
8386 import: common-options
8487 hs-source-dirs: src
8588 exposed-modules: OpenAPI.Checker.Aux
89+ , OpenAPI.Checker.Memo
8690 , OpenAPI.Checker.Report
8791 , OpenAPI.Checker.Run
92+ , OpenAPI.Checker.Subtree
93+ , OpenAPI.Checker.Trace
94+ , OpenAPI.Checker.TracePrefixTree
8895 , OpenAPI.Checker.Validate
8996 , OpenAPI.Checker.Validate.Dereference
9097 , OpenAPI.Checker.Validate.Monad
src/OpenAPI/Checker/Memo.hsnew file+70
@@ -0,0 +1,70 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+-- | Utilities for effectfully memoizing other, more effectful functions.
3+module OpenAPI.Checker.Memo
4+ ( Progress(..)
5+ , MonadMemo
6+ , MemoState
7+ , runMemo
8+ , tryMemo
9+ , tryMemoWithTag
10+ ) where
11+
12+import Control.Monad.State
13+import qualified Data.Map as M
14+import Data.Tagged
15+import qualified Data.TypeRepMap as T
16+import Type.Reflection
17+
18+-- | A computation has either 'Finished', or it is 'InProgress'.
19+data Progress a = Finished a | InProgress
20+ deriving (Eq, Ord, Show)
21+
22+data MemoMap a where
23+ MemoMap :: !(M.Map k (Progress v)) -> MemoMap (k, v)
24+
25+newtype MemoState = MemoState (T.TypeRepMap MemoMap)
26+
27+-- | An effectful memoization monad.
28+type MonadMemo m = MonadState MemoState m
29+
30+memoStateLookup
31+ :: forall k v. (Typeable k, Typeable v, Ord k)
32+ => k -> MemoState -> Maybe (Progress v)
33+memoStateLookup k (MemoState t) = case T.lookup @(k, v) t of
34+ Just (MemoMap m) -> M.lookup k m
35+ Nothing -> Nothing
36+
37+memoStateInsert
38+ :: forall k v. (Typeable k, Typeable v, Ord k)
39+ => k -> Progress v -> MemoState -> MemoState
40+memoStateInsert k x (MemoState t) = MemoState $ T.insert (MemoMap m'') t
41+ where
42+ m'' = M.insert k x m'
43+ m' = case T.lookup @(k, v) t of
44+ Just (MemoMap m) -> m
45+ Nothing -> M.empty
46+
47+-- | Run a memoized computation.
48+runMemo :: Monad m => StateT MemoState m a -> m a
49+runMemo = (`evalStateT` MemoState T.empty)
50+
51+-- | Try to call a function whilst memoizing its result. If we are already
52+-- inside an evaluation of some function with these arguments, this returns
53+-- @InProgress@. Beware of different functions that have the same type.
54+tryMemo
55+ :: forall k v m. (Typeable k, Typeable v, Ord k, MonadMemo m)
56+ => (k -> m v) -> k -> m (Progress v)
57+tryMemo f k = memoStateLookup k <$> get >>= \case
58+ Just x -> pure x
59+ Nothing -> do
60+ modify $ memoStateInsert k (InProgress @v)
61+ result <- Finished <$> f k
62+ modify $ memoStateInsert k result
63+ pure result
64+
65+-- | Disambiguate memoized computations with an arbitrary tag.
66+tryMemoWithTag
67+ :: forall t k v m. (Typeable t, Typeable k, Typeable v, Ord k, MonadMemo m)
68+ => (k -> m v) -> k -> m (Progress v)
69+tryMemoWithTag f k = withTypeable (typeRepKind $ typeRep @t) $
70+ tryMemo (f . unTagged) (Tagged @t k)
src/OpenAPI/Checker/Subtree.hsnew file+111
@@ -0,0 +1,111 @@
1+module OpenAPI.Checker.Subtree
2+ ( APIStep(..)
3+ , Subtree(..)
4+ , CompatM(..)
5+ , runCompatM
6+ , local'
7+ , warnIssue
8+ , throwIssue
9+ , warnIssueAt
10+ , throwIssueAt
11+ , throwWarnings
12+ , warnIssues
13+ ) where
14+
15+import Control.Monad.Error.Class
16+import Control.Monad.Identity
17+import Control.Monad.Reader.Class
18+import Control.Monad.Trans.Except (ExceptT, runExceptT)
19+import Control.Monad.Trans.Reader (ReaderT(..))
20+import Control.Monad.Trans.State (StateT)
21+import Control.Monad.Trans.Writer.Strict (WriterT, runWriterT)
22+import Control.Monad.Writer.Class
23+import Data.Kind
24+import Data.OpenApi
25+import Data.Text
26+import OpenAPI.Checker.Memo
27+import OpenAPI.Checker.Trace
28+import qualified OpenAPI.Checker.TracePrefixTree as T
29+
30+class (Subtree a, Subtree b, Steppable a b)
31+ => APIStep (a :: Type) (b :: Type) where
32+ describeStep :: Step a b -> Text
33+
34+data ProdCons a = ProdCons
35+ { producer :: a
36+ , consumer :: a
37+ } deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
38+
39+instance Applicative ProdCons where
40+ pure x = ProdCons x x
41+ ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)
42+
43+data TracedEnv t = TracedEnv
44+ { getTrace :: Trace OpenApi t
45+ , getEnv :: CheckEnv t
46+ }
47+
48+newtype CompatM t a = CompatM
49+ { unCompatM ::
50+ ReaderT (ProdCons (TracedEnv t))
51+ (ExceptT (T.TracePrefixTree CheckIssue OpenApi)
52+ (WriterT (T.TracePrefixTree CheckIssue OpenApi)
53+ (StateT MemoState Identity))) a
54+ } deriving newtype
55+ ( Functor, Applicative, Monad
56+ , MonadReader (ProdCons (TracedEnv t))
57+ , MonadError (T.TracePrefixTree CheckIssue OpenApi)
58+ , MonadWriter (T.TracePrefixTree CheckIssue OpenApi)
59+ )
60+
61+class (Ord t, Ord (CheckIssue t)) => Subtree (t :: Type) where
62+ type family CheckEnv t :: Type
63+ data family CheckIssue t :: Type
64+ checkCompatibility :: ProdCons t -> CompatM t a
65+
66+runCompatM :: ProdCons (TracedEnv t) -> CompatM t a ->
67+ ( Either (T.TracePrefixTree CheckIssue OpenApi) a -- unrecovarable errors
68+ , T.TracePrefixTree CheckIssue OpenApi ) -- warnings
69+runCompatM env = runIdentity . runMemo . runWriterT
70+ . runExceptT . (`runReaderT` env) . unCompatM
71+
72+local'
73+ :: ProdCons (Trace a b)
74+ -> (ProdCons (CheckEnv a) -> ProdCons (CheckEnv b))
75+ -> CompatM b x
76+ -> CompatM a x
77+local' xs wrapEnv (CompatM k) = CompatM $ ReaderT $ \env ->
78+ runReaderT k $ TracedEnv
79+ <$> (catTrace <$> (getTrace <$> env) <*> xs)
80+ <*> wrapEnv (getEnv <$> env)
81+
82+warnIssue :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatM t ()
83+warnIssue xs issue = tell $ T.singleton $ AnItem xs issue
84+
85+throwIssue :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatM t a
86+throwIssue xs issue = throwError $ T.singleton $ AnItem xs issue
87+
88+warnIssueAt
89+ :: Subtree t
90+ => (forall x. ProdCons x -> x)
91+ -> CheckIssue t
92+ -> CompatM t ()
93+warnIssueAt f issue = do
94+ xs <- asks $ getTrace . f
95+ warnIssue xs issue
96+
97+throwIssueAt
98+ :: Subtree t
99+ => (forall x. ProdCons x -> x)
100+ -> CheckIssue t
101+ -> CompatM t a
102+throwIssueAt f issue = do
103+ xs <- asks $ getTrace . f
104+ throwIssue xs issue
105+
106+throwWarnings :: CompatM t a -> CompatM t a
107+throwWarnings k = listen k >>= \case
108+ (x, issues) -> if T.null issues then pure x else throwError issues
109+
110+warnIssues :: CompatM t a -> CompatM t (Maybe a)
111+warnIssues k = catchError (Just <$> k) (\issues -> Nothing <$ tell issues)
src/OpenAPI/Checker/Trace.hsnew file+101
@@ -0,0 +1,101 @@
1+-- | Utilities for traversing heterogeneous trees. A heterogeneous tree is a
2+-- collection of datatypes that "contain" eachother in some form of tree
3+-- structure.
4+
5+module OpenAPI.Checker.Trace
6+ ( Steppable(..)
7+ , Trace(..)
8+ , catTrace
9+ , DiffTrace(..)
10+ , catDiffTrace
11+ , _DiffTrace
12+ , AnItem(..)
13+ ) where
14+
15+import Control.Lens
16+import Data.Kind
17+import Data.Type.Equality
18+import Type.Reflection
19+
20+class (Typeable a, Typeable b, Ord (Step a b))
21+ => Steppable (a :: k) (b :: k) where
22+ -- | How to get from an @a@ node to a @b@ node
23+ data family Step (a :: k) (b :: k) :: Type
24+
25+-- | How to get from an @a@ node to a @b@ node in possibly multiple steps. Like
26+-- a list, but indexed. The list is in reverse because in practice we append
27+-- items at the end one at a time.
28+data Trace (a :: k) (b :: k) where
29+ Root :: Trace a a
30+ Snoc :: Steppable b c => Trace a b -> !(Step b c) -> Trace a c
31+infixl 5 `Snoc`
32+
33+typeRepRHS :: Typeable b => Trace a b -> TypeRep b
34+typeRepRHS _ = typeRep
35+
36+typeRepLHS :: Typeable b => Trace a b -> TypeRep a
37+typeRepLHS Root = typeRep
38+typeRepLHS (Snoc xs _) = typeRepLHS xs
39+
40+instance TestEquality (Trace a) where
41+ testEquality Root Root = Just Refl
42+ testEquality Root (Snoc ys _) = testEquality (typeRepLHS ys) typeRep
43+ testEquality (Snoc xs _) Root = testEquality typeRep (typeRepLHS xs)
44+ testEquality (Snoc _ _) (Snoc _ _) = testEquality typeRep typeRep
45+
46+instance Eq (Trace a b) where
47+ Root == Root = True
48+ Snoc xs x == Snoc ys y
49+ | Just Refl <- testEquality (typeRepRHS xs) (typeRepRHS ys)
50+ = xs == ys && x == y
51+ _ == _ = False
52+
53+instance Ord (Trace a b) where
54+ compare Root Root = EQ
55+ compare Root (Snoc _ _) = LT
56+ compare (Snoc _ _) Root = GT
57+ compare (Snoc xs x) (Snoc ys y)
58+ = case testEquality (typeRepRHS xs) (typeRepRHS ys) of
59+ Just Refl -> compare xs ys <> compare x y
60+ Nothing -> compare (someTypeRep xs) (someTypeRep ys)
61+
62+catTrace :: Trace a b -> Trace b c -> Trace a c
63+catTrace xs Root = xs
64+catTrace xs (Snoc ys y) = Snoc (catTrace xs ys) y
65+infixl 5 `catTrace`
66+
67+-- | Like a differece list, but indexed.
68+newtype DiffTrace (a :: k) (b :: k)
69+ = DiffTrace (forall c. Trace c a -> Trace c b)
70+
71+catDiffTrace :: DiffTrace a b -> DiffTrace b c -> DiffTrace a c
72+catDiffTrace (DiffTrace f) (DiffTrace g) = DiffTrace (g . f)
73+
74+_DiffTrace :: Iso (DiffTrace a b) (DiffTrace c d) (Trace a b) (Trace c d)
75+_DiffTrace = dimap (\(DiffTrace f) -> f Root)
76+ $ fmap $ \xs -> DiffTrace (`catTrace` xs)
77+
78+-- | An item related to some path relative to the root @r@.
79+data AnItem (f :: k -> Type) (r :: k) where
80+ AnItem :: Ord (f a) => Trace r a -> !(f a) -> AnItem f r
81+ -- the Ord is yuck but we need it and it should be fine in monomorphic cases
82+
83+instance Eq (AnItem f r) where
84+ AnItem xs fx == AnItem ys fy
85+ | Just Refl <- testEquality xs ys
86+ = xs == ys && fx == fy
87+ _ == _ = False
88+
89+instance Typeable r => Ord (AnItem f r) where
90+ compare (AnItem xs fx) (AnItem ys fy)
91+ = case testEquality xs ys of
92+ Just Refl -> compare xs ys <> compare fx fy
93+ Nothing -> case xs of
94+ Root -> case ys of
95+ Root -> compare (someTypeRep xs) (someTypeRep ys)
96+ Snoc _ _ -> compare (someTypeRep xs) (someTypeRep ys)
97+ Snoc _ _ -> case ys of
98+ Root -> compare (someTypeRep xs) (someTypeRep ys)
99+ Snoc _ _ -> compare (someTypeRep xs) (someTypeRep ys)
100+
101+-- type APath = AnItem Proxy
src/OpenAPI/Checker/TracePrefixTree.hsnew file+133
@@ -0,0 +1,133 @@
1+{-# LANGUAGE QuantifiedConstraints #-}
2+module OpenAPI.Checker.TracePrefixTree
3+ ( TracePrefixTree
4+ , empty
5+ , singleton
6+ , fromList
7+ , null
8+ , foldWith
9+ , toList
10+ , filter
11+ ) where
12+
13+import qualified GHC.Exts as T
14+import Data.Foldable hiding (null, toList)
15+import Data.Kind
16+import qualified Data.Map as M
17+import Data.Monoid
18+import qualified Data.Set as S
19+import qualified Data.TypeRepMap as T
20+import OpenAPI.Checker.Trace
21+import Prelude hiding (null, filter)
22+import Type.Reflection
23+import Data.Type.Equality
24+
25+-- | A list of @AnItem r f@, but optimized into a prefix tree.
26+data TracePrefixTree (f :: k -> Type) (r :: k) = TracePrefixTree
27+ { rootItems :: !(ASet (f r))
28+ , snocItems :: !(T.TypeRepMap (AStep f r))
29+ }
30+
31+deriving instance Eq (TracePrefixTree f a)
32+
33+-- Kind of orphan. Treat the map as an infinite tuple of @Maybe (f a)@'s, where
34+-- the components are ordered by the @SomeTypeRep@ of the @a@.
35+compareTRM
36+ :: (forall a. Typeable a => Ord (f a))
37+ => T.TypeRepMap f -> T.TypeRepMap f -> Ordering
38+compareTRM s1 s2
39+ = foldMap (\k -> compareMaybe compareW (M.lookup k m1) (M.lookup k m2)) mKeys
40+ where
41+ (m1, m2) = (toMap s1, toMap s2)
42+ mKeys = S.toAscList $ M.keysSet m1 `S.union` M.keysSet m2
43+ compareMaybe _ Nothing Nothing = EQ
44+ compareMaybe _ Nothing (Just _) = LT
45+ compareMaybe _ (Just _) Nothing = GT
46+ compareMaybe cmp (Just x) (Just y) = cmp x y
47+ compareW
48+ :: (forall a. Typeable a => Ord (f a))
49+ => T.WrapTypeable f -> T.WrapTypeable f -> Ordering
50+ compareW (T.WrapTypeable (x :: f a)) (T.WrapTypeable (y :: f b))
51+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) = compare x y
52+ | otherwise = EQ -- unreachable
53+ toMap s = M.fromList
54+ [(someTypeRep x, w) | w@(T.WrapTypeable x) <- T.toList s]
55+
56+instance Ord (TracePrefixTree f a) where
57+ compare (TracePrefixTree r1 s1) (TracePrefixTree r2 s2)
58+ = compare r1 r2 <> compareTRM s1 s2
59+
60+data ASet (a :: Type) where
61+ AnEmptySet :: ASet a
62+ ASet :: Ord a => S.Set a -> ASet a
63+
64+instance Semigroup (ASet a) where
65+ AnEmptySet <> s = s
66+ s <> AnEmptySet = s
67+ ASet s1 <> ASet s2 = ASet $ S.union s1 s2
68+
69+deriving instance Eq (ASet a)
70+deriving instance Ord (ASet a)
71+
72+-- type traceprefixset = traceprefixtree proxy
73+
74+instance Monoid (ASet a) where
75+ mempty = AnEmptySet
76+
77+data AStep (f :: k -> Type) (r :: k) (a :: k) where
78+ AStep
79+ :: Steppable r a => !(M.Map (Step r a) (TracePrefixTree f a)) -> AStep f r a
80+
81+deriving instance Eq (AStep f r a)
82+deriving instance Ord (AStep f r a)
83+
84+singleton :: AnItem f r -> TracePrefixTree f r
85+singleton (AnItem ys v) = go ys $ TracePrefixTree (ASet $ S.singleton v) T.empty
86+ where
87+ go :: Trace r a -> TracePrefixTree f a -> TracePrefixTree f r
88+ go Root !t = t
89+ go (Snoc xs x) !t = go xs $ TracePrefixTree AnEmptySet $ T.one
90+ $ AStep $ M.singleton x t
91+
92+instance Semigroup (TracePrefixTree f r) where
93+ TracePrefixTree r1 s1 <> TracePrefixTree r2 s2 =
94+ TracePrefixTree (r1 <> r2) (T.unionWith joinSteps s1 s2)
95+ where joinSteps (AStep m1) (AStep m2) = AStep $ M.unionWith (<>) m1 m2
96+
97+instance Monoid (TracePrefixTree f r) where
98+ mempty = TracePrefixTree mempty T.empty
99+
100+empty :: TracePrefixTree f r
101+empty = mempty
102+
103+fromList :: [AnItem f r] -> TracePrefixTree f r
104+fromList = foldMap singleton
105+
106+null :: TracePrefixTree f r -> Bool
107+null (TracePrefixTree AnEmptySet s) = T.size s == 0
108+null _ = False
109+
110+foldWith
111+ :: forall f m r. Monoid m
112+ => (forall a. Ord (f a) => Trace r a -> f a -> m) -> TracePrefixTree f r -> m
113+foldWith k = goTPT Root
114+ where
115+ goTPT :: forall a. Trace r a -> TracePrefixTree f a -> m
116+ goTPT xs t = goASet xs (rootItems t) <> goTRM xs (snocItems t)
117+ goASet :: forall a. Trace r a -> ASet (f a) -> m
118+ goASet _ AnEmptySet = mempty
119+ goASet xs (ASet rs) = foldMap (k xs) rs
120+ goTRM :: forall a. Trace r a -> T.TypeRepMap (AStep f a) -> m
121+ goTRM xs s = foldMap (\(T.WrapTypeable f) -> goAStep xs f) $ T.toList s
122+ goAStep :: forall a b. Trace r a -> AStep f a b -> m
123+ goAStep xs (AStep m)
124+ = M.foldrWithKey (\x t -> (goTPT (Snoc xs x) t <>)) mempty m
125+
126+toList :: TracePrefixTree f r -> [AnItem f r]
127+toList t = appEndo (foldWith (\xs f -> Endo (AnItem xs f:)) t) []
128+
129+-- | Select a subtree by prefix
130+filter :: Trace r a -> TracePrefixTree f r -> TracePrefixTree f a
131+filter Root t = t
132+filter (Snoc xs x) t = foldMap (\(AStep m) -> fold $ M.lookup x m)
133+ $ T.lookup $ snocItems $ filter xs t