/
Tutorial.hs
295 lines (234 loc) · 8.17 KB
/
Tutorial.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# LANGUAGE
OverloadedLists, OverloadedStrings,
ScopedTypeVariables, TemplateHaskell, TypeApplications
#-}
module Test.Tutorial where
import Data.GrabForm
import Data.Text (Text)
import Hedgehog (Group, discover, withTests, property, (===))
testGroup :: Group
testGroup = $$(discover)
example =
withTests 1 . property
x ~> y =
example (x === y)
--- Tutorial ---
{-
We are concerned here with data submitted by web browsers in a normal form
submission. Ignoring the encoding details, we can think of a form as looking
something like this:
name: Alonzo
state: Montana
security_question: What is your favorite hobby?
security_answer: watching cars
This example has four parameters. Each parameter has a name and a value. We
might represent this in Haskell as:
[ ("name", "Alonzo")
, ("state", "Montana")
, ("security_question", "What is your favorite hobby?")
, ("security_answer", "watching cars")
]
Suppose we're only interested in two parts of this form: The name and the state.
-}
nameAndState :: Grab EnglishSentence (Text, Text)
nameAndState =
(,)
<$> at "name" (only text)
<*> at "state" (only text)
{-
If we apply `nameAndState` to the form parameters above, we get the following
result:
("Alonzo", "Montana")
-}
prop_nameAndState_success =
readTextParams (etAlia nameAndState)
[ ("name", "Alonzo")
, ("state", "Montana")
, ("security_question", "What is your favorite hobby?")
, ("security_answer", "watching cars")
]
~>
(Log [], Just ("Alonzo", "Montana"))
{-
When receiving information submitted from an external source, there is usually
some possibility that the input is invalid. Consider the following form that is
missing the "state" field. In this case, the result we get is `Nothing`,
accompanied by an error message indicating that something is missing.
-}
prop_missingState =
readTextParams (etAlia nameAndState)
[ ("name", "Alonzo")
, ("security_question", "What is your favorite hobby?")
, ("security_answer", "watching cars")
]
~>
(Log [("state", "Required parameter is missing.")], Nothing)
{-
The `etAlia` function we've been using signifies that the input is allowed to
contain parameters other than the ones that `nameAndState` grabs. If we use
`only` instead, we can specify that there should be no additional parameters.
-}
prop_unexpectedParameters =
readTextParams (only nameAndState)
[ ("name", "Alonzo")
, ("state", "Montana")
, ("security_question", "What is your favorite hobby?")
, ("security_answer", "watching cars")
]
~>
( Log [ ("security_question", "Unexpected parameter.")
, ("security_answer", "Unexpected parameter.")
]
, Just ("Alonzo", "Montana")
)
{-
However, we still get the result: ("Alonzo", "Montana"). Unexpected parameters
do not prevent us from being able to read the form. Whether you choose `only` or
`etAlia` only determines whether these warnings end up in the log; it does not
affect whether reading the form succeeds or fails.
Duplicate parameters are not permitted, since we cannot know which of the values
to accept as the real one. Alonzo cannot live in both Georgia and Montana:
-}
prop_duplicateParameterWithDifferentValue =
readTextParams (only nameAndState)
[ ("name", "Alonzo")
, ("state", "Georgia")
, ("state", "Montana")
]
~>
(Log [("state", "Parameter may not appear more than once.")], Nothing)
{-
Duplicated parameters are only allowed if they have the same value, because in
that case the problem of deciding which value to accept does not arise.
-}
prop_duplicateParameterWithSameValue =
readTextParams (only nameAndState)
[ ("name", "Alonzo")
, ("state", "Montana")
, ("state", "Montana")
]
~>
(Log [], Just ("Alonzo", "Montana"))
{-
Sometimes a form has a tree structure. Suppose there are multiple security
questions. If we were using a data format like YAML, it might look like this:
name: Alonzo
state: Montana
security:
- Q: What is your favorite hobby?
A: watching cars
- Q: What is your oldest sibling's name?
A: melman
- Q: What was the make and model of your first car?
A: bmw x5
To cajole this data into our concept of a form as a list of parameters, we need
to flatten it somehow. We adopt the following convention:
name: Alonzo
state: Montana
security[1].Q: What is your favorite hobby?
security[1].A: watching cars
security[2].Q: What is your oldest sibling's name?
security[2].A: melman
security[3].Q: What was the make and model of your first car?
security[3].A: bmw x5
Let's define a data type to represent a question and answer:
-}
data QA = QA { qa_question :: Text, qa_answer :: Text } deriving (Eq, Show)
nameStateAndQAs :: Grab EnglishSentence (Text, Text, [QA])
nameStateAndQAs =
(,,)
<$> at "name" (only text)
<*> at "state" (only text)
<*> at "security" (only (natList (only qa)))
qa :: Grab EnglishSentence QA
qa =
QA
<$> at "Q" (only text)
<*> at "A" (only text)
prop_multipleQuestions =
readTextParams (only nameStateAndQAs)
[ ("name", "Alonzo")
, ("state", "Montana")
, ("security[0].Q", "What is your favorite hobby?")
, ("security[0].A", "watching cars")
, ("security[1].Q", "What is your oldest sibling's name?")
, ("security[1].A", "melman")
, ("security[2].Q", "What was the make and model of your first car?")
, ("security[2].A", "bmw x5")
]
~>
( Log []
, Just
( "Alonzo"
, "Montana"
, [ QA
{ qa_question = "What is your favorite hobby?"
, qa_answer = "watching cars"
}
, QA
{ qa_question = "What is your oldest sibling's name?"
, qa_answer = "melman"
}
, QA
{ qa_question = "What was the make and model of your first car?"
, qa_answer = "bmw x5"
}
]
)
)
{-
The parameters of the list may appear in any order. The order of the result is
determined by the numbers in the parameter names.
-}
prop_listOrder =
readTextParams (only (at "security" (only (natList (only qa)))))
[ ("security[2].Q", "What was the make and model of your first car?")
, ("security[1].A", "melman")
, ("security[0].Q", "What is your favorite hobby?")
, ("security[1].Q", "What is your oldest sibling's name?")
, ("security[0].A", "watching cars")
, ("security[2].A", "bmw x5")
]
~>
( Log []
, Just
[ QA
{ qa_question = "What is your favorite hobby?"
, qa_answer = "watching cars"
}
, QA
{ qa_question = "What is your oldest sibling's name?"
, qa_answer = "melman"
}
, QA
{ qa_question = "What was the make and model of your first car?"
, qa_answer = "bmw x5"
}
]
)
{-
Error messages work the same within nested grabs. The result is a complete list
of every error encountered.
-}
prop_manyErrors =
readTextParams (only nameStateAndQAs)
[ ("state", "Montana")
, ("itchy face", "yes")
, ("security[0].Q", "What is your favorite hobby?")
, ("security[0].A", "watching cars")
, ("security[1].Q", "What is your oldest sibling's name?")
, ("security[1].A", "melman")
, ("security[1].A", "iowa")
, ("security[2].Q", "What was the make and model of your first car?")
, ("security[2].A", "bmw x5")
, ("security[2].A2", "xyz")
]
~>
( Log [ ("name", "Required parameter is missing.")
, ("itchy face", "Unexpected parameter.")
, ("security[1].A", "Parameter may not appear more than once.")
, ("security[2].A2", "Unexpected parameter.")
]
, Nothing
)