/
Query.scala
287 lines (233 loc) · 10.1 KB
/
Query.scala
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
/*
* Copyright (c) 2012 Orderly Ltd. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package co.orderly.narcolepsy
// Java
import java.util.UUID
// Narcolepsy
import adapters._
import utils._
/**
* Query is a fluent interface for constructing a call (GET, POST, DELETE, PUT or
* similar) to a RESTful web service. It is typed so that the representations
* can be (un)marshalled in a typesafe way.
*/
abstract class Query[
R <: Representation](
method: HttpMethod,
client: Client,
resource: String,
typeR: Class[R]) {
// TODO 1: would be good to make the Query builder typesafe. So e.g. developer gets a compile time error if a GetQuery hasn't setId()
// TODO: see here for directions: http://www.tikalk.com/java/blog/type-safe-builder-scala-using-type-constraints
// TODO 2: it would also be quite nice to make the Query builder immutable, rather than use vars
// -------------------------------------------------------------------------------------------------------------------
// Flags for the stateful builder
// -------------------------------------------------------------------------------------------------------------------
protected var payload: Option[String] = None
protected val _client: Client = client // Because can't explicit self type on a class constructor arg
protected var id: Option[String] = None
protected var params: Option[RestfulParams] = None
protected var slug: String = resource
protected var console: Boolean = false
protected var exception: Boolean = false
// -------------------------------------------------------------------------------------------------------------------
// Fluent methods which can be used in any Query builder
// -------------------------------------------------------------------------------------------------------------------
/**
* Switches on debug-style printing of the query execution
* @return The updated Query builder
*/
def consolePrint(): this.type = {
this.console = true
this
}
/**
* Overrides the resource 'slug' used for this query
* @param slug
* @return The updated Query builder
*/
def overrideSlug(slug: String): this.type = {
this.slug = slug
this
}
/**
* Throws an exception if we received an HTTP error code back from the web service
* @return The updated Query builder
*/
def throwException(): this.type = {
this.exception = true
this
}
// -------------------------------------------------------------------------------------------------------------------
// Execution methods for the Query
// -------------------------------------------------------------------------------------------------------------------
/**
* Executes the query using all of the parameters set (or not set) through
* the builder.
* @return A RestfulResponse tuple of return code, HTTP headers and body
*/
def run(): RestfulResponse = {
val uri = (slug +
(if (id.isDefined) "/%s".format(id.get) else "") +
(if (params.isDefined) "?%s".format(RestfulHelpers.canonicalize(params.get, client.configuration.encoding)) else "")
)
if (console) {
Console.println("Executing Narcolepsy query against URI: /%s".format(uri))
}
val (code, headers, body) = client.execute(method, payload, uri)
if (console) {
Console.println("Response status code: %s".format(code))
Console.println("Response headers:\n%s".format(RestfulHelpers.stringify(headers)))
Console.println("Response body:\n%s".format(body.getOrElse("<< EMPTY >>")))
}
// TODO: check if we have an error and throw if we do
(code, headers, body)
}
/**
* Executes the query using run() and then unmarshals the result into
* the appropriate Representation object
*/
def unmarshal(): UnmarshalledResponse[_ <: ErrorRepresentation, R] = {
val (code, _, body) = run()
// TODO: I want to decouple this using implicit objects and conversions, Spray-style
if (RestfulHelpers.isError(code)) {
Left(RestfulError(code, body, null)) // TODO: add unmarshalling of errors in here
} else {
Right(body map( r => _client.unmarshaller.toRepresentation(client.configuration.contentType, r, typeR)))
// TODO: pass in client.configuration.contentType
// case "application/json" => null // UnmarshalJson(b, true).toRepresentation[R](typeR) // TODO: remove rootKey bool
// case "text/xml" => null // UnmarshalXml(b).toRepresentation[R](typeR)
// case _ => throw new ClientConfigurationException("Narcolepsy can only unmarshal JSON and XML currently, not %s".format(client.configuration.contentType))
// }))
}
}
}
// -------------------------------------------------------------------------------------------------------------------
// Specific fluent functionalities only found on some Query subclasses
// -------------------------------------------------------------------------------------------------------------------
/**
* Payload allows a Query to have a 'payload' attached. A payload is data submitted to the web service with the
* request. Typically used by PUT and POST requests.
*/
// TODO: need to update this so that payload can be typed
trait Payload[R <: Representation] {
// Grab _payload from Query
self: {
var payload: Option[String]
val _client: Client
} =>
def addPayload(representation: R): this.type = {
this.payload = Option(_client.marshaller.fromRepresentation(
_client.configuration.contentType,
representation)
)
this
}
def addPayload(payload: String): this.type = {
this.payload = Option(payload)
this
}
}
/**
* Id allows a Query to have a resource ID attached. This is used by any Query which wants to operate on a specific
* (already existing) resource, rather than a new resource. Typically used by all DELETE and POST requests, and
* some GET requests.
*/
trait Id {
// Grab id from Query
self: {
var id: Option[String]
} =>
// TODO: add support for multiple IDs. For example PrestaShop supports DELETEing /?id=45,65. Need to make it play nice with other parameters
def setId(id: String): this.type = {
this.id = Option(id)
this
}
def setId(id: Int): this.type = {
this.id = Option(id.toString())
this
}
def setId(id: Long): this.type = {
this.id = Option(id.toString())
this
}
def setId(id: UUID): this.type = {
this.id = Option(id.toString())
this
}
}
trait Listable[RW <: RepresentationWrapper[_]] {
self: {
def unmarshal(): UnmarshalledResponse[_ <: ErrorRepresentation, _ <: RepresentationWrapper[_]]
val exception: Boolean
} =>
/**
* toList runs a command and unmarshals it, then either decomposes the unmarshalled
* object into a List[SR] or returns Nil if that's not possible
*/
def toList(): List[RW#rtype] =
// Pattern match on the unmarshal output
unmarshal() match {
case Left(error) => Nil // Empty list if we received an error
case Right(None) => Nil // Empty list if our unmarshalled object is empty
case Right(Some(data: RepresentationWrapper[RW#rtype])) => data.toList // Turn our unmarshalled object into a list
}
}
// -------------------------------------------------------------------------------------------------------------------
// Define the concrete Query subclasses using the traits above
// -------------------------------------------------------------------------------------------------------------------
/**
* GetQuery is for retrieving a singular representation. Applies the GetMethod and uses the Id trait
*/
class GetQuery[R <: Representation](client: Client, resource: String, typeR: Class[R])
extends Query[R](GetMethod, client, resource, typeR)
with Id
/**
* GetsQuery is for retrieving a list of multiple representations. From an HTTP/RESTful perspective, a
* GetQuery and a GetsQuery are identical: they both execute a GET. From a Narcolepsy typesafety
* perspective they are quite different:
* - A GetQuery takes an ID and returns a singular representation which can be unmarshalled to a Representation subclass
* - A GetsQuery takes no ID and returns a collection-style representation which can be unmarshalled to a RepresentationWrapper subclass
*/
class GetsQuery[RW <: RepresentationWrapper[_]](client: Client, resource: String, typeRW: Class[RW])
extends Query[RW](GetMethod, client, resource, typeRW)
with Listable[RW]
/**
* DeleteQuery is for deleting a resource. Applies the DeleteMethod and uses the Id trait
*/
class DeleteQuery(client: Client, resource: String)
extends Query(DeleteMethod, client, resource, null) // TODO: null not very clean here
with Id
/**
* PutQuery is for performing a PUT on a resource. This is typically used for updating
* an existing resource
*/
class PutQuery[R <: Representation](client: Client, resource: String, typeR: Class[R])
extends Query[R](PutMethod, client, resource, typeR)
with Id
with Payload[R]
/**
* PostQuery is for performing a POST on a resource. This is typically used for creating
* an all-new resource
*/
class PostQuery[R <: Representation](client: Client, resource: String, typeR: Class[R])
extends Query[R](PostMethod, client, resource, typeR)
with Payload[R]
// TODO: add HeadQuery
// -------------------------------------------------------------------------------------------------------------------
// Exceptions
// -------------------------------------------------------------------------------------------------------------------
/**
* Flags that running the Query returned a non-success code
*/
class ResponseCodeException(message: String = "") extends RuntimeException(message)