git.iko.soy/typeable/compaREST
on master help sign in
Added callback errors (#65)
commitb5ed0cb04a93ae234352a3a7b9230ac002d3ec2a authoriko <ilyakooo0@gmail.com> dateFri 28 May 2021, 11:44 (5 years ago) parentb76f48d
5 files changed+332−243
openapi-diff.cabal−1 src/OpenAPI/Checker/Behavior.hs+1 src/OpenAPI/Checker/Validate/OpenApi.hs+47−18 src/OpenAPI/Checker/Validate/Operation.hs+284−2 src/OpenAPI/Checker/Validate/ProcessedPathItem.hs−222
openapi-diff.cabal−1
@@ -123,7 +123,6 @@ library
123123 , OpenAPI.Checker.Validate.Operation
124124 , OpenAPI.Checker.Validate.Param
125125 , OpenAPI.Checker.Validate.PathFragment
126− , OpenAPI.Checker.Validate.ProcessedPathItem
127126 , OpenAPI.Checker.Validate.Products
128127 , OpenAPI.Checker.Validate.RequestBody
129128 , OpenAPI.Checker.Validate.Responses
src/OpenAPI/Checker/Behavior.hs+1
@@ -29,6 +29,7 @@ data BehaviorLevel
2929 | SchemaLevel
3030 | TypedSchemaLevel
3131 | LinkLevel
32+ | CallbackLevel
3233
3334class
3435 (Ord (Behave a b), Show (Behave a b)) =>
src/OpenAPI/Checker/Validate/OpenApi.hs+47−18
@@ -13,43 +13,66 @@ import Data.OpenApi
1313import OpenAPI.Checker.Behavior
1414import OpenAPI.Checker.Paths
1515import OpenAPI.Checker.Subtree
16−import OpenAPI.Checker.Validate.ProcessedPathItem
16+import OpenAPI.Checker.Validate.Operation
1717
1818tracedPaths :: Traced OpenApi -> Traced ProcessedPathItems
19−tracedPaths oa = traced (ask oa >>> step OpenApiPathsStep)
20− (processPathItems . IOHM.toList . _openApiPaths . extract $ oa)
19+tracedPaths oa =
20+ traced
21+ (ask oa >>> step OpenApiPathsStep)
22+ (processPathItems . IOHM.toList . _openApiPaths . extract $ oa)
2123
2224tracedRequestBodies :: Traced OpenApi -> Traced (Definitions RequestBody)
23−tracedRequestBodies oa = traced (ask oa >>> step ComponentsRequestBody)
24− (_componentsRequestBodies . _openApiComponents . extract $ oa)
25+tracedRequestBodies oa =
26+ traced
27+ (ask oa >>> step ComponentsRequestBody)
28+ (_componentsRequestBodies . _openApiComponents . extract $ oa)
2529
2630tracedParameters :: Traced OpenApi -> Traced (Definitions Param)
27−tracedParameters oa = traced (ask oa >>> step ComponentsParam)
28− (_componentsParameters . _openApiComponents . extract $ oa)
31+tracedParameters oa =
32+ traced
33+ (ask oa >>> step ComponentsParam)
34+ (_componentsParameters . _openApiComponents . extract $ oa)
2935
3036tracedSecuritySchemes :: Traced OpenApi -> Traced (Definitions SecurityScheme)
31−tracedSecuritySchemes oa = traced (ask oa >>> step ComponentsSecurityScheme)
32− (_componentsSecuritySchemes . _openApiComponents . extract $ oa)
37+tracedSecuritySchemes oa =
38+ traced
39+ (ask oa >>> step ComponentsSecurityScheme)
40+ (_componentsSecuritySchemes . _openApiComponents . extract $ oa)
3341
3442tracedResponses :: Traced OpenApi -> Traced (Definitions Response)
35−tracedResponses oa = traced (ask oa >>> step ComponentsResponse)
36− (_componentsResponses . _openApiComponents . extract $ oa)
43+tracedResponses oa =
44+ traced
45+ (ask oa >>> step ComponentsResponse)
46+ (_componentsResponses . _openApiComponents . extract $ oa)
3747
3848tracedHeaders :: Traced OpenApi -> Traced (Definitions Header)
39−tracedHeaders oa = traced (ask oa >>> step ComponentsHeader)
40− (_componentsHeaders . _openApiComponents . extract $ oa)
49+tracedHeaders oa =
50+ traced
51+ (ask oa >>> step ComponentsHeader)
52+ (_componentsHeaders . _openApiComponents . extract $ oa)
4153
4254tracedSchemas :: Traced OpenApi -> Traced (Definitions Schema)
43−tracedSchemas oa = traced (ask oa >>> step ComponentsSchema)
44− (_componentsSchemas . _openApiComponents . extract $ oa)
55+tracedSchemas oa =
56+ traced
57+ (ask oa >>> step ComponentsSchema)
58+ (_componentsSchemas . _openApiComponents . extract $ oa)
4559
4660tracedLinks :: Traced OpenApi -> Traced (Definitions Link)
47−tracedLinks oa = traced (ask oa >>> step ComponentsLink)
48− (_componentsLinks . _openApiComponents . extract $ oa)
61+tracedLinks oa =
62+ traced
63+ (ask oa >>> step ComponentsLink)
64+ (_componentsLinks . _openApiComponents . extract $ oa)
65+
66+tracedCallbacks :: Traced OpenApi -> Traced (Definitions Callback)
67+tracedCallbacks (Traced t x) =
68+ Traced
69+ (t >>> step ComponentsCallbacks)
70+ (_componentsCallbacks . _openApiComponents $ x)
4971
5072instance Subtree OpenApi where
5173 type SubtreeLevel OpenApi = 'APILevel
5274 type CheckEnv OpenApi = '[]
75+
5376 -- There is no real reason to do a proper implementation
5477 checkStructuralCompatibility _ _ = structuralIssue
5578 checkSemanticCompatibility _ beh prodCons = do
@@ -62,8 +85,10 @@ instance Subtree OpenApi where
6285 `HCons` (tracedSchemas <$> prodCons)
6386 `HCons` (_openApiServers . extract <$> prodCons)
6487 `HCons` (tracedLinks <$> prodCons)
88+ `HCons` (tracedCallbacks <$> prodCons)
6589 `HCons` HNil)
66− beh (tracedPaths <$> prodCons)
90+ beh
91+ (tracedPaths <$> prodCons)
6792
6893instance Steppable OpenApi ProcessedPathItems where
6994 data Step OpenApi ProcessedPathItems = OpenApiPathsStep
@@ -96,3 +121,7 @@ instance Steppable OpenApi (Definitions Schema) where
96121instance Steppable OpenApi (Definitions Link) where
97122 data Step OpenApi (Definitions Link) = ComponentsLink
98123 deriving (Eq, Ord, Show)
124+
125+instance Steppable OpenApi (Definitions Callback) where
126+ data Step OpenApi (Definitions Callback) = ComponentsCallbacks
127+ deriving (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/Operation.hs+284−2
@@ -1,21 +1,34 @@
11{-# OPTIONS_GHC -Wno-name-shadowing #-}
22{-# OPTIONS_GHC -Wno-orphans #-}
3+{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
34
45module OpenAPI.Checker.Validate.Operation
5− ( MatchedOperation (..)
6+ ( -- * Operation
7+ MatchedOperation (..)
68 , OperationMethod (..)
79 , pathItemMethod
10+
11+ -- * ProcessedPathItem
12+ , ProcessedPathItem (..)
13+ , ProcessedPathItems (..)
14+ , processPathItems
15+ , Step (..)
816 )
917where
1018
19+import Control.Arrow
20+import Control.Comonad.Env
21+import Control.Monad
1122import Data.Foldable as F
1223import Data.Functor
1324import Data.HList
25+import qualified Data.HashMap.Strict.InsOrd as IOHM
1426import qualified Data.List as L
1527import Data.Map.Strict as M
1628import Data.Maybe
1729import Data.OpenApi
1830import Data.Text (Text)
31+import qualified Data.Text as T
1932import OpenAPI.Checker.Behavior
2033import OpenAPI.Checker.References
2134import OpenAPI.Checker.Subtree
@@ -27,6 +40,7 @@ import OpenAPI.Checker.Validate.RequestBody ()
2740import OpenAPI.Checker.Validate.Responses ()
2841import OpenAPI.Checker.Validate.SecurityRequirement ()
2942import OpenAPI.Checker.Validate.Server ()
43+import OpenAPI.Checker.Validate.Sums
3044
3145data MatchedOperation = MatchedOperation
3246 { operation :: !Operation
@@ -62,6 +76,12 @@ tracedSecurity oper =
6276 | (i, x) <- zip [0 ..] $ _operationSecurity . operation $ extract oper
6377 ]
6478
79+tracedCallbacks :: Traced MatchedOperation -> [(Text, Traced (Referenced Callback))]
80+tracedCallbacks (Traced t oper) =
81+ [ (k, Traced (t >>> step (OperationCallbackStep k)) v)
82+ | (k, v) <- IOHM.toList . _operationCallbacks . operation $ oper
83+ ]
84+
6585-- FIXME: https://github.com/typeable/openapi-diff/issues/28
6686getServers
6787 :: [Server] -- ^ Servers from env
@@ -100,6 +120,7 @@ instance Subtree MatchedOperation where
100120 , ProdCons (Traced (Definitions Schema))
101121 , ProdCons [Server]
102122 , ProdCons (Traced (Definitions Link))
123+ , ProdCons (Traced (Definitions Callback))
103124 ]
104125 checkStructuralCompatibility env pc = do
105126 let pParams :: ProdCons [Traced Param]
@@ -216,7 +237,24 @@ instance Subtree MatchedOperation where
216237 resps = tracedResponses <$> prodCons
217238 checkCompatibility respEnv beh $ swapProdCons resps
218239 -- FIXME: https://github.com/typeable/openapi-diff/issues/27
219− checkCallbacks = pure () -- (error "FIXME: not implemented")
240+ checkCallbacks = do
241+ let env' =
242+ HCons (swapProdCons respDefs) $
243+ HCons (swapProdCons headerDefs) $
244+ HCons (swapProdCons schemaDefs) $
245+ HCons (swapProdCons securitySchemeDefs) $
246+ HCons (swapProdCons paramDefs) $
247+ HCons (swapProdCons serversDefs) $
248+ HCons (swapProdCons callbackDefs) $
249+ HCons (swapProdCons bodyDefs) $
250+ HCons (swapProdCons linkDefs) HNil
251+ let ProdCons pCallbacks cCallbacks = swapProdCons $ tracedCallbacks <$> prodCons
252+ for_ pCallbacks $ \(k, pCallback) -> do
253+ let beh' = beh >>> step (OperationCallback k)
254+ anyOfAt beh' CallbacksUnsupported $
255+ cCallbacks <&> \(_, cCallback) -> do
256+ checkCompatibility env' beh' $ ProdCons pCallback cCallback
257+ pure ()
220258 -- FIXME: https://github.com/typeable/openapi-diff/issues/28
221259 checkOperationSecurity = do
222260 let ProdCons pSecs cSecs = tracedSecurity <$> prodCons
@@ -236,6 +274,9 @@ instance Subtree MatchedOperation where
236274 schemaDefs = getH @(ProdCons (Traced (Definitions Schema))) env
237275 paramDefs = getH @(ProdCons (Traced (Definitions Param))) env
238276 linkDefs = getH @(ProdCons (Traced (Definitions Link))) env
277+ callbackDefs = getH @(ProdCons (Traced (Definitions Callback))) env
278+ securitySchemeDefs = getH @(ProdCons (Traced (Definitions SecurityScheme))) env
279+ serversDefs = getH @(ProdCons [Server]) env
239280
240281data OperationMethod
241282 = GetMethod
@@ -275,8 +316,249 @@ instance Steppable MatchedOperation SecurityRequirement where
275316 data Step MatchedOperation SecurityRequirement = OperationSecurityRequirementStep Int
276317 deriving stock (Eq, Ord, Show)
277318
319+instance Steppable MatchedOperation (Referenced Callback) where
320+ data Step MatchedOperation (Referenced Callback) = OperationCallbackStep Text
321+ deriving stock (Eq, Ord, Show)
322+
278323instance Steppable MatchedOperation [Server] where
279324 data Step MatchedOperation [Server]
280325 = OperationServersStep
281326 | EnvServerStep
282327 deriving (Eq, Ord, Show)
328+
329+-- * ProcessedPathItems
330+
331+-- FIXME: There's probably a better name for this, but `PathItem` is already taken ;(
332+data ProcessedPathItem = ProcessedPathItem
333+ { path :: FilePath
334+ , item :: PathItem
335+ }
336+ deriving stock (Eq, Show)
337+
338+processPathItems :: [(FilePath, PathItem)] -> ProcessedPathItems
339+processPathItems = ProcessedPathItems . fmap (uncurry ProcessedPathItem)
340+
341+newtype ProcessedPathItems = ProcessedPathItems {unProcessedPathItems :: [ProcessedPathItem]}
342+ deriving newtype (Eq, Show)
343+
344+instance Issuable 'APILevel where
345+ data Issue 'APILevel
346+ = NoPathsMatched FilePath
347+ | AllPathsFailed FilePath
348+ -- When several paths match given but all checks failed
349+ deriving stock (Eq, Ord, Show)
350+ issueIsUnsupported _ = False
351+
352+instance Behavable 'APILevel 'PathLevel where
353+ data Behave 'APILevel 'PathLevel
354+ = AtPath (ProdCons FilePath) -- TODO: why are there two?
355+ deriving stock (Eq, Ord, Show)
356+
357+instance Subtree ProcessedPathItems where
358+ type SubtreeLevel ProcessedPathItems = 'APILevel
359+ type
360+ CheckEnv ProcessedPathItems =
361+ '[ ProdCons (Traced (Definitions Param))
362+ , ProdCons (Traced (Definitions RequestBody))
363+ , ProdCons (Traced (Definitions SecurityScheme))
364+ , ProdCons (Traced (Definitions Response))
365+ , ProdCons (Traced (Definitions Header))
366+ , ProdCons (Traced (Definitions Schema))
367+ , ProdCons [Server]
368+ , ProdCons (Traced (Definitions Link))
369+ , ProdCons (Traced (Definitions Callback))
370+ ]
371+
372+ -- No real way to check it at this level
373+ checkStructuralCompatibility _ _ = structuralIssue
374+ checkSemanticCompatibility env beh pc@(ProdCons p c) = do
375+ -- Each path generated by producer must be handled by consumer with exactly
376+ -- one way
377+ for_ (unProcessedPathItems . extract $ p) $ \prodItem -> do
378+ let prodPath = path prodItem
379+ matchedItems = do
380+ consItem <- unProcessedPathItems . extract $ c
381+ F.toList $ matchingPathItems $ ProdCons prodItem consItem
382+ case matchedItems of
383+ [] -> issueAt beh $ NoPathsMatched prodPath
384+ [match] -> checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
385+ matches -> anyOfAt beh (AllPathsFailed prodPath) $ do
386+ match <- matches
387+ pure $ checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
388+ where
389+ retraced pc mpi = traced (ask pc >>> step (MatchedPathStep $ matchedPath mpi)) mpi
390+
391+-- | Preliminary checks two paths for compatibility. Returns Nothing if two
392+-- paths obviously do not match: static parts differ or count of path elements
393+-- is not equal
394+matchingPathItems :: ProdCons ProcessedPathItem -> Maybe (ProdCons MatchedPathItem)
395+matchingPathItems prodCons = do
396+ let frags = parsePath . path <$> prodCons
397+ guard $ fragsMatch frags
398+ let mkMatchedItems frag ppi =
399+ MatchedPathItem
400+ { pathItem = item ppi
401+ , matchedPath = path ppi
402+ , pathFragments = frag
403+ }
404+ return $ mkMatchedItems <$> frags <*> prodCons
405+
406+fragsMatch :: ProdCons [PathFragment Text] -> Bool
407+fragsMatch (ProdCons p c) = maybe False and $ zipAllWith check p c
408+ where
409+ check (StaticPath s1) (StaticPath s2) = s1 == s2
410+ check _ _ = True
411+
412+zipAllWith :: (a -> b -> c) -> [a] -> [b] -> Maybe [c]
413+zipAllWith _ [] [] = Just []
414+zipAllWith f (x : xs) (y : ys) = (f x y :) <$> zipAllWith f xs ys
415+zipAllWith _ (_ : _) [] = Nothing
416+zipAllWith _ [] (_ : _) = Nothing
417+
418+data MatchedPathItem = MatchedPathItem
419+ { pathItem :: !PathItem
420+ , matchedPath :: !FilePath
421+ , -- | Pre-parsed path from PathItem
422+ pathFragments :: ![PathFragment Text]
423+ }
424+ deriving stock (Eq)
425+
426+tracedMatchedPathItemParameters :: Traced MatchedPathItem -> [Traced (Referenced Param)]
427+tracedMatchedPathItemParameters mpi =
428+ [ traced (ask mpi >>> step (PathItemParam i)) x
429+ | (i, x) <- L.zip [0 ..] $ _pathItemParameters . pathItem $ extract mpi
430+ ]
431+
432+-- TODO: simplify?
433+tracedFragments :: Traced MatchedPathItem -> [Env (Trace PathFragmentParam) (PathFragment Text)]
434+tracedFragments mpi =
435+ [ env (ask mpi >>> step (PathFragmentStep i)) x
436+ | (i, x) <- L.zip [0 ..] $ pathFragments $ extract mpi
437+ ]
438+
439+tracedMethod
440+ :: OperationMethod
441+ -> Traced MatchedPathItem
442+ -> Maybe (Traced' MatchedOperation Operation)
443+tracedMethod s mpi = env (ask mpi >>> step (OperationMethodStep s)) <$> (pathItemMethod s . pathItem . extract $ mpi)
444+
445+instance Issuable 'PathLevel where
446+ data Issue 'PathLevel
447+ = OperationMissing OperationMethod
448+ deriving stock (Eq, Ord, Show)
449+ issueIsUnsupported _ = False
450+
451+instance Behavable 'PathLevel 'OperationLevel where
452+ data Behave 'PathLevel 'OperationLevel
453+ = InOperation OperationMethod
454+ deriving (Eq, Ord, Show)
455+
456+instance Subtree MatchedPathItem where
457+ type SubtreeLevel MatchedPathItem = 'PathLevel
458+ type
459+ CheckEnv MatchedPathItem =
460+ '[ ProdCons (Traced (Definitions Param))
461+ , ProdCons (Traced (Definitions RequestBody))
462+ , ProdCons (Traced (Definitions SecurityScheme))
463+ , ProdCons (Traced (Definitions Response))
464+ , ProdCons (Traced (Definitions Header))
465+ , ProdCons (Traced (Definitions Schema))
466+ , ProdCons [Server]
467+ , ProdCons (Traced (Definitions Link))
468+ , ProdCons (Traced (Definitions Callback))
469+ ]
470+ checkStructuralCompatibility _ _ = structuralIssue
471+ checkSemanticCompatibility env beh prodCons = do
472+ let paramDefs = getH @(ProdCons (Traced (Definitions Param))) env
473+ pathTracedParams = getPathParams <$> paramDefs <*> prodCons
474+ getPathParams
475+ :: Traced (Definitions Param)
476+ -> Traced MatchedPathItem
477+ -> [Traced Param]
478+ getPathParams defs mpi = do
479+ paramRef <- tracedMatchedPathItemParameters mpi
480+ pure $ dereference defs paramRef
481+ pathTracedFragments = mkPathFragments <$> prodCons
482+ mkPathFragments mpi operationParams =
483+ -- operationParams will be known on Operation check stage, so we give a
484+ -- function, returning fragments
485+ let paramsMap :: Map Text (Traced Param)
486+ paramsMap = M.fromList $ do
487+ tracedParam <- operationParams
488+ let pname = _paramName . extract $ tracedParam
489+ pure (pname, tracedParam)
490+ convertFragment = \case
491+ StaticPath t -> StaticPath t
492+ DynamicPath pname ->
493+ DynamicPath $
494+ fromMaybe (error $ "Param not found " <> T.unpack pname) $
495+ M.lookup pname paramsMap
496+ in tracedFragments mpi <&> fmap convertFragment
497+ operations = getOperations <$> pathTracedParams <*> pathTracedFragments <*> prodCons
498+ getOperations pathParams getPathFragments mpi = M.fromList $ do
499+ (name, getOp) <-
500+ (id &&& tracedMethod)
501+ <$> [GetMethod, PutMethod, PostMethod, DeleteMethod, OptionsMethod, HeadMethod, PatchMethod, DeleteMethod]
502+ operation <- F.toList $ getOp mpi
503+ -- Got only Justs here
504+ let retraced = \op -> MatchedOperation {operation = op, pathParams, getPathFragments}
505+ pure (name, retraced <$> operation)
506+ check name pc = checkCompatibility @MatchedOperation env (beh >>> step (InOperation name)) pc
507+ -- Operations are sum-like entities. Use step to operation as key because
508+ -- why not
509+ checkSums beh OperationMissing check operations
510+
511+instance Steppable ProcessedPathItems MatchedPathItem where
512+ data Step ProcessedPathItems MatchedPathItem = MatchedPathStep FilePath
513+ deriving (Eq, Ord, Show)
514+
515+instance Steppable MatchedPathItem MatchedOperation where
516+ data Step MatchedPathItem MatchedOperation = OperationMethodStep OperationMethod
517+ deriving (Eq, Ord, Show)
518+
519+instance Steppable MatchedPathItem (Referenced Param) where
520+ data Step MatchedPathItem (Referenced Param) = PathItemParam Int
521+ deriving (Eq, Ord, Show)
522+
523+instance Steppable MatchedPathItem PathFragmentParam where
524+ data Step MatchedPathItem PathFragmentParam = PathFragmentStep Int
525+ deriving (Eq, Ord, Show)
526+
527+-- * Callbacks
528+
529+instance Subtree Callback where
530+ type SubtreeLevel Callback = 'CallbackLevel
531+ type
532+ CheckEnv Callback =
533+ '[ ProdCons (Traced (Definitions Param))
534+ , ProdCons (Traced (Definitions RequestBody))
535+ , ProdCons (Traced (Definitions SecurityScheme))
536+ , ProdCons (Traced (Definitions Response))
537+ , ProdCons (Traced (Definitions Header))
538+ , ProdCons (Traced (Definitions Schema))
539+ , ProdCons (Traced (Definitions Link))
540+ , ProdCons [Server]
541+ , ProdCons (Traced (Definitions Callback))
542+ ]
543+ checkStructuralCompatibility env pc =
544+ checkSubstructure env $ tracedCallbackPathItems <$> pc
545+ checkSemanticCompatibility _ bhv _ = issueAt bhv CallbacksUnsupported
546+
547+instance Issuable 'CallbackLevel where
548+ data Issue 'CallbackLevel
549+ = CallbacksUnsupported
550+ deriving (Eq, Ord, Show)
551+ issueIsUnsupported = \case
552+ CallbacksUnsupported -> True
553+
554+tracedCallbackPathItems :: Traced Callback -> Traced ProcessedPathItems
555+tracedCallbackPathItems (Traced t (Callback x)) =
556+ Traced (t >>> step CallbackPathsStep) (processPathItems . fmap (first T.unpack) . IOHM.toList $ x)
557+
558+instance Steppable Callback ProcessedPathItems where
559+ data Step Callback ProcessedPathItems = CallbackPathsStep
560+ deriving (Eq, Ord, Show)
561+
562+instance Behavable 'OperationLevel 'CallbackLevel where
563+ data Behave 'OperationLevel 'CallbackLevel = OperationCallback Text
564+ deriving stock (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/ProcessedPathItem.hsdeleted−222
@@ -1,222 +0,0 @@
1−{-# OPTIONS_GHC -Wno-orphans #-}
2−{-# OPTIONS_GHC -Wno-name-shadowing #-}
3−
4−module OpenAPI.Checker.Validate.ProcessedPathItem
5− ( ProcessedPathItem (..)
6− , ProcessedPathItems (..)
7− , processPathItems
8− , Step (..)
9− )
10−where
11−
12−import Control.Arrow
13−import Control.Comonad.Env
14−import Control.Monad
15−import Data.Foldable as F
16−import Data.Functor
17−import Data.HList
18−import qualified Data.List as L
19−import Data.Map.Strict as M
20−import Data.Maybe
21−import Data.OpenApi
22−import Data.Text as T
23−import OpenAPI.Checker.Behavior
24−import OpenAPI.Checker.Orphans ()
25−import OpenAPI.Checker.Paths
26−import OpenAPI.Checker.References
27−import OpenAPI.Checker.Subtree
28−import OpenAPI.Checker.Validate.Operation
29−import OpenAPI.Checker.Validate.PathFragment
30−import OpenAPI.Checker.Validate.Sums
31−
32−-- FIXME: There's probably a better name for this, but `PathItem` is already taken ;(
33−data ProcessedPathItem = ProcessedPathItem
34− { path :: FilePath
35− , item :: PathItem
36− } deriving stock (Eq, Show)
37−
38−processPathItems :: [(FilePath, PathItem)] -> ProcessedPathItems
39−processPathItems = ProcessedPathItems . fmap (uncurry ProcessedPathItem)
40−
41−newtype ProcessedPathItems =
42− ProcessedPathItems {unProcessedPathItems :: [ProcessedPathItem]}
43− deriving newtype (Eq, Show)
44−
45−instance Issuable 'APILevel where
46− data Issue 'APILevel
47− = NoPathsMatched FilePath
48− | AllPathsFailed FilePath
49− -- When several paths match given but all checks failed
50− deriving stock (Eq, Ord, Show)
51− issueIsUnsupported _ = False
52−
53−instance Behavable 'APILevel 'PathLevel where
54− data Behave 'APILevel 'PathLevel
55− = AtPath (ProdCons FilePath) -- TODO: why are there two?
56− deriving stock (Eq, Ord, Show)
57−
58−instance Subtree ProcessedPathItems where
59− type SubtreeLevel ProcessedPathItems = 'APILevel
60− type
61− CheckEnv ProcessedPathItems =
62− '[ ProdCons (Traced (Definitions Param))
63− , ProdCons (Traced (Definitions RequestBody))
64− , ProdCons (Traced (Definitions SecurityScheme))
65− , ProdCons (Traced (Definitions Response))
66− , ProdCons (Traced (Definitions Header))
67− , ProdCons (Traced (Definitions Schema))
68− , ProdCons [Server]
69− , ProdCons (Traced (Definitions Link))
70− ]
71− -- No real way to check it at this level
72− checkStructuralCompatibility _ _ = structuralIssue
73− checkSemanticCompatibility env beh pc@(ProdCons p c) = do
74− -- Each path generated by producer must be handled by consumer with exactly
75− -- one way
76− for_ (unProcessedPathItems . extract $ p) $ \ prodItem -> do
77− let
78− prodPath = path prodItem
79− matchedItems = do
80− consItem <- unProcessedPathItems . extract $ c
81− F.toList $ matchingPathItems $ ProdCons prodItem consItem
82− case matchedItems of
83− [] -> issueAt beh $ NoPathsMatched prodPath
84− [match] -> checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
85− matches -> anyOfAt beh (AllPathsFailed prodPath) $ do
86− match <- matches
87− pure $ checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
88− where
89− retraced pc mpi = traced (ask pc >>> step (MatchedPathStep $ matchedPath mpi)) mpi
90−
91−-- | Preliminary checks two paths for compatibility. Returns Nothing if two
92−-- paths obviously do not match: static parts differ or count of path elements
93−-- is not equal
94−matchingPathItems :: ProdCons ProcessedPathItem -> Maybe (ProdCons MatchedPathItem)
95−matchingPathItems prodCons = do
96− let frags = parsePath . path <$> prodCons
97− guard $ fragsMatch frags
98− let
99− mkMatchedItems frag ppi = MatchedPathItem
100− { pathItem = item ppi
101− , matchedPath = path ppi
102− , pathFragments = frag }
103− return $ mkMatchedItems <$> frags <*> prodCons
104−
105−fragsMatch :: ProdCons [PathFragment Text] -> Bool
106−fragsMatch (ProdCons p c) = maybe False and $ zipAllWith check p c
107− where
108− check (StaticPath s1) (StaticPath s2) = s1 == s2
109− check _ _ = True
110−
111−zipAllWith :: (a -> b -> c) -> [a] -> [b] -> Maybe [c]
112−zipAllWith _ [] [] = Just []
113−zipAllWith f (x : xs) (y : ys) = (f x y :) <$> zipAllWith f xs ys
114−zipAllWith _ (_ : _) [] = Nothing
115−zipAllWith _ [] (_ : _) = Nothing
116−
117−data MatchedPathItem = MatchedPathItem
118− { pathItem :: !PathItem
119− , matchedPath :: !FilePath
120− , pathFragments :: ![PathFragment Text]
121− -- ^ Pre-parsed path from PathItem
122− } deriving stock (Eq)
123−
124−tracedParameters :: Traced MatchedPathItem -> [Traced (Referenced Param)]
125−tracedParameters mpi =
126− [ traced (ask mpi >>> step (PathItemParam i)) x
127− | (i, x) <- L.zip [0..] $ _pathItemParameters . pathItem $ extract mpi
128− ]
129−
130−-- TODO: simplify?
131−tracedFragments :: Traced MatchedPathItem -> [Env (Trace PathFragmentParam) (PathFragment Text)]
132−tracedFragments mpi =
133− [ env (ask mpi >>> step (PathFragmentStep i)) x
134− | (i, x) <- L.zip [0..] $ pathFragments $ extract mpi
135− ]
136−
137−tracedMethod
138− :: OperationMethod
139− -> Traced MatchedPathItem
140− -> Maybe (Traced' MatchedOperation Operation)
141−tracedMethod s mpi = env (ask mpi >>> step (OperationMethodStep s)) <$> (pathItemMethod s . pathItem . extract $ mpi)
142−
143−instance Issuable 'PathLevel where
144− data Issue 'PathLevel
145− = OperationMissing OperationMethod
146− deriving stock (Eq, Ord, Show)
147− issueIsUnsupported _ = False
148−
149−instance Behavable 'PathLevel 'OperationLevel where
150− data Behave 'PathLevel 'OperationLevel
151− = InOperation OperationMethod
152− deriving (Eq, Ord, Show)
153−
154−instance Subtree MatchedPathItem where
155− type SubtreeLevel MatchedPathItem = 'PathLevel
156− type CheckEnv MatchedPathItem =
157− '[ ProdCons (Traced (Definitions Param))
158− , ProdCons (Traced (Definitions RequestBody))
159− , ProdCons (Traced (Definitions SecurityScheme))
160− , ProdCons (Traced (Definitions Response))
161− , ProdCons (Traced (Definitions Header))
162− , ProdCons (Traced (Definitions Schema))
163− , ProdCons [Server]
164− , ProdCons (Traced (Definitions Link))
165− ]
166− checkStructuralCompatibility _ _ = structuralIssue
167− checkSemanticCompatibility env beh prodCons = do
168− let
169− paramDefs = getH @(ProdCons (Traced (Definitions Param))) env
170− pathTracedParams = getPathParams <$> paramDefs <*> prodCons
171− getPathParams
172− :: Traced (Definitions Param)
173− -> Traced MatchedPathItem
174− -> [Traced Param]
175− getPathParams defs mpi = do
176− paramRef <- tracedParameters mpi
177− pure $ dereference defs paramRef
178− pathTracedFragments = mkPathFragments <$> prodCons
179− mkPathFragments mpi operationParams =
180− -- operationParams will be known on Operation check stage, so we give a
181− -- function, returning fragments
182− let
183− paramsMap :: Map Text (Traced Param)
184− paramsMap = M.fromList $ do
185− tracedParam <- operationParams
186− let pname = _paramName . extract $ tracedParam
187− pure (pname, tracedParam)
188− convertFragment = \case
189− StaticPath t -> StaticPath t
190− DynamicPath pname -> DynamicPath
191− $ fromMaybe (error $ "Param not found " <> T.unpack pname)
192− $ M.lookup pname paramsMap
193− in tracedFragments mpi <&> fmap convertFragment
194− operations = getOperations <$> pathTracedParams <*> pathTracedFragments <*> prodCons
195− getOperations pathParams getPathFragments mpi = M.fromList $ do
196− (name, getOp) <- (id &&& tracedMethod) <$>
197− [GetMethod, PutMethod, PostMethod, DeleteMethod, OptionsMethod, HeadMethod, PatchMethod, DeleteMethod]
198− operation <- F.toList $ getOp mpi
199− -- Got only Justs here
200− let retraced = \op -> MatchedOperation { operation = op, pathParams, getPathFragments }
201− pure (name, retraced <$> operation)
202− check name pc = checkCompatibility @MatchedOperation env (beh >>> step (InOperation name)) pc
203− -- Operations are sum-like entities. Use step to operation as key because
204− -- why not
205− checkSums beh OperationMissing check operations
206−
207−
208−instance Steppable ProcessedPathItems MatchedPathItem where
209− data Step ProcessedPathItems MatchedPathItem = MatchedPathStep FilePath
210− deriving (Eq, Ord, Show)
211−
212−instance Steppable MatchedPathItem MatchedOperation where
213− data Step MatchedPathItem MatchedOperation = OperationMethodStep OperationMethod
214− deriving (Eq, Ord, Show)
215−
216−instance Steppable MatchedPathItem (Referenced Param) where
217− data Step MatchedPathItem (Referenced Param) = PathItemParam Int
218− deriving (Eq, Ord, Show)
219−
220−instance Steppable MatchedPathItem PathFragmentParam where
221− data Step MatchedPathItem PathFragmentParam = PathFragmentStep Int
222− deriving (Eq, Ord, Show)