git.iko.soy/typeable/compaREST
on master help sign in
Added server checking (#42)

* Added server checking * Updated test * Made things work after rebase

commit7df8b856239388b820762e859501262a3428a892 authoriko <ilyakooo0@gmail.com> dateFri 7 May 2021, 17:54 (5 years ago) parentc2968fe
10 files changed+253−19
.hlint.yaml+1 src/OpenAPI/Checker/Subtree.hs+19−1 src/OpenAPI/Checker/Trace.hs+5−2 src/OpenAPI/Checker/Validate/OpenApi.hs+3−1 src/OpenAPI/Checker/Validate/Operation.hs+21−8 src/OpenAPI/Checker/Validate/ProcessedPathItem.hs+2 src/OpenAPI/Checker/Validate/Server.hs+121−7 test/golden/common/servers/a.yaml+38 test/golden/common/servers/b.yaml+34 test/golden/common/servers/trace-tree.yaml+9
.hlint.yamlnew file+1
@@ -0,0 +1 @@
1+- ignore: { name: Redundant pure }
src/OpenAPI/Checker/Subtree.hs+19−1
@@ -5,6 +5,7 @@ module OpenAPI.Checker.Subtree
55 , CompatFormula'
66 , CompatFormula
77 , ProdCons (..)
8+ , anyOfSubtreeAt
89 , HasUnsupportedFeature (..)
910 , swapProdCons
1011 , SubtreeCheckIssue (..)
@@ -13,6 +14,7 @@ module OpenAPI.Checker.Subtree
1314 , anyOfAt
1415 , issueAtTrace
1516 , issueAt
17+ , tracedIssue
1618 , memo
1719 )
1820where
@@ -64,6 +66,7 @@ newtype CompatM a = CompatM
6466 )
6567
6668type CompatFormula' f r = Compose CompatM (FormulaF f r)
69+
6770type CompatFormula = CompatFormula' SubtreeCheckIssue OpenApi
6871
6972class (Typeable t, Ord (CheckIssue t), Show (CheckIssue t)) => Subtree (t :: Type) where
@@ -144,6 +147,12 @@ issueAt
144147 -> CompatFormula' SubtreeCheckIssue r a
145148issueAt x = issueAtTrace (ask x)
146149
150+tracedIssue
151+ :: (Subtree t, ComonadEnv (Trace r t) w)
152+ => w (CheckIssue t)
153+ -> CompatFormula' SubtreeCheckIssue r a
154+tracedIssue x = issueAtTrace (ask x) (extract x)
155+
147156anyOfM
148157 :: Subtree t
149158 => Trace r t
@@ -161,6 +170,15 @@ anyOfAt
161170 -> CompatFormula' SubtreeCheckIssue r a
162171anyOfAt x = anyOfM (ask x)
163172
173+anyOfSubtreeAt
174+ :: (Subtree t, ComonadEnv (Trace r t) w)
175+ => w x
176+ -> CheckIssue t
177+ -> [CompatFormula' SubtreeCheckIssue r a]
178+ -> CompatFormula' SubtreeCheckIssue r a
179+anyOfSubtreeAt _ _ [x] = x
180+anyOfSubtreeAt f i fs = anyOfAt f i fs
181+
164182fixpointKnot
165183 :: MonadState (MemoState VarRef) m
166184 => KnotTier (FormulaF f r ()) VarRef m
@@ -175,5 +193,5 @@ memo
175193 :: (Typeable r, Subtree t)
176194 => (ProdCons (Traced r t) -> CompatFormula ())
177195 -> (ProdCons (Traced r t) -> CompatFormula ())
178−memo f pc = Compose $ do
196+memo f pc = Compose $ do
179197 memoWithKnot fixpointKnot (getCompose $ f pc) (ask <$> pc)
src/OpenAPI/Checker/Trace.hs+5−2
@@ -10,6 +10,7 @@ module OpenAPI.Checker.Trace
1010 , AnItem (..)
1111 , step
1212 , Traced
13+ , Traced'
1314 , traced
1415
1516 -- * Reexports
@@ -120,7 +121,9 @@ instance Typeable r => Ord (AnItem f r) where
120121 Root -> compare (someTypeRep xs) (someTypeRep ys)
121122 Snoc _ _ -> compare (someTypeRep xs) (someTypeRep ys)
122123
123−type Traced r a = Env (Trace r a) a
124+type Traced r a = Traced' r a a
124125
125−traced :: Trace r a -> a -> Traced r a
126+type Traced' r a b = Env (Trace r a) b
127+
128+traced :: Trace r a -> b -> Traced' r a b
126129traced = env
src/OpenAPI/Checker/Validate/OpenApi.hs+3−1
@@ -1,5 +1,6 @@
11{-# OPTIONS_GHC -Wno-orphans #-}
2−{-# OPTIONS_GHC -fconstraint-solver-iterations=5 #-} -- Not compiles without
2+-- Does not compiles otherwise
3+{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
34
45module OpenAPI.Checker.Validate.OpenApi
56 (
@@ -30,6 +31,7 @@ instance Subtree OpenApi where
3031 `HCons` (_componentsResponses <$> cs)
3132 `HCons` (_componentsHeaders <$> cs)
3233 `HCons` (_componentsSchemas <$> cs)
34+ `HCons` (_openApiServers . extract <$> prodCons)
3335 `HCons` HNil)
3436 (tracedPaths <$> prodCons)
3537
src/OpenAPI/Checker/Validate/Operation.hs+21−8
@@ -25,6 +25,7 @@ import OpenAPI.Checker.Validate.PathFragment
2525import OpenAPI.Checker.Validate.Products
2626import OpenAPI.Checker.Validate.RequestBody
2727import OpenAPI.Checker.Validate.Responses ()
28+import OpenAPI.Checker.Validate.Server ()
2829
2930data MatchedOperation = MatchedOperation
3031 { operation :: !Operation
@@ -59,11 +60,16 @@ tracedSecurity oper =
5960 | (i, x) <- zip [0..] $ _operationSecurity . operation $ extract oper
6061 ]
6162
62−tracedServers :: Traced r MatchedOperation -> [Traced r Server]
63−tracedServers oper =
64− [ traced (ask oper >>> step (OperationServerStep i)) x
65− | (i, x) <- zip [0..] $ _operationServers . operation $ extract oper
66− ]
63+-- FIXME: https://github.com/typeable/openapi-diff/issues/28
64+tracedServers
65+ :: [Server] -- ^ Servers from env
66+ -> Traced r MatchedOperation
67+ -> Traced r [Server]
68+tracedServers env oper =
69+ traced (ask oper >>> step OperationServersStep) $
70+ case _operationServers . operation $ extract oper of
71+ [] -> env
72+ ss -> ss
6773
6874instance Subtree MatchedOperation where
6975 type CheckEnv MatchedOperation =
@@ -73,6 +79,7 @@ instance Subtree MatchedOperation where
7379 , ProdCons (Definitions Response)
7480 , ProdCons (Definitions Header)
7581 , ProdCons (Definitions Schema)
82+ , ProdCons [Server]
7683 ]
7784 data CheckIssue MatchedOperation
7885 = OperationMissing OperationMethod
@@ -165,9 +172,13 @@ instance Subtree MatchedOperation where
165172 $ HCons (swapProdCons schemaDefs) HNil
166173 resps = tracedResponses <$> prodCons
167174 checkCompatibility respEnv $ swapProdCons resps
175+ -- FIXME: https://github.com/typeable/openapi-diff/issues/27
168176 checkCallbacks = pure () -- (error "FIXME: not implemented")
177+ -- FIXME: https://github.com/typeable/openapi-diff/issues/28
169178 checkOperationSecurity = pure () -- (error "FIXME: not implemented")
170− checkServers = pure () -- (error "FIXME: not implemented")
179+ checkServers =
180+ checkCompatibility env $
181+ tracedServers <$> getH @(ProdCons [Server]) env <*> prodCons
171182 bodyDefs = getH @(ProdCons (Definitions RequestBody)) env
172183 respDefs = getH @(ProdCons (Definitions Response)) env
173184 headerDefs = getH @(ProdCons (Definitions Header)) env
@@ -212,6 +223,8 @@ instance Steppable MatchedOperation SecurityRequirement where
212223 data Step MatchedOperation SecurityRequirement = OperationSecurityRequirementStep Int
213224 deriving (Eq, Ord, Show)
214225
215−instance Steppable MatchedOperation Server where
216− data Step MatchedOperation Server = OperationServerStep Int
226+instance Steppable MatchedOperation [Server] where
227+ data Step MatchedOperation [Server]
228+ = OperationServersStep
229+ | EnvServerStep
217230 deriving (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/ProcessedPathItem.hs+2
@@ -47,6 +47,7 @@ instance Subtree ProcessedPathItems where
4747 , ProdCons (Definitions Response)
4848 , ProdCons (Definitions Header)
4949 , ProdCons (Definitions Schema)
50+ , ProdCons [Server]
5051 ]
5152 data CheckIssue ProcessedPathItems
5253 = NoPathsMatched FilePath
@@ -132,6 +133,7 @@ instance Subtree MatchedPathItem where
132133 , ProdCons (Definitions Response)
133134 , ProdCons (Definitions Header)
134135 , ProdCons (Definitions Schema)
136+ , ProdCons [Server]
135137 ]
136138 data CheckIssue MatchedPathItem
137139 deriving (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/Server.hs+121−7
@@ -1,15 +1,129 @@
11{-# OPTIONS_GHC -Wno-orphans #-}
22
33module OpenAPI.Checker.Validate.Server
4− ( CheckIssue(..)
5− ) where
4+ ( CheckIssue (..)
5+ )
6+where
67
8+import Control.Applicative
9+import Control.Comonad
10+import Control.Monad
11+import Data.Attoparsec.Text
12+import Data.Either
13+import Data.Foldable
14+import Data.Function
15+import Data.Functor
16+import Data.HashMap.Strict.InsOrd as IOHM
17+import qualified Data.HashSet.InsOrd as IOHS
18+import Data.Maybe
719import Data.OpenApi
20+import Data.Text (Text)
21+import qualified Data.Text as T
22+import Data.Traversable
823import OpenAPI.Checker.Subtree
24+import OpenAPI.Checker.Trace
25+import Prelude as P
926
10−instance Subtree Server where
11− type CheckEnv Server = '[]
12− data CheckIssue Server
13− = ServerNotConsumed
27+tracedParsedServerUrlParts
28+ :: Traced' r [Server] Server
29+ -> Traced' r ProcessedServer (Either (CheckIssue ProcessedServer) ProcessedServer)
30+tracedParsedServerUrlParts s =
31+ let rawURL = _serverUrl $ extract s
32+ parsedUrl = parseServerUrl rawURL
33+ serverVariables = _serverVariables $ extract s
34+ in traced (ask s >>> step (ServerStep rawURL)) $
35+ parsedUrl
36+ & (traverse . traverse)
37+ (\var -> case IOHM.lookup var serverVariables of
38+ Nothing -> Left VariableNotDefined
39+ Just x -> Right x)
40+
41+instance Subtree [Server] where
42+ type CheckEnv [Server] = '[]
43+ data CheckIssue [Server]
44+ deriving (Eq, Ord, Show)
45+ checkCompatibility env pcServer = do
46+ let (ProdCons (pErrs, pUrls) (cErrs, cUrls)) =
47+ pcServer <&> partitionEithers . fmap (bicosequence . tracedParsedServerUrlParts) . sequence
48+ bicosequence :: Comonad f => f (Either a b) -> Either (f a) (f b)
49+ bicosequence x = case extract x of
50+ Left e -> Left $ x $> e
51+ Right a -> Right $ x $> a
52+ throwAllErrors = traverse_ tracedIssue
53+ throwAllErrors pErrs
54+ throwAllErrors cErrs
55+ for_ pUrls $ \cUrl -> do
56+ let potentiallyCompatible = P.filter ((staticCompatible `on` extract) cUrl) cUrls
57+ anyOfSubtreeAt cUrl ServerNotMatched $ potentiallyCompatible <&> (checkCompatibility env . ProdCons cUrl)
58+ pure ()
59+
60+type ProcessedServer = [ServerUrlPart ServerVariable]
61+
62+-- | Nothing means "open variable" – can have any value
63+unifyPart :: ServerUrlPart ServerVariable -> Maybe (IOHS.InsOrdHashSet Text)
64+unifyPart (ServerUrlVariable v) = _serverVariableEnum v
65+unifyPart (ServerUrlConstant c) = Just $ IOHS.singleton c
66+
67+zipAll :: [a] -> [b] -> Maybe [(a, b)]
68+zipAll [] [] = Just []
69+zipAll (x : xs) (y : ys) = ((x, y) :) <$> zipAll xs ys
70+zipAll (_ : _) [] = Nothing
71+zipAll [] (_ : _) = Nothing
72+
73+staticCompatiblePart :: ServerUrlPart x -> ServerUrlPart x -> Bool
74+staticCompatiblePart (ServerUrlConstant x) (ServerUrlConstant y) = x == y
75+staticCompatiblePart _ _ = True
76+
77+staticCompatible :: [ServerUrlPart x] -> [ServerUrlPart x] -> Bool
78+staticCompatible a b = maybe False (all $ uncurry staticCompatiblePart) $ zipAll a b
79+
80+data ServerUrlPart var
81+ = ServerUrlVariable var
82+ | ServerUrlConstant Text
83+ deriving stock (Show, Functor, Foldable, Traversable)
84+
85+-- | This is super rough. Things like @{a|b}c@ will not match @ac@.
86+-- FIXME: https://github.com/typeable/openapi-diff/issues/46
87+--
88+-- NOTE: syntax is defined vaguely in the spec.
89+parseServerUrl :: Text -> [ServerUrlPart Text]
90+-- There really is no way it can fail
91+parseServerUrl = fromRight undefined . parseOnly (serverUrlParser <* endOfInput)
92+ where
93+ serverUrlParser :: Parser [ServerUrlPart Text]
94+ serverUrlParser = many $ do
95+ variableUrlParser <|> do
96+ a <- anyChar
97+ aa <- takeTill (== '{')
98+ return (ServerUrlConstant $ T.cons a aa)
99+
100+ variableUrlParser :: Parser (ServerUrlPart Text)
101+ variableUrlParser = do
102+ char '{'
103+ res <- takeTill (== '}')
104+ char '}'
105+ return $ ServerUrlVariable res
106+
107+instance Steppable [Server] ProcessedServer where
108+ data Step [Server] ProcessedServer = ServerStep Text
109+ deriving (Eq, Ord, Show)
110+
111+instance Subtree ProcessedServer where
112+ type CheckEnv ProcessedServer = '[]
113+ data CheckIssue ProcessedServer
114+ = VariableNotDefined
115+ | ServerNotMatched
116+ | EnumValueNotConsumed Int Text
117+ | ConsumerNotOpen Int
14118 deriving (Eq, Ord, Show)
15− checkCompatibility = undefined
119+ checkCompatibility _ pc@(ProdCons p _) =
120+ -- traversing here is fine because we have already filtered for length
121+ for_ (zip [0 ..] $ zipProdCons . fmap (fmap unifyPart . extract) $ pc) $ \(i, pcPart) -> case pcPart of
122+ (Just x, Just y) -> for_ x $ \v -> unless (v `IOHS.member` y) (issueAt p $ EnumValueNotConsumed i v)
123+ -- Consumer can consume anything
124+ (_, Nothing) -> pure ()
125+ -- Producer can produce anythings, but consumer has a finite enum ;(
126+ (Nothing, Just _) -> issueAt p (ConsumerNotOpen i)
127+ where
128+ zipProdCons :: ProdCons [a] -> [(a, a)]
129+ zipProdCons (ProdCons x y) = zip x y
test/golden/common/servers/a.yamlnew file+38
@@ -0,0 +1,38 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Swagger Petstore
5+servers:
6+ - url: http://petstore.swagger.io/v1
7+ variables:
8+ variableThatDoesntDoAnything:
9+ default: a
10+ enum:
11+ - a
12+ - bbb
13+ - url: http://missing.url
14+ - url: http://{x}variable.path/{y}/{openVariable1}/{openVariable2}
15+ variables:
16+ x:
17+ default: a
18+ enum:
19+ - a
20+ - b
21+ - c
22+ y:
23+ default: aa
24+ enum:
25+ - a
26+ - aa
27+ - aaa
28+ openVariable1:
29+ default: henlo
30+ openVariable2:
31+ default: henlo
32+paths:
33+ /pets:
34+ get:
35+ responses:
36+ "200":
37+ description: A paged array of pets
38+components: {}
test/golden/common/servers/b.yamlnew file+34
@@ -0,0 +1,34 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Swagger Petstore
5+servers:
6+ - url: http://petstore.swagger.io/v1
7+ - url: http://{x}variable.path/{y}/{openVariable1}/{openVariable2}
8+ variables:
9+ x:
10+ default: a
11+ enum:
12+ - b
13+ - c
14+ y:
15+ default: aa
16+ enum:
17+ - a
18+ - aa
19+ - aaa
20+ - bbb
21+ openVariable1:
22+ default: henlo
23+ openVariable2:
24+ default: henlo
25+ enum:
26+ - a
27+ - aaa
28+paths:
29+ /pets:
30+ get:
31+ responses:
32+ "200":
33+ description: A paged array of pets
34+components: {}
test/golden/common/servers/trace-tree.yamlnew file+9
@@ -0,0 +1,9 @@
1+Left:
2+ OpenApiPathsStep:
3+ MatchedPathStep "/pets":
4+ OperationMethodStep GetMethod:
5+ OperationServersStep:
6+ ServerStep "http://missing.url": ServerNotMatched
7+ ServerStep "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}":
8+ - EnumValueNotConsumed 1 "a"
9+ - ConsumerNotOpen 7