git.iko.soy/typeable/compaREST
on master help sign in
Path fragments and OpenApi (#22)

* wip * Things are now becoming fun * Cleaned things up a bit * Moved things around * Added some comments * Sorted imports * Cleaned things up * Switched to google formatting * Fixed some of the comments * updated gitignore * Use list comprehension * Removed catTrace * Fixed yet more comments

commit94d03acb126e5077f4ca48d90762a0416c10ffe9 authoriko <ilyakooo0@gmail.com> dateThu 25 Mar 2021, 19:02 (6 years ago) parentf53aae3
12 files changed+491−75
.gitignore+1 openapi-diff.cabal+17−8 src/Data/HList.hs+10−6 src/OpenAPI/Checker/Formula.hs+1−1 src/OpenAPI/Checker/Orphans.hs+15 src/OpenAPI/Checker/References.hs+24 src/OpenAPI/Checker/Subtree.hs+55−30 src/OpenAPI/Checker/Trace.hs+58−30 src/OpenAPI/Checker/Validate/OpenApi.hs+168 src/OpenAPI/Checker/Validate/Operation.hs+26 src/OpenAPI/Checker/Validate/Param.hs+13 src/OpenAPI/Checker/Validate/PathFragment.hs+103
.gitignore+1
@@ -1,2 +1,3 @@
11.stack-work
22TAGS
3+.vscode
openapi-diff.cabal+17−8
@@ -31,22 +31,24 @@ common common-options
3131 default-language: Haskell2010
3232 build-depends: base >= 4.12.0.0 && < 4.15
3333 , aeson
34− , deriving-aeson
34+ , attoparsec
3535 , containers
36+ , deriving-aeson
37+ , generic-data
3638 , generic-monoid
3739 , insert-ordered-containers
3840 , lens
3941 , mtl
4042 , openapi3
43+ , optparse-applicative
44+ , tagged
4145 , text
4246 , transformers
43− , yaml
44− , attoparsec
45− , optparse-applicative
4647 , typerep-map
47− , tagged
48+ , yaml
4849
49− default-extensions: BangPatterns
50+ default-extensions: ApplicativeDo
51+ , BangPatterns
5052 , ConstraintKinds
5153 , DataKinds
5254 , DeriveAnyClass
@@ -57,6 +59,7 @@ common common-options
5759 , DerivingStrategies
5860 , DerivingVia
5961 , DuplicateRecordFields
62+ , EmptyDataDeriving
6063 , FlexibleContexts
6164 , FlexibleInstances
6265 , FunctionalDependencies
@@ -86,8 +89,12 @@ library
8689 import: common-options
8790 hs-source-dirs: src
8891 exposed-modules: OpenAPI.Checker.Aux
92+ , Data.HList
8993 , OpenAPI.Checker.Formula
9094 , OpenAPI.Checker.Memo
95+ , OpenAPI.Checker.Options
96+ , OpenAPI.Checker.Orphans
97+ , OpenAPI.Checker.References
9198 , OpenAPI.Checker.Report
9299 , OpenAPI.Checker.Run
93100 , OpenAPI.Checker.Subtree
@@ -96,8 +103,10 @@ library
96103 , OpenAPI.Checker.Validate
97104 , OpenAPI.Checker.Validate.Dereference
98105 , OpenAPI.Checker.Validate.Monad
99− , OpenAPI.Checker.Options
100− , Data.HList
106+ , OpenAPI.Checker.Validate.OpenApi
107+ , OpenAPI.Checker.Validate.Operation
108+ , OpenAPI.Checker.Validate.Param
109+ , OpenAPI.Checker.Validate.PathFragment
101110
102111executable openapi-diff
103112 import: common-options
src/Data/HList.hs+10−6
@@ -1,8 +1,9 @@
11module Data.HList
2− ( Has,
3− HasAll,
4− getH,
5− HList (..),
2+ ( Has
3+ , HasAll
4+ , getH
5+ , HList (..)
6+ , singletonH
67 )
78where
89
@@ -33,7 +34,10 @@ instance (Has' x xs t, HeadEq x (y : xs) ~ 'False) => Has' x (y ': xs) 'False wh
3334 getH (HCons _ xs) = getH xs
3435
3536instance
36− TypeError ( 'ShowType x ':<>: 'Text " is not a part of the list.") =>
37− Has' x '[] 'False
37+ TypeError ('ShowType x ':<>: 'Text " is not a part of the list.")
38+ => Has' x '[] 'False
3839 where
3940 getH HNil = undefined
41+
42+singletonH :: a -> HList '[a]
43+singletonH a = a `HCons` HNil
src/OpenAPI/Checker/Formula.hs+1−1
@@ -42,7 +42,7 @@ mkApply f x h = Apply f x h
4242mkSelectFirst :: [SomeFormulaF b] -> AnItem f r -> (b -> a) -> FormulaF f r a
4343mkSelectFirst fs allE h = case foldMap check fs of
4444 (First (Just x), _) -> Result (h x)
45− (First Nothing, (x:xs)) -> SelectFirst (x NE.:| xs) allE h
45+ (First Nothing, x:xs) -> SelectFirst (x NE.:| xs) allE h
4646 (First Nothing, []) -> Errors $ T.singleton allE
4747 where
4848 check (SomeFormulaF (Result x)) = (First (Just x), mempty)
src/OpenAPI/Checker/Orphans.hsnew file+15
@@ -0,0 +1,15 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Orphans (Step (..)) where
4+
5+import Data.OpenApi
6+import Data.Typeable
7+import OpenAPI.Checker.Trace
8+
9+deriving newtype instance Ord Reference
10+
11+instance Typeable a => Steppable (Referenced a) a where
12+ data Step (Referenced a) a
13+ = InlineStep
14+ | ReferencedStep Reference
15+ deriving (Eq, Ord)
src/OpenAPI/Checker/References.hsnew file+24
@@ -0,0 +1,24 @@
1+module OpenAPI.Checker.References
2+ ( TracedReferences
3+ , dereferenceTraced
4+ )
5+where
6+
7+import qualified Data.HashMap.Strict.InsOrd as IOHM
8+import Data.Map (Map)
9+import Data.Maybe
10+import Data.OpenApi
11+import Data.Typeable
12+import OpenAPI.Checker.Orphans
13+import OpenAPI.Checker.Trace
14+
15+type TracedReferences root a = Map Reference (Traced root a)
16+
17+dereferenceTraced
18+ :: Typeable a
19+ => Definitions a
20+ -> Referenced a
21+ -> Traced (Referenced a) a
22+dereferenceTraced _ (Inline a) = Traced (step InlineStep) a
23+dereferenceTraced defs (Ref r@(Reference ref)) =
24+ Traced (step $ ReferencedStep r) (fromJust $ IOHM.lookup ref defs)
src/OpenAPI/Checker/Subtree.hs+55−30
@@ -1,16 +1,19 @@
11module OpenAPI.Checker.Subtree
2− ( APIStep(..)
3− , Subtree(..)
4− , CompatM(..)
2+ ( APIStep (..)
3+ , Subtree (..)
4+ , CompatM (..)
55 , CompatFormula
6+ , ProdCons (..)
67 , runCompatFormula
78 , localM
8− , local'
9+ , localTrace
910 , anyOfM
11+ , anyOfAt
1012 , issueAtTrace
1113 , issueAt
1214 , memo
13− ) where
15+ )
16+where
1417
1518import Control.Monad.Identity
1619import Control.Monad.Reader
@@ -26,57 +29,67 @@ import OpenAPI.Checker.Memo
2629import OpenAPI.Checker.Trace
2730import qualified OpenAPI.Checker.TracePrefixTree as T
2831
29−class (Subtree a, Subtree b, Steppable a b)
30− => APIStep (a :: Type) (b :: Type) where
32+class
33+ (Subtree a, Subtree b, Steppable a b) =>
34+ APIStep (a :: Type) (b :: Type)
35+ where
3136 describeStep :: Step a b -> Text
3237
3338data ProdCons a = ProdCons
3439 { producer :: a
3540 , consumer :: a
36− } deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
41+ }
42+ deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
3743
3844instance Applicative ProdCons where
3945 pure x = ProdCons x x
4046 ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)
4147
4248newtype CompatM t a = CompatM
43− { unCompatM ::
44− ReaderT (ProdCons (Trace OpenApi t))
45− (StateT (MemoState VarRef) Identity) a
46− } deriving newtype
47− ( Functor, Applicative, Monad
49+ { unCompatM
50+ :: ReaderT
51+ (ProdCons (Trace OpenApi t))
52+ (StateT (MemoState VarRef) Identity)
53+ a
54+ }
55+ deriving newtype
56+ ( Functor
57+ , Applicative
58+ , Monad
4859 , MonadReader (ProdCons (Trace OpenApi t))
4960 , MonadState (MemoState VarRef)
5061 )
5162
5263type CompatFormula t = Compose (CompatM t) (FormulaF CheckIssue OpenApi)
5364
54−class (Typeable t, Ord t, Ord (CheckIssue t)) => Subtree (t :: Type) where
55− type family CheckEnv t :: [Type]
56− data family CheckIssue t :: Type
65+class (Typeable t, Ord (CheckIssue t)) => Subtree (t :: Type) where
66+ type CheckEnv t :: [Type]
67+ data CheckIssue t :: Type
68+
5769 -- | If we ever followed a reference, reroute the path through "components"
5870 normalizeTrace :: Trace OpenApi t -> Trace OpenApi t
71+
5972 checkCompatibility :: HasAll (CheckEnv t) xs => HList xs -> ProdCons t -> CompatFormula t ()
6073
6174runCompatFormula
6275 :: ProdCons (Trace OpenApi t)
6376 -> Compose (CompatM t) (FormulaF f r) a
6477 -> Either (T.TracePrefixTree f r) a
65−runCompatFormula env (Compose f)
66− = calculate . runIdentity . runMemo 0 . (`runReaderT` env) . unCompatM $ f
78+runCompatFormula env (Compose f) =
79+ calculate . runIdentity . runMemo 0 . (`runReaderT` env) . unCompatM $ f
6780
6881localM
6982 :: ProdCons (Trace a b)
7083 -> CompatM b x
7184 -> CompatM a x
72−localM xs (CompatM k) =
73− CompatM $ ReaderT $ \env -> runReaderT k (catTrace <$> env <*> xs)
85+localM xs (CompatM k) =
86+ CompatM $ ReaderT $ \env -> runReaderT k ((>>>) <$> env <*> xs)
7487
75−local'
88+localTrace
7689 :: ProdCons (Trace a b)
7790 -> Compose (CompatM b) (FormulaF f r) x
7891 -> Compose (CompatM a) (FormulaF f r) x
79−local' xs (Compose h) = Compose (localM xs h)
92+localTrace xs (Compose h) = Compose (localM xs h)
8093
8194issueAtTrace
8295 :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatFormula t a
@@ -93,20 +106,32 @@ issueAt f issue = Compose $ do
93106
94107anyOfM
95108 :: Ord (f t)
96− => Trace r t -> f t
109+ => Trace r t
110+ -> f t
97111 -> [Compose (CompatM t) (FormulaF f r) a]
98112 -> Compose (CompatM t) (FormulaF f r) a
99−anyOfM xs issue fs
100− = Compose $ (`eitherOf` AnItem xs issue) <$> sequenceA (getCompose <$> fs)
113+anyOfM xs issue fs =
114+ Compose $ (`eitherOf` AnItem xs issue) <$> sequenceA (getCompose <$> fs)
115+
116+anyOfAt
117+ :: Subtree t
118+ => (forall x. ProdCons x -> x)
119+ -> CheckIssue t
120+ -> [CompatFormula t a]
121+ -> CompatFormula t a
122+anyOfAt f issue fs = Compose $ do
123+ xs <- asks f
124+ (`eitherOf` AnItem xs issue) <$> sequenceA (getCompose <$> fs)
101125
102126fixpointKnot
103127 :: MonadState (MemoState VarRef) m
104128 => KnotTier (FormulaF f r ()) VarRef m
105−fixpointKnot = KnotTier
106− { onKnotFound = modifyMemoNonce succ
107− , onKnotUsed = \i -> pure $ variable i
108− , tieKnot = \i x -> pure $ maxFixpoint i x
109− }
129+fixpointKnot =
130+ KnotTier
131+ { onKnotFound = modifyMemoNonce succ
132+ , onKnotUsed = \i -> pure $ variable i
133+ , tieKnot = \i x -> pure $ maxFixpoint i x
134+ }
110135
111136memo :: Subtree t => CompatFormula t () -> CompatFormula t ()
112137memo (Compose f) = Compose $ do
src/OpenAPI/Checker/Trace.hs+58−30
@@ -1,26 +1,38 @@
11-- | Utilities for traversing heterogeneous trees. A heterogeneous tree is a
22-- collection of datatypes that "contain" eachother in some form of tree
33-- structure.
4−
54module OpenAPI.Checker.Trace
6− ( Steppable(..)
7− , Trace(..)
8− , catTrace
9− , DiffTrace(..)
5+ ( Steppable (..)
6+ , Trace (..)
7+ , DiffTrace (..)
108 , catDiffTrace
119 , _DiffTrace
12− , AnItem(..)
13− ) where
14−
10+ , AnItem (..)
11+ , step
12+ , Traced (..)
13+ , mapTraced
14+ , retrace
15+ , deTraced
16+
17+ -- * Reexports
18+ , (>>>)
19+ , (<<<)
20+ )
21+where
22+
23+import Control.Category
1524import Control.Lens
1625import Data.Kind
1726import Data.Type.Equality
1827import Type.Reflection
28+import Prelude hiding ((.))
1929
20−class (Typeable a, Typeable b, Ord (Step a b))
21− => Steppable (a :: k) (b :: k) where
30+class
31+ (Typeable a, Typeable b, Ord (Step a b)) =>
32+ Steppable (a :: k) (b :: k)
33+ where
2234 -- | How to get from an @a@ node to a @b@ node
23− data family Step (a :: k) (b :: k) :: Type
35+ data Step (a :: k) (b :: k) :: Type
2436
2537-- | How to get from an @a@ node to a @b@ node in possibly multiple steps. Like
2638-- a list, but indexed. The list is in reverse because in practice we append
@@ -28,8 +40,17 @@ class (Typeable a, Typeable b, Ord (Step a b))
2840data Trace (a :: k) (b :: k) where
2941 Root :: Trace a a
3042 Snoc :: Steppable b c => Trace a b -> !(Step b c) -> Trace a c
43+
3144infixl 5 `Snoc`
3245
46+step :: Steppable a b => Step a b -> Trace a b
47+step s = Root `Snoc` s
48+
49+instance Category Trace where
50+ id = Root
51+ Root . xs = xs
52+ (Snoc ys y) . xs = Snoc (ys . xs) y
53+
3354typeRepRHS :: Typeable b => Trace a b -> TypeRep b
3455typeRepRHS _ = typeRep
3556
@@ -46,24 +67,19 @@ instance TestEquality (Trace a) where
4667instance Eq (Trace a b) where
4768 Root == Root = True
4869 Snoc xs x == Snoc ys y
49− | Just Refl <- testEquality (typeRepRHS xs) (typeRepRHS ys)
50− = xs == ys && x == y
70+ | Just Refl <- testEquality (typeRepRHS xs) (typeRepRHS ys) =
71+ xs == ys && x == y
5172 _ == _ = False
5273
5374instance Ord (Trace a b) where
5475 compare Root Root = EQ
5576 compare Root (Snoc _ _) = LT
5677 compare (Snoc _ _) Root = GT
57− compare (Snoc xs x) (Snoc ys y)
58− = case testEquality (typeRepRHS xs) (typeRepRHS ys) of
78+ compare (Snoc xs x) (Snoc ys y) =
79+ case testEquality (typeRepRHS xs) (typeRepRHS ys) of
5980 Just Refl -> compare xs ys <> compare x y
6081 Nothing -> compare (someTypeRep xs) (someTypeRep ys)
6182
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−
6783-- | Like a differece list, but indexed.
6884newtype DiffTrace (a :: k) (b :: k)
6985 = DiffTrace (forall c. Trace c a -> Trace c b)
@@ -72,30 +88,42 @@ catDiffTrace :: DiffTrace a b -> DiffTrace b c -> DiffTrace a c
7288catDiffTrace (DiffTrace f) (DiffTrace g) = DiffTrace (g . f)
7389
7490_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)
91+_DiffTrace = dimap (\(DiffTrace f) -> f Root) $
92+ fmap $ \xs -> DiffTrace (>>> xs)
7793
7894-- | An item related to some path relative to the root @r@.
7995data AnItem (f :: k -> Type) (r :: k) where
8096 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
97+
98+-- the Ord is yuck but we need it and it should be fine in monomorphic cases
8299
83100instance Eq (AnItem f r) where
84101 AnItem xs fx == AnItem ys fy
85− | Just Refl <- testEquality xs ys
86− = xs == ys && fx == fy
102+ | Just Refl <- testEquality xs ys =
103+ xs == ys && fx == fy
87104 _ == _ = False
88105
89106instance Typeable r => Ord (AnItem f r) where
90− compare (AnItem xs fx) (AnItem ys fy)
91− = case testEquality xs ys of
107+ compare (AnItem xs fx) (AnItem ys fy) =
108+ case testEquality xs ys of
92109 Just Refl -> compare xs ys <> compare fx fy
93110 Nothing -> case xs of
94− Root -> case ys of
95− Root -> compare (someTypeRep xs) (someTypeRep ys)
111+ Root -> case ys of
112+ Root -> compare (someTypeRep xs) (someTypeRep ys)
96113 Snoc _ _ -> compare (someTypeRep xs) (someTypeRep ys)
97114 Snoc _ _ -> case ys of
98− Root -> compare (someTypeRep xs) (someTypeRep ys)
115+ Root -> compare (someTypeRep xs) (someTypeRep ys)
99116 Snoc _ _ -> compare (someTypeRep xs) (someTypeRep ys)
100117
118+data Traced r a = Traced {getTrace :: Trace r a, getTraced :: a}
119+
120+mapTraced :: (Trace r a -> Trace r b) -> (a -> b) -> Traced r a -> Traced r b
121+mapTraced f g (Traced t a) = Traced (f t) (g a)
122+
123+retrace :: (Trace r a -> Trace r' a) -> Traced r a -> Traced r' a
124+retrace f (Traced t a) = Traced (f t) a
125+
126+deTraced :: Traced r a -> (Trace r a, a)
127+deTraced (Traced a b) = (a, b)
128+
101129-- type APath = AnItem Proxy
src/OpenAPI/Checker/Validate/OpenApi.hsnew file+168
@@ -0,0 +1,168 @@
1+{-# LANGUAGE PartialTypeSignatures #-}
2+{-# OPTIONS_GHC -Wno-orphans #-}
3+
4+module OpenAPI.Checker.Validate.OpenApi
5+ (
6+ )
7+where
8+
9+import Data.Foldable
10+import Data.HList
11+import qualified Data.HashMap.Strict.InsOrd as IOHM
12+import Data.Maybe
13+import Data.OpenApi
14+import Generic.Data
15+import OpenAPI.Checker.Subtree
16+import OpenAPI.Checker.Trace
17+import OpenAPI.Checker.Validate.Operation
18+import OpenAPI.Checker.Validate.PathFragment
19+
20+instance Subtree OpenApi where
21+ type CheckEnv OpenApi = '[]
22+ data CheckIssue OpenApi = NoPathsMatched | WrongNumberOfFragments
23+ deriving (Eq, Ord)
24+ normalizeTrace = undefined
25+ checkCompatibility _ prodCons = do
26+ let ProdCons {producer = p, consumer = c} = processOpenApi <$> prodCons
27+ sequenceA_
28+ [ anyOfAt
29+ producer
30+ NoPathsMatched
31+ [ localTrace (step <$> ProdCons pSPath cSPath) $ do
32+ -- make sure every path fragment is compatible
33+ sequenceA_
34+ [ localTrace (pure . step $ PathFragmentStep i) $
35+ checkCompatibility (singletonH $ ProdCons pPathFragmentParams cPathFragmentParams) pair
36+ | (i, pair) <- zip [0 ..] pathFragments
37+ ]
38+ -- make sure the operation is compatible.
39+ localTrace (pure . step $ getter stepProcessedPathItem) $
40+ checkCompatibility HNil $ ProdCons pOperation cOperation
41+ pure ()
42+ | (cSPath, cPath, cPathItem) <- c
43+ , -- ... and try to match it with every endpoint in the consumer.
44+ --
45+ -- This is required because the meaning of path fragments can change on
46+ -- a per-method basis even within the same 'PathItem'
47+ --
48+ -- Here we only need to look for the method that the current producer
49+ -- endpoint is using.
50+ (cParams, cOperation) <- maybeToList $ getter cPathItem
51+ , let cPathFragmentParams = retrace (step PathFragmentParentStep >>>) <$> cParams
52+ , -- make sure the paths are the same length
53+ pathFragments <- maybeToList $ zipAllWith ProdCons pPath cPath
54+ ]
55+ | (pSPath, pPath, pPathItem) <- p
56+ , -- look at every endpoint in the producer ...
57+ (ProcessedPathItemGetter getter, (pParams, pOperation)) <-
58+ toList (fmap . (,) <$> processedPathItemGetters <*> pPathItem) >>= maybeToList
59+ , let pPathFragmentParams = retrace (step PathFragmentParentStep >>>) <$> pParams
60+ ]
61+
62+zipAllWith :: (a -> b -> c) -> [a] -> [b] -> Maybe [c]
63+zipAllWith _ [] [] = Just []
64+zipAllWith f (x : xs) (y : ys) = (f x y :) <$> zipAllWith f xs ys
65+zipAllWith _ (_ : _) [] = Nothing
66+zipAllWith _ [] (_ : _) = Nothing
67+
68+processOpenApi
69+ :: OpenApi
70+ -> [ ( Step OpenApi PathItem
71+ , [PathFragment]
72+ , ForeachOperation (Maybe (TracedReferences PathItem Param, Operation))
73+ )
74+ ]
75+processOpenApi o = do
76+ let cs = _openApiComponents o
77+ (pathS, pathItem) <- IOHM.toList . _openApiPaths $ o
78+ let path = parsePath pathS
79+ componentParams = _componentsParameters cs
80+ commonPathParams =
81+ retrace (step PathItemParametersStep >>>)
82+ <$> getPathParamRefs componentParams (_pathItemParameters pathItem)
83+ processOperation (s :: Step PathItem Operation) op =
84+ let operationParams =
85+ retrace (Root `Snoc` s `Snoc` OperationParametersStep >>>)
86+ <$> getPathParamRefs componentParams (_operationParameters op)
87+ pathParams =
88+ operationParams <> commonPathParams
89+ in (pathParams, op)
90+ pure
91+ ( PathStep pathS
92+ , path
93+ , fmap . processOperation
94+ <$> stepProcessedPathItem
95+ <*> ForeachOperation
96+ { processedPathItemGet = _pathItemGet pathItem
97+ , processedPathItemPut = _pathItemPut pathItem
98+ , processedPathItemPost = _pathItemPost pathItem
99+ , processedPathItemDelete = _pathItemDelete pathItem
100+ , processedPathItemOptions = _pathItemOptions pathItem
101+ , processedPathItemHead = _pathItemHead pathItem
102+ , processedPathItemPatch = _pathItemPatch pathItem
103+ , processedPathItemTrace = _pathItemTrace pathItem
104+ }
105+ )
106+
107+instance Steppable OpenApi PathItem where
108+ data Step OpenApi PathItem = PathStep FilePath
109+ deriving (Eq, Ord)
110+
111+instance Steppable PathItem PathFragment where
112+ data Step PathItem PathFragment
113+ = -- | The index of the path item
114+ PathFragmentStep Int
115+ deriving (Eq, Ord)
116+
117+instance Steppable PathFragment PathItem where
118+ data Step PathFragment PathItem = PathFragmentParentStep
119+ deriving (Eq, Ord)
120+
121+instance Steppable PathItem (Referenced Param) where
122+ data Step PathItem (Referenced Param) = PathItemParametersStep
123+ deriving (Eq, Ord)
124+
125+instance Steppable Operation (Referenced Param) where
126+ data Step Operation (Referenced Param) = OperationParametersStep
127+ deriving (Eq, Ord)
128+
129+data ForeachOperation a = ForeachOperation
130+ { processedPathItemGet :: a
131+ , processedPathItemPut :: a
132+ , processedPathItemPost :: a
133+ , processedPathItemDelete :: a
134+ , processedPathItemOptions :: a
135+ , processedPathItemHead :: a
136+ , processedPathItemPatch :: a
137+ , processedPathItemTrace :: a
138+ }
139+ deriving stock (Functor, Generic1)
140+ deriving (Applicative, Foldable) via Generically1 ForeachOperation
141+
142+newtype ProcessedPathItemGetter = ProcessedPathItemGetter (forall a. ForeachOperation a -> a)
143+
144+processedPathItemGetters :: ForeachOperation ProcessedPathItemGetter
145+processedPathItemGetters =
146+ ForeachOperation
147+ { processedPathItemGet = ProcessedPathItemGetter processedPathItemGet
148+ , processedPathItemPut = ProcessedPathItemGetter processedPathItemPut
149+ , processedPathItemPost = ProcessedPathItemGetter processedPathItemPost
150+ , processedPathItemDelete = ProcessedPathItemGetter processedPathItemDelete
151+ , processedPathItemOptions = ProcessedPathItemGetter processedPathItemOptions
152+ , processedPathItemHead = ProcessedPathItemGetter processedPathItemHead
153+ , processedPathItemPatch = ProcessedPathItemGetter processedPathItemPatch
154+ , processedPathItemTrace = ProcessedPathItemGetter processedPathItemTrace
155+ }
156+
157+stepProcessedPathItem :: ForeachOperation (Step PathItem Operation)
158+stepProcessedPathItem =
159+ ForeachOperation
160+ { processedPathItemGet = GetStep
161+ , processedPathItemPut = PutStep
162+ , processedPathItemPost = PostStep
163+ , processedPathItemDelete = DeleteStep
164+ , processedPathItemOptions = OptionsStep
165+ , processedPathItemHead = HeadStep
166+ , processedPathItemPatch = PatchStep
167+ , processedPathItemTrace = TraceStep
168+ }
src/OpenAPI/Checker/Validate/Operation.hsnew file+26
@@ -0,0 +1,26 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.Operation (Step (..)) where
4+
5+import Data.OpenApi
6+import OpenAPI.Checker.Subtree
7+import OpenAPI.Checker.Trace
8+
9+instance Subtree Operation where
10+ type CheckEnv Operation = '[]
11+ data CheckIssue Operation
12+ deriving (Eq, Ord)
13+ normalizeTrace = undefined
14+ checkCompatibility = undefined
15+
16+instance Steppable PathItem Operation where
17+ data Step PathItem Operation
18+ = GetStep
19+ | PutStep
20+ | PostStep
21+ | DeleteStep
22+ | OptionsStep
23+ | HeadStep
24+ | PatchStep
25+ | TraceStep
26+ deriving (Eq, Ord)
src/OpenAPI/Checker/Validate/Param.hsnew file+13
@@ -0,0 +1,13 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.Param () where
4+
5+import Data.OpenApi
6+import OpenAPI.Checker.Subtree
7+
8+instance Subtree Param where
9+ type CheckEnv Param = '[]
10+ data CheckIssue Param
11+ deriving (Eq, Ord)
12+ normalizeTrace = undefined
13+ checkCompatibility = undefined
src/OpenAPI/Checker/Validate/PathFragment.hsnew file+103
@@ -0,0 +1,103 @@
1+module OpenAPI.Checker.Validate.PathFragment
2+ ( PathParamRefs
3+ , TracedReferences
4+ , getPathParamRefs
5+ , parsePath
6+ , PathFragment (..)
7+ )
8+where
9+
10+import Control.Lens
11+import Control.Monad.Reader
12+import qualified Data.Aeson as A
13+import Data.HList
14+import qualified Data.List.NonEmpty as NE
15+import Data.Map.Strict (Map)
16+import qualified Data.Map.Strict as M
17+import Data.Maybe
18+import Data.OpenApi
19+import Data.Text (Text)
20+import qualified Data.Text as T
21+import OpenAPI.Checker.References
22+import OpenAPI.Checker.Subtree
23+import OpenAPI.Checker.Trace
24+import OpenAPI.Checker.Validate.Param ()
25+
26+getPathParamRefs
27+ :: Definitions Param
28+ -> [Referenced Param]
29+ -> Map Reference (Traced (Referenced Param) Param)
30+getPathParamRefs defs xs =
31+ M.fromList $ do
32+ x <- xs
33+ let (Traced t param) = dereferenceTraced defs x
34+ guard (_paramIn param == ParamPath)
35+ return (Reference $ _paramName param, Traced t param)
36+
37+-- TODO: templates can be only part of the PathFragment. Currently only supports templates as full PathFragment.
38+-- https://github.com/typeable/openapi-diff/issues/23
39+parsePath :: FilePath -> [PathFragment]
40+parsePath = fmap partition . T.splitOn "/" . T.pack
41+ where
42+ partition :: Text -> PathFragment
43+ partition t
44+ | Just ('{', rest) <- T.uncons t
45+ , Just (ref, '}') <- T.unsnoc rest =
46+ DynamicPath $ Reference ref
47+ partition t = StaticPath t
48+
49+data PathFragment
50+ = StaticPath Text
51+ | DynamicPath Reference
52+ deriving stock (Eq, Ord)
53+
54+instance Steppable PathFragment Param where
55+ data Step PathFragment Param = StaticPathParam
56+ deriving (Eq, Ord)
57+
58+type PathParamRefs = TracedReferences PathFragment Param
59+
60+instance Subtree PathFragment where
61+ type CheckEnv PathFragment = '[ProdCons PathParamRefs]
62+ data CheckIssue PathFragment = PathFragmentsDontMatch Text Text
63+ deriving (Eq, Ord)
64+
65+ normalizeTrace = undefined
66+
67+ -- This case isn't strictly needed. It is here for optimization.
68+ checkCompatibility _ ProdCons {producer = (StaticPath x), consumer = (StaticPath y)} =
69+ if x == y
70+ then pure ()
71+ else issueAt consumer (PathFragmentsDontMatch x y)
72+ checkCompatibility env prodCons = do
73+ let (t, param) =
74+ fsplit . fmap deTraced $
75+ dePathFragment
76+ <$> (singletonH <$> getH @(ProdCons PathParamRefs) env)
77+ <*> prodCons
78+ localTrace t $ checkCompatibility env param
79+
80+-- | A clearer name for 'NE.unzip' that can be used without qualifying it.
81+fsplit :: Functor f => f (a, b) -> (f a, f b)
82+fsplit = NE.unzip
83+
84+dePathFragment :: Has PathParamRefs xs => HList xs -> PathFragment -> Traced PathFragment Param
85+dePathFragment (getH @PathParamRefs -> params) = \case
86+ (StaticPath s) ->
87+ Traced (step StaticPathParam) $
88+ mempty
89+ { _paramRequired = Just True
90+ , _paramIn = ParamPath
91+ , _paramAllowEmptyValue = Just False
92+ , _paramAllowReserved = Just False
93+ , _paramSchema = Just $ Inline $ staticStringSchema s
94+ }
95+ (DynamicPath ref) -> M.lookup ref params & fromMaybe (error $ show ref <> " not found.")
96+
97+staticStringSchema :: Text -> Schema
98+staticStringSchema t =
99+ mempty
100+ { _schemaNullable = Just False
101+ , _schemaType = Just OpenApiString
102+ , _schemaEnum = Just [A.String t]
103+ }