git.iko.soy/typeable/compaREST
on master help sign in
Finish instances Subtree and Steppable (#34)

* dirlocals * none * param check * wip * no undefs * req body * responses * responses are close * responses done * cleanup * Responses * schema check * parse yaml also

commit556859135bc2d842c33d46902d8193606cf4a4fe authorAlexey Uimanov <s9gf4ult@gmail.com> dateThu 8 Apr 2021, 12:03 (6 years ago) parent45df058
15 files changed+260−31
.gitignore+1 openapi-diff.cabal+9−7 src/OpenAPI/Checker/Run.hs+8−3 src/OpenAPI/Checker/Subtree.hs+15 src/OpenAPI/Checker/Validate/MediaTypeObject.hs+64 src/OpenAPI/Checker/Validate/OpenApi.hs+4−2 src/OpenAPI/Checker/Validate/Operation.hs+5−3 src/OpenAPI/Checker/Validate/Param.hs+20−2 src/OpenAPI/Checker/Validate/PathFragment.hs−2 src/OpenAPI/Checker/Validate/ProcessedPathItem.hs+3−1 src/OpenAPI/Checker/Validate/RequestBody.hs+27−3 src/OpenAPI/Checker/Validate/Responses.hs+104−4 src/OpenAPI/Checker/Validate/Schema.hs−2 src/OpenAPI/Checker/Validate/SecurityRequirement.hs−1 src/OpenAPI/Checker/Validate/Server.hs−1
.gitignore+1
@@ -1,3 +1,4 @@
11.stack-work
22TAGS
33.vscode
4+.dir-locals.el
openapi-diff.cabal+9−7
@@ -32,25 +32,26 @@ common common-options
3232 build-depends: base >= 4.12.0.0 && < 4.15
3333 , aeson
3434 , attoparsec
35+ , bytestring
3536 , containers
3637 , deriving-aeson
3738 , generic-data
3839 , generic-monoid
40+ , http-media
3941 , insert-ordered-containers
42+ , lattices
4043 , lens
4144 , mtl
4245 , openapi3
4346 , optparse-applicative
47+ , scientific
4448 , tagged
4549 , text
4650 , transformers
4751 , typerep-map
48− , yaml
4952 , unordered-containers
5053 , vector
51− , bytestring
52− , scientific
53− , lattices
54+ , yaml
5455
5556 default-extensions: ApplicativeDo
5657 , BangPatterns
@@ -105,16 +106,17 @@ library
105106 , OpenAPI.Checker.Subtree
106107 , OpenAPI.Checker.Trace
107108 , OpenAPI.Checker.TracePrefixTree
109+ , OpenAPI.Checker.Validate.MediaTypeObject
108110 , OpenAPI.Checker.Validate.OpenApi
109111 , OpenAPI.Checker.Validate.Operation
110112 , OpenAPI.Checker.Validate.Param
111113 , OpenAPI.Checker.Validate.PathFragment
114+ , OpenAPI.Checker.Validate.ProcessedPathItem
112115 , OpenAPI.Checker.Validate.RequestBody
113116 , OpenAPI.Checker.Validate.Responses
114− , OpenAPI.Checker.Validate.ProcessedPathItem
115− , OpenAPI.Checker.Validate.Server
116− , OpenAPI.Checker.Validate.SecurityRequirement
117117 , OpenAPI.Checker.Validate.Schema
118+ , OpenAPI.Checker.Validate.SecurityRequirement
119+ , OpenAPI.Checker.Validate.Server
118120
119121executable openapi-diff
120122 import: common-options
src/OpenAPI/Checker/Run.hs+8−3
@@ -15,9 +15,14 @@ runChecker = do
1515 opts <- execParser optionsParserInfo
1616 let parseSchema path =
1717 eitherDecodeFileStrict path >>= \case
18− Left e -> do
19− putStrLn $ "Failed to parse: " ++ path
20− fail e
18+ Left jsonErr -> do
19+ Yaml.decodeFileEither path >>= \case
20+ Left yamlErr -> do
21+ putStrLn "Could not parse as json or yaml"
22+ putStrLn $ show jsonErr
23+ putStrLn $ show yamlErr
24+ fail "Exiting"
25+ Right s -> pure s
2126 Right s -> pure s
2227 a <- parseSchema (clientFile opts)
2328 b <- parseSchema (serverFile opts)
src/OpenAPI/Checker/Subtree.hs+15
@@ -5,6 +5,8 @@ module OpenAPI.Checker.Subtree
55 , CompatFormula
66 , ProdCons (..)
77 , HasUnsupportedFeature (..)
8+ , swapRoles
9+ , swapProdCons
810 , checkProdCons
911 , SubtreeCheckIssue (..)
1012 , runCompatFormula
@@ -49,6 +51,9 @@ data ProdCons a = ProdCons
4951 }
5052 deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
5153
54+swapProdCons :: ProdCons a -> ProdCons a
55+swapProdCons (ProdCons a b) = ProdCons b a
56+
5257instance Applicative ProdCons where
5358 pure x = ProdCons x x
5459 ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)
@@ -70,6 +75,13 @@ newtype CompatM t a = CompatM
7075
7176type CompatFormula t = Compose (CompatM t) (FormulaF SubtreeCheckIssue OpenApi)
7277
78+-- | Swaps roles of producer and consumer. Used when we check the @Response@ at
79+-- least. In response producer and consumer swap their places because response
80+-- is generated by consumer and parsed by producer.
81+swapRoles :: CompatFormula t a -> CompatFormula t a
82+swapRoles (Compose r) = Compose $ do
83+ local swapProdCons r
84+
7385class (Typeable t, Ord (CheckIssue t), Show (CheckIssue t)) => Subtree (t :: Type) where
7486 type CheckEnv t :: [Type]
7587 data CheckIssue t :: Type
@@ -79,9 +91,12 @@ class (Typeable t, Ord (CheckIssue t), Show (CheckIssue t)) => Subtree (t :: Typ
7991
8092 -- | If we ever followed a reference, reroute the path through "components"
8193 normalizeTrace :: Trace OpenApi t -> Trace OpenApi t
94+ normalizeTrace = id
8295
8396 checkCompatibility :: HasAll (CheckEnv t) xs => HList xs -> ProdCons t -> CompatFormula t ()
8497
98+{-# WARNING normalizeTrace "It must be refactored. Does nothing for now" #-}
99+
85100checkProdCons :: (Subtree t, HasAll (CheckEnv t) env) => HList env -> ProdCons (Traced r t) -> CompatFormula r ()
86101checkProdCons env (ProdCons (Traced p x) (Traced c y)) =
87102 localTrace (ProdCons p c) $ checkCompatibility env $ ProdCons x y
src/OpenAPI/Checker/Validate/MediaTypeObject.hsnew file+64
@@ -0,0 +1,64 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.MediaTypeObject () where
4+
5+import Data.Foldable as F
6+import Data.HList
7+import Data.HashMap.Strict.InsOrd as IOHM
8+import Data.OpenApi
9+import Data.Text (Text)
10+import Network.HTTP.Media (MediaType, mainType, subType)
11+import OpenAPI.Checker.Subtree
12+import OpenAPI.Checker.Trace
13+import OpenAPI.Checker.Validate.Schema
14+
15+instance Subtree MediaTypeObject where
16+ type CheckEnv MediaTypeObject =
17+ '[ MediaType
18+ , ProdCons (Definitions Schema)
19+ ]
20+ data CheckIssue MediaTypeObject
21+ = MediaEncodingMissing
22+ | MediaEncodingIncompat
23+ | MediaTypeSchemaRequired
24+ deriving (Eq, Ord, Show)
25+ checkCompatibility env (ProdCons p c) = do
26+ tryCheckEncoding
27+ checkSchema
28+ pure ()
29+ where
30+ mediaType = getH @MediaType env
31+ tryCheckEncoding =
32+ if | "multipart" == mainType mediaType -> checkEncoding
33+ | "application" == mainType mediaType &&
34+ "x-www-form-urlencoded" == subType mediaType -> checkEncoding
35+ | otherwise -> pure ()
36+ where
37+ checkEncoding = for_ (IOHM.toList $ _mediaTypeObjectEncoding c) $ \(paramName, consEncoding) ->
38+ case IOHM.lookup paramName $ _mediaTypeObjectEncoding p of
39+ Nothing -> issueAt producer MediaEncodingMissing
40+ Just prodEncoding -> localStep (MediaTypeParamEncoding paramName)
41+ $ checkCompatibility HNil
42+ $ ProdCons prodEncoding consEncoding
43+ checkSchema = for_ (_mediaTypeObjectSchema c) $ \consRef ->
44+ case _mediaTypeObjectSchema p of
45+ Nothing -> issueAt producer MediaTypeSchemaRequired
46+ Just prodRef -> localStep MediaTypeSchema
47+ $ checkCompatibility env $ ProdCons prodRef consRef
48+
49+instance Subtree Encoding where
50+ type CheckEnv Encoding = '[]
51+ data CheckIssue Encoding = EncodingNotSupported
52+ -- FIXME: Support only JSON body for now. Then Encoding is checked only for
53+ -- multipart/form-url-encoded
54+ deriving (Eq, Ord, Show)
55+ checkCompatibility _env _prodCons =
56+ issueAt producer EncodingNotSupported
57+
58+instance Steppable MediaTypeObject (Referenced Schema) where
59+ data Step MediaTypeObject (Referenced Schema) = MediaTypeSchema
60+ deriving (Eq, Ord, Show)
61+
62+instance Steppable MediaTypeObject Encoding where
63+ data Step MediaTypeObject Encoding = MediaTypeParamEncoding Text
64+ deriving (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/OpenApi.hs+4−2
@@ -1,5 +1,5 @@
1−{-# LANGUAGE PartialTypeSignatures #-}
21{-# OPTIONS_GHC -Wno-orphans #-}
2+{-# OPTIONS_GHC -fconstraint-solver-iterations=5 #-} -- Not compiles without
33
44module OpenAPI.Checker.Validate.OpenApi
55 (
@@ -17,7 +17,6 @@ instance Subtree OpenApi where
1717 type CheckEnv OpenApi = '[]
1818 data CheckIssue OpenApi
1919 deriving (Eq, Ord, Show)
20− normalizeTrace = undefined
2120 checkCompatibility _ prodCons = do
2221 let cs = _openApiComponents <$> prodCons
2322 localStep OpenApiPathsStep $
@@ -25,6 +24,9 @@ instance Subtree OpenApi where
2524 ((_componentsRequestBodies <$> cs)
2625 `HCons` (_componentsParameters <$> cs)
2726 `HCons` (_componentsSecuritySchemes <$> cs)
27+ `HCons` (_componentsResponses <$> cs)
28+ `HCons` (_componentsHeaders <$> cs)
29+ `HCons` (_componentsSchemas <$> cs)
2830 `HCons` HNil)
2931 (processPathItems . IOHM.toList . _openApiPaths <$> prodCons)
3032
src/OpenAPI/Checker/Validate/Operation.hs+5−3
@@ -22,6 +22,9 @@ instance Subtree Operation where
2222 '[ ProdCons (Definitions Param)
2323 , ProdCons (Definitions RequestBody)
2424 , ProdCons (Definitions SecurityScheme)
25+ , ProdCons (Definitions Response)
26+ , ProdCons (Definitions Header)
27+ , ProdCons (Definitions Schema)
2528 ]
2629 data CheckIssue Operation
2730 = ParamNotMatched Text -- Param name
@@ -30,7 +33,6 @@ instance Subtree Operation where
3033 | SecurityRequirementNotMet Int -- security indexs
3134 | ServerNotConsumed Int -- server index
3235 deriving (Eq, Ord, Show)
33− normalizeTrace = undefined
3436 checkCompatibility env prodCons = do
3537 let ProdCons {producer = pNonPathParams, consumer = cNonPathParams} = do
3638 op <- _operationParameters <$> prodCons
@@ -54,11 +56,11 @@ instance Subtree Operation where
5456 ProdCons Nothing Nothing -> pure ()
5557 ProdCons (Just pBody) (Just cBody) ->
5658 localStep OperationRequestBodyStep $
57− checkProdCons HNil (ProdCons pBody cBody)
59+ checkProdCons env (ProdCons pBody cBody)
5860 ProdCons Nothing (Just _) -> issueAt producer NoRequestBody
5961 ProdCons (Just _) Nothing -> issueAt consumer NoRequestBody
6062 localStep OperationResponsesStep $
61− checkCompatibility HNil $ _operationResponses <$> prodCons
63+ checkCompatibility env $ _operationResponses <$> prodCons
6264 -- FIXME: https://github.com/typeable/openapi-diff/issues/27
6365 case IOHM.null . _operationCallbacks <$> prodCons of
6466 (ProdCons True True) -> pure ()
src/OpenAPI/Checker/Validate/Param.hs+20−2
@@ -3,11 +3,29 @@
33module OpenAPI.Checker.Validate.Param () where
44
55import Data.OpenApi
6+import Data.Maybe
67import OpenAPI.Checker.Subtree
78
89instance Subtree Param where
910 type CheckEnv Param = '[]
1011 data CheckIssue Param
12+ = ParamNameMismatch
13+ -- ^ Params have different names
14+ | ParamOptionalityIncompatible
15+ -- ^ Consumer requires non-empty param, but producer gives optional
16+ | ParamPlaceIncompatible
1117 deriving (Eq, Ord, Show)
12− normalizeTrace = undefined
13− checkCompatibility = undefined
18+ checkCompatibility _ (ProdCons p c) = case ( _paramIn p, _paramIn c) of
19+ (ParamQuery, ParamQuery) ->
20+ namesCompatible $
21+ if (fromMaybe False $ _paramAllowEmptyValue p)
22+ && not (fromMaybe False $ _paramAllowEmptyValue c)
23+ then issueAt producer ParamOptionalityIncompatible
24+ else pure ()
25+ (a, b) | a == b -> namesCompatible $ pure ()
26+ _ -> issueAt producer ParamPlaceIncompatible
27+ where
28+ namesCompatible next =
29+ if _paramName p /= _paramName c
30+ then issueAt producer ParamNameMismatch
31+ else next
src/OpenAPI/Checker/Validate/PathFragment.hs−2
@@ -62,8 +62,6 @@ instance Subtree PathFragment where
6262 data CheckIssue PathFragment = PathFragmentsDontMatch Text Text
6363 deriving (Eq, Ord, Show)
6464
65− normalizeTrace = undefined
66−
6765 -- This case isn't strictly needed. It is here for optimization.
6866 checkCompatibility _ ProdCons {producer = (StaticPath x), consumer = (StaticPath y)} =
6967 if x == y
src/OpenAPI/Checker/Validate/ProcessedPathItem.hs+3−1
@@ -36,10 +36,12 @@ instance Subtree ProcessedPathItems where
3636 '[ ProdCons (Definitions Param)
3737 , ProdCons (Definitions RequestBody)
3838 , ProdCons (Definitions SecurityScheme)
39+ , ProdCons (Definitions Response)
40+ , ProdCons (Definitions Header)
41+ , ProdCons (Definitions Schema)
3942 ]
4043 data CheckIssue ProcessedPathItems = NoPathsMatched | WrongNumberOfFragments
4144 deriving (Eq, Ord, Show)
42− normalizeTrace = undefined
4345 checkCompatibility env prodCons = do
4446 let ProdCons {producer = p, consumer = c} =
4547 (\paramDefs -> fmap (processPathItem paramDefs) . unProcessedPathItems)
src/OpenAPI/Checker/Validate/RequestBody.hs+27−3
@@ -5,12 +5,36 @@ module OpenAPI.Checker.Validate.RequestBody
55 )
66where
77
8+import Data.Foldable as F
9+import Data.HList
10+import Data.HashMap.Strict.InsOrd as IOHM
11+import Data.Maybe
812import Data.OpenApi
13+import Network.HTTP.Media (MediaType)
914import OpenAPI.Checker.Subtree
15+import OpenAPI.Checker.Trace
16+import OpenAPI.Checker.Validate.MediaTypeObject ()
1017
1118instance Subtree RequestBody where
12− type CheckEnv RequestBody = '[]
19+ type CheckEnv RequestBody =
20+ '[ ProdCons (Definitions Schema) ]
1321 data CheckIssue RequestBody
22+ = RequestBodyRequired
23+ | RequestMediaTypeNotFound
24+ deriving (Eq, Ord, Show)
25+ checkCompatibility env (ProdCons p c) =
26+ if not (fromMaybe False $ _requestBodyRequired p)
27+ && (fromMaybe False $ _requestBodyRequired c)
28+ then issueAt producer RequestBodyRequired
29+ else
30+ -- For each consumer we must find at least one compatible producer media
31+ -- type
32+ for_ (IOHM.toList $ _requestBodyContent c) $ \(mediaType, consMedia) ->
33+ case IOHM.lookup mediaType $ _requestBodyContent p of
34+ Nothing -> issueAt producer RequestMediaTypeNotFound
35+ Just prodMedia -> localStep (MediaTypeStep mediaType) $
36+ checkCompatibility (HCons mediaType env) (ProdCons prodMedia consMedia)
37+
38+instance Steppable RequestBody MediaTypeObject where
39+ data Step RequestBody MediaTypeObject = MediaTypeStep MediaType
1440 deriving (Eq, Ord, Show)
15− normalizeTrace = undefined
16− checkCompatibility = undefined
src/OpenAPI/Checker/Validate/Responses.hs+104−4
@@ -1,14 +1,114 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
13module OpenAPI.Checker.Validate.Responses
24 (
35 )
46where
57
8+import Data.Foldable
9+import Data.HList
10+import Data.HashMap.Strict.InsOrd as IOHM
11+import Data.Maybe
612import Data.OpenApi
13+import Network.HTTP.Media (MediaType)
14+import OpenAPI.Checker.References
715import OpenAPI.Checker.Subtree
16+import OpenAPI.Checker.Trace
17+import OpenAPI.Checker.Validate.MediaTypeObject ()
18+import OpenAPI.Checker.Validate.Schema ()
819
920instance Subtree Responses where
10− type CheckEnv Responses = '[]
11− data CheckIssue Responses
21+ type CheckEnv Responses =
22+ '[ ProdCons (Definitions Response)
23+ , ProdCons (Definitions Header)
24+ , ProdCons (Definitions Schema)
25+ ]
26+ data CheckIssue Responses = ResponseCodeNotFound HttpStatusCode
27+ deriving (Eq, Ord, Show)
28+ -- Here we are checking responses, so, the consumer and producer swap their
29+ -- roles. The consumer now generates the response and producer consumes
30+ -- it. So, the logic is swapped.
31+ checkCompatibility env (ProdCons p c) = do
32+ let defs = getH @(ProdCons (Definitions Response)) env
33+ for_ (IOHM.toList $ _responsesResponses p) $ \ (prodStatus, prodRef) ->
34+ case IOHM.lookup prodStatus $ _responsesResponses c of
35+ Nothing -> issueAt consumer $ ResponseCodeNotFound prodStatus
36+ Just consRef -> do
37+ let tracedRefs = dereference <$> defs <*> ProdCons prodRef consRef
38+ localStep (ResponseCodeStep prodStatus)
39+ $ checkProdCons env tracedRefs
40+ -- FIXME: Do we need to check "default" fields somehow here?
41+
42+instance Subtree Response where
43+ type CheckEnv Response =
44+ '[ ProdCons (Definitions Header)
45+ , ProdCons (Definitions Schema)
46+ ]
47+ data CheckIssue Response
48+ = ResponseMediaTypeMissing MediaType
49+ | ResponseHeaderMissing HeaderName
50+ deriving (Eq, Ord, Show)
51+ -- Here we are checking responses, so, the consumer and producer swap their
52+ -- roles. The consumer now generates the response and producer consumes
53+ -- it. So, the logic is swapped.
54+ checkCompatibility env (ProdCons p c) = do
55+ checkMediaTypes
56+ checkHeaders
57+ pure ()
58+ where
59+ -- Each response type expected by producer must be in the consumer (logic
60+ -- is swapped)
61+ checkMediaTypes = do
62+ for_ (IOHM.toList $ _responseContent p) $ \ (mediaType, prodMediaObject) ->
63+ case IOHM.lookup mediaType $ _responseContent c of
64+ Nothing -> issueAt consumer $ ResponseMediaTypeMissing mediaType
65+ Just consMediaObject -> localStep (ResponseMediaObject mediaType)
66+ $ swapRoles
67+ $ checkCompatibility @MediaTypeObject (HCons mediaType env)
68+ $ ProdCons consMediaObject prodMediaObject
69+ checkHeaders = do
70+ for_ (IOHM.toList $ _responseHeaders p) $ \ (hname, prodRef) ->
71+ case IOHM.lookup hname $ _responseHeaders c of
72+ Nothing -> issueAt consumer $ ResponseHeaderMissing hname
73+ Just consRef -> do
74+ let headerRefs = dereference <$> headerDefs <*> ProdCons prodRef consRef
75+ localStep (ResponseHeader hname)
76+ $ swapRoles
77+ $ checkProdCons env $ swapProdCons headerRefs
78+ headerDefs = getH @(ProdCons (Definitions Header)) env
79+
80+instance Subtree Header where
81+ type CheckEnv Header = '[ProdCons (Definitions Schema)]
82+ data CheckIssue Header
83+ = RequiredHeaderMissing
84+ | NonEmptyHeaderRequired
85+ | HeaderSchemaRequired
86+ deriving (Eq, Ord, Show)
87+ checkCompatibility env (ProdCons p c) = do
88+ if (fromMaybe False $ _headerRequired c) && not (fromMaybe False $ _headerRequired p)
89+ then issueAt producer RequiredHeaderMissing else pure ()
90+ if not (fromMaybe False $ _headerAllowEmptyValue c) && (fromMaybe False $ _headerAllowEmptyValue p)
91+ then issueAt producer NonEmptyHeaderRequired else pure ()
92+ for_ (_headerSchema c) $ \consRef ->
93+ case (_headerSchema p) of
94+ Nothing -> issueAt producer HeaderSchemaRequired
95+ Just prodRef -> do
96+ localStep HeaderSchema
97+ $ checkCompatibility env $ ProdCons prodRef consRef
98+ pure ()
99+
100+instance Steppable Header (Referenced Schema) where
101+ data Step Header (Referenced Schema) = HeaderSchema
102+ deriving (Eq, Ord, Show)
103+
104+instance Steppable Response (Referenced Header) where
105+ data Step Response (Referenced Header) = ResponseHeader HeaderName
106+ deriving (Eq, Ord, Show)
107+
108+instance Steppable Response MediaTypeObject where
109+ data Step Response MediaTypeObject = ResponseMediaObject MediaType
110+ deriving (Eq, Ord, Show)
111+
112+instance Steppable Responses (Referenced Response) where
113+ data Step Responses (Referenced Response) = ResponseCodeStep HttpStatusCode
12114 deriving (Eq, Ord, Show)
13− normalizeTrace = undefined
14− checkCompatibility = undefined
src/OpenAPI/Checker/Validate/Schema.hs−2
@@ -761,7 +761,6 @@ instance Subtree Schema where
761761 | NoMatchingCondition [SomeCondition]
762762 deriving stock (Eq, Ord, Show)
763763 type CheckEnv Schema = '[ProdCons (Definitions Schema)]
764− normalizeTrace = undefined
765764 checkCompatibility env schs = withTrace $ \traces -> do
766765 let defs = getH env
767766 checkFormulas env (producer traces) $ schemaToFormula <$> defs <*> (Traced <$> traces <*> schs)
@@ -770,7 +769,6 @@ instance Subtree (Referenced Schema) where
770769 data CheckIssue (Referenced Schema)
771770 deriving stock (Eq, Ord, Show)
772771 type CheckEnv (Referenced Schema) = CheckEnv Schema
773− normalizeTrace = undefined
774772 checkCompatibility env refs = withTrace $ \traces -> do
775773 let
776774 defs = getH env
src/OpenAPI/Checker/Validate/SecurityRequirement.hs−1
@@ -15,5 +15,4 @@ instance Subtree SecurityRequirement where
1515 ]
1616 data CheckIssue SecurityRequirement
1717 deriving (Eq, Ord, Show)
18− normalizeTrace = undefined
1918 checkCompatibility = undefined
src/OpenAPI/Checker/Validate/Server.hs−1
@@ -12,5 +12,4 @@ instance Subtree Server where
1212 type CheckEnv Server = '[]
1313 data CheckIssue Server
1414 deriving (Eq, Ord, Show)
15− normalizeTrace = undefined
1615 checkCompatibility = undefined