curl-runnings 0.3.0 → 0.6.0
raw patch · 12 files changed
+769/−212 lines, 12 filesdep +megaparsecPVP ok
version bump matches the API change (PVP)
Dependencies added: megaparsec
API changes (from Hackage documentation)
+ Testing.CurlRunnings.Internal: DEBUG :: LogLevel
+ Testing.CurlRunnings.Internal: ERROR :: LogLevel
+ Testing.CurlRunnings.Internal: INFO :: LogLevel
+ Testing.CurlRunnings.Internal: arrayGet :: [a] -> Int -> a
+ Testing.CurlRunnings.Internal: data LogLevel
+ Testing.CurlRunnings.Internal: instance GHC.Classes.Eq Testing.CurlRunnings.Internal.LogLevel
+ Testing.CurlRunnings.Internal: instance GHC.Classes.Ord Testing.CurlRunnings.Internal.LogLevel
+ Testing.CurlRunnings.Internal: instance GHC.Enum.Enum Testing.CurlRunnings.Internal.LogLevel
+ Testing.CurlRunnings.Internal: instance GHC.Show.Show Testing.CurlRunnings.Internal.LogLevel
+ Testing.CurlRunnings.Internal: makeLogger :: LogLevel -> CurlRunningsLogger
+ Testing.CurlRunnings.Internal: makeUnsafeLogger :: Show a => LogLevel -> CurlRunningsUnsafeLogger a
+ Testing.CurlRunnings.Internal: mapRight :: (b -> c) -> Either a b -> Either a c
+ Testing.CurlRunnings.Internal: tracer :: Show a => String -> a -> a
+ Testing.CurlRunnings.Internal: type CurlRunningsLogger = (LogLevel -> String -> IO ())
+ Testing.CurlRunnings.Internal: type CurlRunningsUnsafeLogger a = (LogLevel -> String -> a -> a)
+ Testing.CurlRunnings.Internal.Parser: parseQuery :: FullQueryText -> Either QueryError [InterpolatedQuery]
+ Testing.CurlRunnings.Types: ArrayIndex :: Integer -> Index
+ Testing.CurlRunnings.Types: CaseResultIndex :: Integer -> Index
+ Testing.CurlRunnings.Types: CurlRunningsState :: Environment -> [CaseResult] -> LogLevel -> CurlRunningsState
+ Testing.CurlRunnings.Types: EnvironmentVariable :: Text -> Query
+ Testing.CurlRunnings.Types: InterpolatedQuery :: Text -> Query -> InterpolatedQuery
+ Testing.CurlRunnings.Types: KeyIndex :: Text -> Index
+ Testing.CurlRunnings.Types: LiteralText :: Text -> InterpolatedQuery
+ Testing.CurlRunnings.Types: NonInterpolatedQuery :: Query -> InterpolatedQuery
+ Testing.CurlRunnings.Types: NullPointer :: Text -> Text -> QueryError
+ Testing.CurlRunnings.Types: Query :: [Index] -> Query
+ Testing.CurlRunnings.Types: QueryFailure :: CurlCase -> QueryError -> AssertionFailure
+ Testing.CurlRunnings.Types: QueryParseError :: Text -> Text -> QueryError
+ Testing.CurlRunnings.Types: QueryTypeMismatch :: Text -> Value -> QueryError
+ Testing.CurlRunnings.Types: QueryValidationError :: Text -> QueryError
+ Testing.CurlRunnings.Types: data CurlRunningsState
+ Testing.CurlRunnings.Types: data Index
+ Testing.CurlRunnings.Types: data InterpolatedQuery
+ Testing.CurlRunnings.Types: data Query
+ Testing.CurlRunnings.Types: data QueryError
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.Index
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.InterpolatedQuery
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.Query
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.QueryError
+ Testing.CurlRunnings.Types: logger :: CurlRunningsState -> CurlRunningsLogger
+ Testing.CurlRunnings.Types: type FullQueryText = Text
+ Testing.CurlRunnings.Types: type SingleQueryText = Text
+ Testing.CurlRunnings.Types: unsafeLogger :: Show a => CurlRunningsState -> CurlRunningsUnsafeLogger a
- Testing.CurlRunnings: runCase :: CurlCase -> IO CaseResult
+ Testing.CurlRunnings: runCase :: CurlRunningsState -> CurlCase -> IO CaseResult
- Testing.CurlRunnings: runSuite :: CurlSuite -> IO [CaseResult]
+ Testing.CurlRunnings: runSuite :: CurlSuite -> LogLevel -> IO [CaseResult]
- Testing.CurlRunnings.Types: CaseFail :: CurlCase -> [AssertionFailure] -> CaseResult
+ Testing.CurlRunnings.Types: CaseFail :: CurlCase -> (Maybe Headers) -> (Maybe Value) -> [AssertionFailure] -> CaseResult
- Testing.CurlRunnings.Types: CasePass :: CurlCase -> CaseResult
+ Testing.CurlRunnings.Types: CasePass :: CurlCase -> (Maybe Headers) -> (Maybe Value) -> CaseResult
Files
- README.md +29/−25
- app/Main.hs +26/−20
- curl-runnings.cabal +5/−3
- examples/example-spec.json +21/−2
- examples/example-spec.yaml +21/−2
- examples/interpolation-spec.yaml +46/−0
- examples/test.json +0/−75
- src/Testing/CurlRunnings.hs +288/−75
- src/Testing/CurlRunnings/Internal.hs +48/−0
- src/Testing/CurlRunnings/Internal/Parser.hs +141/−0
- src/Testing/CurlRunnings/Types.hs +119/−10
- test/Spec.hs +25/−0
README.md view
@@ -4,11 +4,13 @@ _Feel the rhythm! Feel the rhyme! Get on up, it's testing time! curl-runnings!_ -curl-runnings is a framework for writing declarative, curl based tests for your APIs. +curl-runnings is a framework for writing declarative, curl based tests for your+APIs. Write your tests quickly and correctly with a straight-forward+specification in yaml or json that can encode simple but powerful matchers+against responses. -Write your tests quickly and correctly with a straight-forward specification in-yaml or json. A DSL for writing your tests is on the way! Alternatively, you can-use the curl-runnings library to write your tests directly in Haskell.+Alternatively, you can use the curl-runnings library to write your tests in+Haskell (though a haskell setup is absolutely not required to use this tool). ### Why? @@ -17,28 +19,29 @@ json output quickly becomes unweildy and error prone. Writing these sorts of tests in a more traditional programming language is fine, but certainly more time consuming to write than some simple curl requests. curl-runnings aims to-make it very simple to write tests that curl some endpoints and verify the+make it very easy to write tests that just hit some endpoints and verify the output looks sane. -With curl-runnings, you can write your tests just as data in a yaml or json file,-curl runnings will take care of the rest!+Now you can write your tests just as data in a yaml or json file,+and curl-runnings will take care of the rest! While yaml/json is the current way to write curl-runnings tests, this project is being built in a way that should lend itself well to an embedded domain specific-language, which is a future goal for the project+language, which is a future goal for the project. curl-runnings specs in Dhall+is also being developed and may fufill the same needs. ### Installing There are few options to install: -- download the releases from the github [releases page](https://github.com/aviaviavi/curl-runnings/releases)-- `stack install curl-runnings`-- `cabal install curl-runnings`+- download the releases from the+ github [releases page](https://github.com/aviaviavi/curl-runnings/releases)+- install the binary with `stack` or `cabal` - build from source with `stack` ### Writing a test specification -For now, you write your tests specs in a yaml or json file. See /examples to get+Write your tests specs in a yaml or json file. See /examples to get started. A test spec is a top level array of test cases, each item represents a single curl and set of assertions about the response. @@ -46,30 +49,31 @@ Once you've written a spec, simply run it with: -```bash-$ curl-runnings -f path/to/your/spec.yaml-```+```bash $ curl-runnings -f path/to/your/spec.yaml ``` If all your tests pass, curl-runnings will cleanly exit with a 0 code. A code of 1 will be returned if any tests failed. For more info: -```bash-$ curl-runnings --help-```+```bash $ curl-runnings --help ``` -### Roadmap+### Contributing +Curl-runnings is totally usable now but is also being actively developed. Contributions in any form are welcome and encouraged. Don't be shy! :D +### Roadmap+ - [x] Json specifications for tests - [x] Yaml specifications for tests-- [ ] Embedded dsl for specifications for tests. As the specification gets more complex.- - [ ] Spec out dsl that can compile down into a yaml/json spec- - [ ] Implement dsl+- [ ] Dhall specifications for tests - [ ] More specification features- - [ ] timeouts- - [ ] retry logic- - [ ] ability to configure alerts+ - [x] Reference values from previous json responses in matchers+ - [x] Environment variable interpolation+ - [ ] Call out to arbitrary shell commands in and between test cases+ - [ ] Timeouts+ - [ ] Support for non-json content type+ - [ ] Retry logic+- [ ] A DSL for writing test specs
app/Main.hs view
@@ -3,16 +3,15 @@ module Main where -import qualified Data.Text as T-import Data.Version (showVersion)-import Paths_curl_runnings (version)+import qualified Data.Text as T+import Data.Version (showVersion)+import Paths_curl_runnings (version) import System.Console.CmdArgs import System.Environment import System.Exit import Testing.CurlRunnings import Testing.CurlRunnings.Internal import Testing.CurlRunnings.Types-import Text.Printf -- | Command line flags data CurlRunnings = CurlRunnings@@ -25,23 +24,30 @@ CurlRunnings {file = def &= typFile &= help "File to run"} &= summary ("curl-runnings " ++ showVersion version) &= program "curl-runnings" &=+ verbosity &= help "Use the --file or -f flag to specify an intput file spec to run" +runFile :: FilePath -> Verbosity -> IO ()+runFile "" _ =+ putStrLn+ "Please specify an input file with the --file (-f) flag or use --help for more information"+runFile path verbosityLevel = do+ home <- getEnv "HOME"+ suite <- decodeFile . T.unpack $ T.replace "~" (T.pack home) (T.pack path)+ case suite of+ Right s -> do+ results <- runSuite s $ toLogLevel verbosityLevel+ if any isFailing results+ then putStrLn (makeRed "Some tests failed") >> exitWith (ExitFailure 1)+ else putStrLn $ makeGreen "All tests passed!"+ Left messgage ->+ putStrLn . makeRed $ "Couldn't read input json or yaml file: " ++ messgage++toLogLevel :: Verbosity -> LogLevel+toLogLevel v = toEnum $ fromEnum v+ main :: IO () main = do- mainArgs <- cmdArgs argParser- case file mainArgs of- "" ->- putStrLn- "Please specify an input file with the --file (-f) flag or use --help for more information"- path -> do- home <- getEnv "HOME"- suite <- decodeFile . T.unpack $ T.replace "~" (T.pack home) (T.pack path)- case suite of- Right s -> do- results <- runSuite s- if any isFailing results- then putStrLn (makeRed "Some tests failed") >>- exitWith (ExitFailure 1)- else putStrLn $ makeGreen "All tests passed!"- Left messgage -> putStrLn $ "Couldn't read input json or yaml file: " ++ messgage+ userArgs <- cmdArgs argParser+ verbosityLevel <- getVerbosity+ runFile (file userArgs) verbosityLevel
curl-runnings.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 9530ddd7493ff79858e3c01c7064eb8119932f625cdb1fa3f5cb93e3b3786189+-- hash: e22614c4e739f355b79fac77eb1cf5c8111237f2adc4aebcccbbb3b118e8d817 name: curl-runnings-version: 0.3.0+version: 0.6.0 synopsis: A framework for declaratively writing curl based API tests description: Please see the README on Github at <https://github.com/aviaviavi/curl-runnings#readme> category: Testing@@ -22,7 +22,7 @@ extra-source-files: examples/example-spec.json examples/example-spec.yaml- examples/test.json+ examples/interpolation-spec.yaml README.md source-repository head@@ -43,6 +43,7 @@ , hspec-expectations >=0.8.2 , http-conduit >=2.2.4 , http-types >=0.9.1+ , megaparsec >=6.3.0 , text >=1.2.2.2 , unordered-containers >=0.2.8.0 , vector >=0.12.0@@ -51,6 +52,7 @@ Testing.CurlRunnings Testing.CurlRunnings.Types Testing.CurlRunnings.Internal+ Testing.CurlRunnings.Internal.Parser other-modules: Paths_curl_runnings default-language: Haskell2010
examples/example-spec.json view
@@ -28,12 +28,31 @@ "name": "test 3", "url": "http://your-url.com/other/path", "requestMethod": "GET",+ "expectData": {+ "contains": [+ {+ "keyValueMatch": {+ "key": "okay",+ "value": true+ }+ },+ {+ "valueMatch": true+ }+ ]+ },+ "expectStatus": 200+ },+ {+ "name": "test 4",+ "url": "http://your-url.com/other/path",+ "requestMethod": "GET", "headers": "Content-Type: application/json", "expectStatus": 200, "expectHeaders": "Content-Type: application/json; Hello: world" }, {- "name": "test 4",+ "name": "test 5", "url": "http://your-url.com/other/path", "requestMethod": "GET", "headers": "Content-Type: application/json",@@ -45,7 +64,7 @@ ] }, {- "name": "test 5",+ "name": "test 6", "url": "http://your-url.com/other/path", "requestMethod": "GET", "headers": "Content-Type: application/json",
examples/example-spec.yaml view
@@ -32,6 +32,25 @@ - name: test 3 url: http://your-url.com/other/path requestMethod: GET+ expectData:+ # In the `contains` case of data validation, a list of matchers is specified. Currently,+ # possible types are `valueMatch` | `keyValueMatch`.+ contains:+ # `keyValueMatch` looks for the key/value pair anywhere in the payload+ # here, {'okay': true} must be somewhere in the return payload+ - keyValueMatch:+ key: okay+ value: true+ # `valueMatch` searches for a value anywhere in the payload (note: _not_ a key).+ # Here, we look for the value `true` anywhere in the payload.+ # This can be useful for matching against values where you don't know the key ahead of time,+ # or for values in a top level array.+ - valueMatch: true+ expectStatus: 200++- name: test 4+ url: http://your-url.com/other/path+ requestMethod: GET # Specify the headers you want to sent, just like the -H flag in a curl command # IE "key: value; key: value; ..." headers: "Content-Type: application/json"@@ -40,7 +59,7 @@ # Header strings again match the -H syntax from curl expectHeaders: "Content-Type: application/json; Hello: world" -- name: test 4+- name: test 5 url: http://your-url.com/other/path requestMethod: GET headers: "Content-Type: application/json"@@ -50,7 +69,7 @@ - key: "Key-With-Val-We-Dont-Care-About" -- name: test 5+- name: test 6 url: http://your-url.com/other/path requestMethod: GET headers: "Content-Type: application/json"
+ examples/interpolation-spec.yaml view
@@ -0,0 +1,46 @@+---+- name: test 1 + # interpolate environment variables with ${...} just like you would in bash+ url: 'https://tabdextension.com/${ping_path}'+ headers: 'ping: ${ping_path}'+ requestMethod: GET+ expectData:+ exactly:+ ping: pong+ expectStatus: 200++- name: test should pass+ url: https://tabdextension.com/ping+ requestMethod: GET+ expectData:+ exactly:+ # a string can be interpolated with variables inside of a $<...>+ # a query should start by indexing the array `SUITE` which contains+ # the json results of all previous test results. otherwise, use normal json / jq syntax.+ # you can put just a bare query and subsitute any json value, or use several queries+ # combined with raw text and substitue string values+ ping: $<SUITE[0].ping>+ expectStatus: 200++- name: test will fail+ url: https://tabdextension.com/ping+ requestMethod: GET+ expectHeaders: "ping: $<SUITE[-1].ping>; ${ping_path}: pong"+ expectStatus: 200++- name: test will fail+ url: https://tabdextension.com/ping+ requestMethod: GET+ expectData:+ contains:+ - keyValueMatch:+ # environment interpolation just like bash+ key: 'interpolated ${env_variable} ${another_variable}-key'+ value: ${ping_value}+ - keyValueMatch:+ key: msg+ value: $<SUITE[0].msg>+ # here, we are interpolating several string values into a string+ - valueMatch: 'hello $<SUITE[0].ping> : $<SUITE[0].ping> trailing text'+ - valueMatch: $<SUITE[0].okay>+ expectStatus: 200
− examples/test.json
@@ -1,75 +0,0 @@-[- {- "name": "test 1",- "url": "http://sd.ddpay.io/status",- "requestMethod": "GET",- "expectData": {- "tag": "Exactly",- "contents": {- "okay": true,- "msg": ""- }- },- "expectStatus": {- "tag": "ExactCode",- "contents": 200- },- "requestData": null- },-- {- "name": "test 2",- "url": "http://sd.ddpay.io/status",- "requestMethod": "GET",- "expectData": {- "tag": "Contains",- "contents": [- {- "tag": "ValueMatch",- "contents": true- },- {- "tag": "ValueMatch",- "contents": ""- },- {- "tag": "KeyValueMatch",- "matchKey": "msg",- "matchValue": ""- },- {- "tag": "KeyValueMatch",- "matchKey": "okay",- "matchValue": true- }- ]- },- "expectStatus": {- "tag": "AnyCodeIn",- "contents": [200, 300]- },- "requestData": null- },-- {- "name": "test 4",- "url": "http://sd.ddpay.io/v1/DevOnlyGetAllCustomerIds",- "requestMethod": "POST",- "expectStatus": {- "tag": "ExactCode",- "contents": 400- },- "requestData": {}- },- {- "name": "test 5",- "url": "http://sd.ddpay.io/v1/DevOnlyGetAllCustomerIds",- "requestMethod": "POST",- "expectData": null,- "expectStatus": {- "tag": "AnyCodeIn",- "contents": [401]- },- "requestData": {"api_token": "asdf"}- }-]
src/Testing/CurlRunnings.hs view
@@ -1,10 +1,10 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} -- | curl-runnings is a framework for writing declaratively writing curl based tests for your API's. -- Write your test specifications with yaml or json, and you're done!--- module Testing.CurlRunnings- (+ ( runCase , runSuite , decodeFile@@ -12,74 +12,136 @@ import Control.Monad import Data.Aeson-import qualified Data.ByteString.Char8 as B8S-import qualified Data.ByteString.Lazy as B-import qualified Data.CaseInsensitive as CI-import qualified Data.HashMap.Strict as H+import Data.Aeson.Types+import qualified Data.ByteString.Char8 as B8S+import qualified Data.ByteString.Lazy as B+import qualified Data.CaseInsensitive as CI+import Data.Either+import qualified Data.HashMap.Strict as H+import Data.List import Data.Maybe-import qualified Data.Text as T-import qualified Data.Yaml as Y+import Data.Monoid+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Yaml as Y import Network.HTTP.Conduit import Network.HTTP.Simple-import qualified Network.HTTP.Types.Header as HTTP+import qualified Network.HTTP.Types.Header as HTTP+import System.Directory+import System.Environment+import Testing.CurlRunnings.Internal+import Testing.CurlRunnings.Internal.Parser import Testing.CurlRunnings.Types import Text.Printf -- | decode a json or yaml file into a suite object decodeFile :: FilePath -> IO (Either String CurlSuite)-decodeFile specPath =- case last $ T.splitOn "." (T.pack specPath) of- "json" ->- eitherDecode' <$> B.readFile specPath :: IO (Either String CurlSuite)- "yaml" ->- Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)- "yml" ->- Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)- _ -> return . Left $ printf "Invalid spec path %s" (T.pack specPath)+decodeFile specPath = doesFileExist specPath >>= \exists ->+ if exists then+ case last $ T.splitOn "." (T.pack specPath) of+ "json" ->+ eitherDecode' <$> B.readFile specPath :: IO (Either String CurlSuite)+ "yaml" ->+ Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)+ "yml" ->+ Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)+ _ -> return . Left $ printf "Invalid spec path %s" specPath+ else return . Left $ printf "%s not found" specPath -- | Run a single test case, and returns the result. IO is needed here since this method is responsible -- for actually curling the test case endpoint and parsing the result.-runCase :: CurlCase -> IO CaseResult-runCase curlCase = do- initReq <- parseRequest $ url curlCase- response <-- httpBS .- (setRequestHeaders- (toHTTPHeaders $ fromMaybe (HeaderSet []) (headers curlCase))) .- setRequestBodyJSON (requestData curlCase) $- initReq {method = B8S.pack . show $ requestMethod curlCase}- returnVal <-- (return . decode . B.fromStrict $ getResponseBody response) :: IO (Maybe Value)- let returnCode = getResponseStatusCode response- receivedHeaders = responseHeaders response- assertionErrors =- map fromJust $- filter- isJust- [ checkBody curlCase returnVal- , checkCode curlCase returnCode- , checkHeaders curlCase receivedHeaders- ]- return $- case assertionErrors of- [] -> CasePass curlCase- failures -> CaseFail curlCase failures+runCase :: CurlRunningsState -> CurlCase -> IO CaseResult+runCase state curlCase = do+ let eInterpolatedUrl = interpolateQueryString state $ T.pack $ url curlCase+ eInterpolatedHeaders =+ interpolateHeaders state $ fromMaybe (HeaderSet []) (headers curlCase)+ case (eInterpolatedUrl, eInterpolatedHeaders) of+ (Left err, _) ->+ return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase err]+ (_, Left err) ->+ return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase err]+ (Right interpolatedUrl, Right interpolatedHeaders) ->+ case sequence $ runReplacements state <$> requestData curlCase of+ Left l ->+ return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase l]+ Right replacedJSON -> do+ initReq <- parseRequest $ T.unpack interpolatedUrl+ let request =+ setRequestBodyJSON (fromMaybe emptyObject replacedJSON) .+ setRequestHeaders (toHTTPHeaders interpolatedHeaders) $+ initReq {method = B8S.pack . show $ requestMethod curlCase}+ logger state DEBUG (show request)+ response <- httpBS request+ logger state DEBUG (show response)+ returnVal <-+ (return . decode . B.fromStrict $ getResponseBody response) :: IO (Maybe Value)+ let returnCode = getResponseStatusCode response+ receivedHeaders = fromHTTPHeaders $ responseHeaders response+ assertionErrors =+ map fromJust $+ filter+ isJust+ [ checkBody state curlCase returnVal+ , checkCode curlCase returnCode+ , checkHeaders state curlCase receivedHeaders+ ]+ return $+ case assertionErrors of+ [] -> CasePass curlCase (Just receivedHeaders) returnVal+ failures ->+ CaseFail curlCase (Just receivedHeaders) returnVal failures -checkHeaders :: CurlCase -> [HTTP.Header] -> Maybe AssertionFailure-checkHeaders (CurlCase _ _ _ _ _ _ _ Nothing) _ = Nothing-checkHeaders curlCase@(CurlCase _ _ _ _ _ _ _ (Just matcher@(HeaderMatcher m))) l =- let receivedHeaders = fromHTTPHeaders l- notFound = filter (not . headerIn receivedHeaders) m- in- if null $ notFound then- Nothing- else- Just $ HeaderFailure curlCase matcher receivedHeaders+checkHeaders :: CurlRunningsState -> CurlCase -> Headers -> Maybe AssertionFailure+checkHeaders _ (CurlCase _ _ _ _ _ _ _ Nothing) _ = Nothing+checkHeaders state curlCase@(CurlCase _ _ _ _ _ _ _ (Just (HeaderMatcher m))) receivedHeaders =+ let interpolatedHeaders = mapM (interpolatePartialHeader state) m+ in case interpolatedHeaders of+ Left f -> Just $ QueryFailure curlCase f+ Right headerList ->+ let notFound =+ filter+ (not . headerIn receivedHeaders)+ (unsafeLogger state DEBUG "header matchers" headerList)+ in if null notFound+ then Nothing+ else Just $+ HeaderFailure+ curlCase+ (HeaderMatcher headerList)+ receivedHeaders +interpolatePartialHeader :: CurlRunningsState -> PartialHeaderMatcher -> Either QueryError PartialHeaderMatcher+interpolatePartialHeader state (PartialHeaderMatcher k v) =+ let k' = interpolateQueryString state <$> k+ v' = interpolateQueryString state <$> v+ in case (k', v') of+ (Just (Left err), _) -> Left err+ (_, Just (Left err)) -> Left err+ (Just (Right p), Just (Right q)) ->+ Right $ PartialHeaderMatcher (Just p) (Just q)+ (Just (Right p), Nothing) ->+ Right $ PartialHeaderMatcher (Just p) Nothing+ (Nothing, Just (Right p)) ->+ Right $ PartialHeaderMatcher Nothing (Just p)+ _ ->+ unsafeLogger state ERROR "WARNING: empty header matcher found" . Right $+ PartialHeaderMatcher Nothing Nothing++interpolateHeaders :: CurlRunningsState -> Headers -> Either QueryError Headers+interpolateHeaders state (HeaderSet headerList) =+ mapM+ (\(Header k v) ->+ case sequence+ [interpolateQueryString state k, interpolateQueryString state v] of+ Left err -> Left err+ Right [k', v'] -> Right $ Header k' v')+ headerList >>=+ (Right . HeaderSet)+ -- | Does this header contain our matcher? headerMatches :: PartialHeaderMatcher -> Header -> Bool headerMatches (PartialHeaderMatcher mk mv) (Header k v) =- (maybe True (== k) mk) && (maybe True (== v) mv)+ maybe True (== k) mk && maybe True (== v) mv -- | Does any of these headers contain our matcher? headerIn :: Headers -> PartialHeaderMatcher -> Bool@@ -94,44 +156,195 @@ printR x = print x >> return x -- | Runs the test cases in order and stop when an error is hit. Returns all the results-runSuite :: CurlSuite -> IO [CaseResult]-runSuite (CurlSuite cases) =+runSuite :: CurlSuite -> LogLevel -> IO [CaseResult]+runSuite (CurlSuite cases) logLevel = do+ fullEnv <- getEnvironment+ let envMap = H.fromList $ map (\(x, y) -> (T.pack x, T.pack y)) fullEnv foldM (\prevResults curlCase -> case safeLast prevResults of- Just (CaseFail _ _) -> return prevResults- Just (CasePass _) -> do- result <- runCase curlCase >>= printR+ Just CaseFail {} -> return prevResults+ Just CasePass {} -> do+ result <- runCase (CurlRunningsState envMap prevResults logLevel) curlCase >>= printR return $ prevResults ++ [result] Nothing -> do- result <- runCase curlCase >>= printR+ result <- runCase (CurlRunningsState envMap [] logLevel) curlCase >>= printR return [result]) [] cases -- | Check if the retrieved value fail's the case's assertion-checkBody :: CurlCase -> Maybe Value -> Maybe AssertionFailure+checkBody :: CurlRunningsState -> CurlCase -> Maybe Value -> Maybe AssertionFailure -- | We are looking for an exact payload match, and we have a payload to check-checkBody curlCase@(CurlCase _ _ _ _ _ (Just matcher@(Exactly expectedValue)) _ _) (Just receivedBody)- | expectedValue /= receivedBody =- Just $ DataFailure curlCase matcher (Just receivedBody)- | otherwise = Nothing+checkBody state curlCase@(CurlCase _ _ _ _ _ (Just (Exactly expectedValue)) _ _) (Just receivedBody) =+ case runReplacements state expectedValue of+ (Left err) -> Just $ QueryFailure curlCase err+ (Right interpolated) ->+ if (unsafeLogger state DEBUG "exact body matcher" interpolated) /= receivedBody+ then Just $+ DataFailure+ (curlCase {expectData = Just $ Exactly interpolated})+ (Exactly interpolated)+ (Just receivedBody)+ else Nothing+ -- | We are checking a list of expected subvalues, and we have a payload to check-checkBody curlCase@(CurlCase _ _ _ _ _ (Just matcher@(Contains expectedSubvalues)) _ _) (Just receivedBody)- | jsonContainsAll receivedBody expectedSubvalues = Nothing- | otherwise = Just $ DataFailure curlCase matcher (Just receivedBody)+checkBody state curlCase@(CurlCase _ _ _ _ _ (Just (Contains subexprs)) _ _) (Just receivedBody) =+ case runReplacementsOnSubvalues state subexprs of+ Left f -> Just $ QueryFailure curlCase f+ Right updatedMatcher ->+ if jsonContainsAll receivedBody (unsafeLogger state DEBUG "partial json body matcher" updatedMatcher)+ then Nothing+ else Just $+ DataFailure curlCase (Contains updatedMatcher) (Just receivedBody)+ -- | We expected a body but didn't get one-checkBody curlCase@(CurlCase _ _ _ _ _ (Just anything) _ _) Nothing = Just $ DataFailure curlCase anything Nothing+checkBody _ curlCase@(CurlCase _ _ _ _ _ (Just anything) _ _) Nothing =+ Just $ DataFailure curlCase anything Nothing+ -- | No assertions on the body-checkBody (CurlCase _ _ _ _ _ Nothing _ _) _ = Nothing+checkBody _ (CurlCase _ _ _ _ _ Nothing _ _) _ = Nothing +runReplacementsOnSubvalues :: CurlRunningsState -> [JsonSubExpr] -> Either QueryError [JsonSubExpr]+runReplacementsOnSubvalues state =+ mapM+ (\expr ->+ case expr of+ ValueMatch v ->+ case runReplacements state v of+ Left l -> Left l+ Right newVal -> Right $ ValueMatch newVal+ KeyValueMatch k v ->+ case (interpolateQueryString state k, runReplacements state v) of+ (Left l, _) -> Left l+ (_, Left l) -> Left l+ (Right k', Right v') ->+ Right KeyValueMatch {matchKey = k', matchValue = v'})++-- | runReplacements+runReplacements :: CurlRunningsState -> Value -> Either QueryError Value+runReplacements state (Object o) =+ let keys = H.keys o+ keysWithUpdatedKeyVal =+ map+ (\key ->+ let value = fromJust $ H.lookup key o+ -- (old key, new key, new value)+ in ( key+ , interpolateQueryString state key+ , runReplacements state value))+ keys+ in mapRight Object $+ foldr+ (\((key, eKeyResult, eValueResult) :: ( T.Text+ , Either QueryError T.Text+ , Either QueryError Value)) (eObjectToUpdate :: Either QueryError Object) ->+ case (eKeyResult, eValueResult, eObjectToUpdate)+ -- TODO there should be a more elegant way to write this error+ -- handling below+ of+ (Left queryErr, _, _) -> Left queryErr+ (_, Left queryErr, _) -> Left queryErr+ (_, _, Left queryErr) -> Left queryErr+ (Right newKey, Right newValue, Right objectToUpdate) ->+ if key /= newKey+ then let inserted = H.insert newKey newValue objectToUpdate+ deleted = H.delete key inserted+ in Right deleted+ else Right $ H.insert key newValue objectToUpdate)+ (Right o)+ keysWithUpdatedKeyVal+runReplacements p (Array a) =+ let results = V.mapM (runReplacements p) a+ in case results of+ Left l -> Left l+ Right r -> Right $ Array r+-- special case, i can't figure out how to get the parser to parse empty strings :'(+runReplacements _ s@(String "") = Right s+runReplacements state (String s) =+ case parseQuery s of+ Right [LiteralText t] -> Right $ String t+ Right [q@(InterpolatedQuery _ _)] -> getStringValueForQuery state q >>= (Right . String)+ Right [q@(NonInterpolatedQuery _)] -> getValueForQuery state q+ Right _ -> mapRight String $ interpolateQueryString state s+ Left parseErr -> Left parseErr+runReplacements _ valToUpdate = Right valToUpdate++-- | Given a query string, return some text with interpolated values. Type+-- errors will be returned if queries don't resolve to strings+interpolateQueryString :: CurlRunningsState -> FullQueryText -> Either QueryError T.Text+interpolateQueryString state query =+ let parsedQuery = parseQuery query+ in case parsedQuery of+ (Left err) -> Left err+ (Right interpolatedQ) ->+ let lookups :: [Either QueryError T.Text] =+ map (getStringValueForQuery state) interpolatedQ+ failure = find isLeft lookups+ goodLookups :: [T.Text] =+ Prelude.map (fromRight (T.pack "error")) lookups+ in fromMaybe (Right $ foldr (<>) (T.pack "") goodLookups) failure++-- | Lookup the text at the specified query+getStringValueForQuery :: CurlRunningsState -> InterpolatedQuery -> Either QueryError T.Text+getStringValueForQuery _ (LiteralText rawText) = Right rawText+getStringValueForQuery state (NonInterpolatedQuery q) =+ getStringValueForQuery state $ InterpolatedQuery "" q+getStringValueForQuery state i@(InterpolatedQuery rawText (Query _)) =+ case getValueForQuery state i of+ Left l -> Left l+ Right (String s) -> Right $ rawText <> s+ (Right o) -> Left $ QueryTypeMismatch "Expected a string" o+getStringValueForQuery (CurlRunningsState env _ _) (InterpolatedQuery rawText (EnvironmentVariable v)) =+ Right $ rawText <> H.lookupDefault "" v env++-- | Lookup the value for the specified query+getValueForQuery :: CurlRunningsState -> InterpolatedQuery -> Either QueryError Value+getValueForQuery _ (LiteralText rawText) = Right $ String rawText+getValueForQuery (CurlRunningsState _ previousResults _) full@(NonInterpolatedQuery (Query indexes)) =+ case head indexes of+ (CaseResultIndex i) ->+ let (CasePass _ _ returnedJSON) = arrayGet previousResults $ fromInteger i+ jsonToIndex =+ case returnedJSON of+ Just v -> Right v+ Nothing ->+ Left $+ NullPointer+ (T.pack $ show full)+ "No data was returned from this case"+ in foldr+ (\index eitherVal ->+ case (eitherVal, index) of+ (Left l, _) -> Left l+ (Right (Object o), KeyIndex k) ->+ Right $ H.lookupDefault Null k o+ (Right (Array a), ArrayIndex i') -> Right $ arrayGet (V.toList a) $ fromInteger i'+ (Right Null, q) ->+ Left $ NullPointer (T.pack $ show full) (T.pack $ show q)+ (Right o, _) -> Left $ QueryTypeMismatch (T.pack $ show index) o)+ jsonToIndex+ (tail indexes)+ _ ->+ Left . QueryValidationError $+ T.pack $ "$<> queries must start with a SUITE[index] query: " ++ show full+getValueForQuery (CurlRunningsState env _ _) (NonInterpolatedQuery (EnvironmentVariable var)) =+ Right . String $ H.lookupDefault "" var env+getValueForQuery state (InterpolatedQuery _ q) =+ case getValueForQuery state (NonInterpolatedQuery q) of+ Right (String s) -> Right . String $ s+ Right Null -> Right Null+ Right v -> Left $ QueryTypeMismatch (T.pack "Expected a string") v+ Left l -> Left l+ -- | Does the json value contain all of these sub-values? jsonContainsAll :: Value -> [JsonSubExpr] -> Bool jsonContainsAll jsonValue =- all $ \match -> case match of- ValueMatch subval -> subval `elem` traverseValue jsonValue- KeyValueMatch key subval ->- containsKeyVal jsonValue key subval+ all $ \match ->+ case match of+ ValueMatch subval -> subval `elem` traverseValue jsonValue+ KeyValueMatch key subval ->+ any (\o -> containsKeyVal o key subval) (traverseValue jsonValue) -- | Does the json value contain the given key value pair? containsKeyVal :: Value -> T.Text -> Value -> Bool
src/Testing/CurlRunnings/Internal.hs view
@@ -3,10 +3,58 @@ module Testing.CurlRunnings.Internal ( makeRed , makeGreen+ , tracer+ , mapRight+ , arrayGet+ , makeLogger+ , makeUnsafeLogger++ , LogLevel(..)+ , CurlRunningsLogger+ , CurlRunningsUnsafeLogger ) where +import Control.Monad+import Debug.Trace+ makeGreen :: String -> String makeGreen s = "\x1B[32m" ++ s ++ "\x1B[0m" makeRed :: String -> String makeRed s = "\x1B[31m" ++ s ++ "\x1B[0m"++tracer :: Show a => String -> a -> a+tracer a b = trace (a ++ ": " ++ show b) b++mapRight :: (b -> c) -> Either a b -> Either a c+mapRight f (Right v) = Right $ f v+mapRight _ (Left v) = Left v++-- | Array indexing with negative values allowed+arrayGet :: [a] -> Int -> a+arrayGet a i+ | i >= 0 = a !! i+ | otherwise = reverse a !! (-i)++data LogLevel = ERROR | INFO | DEBUG deriving (Show, Eq, Ord, Enum)++-- | A logger that respects the verbosity level given by input args+type CurlRunningsLogger = (LogLevel -> String -> IO ())++-- | A tracer that respects the verbosity level given by input args. Logging+-- with this calls out to Debug.trace and can be used in pure code, but be aware+-- of the unsafe IO.+type CurlRunningsUnsafeLogger a = (LogLevel -> String -> a -> a)++makeLogger :: LogLevel -> CurlRunningsLogger+makeLogger threshold level text =+ when (level <= threshold) $ putStrLn text++makeUnsafeLogger :: Show a => LogLevel -> CurlRunningsUnsafeLogger a+makeUnsafeLogger threshold level text object =+ if level <= threshold then+ tracer text object+ else+ object++
+ src/Testing/CurlRunnings/Internal/Parser.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Internal module for parsing string based directives inside of curl runnings+-- suites. Use this module at your own risk, it may change. Currently, string+-- interpolation can be performed, where interpolated values are json quries+-- into responses from past test cases.+--+-- > "$<SUITE[0].key[0].another_key>"+--+-- here the `SUITE` keyword references the results of previous test cases. Here, the+-- whole string is a query, so if the value referenced by this query is itself a+-- json value, the entire value will replace this string in a json matcher.+-- Additionally, interpolation of the form:+--+-- >+-- > "some text to interpolate with $<SUITE[0].key.key>"+-- >+--+-- will substitute a string found at the specified query+-- and subsitute the string.+--+-- Rules for the language are similar to JQ or regular JSON indexing rules. All+-- queries must start with a SUITE[integer] index, and be written between a+--+-- >+-- > $< ... >+-- >+--+-- to signify an interpolation. You can have mutliple queries inside a+-- single string, but if interpolation is occuring, then the query specified+-- must resolve to a string value. Otheriwse, a type error will be thrown.+module Testing.CurlRunnings.Internal.Parser+ (+ parseQuery+ ) where++import Data.List+import qualified Data.Text as T+import Data.Void+import Testing.CurlRunnings.Types+import Text.Megaparsec+import Text.Megaparsec.Char+import qualified Text.Megaparsec.Char.Lexer as L++-- | Given some query text, attempt to parse it to a list of interplated query objects. This data representation may change.+parseQuery :: FullQueryText -> Either QueryError [InterpolatedQuery]+parseQuery q =+ let trimmed = T.strip q+ in case Text.Megaparsec.parse parseFullTextWithQuery "" trimmed of+ Right a -> Right a+ Left a -> Left $ QueryParseError (T.pack $ parseErrorPretty a) q++type Parser = Parsec Void T.Text++parseSuiteIndex' :: Parser Index+parseSuiteIndex' = do+ notFollowedBy gtlt+ _ <- string "SUITE"+ (ArrayIndex i) <- arrayIndexParser+ return $ CaseResultIndex i++spaceOrDot :: Parser ()+spaceOrDot = (try $ char '.' >> space) <|> space++lexeme :: Parser a -> Parser a+lexeme = L.lexeme spaceOrDot++symbol :: T.Text -> Parser T.Text+symbol = L.symbol spaceOrDot++inGTLT :: Parser a -> Parser a+inGTLT = between (symbol "$<") (string ">")++gtlt :: Parser T.Text+gtlt = symbol "<" <|> symbol ">"++brackets :: Parser a -> Parser a+brackets = between (symbol "[") (symbol "]")++bracket :: Parser T.Text+bracket = symbol "[" <|> symbol "]"++braces :: Parser a -> Parser a+braces = between (symbol "${") (string "}")++brace :: Parser T.Text+brace = symbol "{" <|> symbol "}"++integer :: Parser Integer+integer = lexeme $ L.signed spaceOrDot L.decimal++dot :: Parser T.Text+dot = symbol "."++arrayIndexParser :: Parser Index+arrayIndexParser = notFollowedBy gtlt >> ArrayIndex <$> brackets integer++environmentVariableParser :: Parser Query+environmentVariableParser = do+ notFollowedBy endingChars+ (EnvironmentVariable . T.pack) <$> braces (lexeme $ many (noneOf ['[', ']', '<', '>', ' ', '{', '}']))++endingChars :: Parser T.Text+endingChars = dot <|> eol <|> bracket <|> gtlt <|> brace++keyIndexParser :: Parser Index+keyIndexParser = do+ notFollowedBy endingChars+ (lexeme . try) ((KeyIndex . T.pack) <$> p)+ where+ p = (:) <$> letterChar <*> many (noneOf ['.', '[', ']', '<', '>', ' ', '{', '}'])++jsonIndexParser :: Parser Query+jsonIndexParser =+ leadingText >>+ (inGTLT $ some (parseSuiteIndex' <|> keyIndexParser <|> arrayIndexParser)) >>=+ return . Query++interpolatedQueryParser :: Parser InterpolatedQuery+interpolatedQueryParser = do+ text <- leadingText+ q <- environmentVariableParser <|> jsonIndexParser+ if null text then return $ NonInterpolatedQuery q+ else return $ InterpolatedQuery (T.pack text) q++leadingText :: Parser String+leadingText = manyTill anyChar $ lookAhead (symbol "$<" <|> "${")++noQueryText :: Parser InterpolatedQuery+noQueryText = do+ str <- some anyChar+ eof+ if "$<" `isInfixOf` str+ then fail "invalid `$<` found"+ else if "${" `isInfixOf` str+ then fail "invalid `${` found"+ else return $ LiteralText $ T.pack str++parseFullTextWithQuery :: Parser [InterpolatedQuery]+parseFullTextWithQuery = many ((try interpolatedQueryParser) <|> noQueryText)+
src/Testing/CurlRunnings/Types.hs view
@@ -1,5 +1,5 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-} -- | Data types for curl-runnings tests @@ -16,9 +16,18 @@ , JsonSubExpr(..) , PartialHeaderMatcher(..) , StatusCodeMatcher(..)+ , QueryError(..)+ , Index(..)+ , Query(..)+ , InterpolatedQuery(..)+ , FullQueryText+ , SingleQueryText+ , CurlRunningsState(..) , isFailing , isPassing+ , logger+ , unsafeLogger ) where @@ -96,6 +105,26 @@ instance ToJSON HeaderMatcher +-- | Different errors relating to querying json from previous test cases+data QueryError+ -- | The query was malformed and couldn't be parsed+ = QueryParseError T.Text T.Text+ -- | The retrieved a value of the wrong type or was otherwise operating on the+ -- wrong type of thing+ | QueryTypeMismatch T.Text+ Value+ -- | The query was parse-able+ | QueryValidationError T.Text+ -- | Tried to access a value in a null object+ | NullPointer T.Text+ T.Text++instance Show QueryError where+ show (QueryParseError t q) = printf "error parsing query %s: %s" q $ T.unpack t+ show (NullPointer full part) = printf "null pointer in %s at %s" (T.unpack full) $ T.unpack part+ show (QueryTypeMismatch message val) = printf "type error: %s %s" (message) $ show val+ show (QueryValidationError message) = printf "invalid query: %s" message+ parseHeader :: T.Text -> Either T.Text Header parseHeader str = case map T.strip $ T.splitOn ":" str of@@ -207,56 +236,79 @@ | HeaderFailure CurlCase HeaderMatcher Headers+ -- | Something went wrong with a test case json query+ | QueryFailure CurlCase+ QueryError -- | Something else | UnexpectedFailure ++colorizeExpects :: String -> String+colorizeExpects t =+ let expectedColor = makeRed "Excpected:"+ actualColor = makeRed "Actual:"+ replacedExpected = T.replace "Expected:" (T.pack expectedColor) (T.pack t)+ in T.unpack $ T.replace "Actual:" (T.pack actualColor) replacedExpected+ instance Show AssertionFailure where show (StatusFailure c receivedCode) = case expectStatus c of ExactCode code ->+ colorizeExpects $ printf "Incorrect status code from %s. Expected: %s. Actual: %s" (url c) (show code) (show receivedCode) AnyCodeIn codes ->+ colorizeExpects $ printf- "Incorrect status code from %s. Expected one of: %s. Actual: %s"+ "Incorrect status code from %s. Expected: %s. Actual: %s" (url c) (show codes) (show receivedCode) show (DataFailure curlCase expected receivedVal) = case expected of Exactly expectedVal ->+ colorizeExpects $ printf "JSON response from %s didn't match spec. Expected: %s. Actual: %s" (url curlCase) (B8.unpack (encodePretty expectedVal)) (B8.unpack (encodePretty receivedVal)) (Contains expectedVals) ->+ colorizeExpects $ printf "JSON response from %s didn't contain the matcher. Expected: %s to be each be subvalues in: %s" (url curlCase) (B8.unpack (encodePretty expectedVals)) (B8.unpack (encodePretty receivedVal)) show (HeaderFailure curlCase expected receivedHeaders) =+ colorizeExpects $ printf- "Headers from %s didn't contain expected headers. Expected headers: %s. Received headers: %s"+ "Headers from %s didn't contain expected headers. Expected: %s. Actual: %s" (url curlCase) (show expected) (show receivedHeaders)+ show (QueryFailure curlCase queryErr) =+ colorizeExpects $+ printf "JSON query error in spec %s: %s" (name curlCase) (show queryErr) show UnexpectedFailure = "Unexpected Error D:" -- | A type representing the result of a single curl, and all associated -- assertions data CaseResult = CasePass CurlCase+ (Maybe Headers)+ (Maybe Value) | CaseFail CurlCase+ (Maybe Headers)+ (Maybe Value) [AssertionFailure] instance Show CaseResult where- show (CasePass c) = makeGreen "[PASS] " ++ name c- show (CaseFail c failures) =+ show (CasePass c _ _) = makeGreen "[PASS] " ++ name c+ show (CaseFail c _ _ failures) = makeRed "[FAIL] " ++ name c ++ "\n" ++@@ -274,10 +326,67 @@ -- | Simple predicate that checks if the result is passing isPassing :: CaseResult -> Bool-isPassing (CasePass _) = True-isPassing (CaseFail _ _) = False+isPassing CasePass {} = True+isPassing CaseFail {} = False -- | Simple predicate that checks if the result is failing isFailing :: CaseResult -> Bool-isFailing (CasePass _) = False-isFailing (CaseFail _ _) = True+isFailing = not . isPassing++-- | A map of the system environment+type Environment = H.HashMap T.Text T.Text++-- | The state of a suite. Tracks environment variables, and all the test results so far+data CurlRunningsState = CurlRunningsState Environment [CaseResult] LogLevel++logger :: CurlRunningsState -> CurlRunningsLogger+logger (CurlRunningsState _ _ l) = makeLogger l++unsafeLogger :: Show a => CurlRunningsState -> CurlRunningsUnsafeLogger a+unsafeLogger (CurlRunningsState _ _ l) = makeUnsafeLogger l++-- | A single lookup operation in a json query+data Index+ -- | Drill into the json of a specific test case. The SUITE object is+ -- accessible as an array of values that have come back from previous test+ -- cases+ = CaseResultIndex Integer+ -- | A standard json key lookup.+ | KeyIndex T.Text+ -- | A standard json array index lookup.+ | ArrayIndex Integer+ deriving (Show)++printOriginalQuery (CaseResultIndex t) = "SUITE[" ++ show t ++ "]"+printOriginalQuery (KeyIndex key) = "." ++ T.unpack key+printOriginalQuery (ArrayIndex i) = printf "[%d]" i++-- | A single entity to be queries from a json value+data Query =+ -- | A single query contains a list of discrete index operations+ Query [Index] |+ -- | Lookup a string in the environment+ EnvironmentVariable T.Text+ deriving (Show)++-- | A distinct parsed unit in a query+data InterpolatedQuery+ -- | Regular text, no query+ = LiteralText T.Text+ -- | Some leading text, then a query+ | InterpolatedQuery T.Text+ Query+ -- | Just a query, no leading text+ | NonInterpolatedQuery Query+ deriving (Show)++printQueryString :: InterpolatedQuery -> String+printQueryString (LiteralText t) = show t+printQueryString (InterpolatedQuery raw (Query indexes)) =+ printf "%s$<%s>" raw (concat $ map show indexes)+printQueryString (NonInterpolatedQuery (Query indexes)) = printf "$<%s>" (concat $ map show indexes)++-- | The full string in which a query appears, eg "prefix-${{SUITE[0].key.another_key[0].last_key}}"+type FullQueryText = T.Text+-- | The string for one query given the FullQueryText above, the single query text would be SUITE[0].key.another_key[0].last_key+type SingleQueryText = T.Text
test/Spec.hs view
@@ -1,9 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}+ module Main where import Data.Either import System.Directory import Test.Hspec import Testing.CurlRunnings+import Testing.CurlRunnings.Internal.Parser main :: IO () main = hspec $@@ -13,6 +16,28 @@ it "should provide valid example json specs" $ testValidSpec "/examples/example-spec.json"++ -- note that this doesn't actually try to parse the interpolations themselves,+ -- but that would be useful thing to add here+ it "should provid a valid interpolation spec" $+ testValidSpec "/examples/interpolation-spec.yaml"++ it "should parse valid queries" $ do+ parseQuery "just some text" `shouldSatisfy` isRight+ parseQuery "$<SUITE[0].key.key>" `shouldSatisfy` isRight+ parseQuery "$<SUITE[0].key.key[0].key_with_underscores>" `shouldSatisfy` isRight+ parseQuery "$<SUITE[100].key.key[0].key_with_underscores>" `shouldSatisfy` isRight+ parseQuery "some text before $<SUITE[100].key.key[0].key_with_underscores> and after" `shouldSatisfy` isRight+ parseQuery "some $<SUITE[100]> querires $<SUITE[100]>" `shouldSatisfy` isRight+ parseQuery "some $<SUITE[100]> querires $<SUITE[100]> ${SOME_ENV_VARIABLE} asdf" `shouldSatisfy` isRight++ it "should reject invalid queries" $ do+ parseQuery "$<" `shouldSatisfy` isLeft+ parseQuery "$<SUITE[f].key>" `shouldSatisfy` isLeft+ parseQuery "$<SUITE[1].key[r]>" `shouldSatisfy` isLeft+ parseQuery "$<SUITE[1].key[1][1] $<>>" `shouldSatisfy` isLeft+ parseQuery "$<SUITE[1].key[1][1]" `shouldSatisfy` isLeft+ parseQuery "$<SUITE[1]>.key[1][1] ${" `shouldSatisfy` isLeft testValidSpec :: String -> IO () testValidSpec file = do