Newer
Older
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
-- |
-- Module: JSONTransformer
-- Description: One-to-many JSON Transformation combinators
--
-- The design of these transformers is based on the design of the Jq
-- tool, and this paper on a Haskell-based Domain Specific Language
-- (DSL) for querying XML data:
--
-- "Haskell and XML: generic combinators or type-based translation?"
-- by Malcolm Wallace and Colin Runciman
-- https://dl.acm.org/doi/10.1145/317765.317794
--
-- Section 2 of the paper covers what we're doing. The 'CFilter' type
-- is what we called 'Transformer' here.
module JSONTransformer where
import JSON
-- | A converter from JSON values to zero or more JSON values.
type Transformer = JSON -> [JSON]
------------------------------------------------------------------------------
-- Transformers for constructing JSON values
-- | The @constant@ transformer is one that always outputs a fixed
-- value for all transformer inputs.
constant :: JSON -> Transformer
constant = error "UNIMPLEMENTED: constant"
-- HINT: you can use 'constant' to implement the next four functions
-- more easily.
-- | A transformer that always generates a fixed string value.
string :: String -> Transformer
string = error "UNIMPLEMENTED: string"
-- | A transformer that always generates a fixed integer value.
integer :: Integer -> Transformer
integer = error "UNIMPLEMENTED: integer"
-- | A transformer that always generates a fixed boolean value.
bool :: Bool -> Transformer
bool = error "UNIMPLEMENTED: bool"
-- | A transformer that always generates the null value.
jnull :: Transformer
jnull = error "UNIMPLEMENTED: jnull"
-- | Filters the input using another transformer. If the transformer
-- argument returns a truthy value (as determined by 'JSON.isTruthy')
-- for the input, then return the input in a single element
-- list. Otheriwse, return the empty list.
--
-- For example, if the condition is always true, then you get back the
-- input:
-- >>> select (bool True) (JsonArray [JsonInteger 1, JsonInteger 2])
-- [JsonArray [JsonInteger 1,JsonInteger 2]]
--
-- If the condition is never true, then you get back the empty list:
-- >>> select (bool False) (JsonArray [JsonInteger 1, JsonInteger 2])
-- []
--
-- Selecting for the `"a"` field being `1`, when it is:
-- >>> select (binaryOp equal (getField "a") (integer 1)) (JsonObject [("a", JsonInteger 1)])
-- [JsonObject [("a", JsonInteger 1)]]
--
-- Selecting for the `"a"` field being `1`, when it isn't:
-- >>> select (binaryOp equal (getField "a") (integer 1)) (JsonObject [("a", JsonInteger 2)])
-- []
--
-- If the `equal` returns multiple values, then only one of them needs
-- to be `True` for it to select that thing, so we can check to see if
-- a certain element is in an array:
-- >>> select (binaryOp equal getElements (integer 1)) (JsonArray [JsonInteger 1, JsonInteger 3, JsonInteger 4])
-- [JsonArray [JsonInteger 1,JsonInteger 3,JsonInteger 4]]
--
-- Same test, but this time with an array that doesn't contain `1`:
-- >>> select (binaryOp equal getElements (integer 1)) (JsonArray [JsonInteger 3, JsonInteger 4])
-- []
--
-- The following example tests to see whether or not the @"a"@ field
-- of the input contains an array that contains the value @1@:
-- > select (binaryOp equal (pipe (getField "a") getElements) (integer 1))
--
-- In Jq syntax, this is @select(.a | .[] == 1)@
select :: Transformer -> Transformer
select = error "UNIMPLEMENTED: select"
-- HINT: you'll need to check to see if the transformer argument
-- returns an isTruthy value at any point in its list for the
-- input. You can use the 'any' function (Week 05) to do this.
-- | Converts any binary operation (i.e. a two argument function) from
-- working on 'JSON' values to work on transformers. The same input is
-- fed to the two transformers and all pairs of their outputs are
-- combined using the operation.
--
-- >>> binaryOp JSON.equal (string "a") (string "a") JsonNull
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
-- [JsonBoolean True]
-- >>> binaryOp JSON.equal (string "a") (integer 5) JsonNull
-- [JsonBoolean False]
-- >>> binaryOp JSON.equal (integer 1) getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean True,JsonBoolean False]
-- >>> binaryOp JSON.notEqual (integer 1) getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean False,JsonBoolean True]
-- >>> binaryOp JSON.equal getElements getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean True,JsonBoolean False,JsonBoolean False,JsonBoolean True]
binaryOp :: (JSON -> JSON -> JSON) ->
Transformer -> Transformer -> Transformer
binaryOp = error "UNIMPLEMENTED: binaryOp"
-- | Connects two transformers together, feeding the output of the
-- first into the input of the second, and then flattening all the
-- results.
--
-- A picture, where 'x' is the input, 'f' is the first transformer,
-- and 'g' is the second.
--
-- >
-- > [v1, --g--> [[x1, [x1,
-- > x2], x2,
-- >x --f--> v2, --g--> [x3, --> x3,
-- > x4], x4,
-- > v3] --g--> [x5, x5,
-- > x6]] x6]
-- >
--
-- Connecting 'getElements' to 'getElements' via a pipe "unwraps" two
-- levels of arrays.
-- >>> pipe getElements getElements (JsonArray [JsonArray [JsonInteger 1, JsonInteger 2], JsonArray [JsonInteger 3, JsonInteger 4]])
-- [JsonInteger 1,JsonInteger 2,JsonInteger 3,JsonInteger 4]
--
-- Connecting 'getElements' to @field "a"@ via a pipe takes everything
-- from an array, and then all the @"a"@ fields.
-- >>> pipe getElements (getField "a") (JsonArray [JsonObject [("a", JsonInteger 1)],JsonObject [("a", JsonInteger 2)], JsonObject []])
-- [JsonInteger 1,JsonInteger 2]
--
-- Connecting @field "a"@ to elements via a pipe will look up the
-- field @"a"@ in an object and then get all the elements from the
-- array stored in that field.
-- >>> pipe (getField "a") getElements (JsonObject [("a", JsonArray [JsonInteger 1, JsonString "abc", JsonNull])])
-- [JsonInteger 1,JsonString "abc",JsonNull]
pipe :: Transformer -> Transformer -> Transformer
pipe = error "UNIMPLEMENTED: pipe"
-- HINT: this function is very similar to the 'o' function in the
-- paper linked above.
-- | Extracts the elements of a @JsonArray@. If the input is not an
-- array, then the empty list of results is returned.
--
-- >>> getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonInteger 1, JsonString "a"]
-- >>> getElements (JsonObject [])
-- []
--
getElements :: Transformer
getElements = error "UNIMPLEMENTED: getElements"
-- | Extracts the value of a named field from the input JSON, if it is
-- a 'JsonObject'. If the field does not exist, or the input is not an
-- object, then the empty list of results is returned.
--
-- Examples:
--
-- >>> getField "a" (JsonObject [("a", JsonInteger 5)])
-- [JsonInteger 5]
-- >>> getField "b" (JsonObject [("a", JsonInteger 5)]
-- []
-- >>> getField "a" (JsonArray [JsonInteger 1, JsonNull])
-- []
--
-- The behaviour when the same field appears multiple times is
-- unspecified.
getField :: String -> Transformer
getField = error "UNIMPLEMENTED: getField"
-- HINT: the 'lookup' function from the standard library will do the
-- lookup in the list of (name,value) pairs inside a JsonObject for
-- you.