Skip to content

Commit 3921500

Browse files
authored
Basic Dhall support (#11)
* basic dhall support * add linux modifier to archive name, for consistency * dhall in docs * readme updates * bump version
1 parent af1911a commit 3921500

File tree

10 files changed

+434
-45
lines changed

10 files changed

+434
-45
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ script: |
3737
ln -s curl-runnings-${version}.tar.gz curl-runnings.tar.gz &&
3838
mkdir -p $HOME/cr-release &&
3939
cd $HOME/cr-release &&
40-
ln -s $HOME/.local/bin/curl-runnings-${version}.tar.gz curl-runnings-${version}.tar.gz
40+
ln -s $HOME/.local/bin/curl-runnings-${version}.tar.gz curl-runnings-${version}-linux.tar.gz
4141
cache:
4242
directories:
4343
- $HOME/.stack

README.md

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,19 @@
66

77
_Feel the rhythm! Feel the rhyme! Get on up, it's testing time! curl-runnings!_
88

9-
curl-runnings is a framework for writing declarative, curl based tests for your
10-
APIs. Write your tests quickly and correctly with a straight-forward
11-
specification in yaml or json that can encode simple but powerful matchers
12-
against responses.
9+
A common form of black-box API testing boils down to simply making requests to
10+
an endpoint and verifying properties of the response. curl-runnings aims to make
11+
writing tests like this fast and easy.
12+
13+
curl-runnings is a framework for writing declarative tests for your APIs in a
14+
fashion equivalent to performing `curl`s and verifying the responses. Write your
15+
tests quickly and correctly with a straight-forward specification in
16+
[Dhall](https://dhall-lang.org/), yaml, or json that can encode simple but
17+
powerful matchers against responses.
1318

1419
Alternatively, you can use the curl-runnings library to write your tests in
1520
Haskell (though a Haskell setup is absolutely not required to use this tool).
1621

17-
### Why?
18-
19-
This library came out of a pain-point my coworkers and I were running into
20-
during development: Writing integration tests for our APIs was generally
21-
annoying. They were time consuming to write especially considering how basic
22-
they were, and we are a small startup where developer time is in short supply.
23-
Over time, we found ourselves sometimes just writing bash scripts that would
24-
`curl` our various endpoints and check the output with very basic matchers.
25-
These tests were fast to write, but quickly became difficult to maintain as
26-
complexity was added. Not only did maintenance become challenging, but the whole
27-
system was very error prone and confidence in the tests overall was decreasing.
28-
At the end of the day, we needed to just curl some endpoints and verify the
29-
output looks sane, and do this quickly and correctly. This is precisely the goal
30-
of curl-runnings.
31-
32-
Now you can write your tests just as data in a yaml or json file,
33-
and curl-runnings will take care of the rest!
34-
35-
While yaml/json is the current way to write curl-runnings tests, this project is
36-
being built in a way that should lend itself well to an embedded domain specific
37-
language, which is a future goal for the project. curl-runnings specs in Dhall
38-
is also being developed and may fulfill the same needs.
3922

4023
### Installing
4124

@@ -55,10 +38,31 @@ Alternatively, you can compile from source with stack.
5538

5639
Curl runnings tests are just data! A test spec is an object containing an array
5740
of `cases`, where each item represents a single curl and set of assertions about
58-
the response. Write your tests specs in a yaml or json file. Note: the legacy
41+
the response. Write your tests specs in a Dhall, yaml or json file. Note: the legacy
5942
format of a top level array of test cases is still supported, but may not be in
6043
future releases.
6144

45+
```dhall
46+
let JSON = https://prelude.dhall-lang.org/JSON/package.dhall
47+
48+
let CurlRunnings = ./dhall/curl-runnings.dhall
49+
50+
in CurlRunnings.hydrateCase
51+
CurlRunnings.Case::{
52+
, expectData = Some
53+
( CurlRunnings.ExpectData.Exactly
54+
( JSON.object
55+
[ { mapKey = "okay", mapValue = JSON.bool True },
56+
{ mapKey = "message", mapValue = JSON.string "a message" }]
57+
)
58+
)
59+
, expectStatus = 200
60+
, name = "test 1"
61+
, requestMethod = CurlRunnings.HttpMethod.GET
62+
, url = "http://your-endpoing.com/status"
63+
}
64+
```
65+
6266

6367
```yaml
6468
---

dhall/curl-runnings.dhall

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
let JSON = https://prelude.dhall-lang.org/JSON/package.dhall
2+
3+
let List/map = https://prelude.dhall-lang.org/List/map
4+
5+
let Optional/map = https://prelude.dhall-lang.org/Optional/map
6+
7+
let Map = https://prelude.dhall-lang.org/Map/Type
8+
9+
let HttpMethod
10+
: Type
11+
= < GET | POST | PUT | PATCH | DELETE >
12+
13+
let PartialMatcher =
14+
< KeyMatch : Text
15+
| ValueMatch : JSON.Type
16+
| KeyValueMatch : { key : Text, value : JSON.Type }
17+
>
18+
19+
let ExpectData =
20+
< Exactly : JSON.Type
21+
| Contains : List PartialMatcher
22+
| NotContains : List PartialMatcher
23+
| MixedContains :
24+
{ contains : List PartialMatcher, notContains : List PartialMatcher }
25+
>
26+
27+
let KeyValMatchHydrated =
28+
{ keyMatch : Optional PartialMatcher
29+
, valueMatch : Optional PartialMatcher
30+
, keyValueMatch : Optional PartialMatcher
31+
}
32+
33+
let ExpectHeaders =
34+
< HeaderString : Text
35+
| HeaderKeyVal : { key : Optional Text, value : Optional Text }
36+
>
37+
38+
let hydrateContains =
39+
λ(containsMatcher : PartialMatcher)
40+
merge
41+
{ KeyMatch =
42+
λ(k : Text)
43+
{ keyMatch = Some (PartialMatcher.KeyMatch k)
44+
, valueMatch = None PartialMatcher
45+
, keyValueMatch = None PartialMatcher
46+
}
47+
, ValueMatch =
48+
λ(v : JSON.Type)
49+
{ keyMatch = None PartialMatcher
50+
, valueMatch = Some (PartialMatcher.ValueMatch v)
51+
, keyValueMatch = None PartialMatcher
52+
}
53+
, KeyValueMatch =
54+
λ(args : { key : Text, value : JSON.Type })
55+
{ keyMatch = None PartialMatcher
56+
, valueMatch = None PartialMatcher
57+
, keyValueMatch = Some
58+
( PartialMatcher.KeyValueMatch
59+
{ key = args.key, value = args.value }
60+
)
61+
}
62+
}
63+
containsMatcher
64+
65+
let ExpectResponseHydrated =
66+
{ exactly : Optional ExpectData
67+
, contains : Optional (List KeyValMatchHydrated)
68+
, notContains : Optional (List KeyValMatchHydrated)
69+
}
70+
71+
let hydrateExpectData =
72+
λ(matcher : ExpectData)
73+
merge
74+
{ Exactly =
75+
λ(j : JSON.Type)
76+
{ exactly = Some (ExpectData.Exactly j)
77+
, contains = None (List KeyValMatchHydrated)
78+
, notContains = None (List KeyValMatchHydrated)
79+
}
80+
, Contains =
81+
λ(ms : List PartialMatcher)
82+
{ exactly = None ExpectData
83+
, contains = Some
84+
( List/map
85+
PartialMatcher
86+
KeyValMatchHydrated
87+
hydrateContains
88+
ms
89+
)
90+
, notContains = None (List KeyValMatchHydrated)
91+
}
92+
, NotContains =
93+
λ(ms : List PartialMatcher)
94+
{ exactly = None ExpectData
95+
, contains = None (List KeyValMatchHydrated)
96+
, notContains = Some
97+
( List/map
98+
PartialMatcher
99+
KeyValMatchHydrated
100+
hydrateContains
101+
ms
102+
)
103+
}
104+
, MixedContains =
105+
λ ( args
106+
: { contains : List PartialMatcher
107+
, notContains : List PartialMatcher
108+
}
109+
)
110+
{ exactly = None ExpectData
111+
, contains = Some
112+
( List/map
113+
PartialMatcher
114+
KeyValMatchHydrated
115+
hydrateContains
116+
args.contains
117+
)
118+
, notContains = Some
119+
( List/map
120+
PartialMatcher
121+
KeyValMatchHydrated
122+
hydrateContains
123+
args.notContains
124+
)
125+
}
126+
}
127+
matcher
128+
129+
let BodyType = < json | urlencoded >
130+
131+
let RequestData = < JSON : JSON.Type | UrlEncoded : Map Text Text >
132+
133+
let RequestDataHydrated = { bodyType : BodyType, content : RequestData }
134+
135+
let hydrateRquestData =
136+
λ(reqData : RequestData)
137+
merge
138+
{ JSON =
139+
λ(json : JSON.Type)
140+
{ bodyType = BodyType.json, content = RequestData.JSON json }
141+
, UrlEncoded =
142+
λ(encoded : Map Text Text)
143+
{ bodyType = BodyType.urlencoded
144+
, content = RequestData.UrlEncoded encoded
145+
}
146+
}
147+
reqData
148+
149+
let makeQueryParams =
150+
λ(params : Map Text Text)
151+
JSON.object
152+
( List/map
153+
{ mapKey : Text, mapValue : Text }
154+
{ mapKey : Text, mapValue : JSON.Type }
155+
( λ(args : { mapKey : Text, mapValue : Text })
156+
{ mapKey = args.mapKey, mapValue = JSON.string args.mapValue }
157+
)
158+
params
159+
)
160+
161+
let QueryParams = List { mapKey : Text, mapValue : JSON.Type }
162+
163+
let Case =
164+
{ Type =
165+
{ name : Text
166+
, url : Text
167+
, requestMethod : HttpMethod
168+
, queryParameters : Map Text Text
169+
, expectData : Optional ExpectData
170+
, expectStatus : Natural
171+
, headers : Optional Text
172+
, expectHeaders : Optional (List ExpectHeaders)
173+
, allowedRedirects : Natural
174+
, requestData : Optional RequestData
175+
}
176+
, default =
177+
{ expectData = None ExpectData
178+
, headers = None Text
179+
, expectHeaders = None (List ExpectHeaders)
180+
, allowedRedirects = 10
181+
, queryParameters = [] : Map Text Text
182+
, requestData = None RequestData
183+
}
184+
}
185+
186+
let HydratedCase =
187+
{ Type =
188+
{ name : Text
189+
, url : Text
190+
, requestMethod : HttpMethod
191+
, queryParameters : JSON.Type
192+
, expectData : Optional ExpectResponseHydrated
193+
, expectStatus : Natural
194+
, headers : Optional Text
195+
, expectHeaders : Optional (List ExpectHeaders)
196+
, allowedRedirects : Natural
197+
, requestData : Optional RequestDataHydrated
198+
}
199+
, default =
200+
{ expectData = None ExpectResponseHydrated
201+
, headers = None Text
202+
, expectHeaders = None (List ExpectHeaders)
203+
, allowedRedirects = 10
204+
, queryParameters = JSON.null
205+
, requestData = None RequestDataHydrated
206+
}
207+
}
208+
209+
let hydrateCase =
210+
λ(c : Case.Type)
211+
c
212+
{ queryParameters = makeQueryParams c.queryParameters
213+
, expectData =
214+
Optional/map
215+
ExpectData
216+
ExpectResponseHydrated
217+
hydrateExpectData
218+
c.expectData
219+
, requestData =
220+
Optional/map
221+
RequestData
222+
RequestDataHydrated
223+
hydrateRquestData
224+
c.requestData
225+
}
226+
227+
let hydrateCases =
228+
λ(cases : List Case.Type)
229+
List/map Case.Type HydratedCase.Type hydrateCase cases
230+
231+
in { Case
232+
, HydratedCase
233+
, hydrateCase
234+
, hydrateCases
235+
, HttpMethod
236+
, ExpectData
237+
, PartialMatcher
238+
, ExpectHeaders
239+
, RequestData
240+
}

0 commit comments

Comments
 (0)