-
Notifications
You must be signed in to change notification settings - Fork 788
/
Query.scala
403 lines (337 loc) · 11.9 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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
/*
* Copyright 2013 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.http4s
import cats.Eval
import cats.Foldable
import cats.Hash
import cats.Order
import cats.Show
import cats.parse.Parser0
import cats.syntax.all._
import org.http4s.Query._
import org.http4s.internal.UriCoding
import org.http4s.internal.parsing.Rfc3986
import org.http4s.parser.QueryParser
import org.http4s.util.Renderable
import org.http4s.util.Writer
import java.nio.charset.StandardCharsets
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
/** Representation of a query string.
*
* When a query is none, it is represented by the [[Query.Empty]].
*
* When a query is parsed – it is represented by the [[Query.Parsed]],
* an indexed sequence of key and maybe value pairs
* which maps precisely to a query string, modulo
* [[https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 percent-encoding]].
* The resulting `String` will have the pairs separated
* by '&' while the key is separated from the value with '='.
*
* Otherwise, a query is represented by the [[Query.Raw]] containing unparsed string.
*/
sealed trait Query extends QueryOps with Renderable {
def pairs: Vector[KeyValue]
def get(idx: Int): Option[KeyValue] = this match {
case Query.Empty => None
case parsed: Query.Parsed => parsed.pairs.get(idx.toLong)
case raw: Query.Raw => raw.pairs.get(idx.toLong)
}
def length: Int = this match {
case Query.Empty => 0
case parsed: Query.Parsed => parsed.pairs.length
case raw: Query.Raw => raw.pairs.length
}
def slice(from: Int, until: Int): Query = this match {
case Query.Empty => this
case parsed: Query.Parsed =>
val sliced = parsed.pairs.slice(from, until)
if (sliced.lengthIs == 0) Query.Empty
else new Query.Parsed(sliced)
case raw: Query.Raw =>
val sliced = raw.pairs.slice(from, until)
if (sliced.lengthIs == 0) Query.Empty
else new Query.Parsed(sliced)
}
def isEmpty: Boolean = this match {
case Query.Empty => true
case parsed: Query.Parsed => parsed.pairs.isEmpty
case raw: Query.Raw => raw.pairs.isEmpty
}
def nonEmpty: Boolean = !isEmpty
def drop(n: Int): Query = this match {
case Query.Empty => this
case parsed: Query.Parsed =>
val prepared = parsed.pairs.drop(n)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
case raw: Query.Raw =>
val prepared = raw.pairs.drop(n)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
}
def dropRight(n: Int): Query = this match {
case Query.Empty => this
case parsed: Query.Parsed =>
val prepared = parsed.pairs.dropRight(n)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
case raw: Query.Raw =>
val prepared = raw.pairs.dropRight(n)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
}
def exists(f: KeyValue => Boolean): Boolean = this match {
case Query.Empty => false
case parsed: Query.Parsed => parsed.pairs.exists(f)
case raw: Query.Raw => raw.pairs.exists(f)
}
def filter(f: KeyValue => Boolean): Query = this match {
case Query.Empty => this
case parsed: Query.Parsed =>
val prepared = parsed.pairs.filter(f)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
case raw: Query.Raw =>
val prepared = raw.pairs.filter(f)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
}
def filterNot(f: KeyValue => Boolean): Query = this match {
case Query.Empty => this
case parsed: Query.Parsed =>
val prepared = parsed.pairs.filterNot(f)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
case raw: Query.Raw =>
val prepared = raw.pairs.filterNot(f)
if (prepared.sizeIs == 0) Query.Empty
else new Query.Parsed(prepared)
}
def foreach(f: KeyValue => Unit): Unit = this match {
case Query.Empty => ()
case parsed: Query.Parsed => parsed.pairs.foreach(f)
case raw: Query.Raw => raw.pairs.foreach(f)
}
def foldLeft[Z](z: Z)(f: (Z, KeyValue) => Z): Z = this match {
case Query.Empty => z
case parsed: Query.Parsed => parsed.pairs.foldLeft(z)(f)
case raw: Query.Raw => raw.pairs.foldLeft(z)(f)
}
def foldRight[Z](z: Eval[Z])(f: (KeyValue, Eval[Z]) => Eval[Z]): Eval[Z] =
this match {
case Query.Empty => z
case parsed: Query.Parsed =>
Foldable[Vector].foldRight(parsed.pairs, z)(f)
case raw: Query.Raw =>
Foldable[Vector].foldRight(raw.pairs, z)(f)
}
def +:(elem: KeyValue): Query = this match {
case Query.Empty =>
new Query.Parsed(Vector(elem))
case parsed: Query.Parsed =>
new Query.Parsed(elem +: parsed.pairs)
case raw: Query.Raw =>
new Query.Parsed(elem +: raw.pairs)
}
def :+(elem: KeyValue): Query = this match {
case Query.Empty =>
new Query.Parsed(Vector(elem))
case parsed: Query.Parsed =>
new Query.Parsed(parsed.pairs :+ elem)
case raw: Query.Raw =>
new Query.Parsed(raw.pairs :+ elem)
}
def ++(pairs: collection.Iterable[(String, Option[String])]): Query =
this match {
case Query.Empty =>
new Query.Parsed(pairs.toVector)
case parsed: Query.Parsed =>
new Query.Parsed(parsed.pairs ++ pairs)
case raw: Query.Raw =>
new Query.Parsed(raw.pairs ++ pairs)
}
def toVector: Vector[(String, Option[String])] = this match {
case Query.Empty => Vector.empty
case parsed: Query.Parsed => parsed.pairs
case raw: Query.Raw => raw.pairs
}
def toList: List[(String, Option[String])] = this match {
case Query.Empty => List.empty
case parsed: Query.Parsed => parsed.pairs.toList
case raw: Query.Raw => raw.pairs.toList
}
/** Map[String, String] representation of the [[Query]]
*
* If multiple values exist for a key, the first is returned. If
* none exist, the empty `String` "" is returned.
*/
lazy val params: Map[String, String] = this match {
case Query.Empty => Map.empty
case _: Query.Parsed | _: Query.Raw =>
multiParams.map { case (k, v) =>
k -> v.headOption.getOrElse("")
}
}
/** `Map[String, List[String]]` representation of the [[Query]]
*
* Params are represented as a `List[String]` and may be empty.
*/
lazy val multiParams: Map[String, List[String]] = this match {
case Query.Empty => Map.empty
case _: Query.Parsed | _: Query.Raw =>
val pairs = toVector
if (pairs.isEmpty) Map.empty
else {
val m = mutable.Map.empty[String, ListBuffer[String]]
pairs.foreach {
case (k, None) => m.getOrElseUpdate(k, new ListBuffer)
case (k, Some(v)) => m.getOrElseUpdate(k, new ListBuffer) += v
}
m.view.mapValues(_.toList).toMap
}
}
override protected type Self = Query
override protected val query: Query = this
override protected def self: Self = this
override protected def replaceQuery(query: Query): Self = query
}
object Query {
case object Empty extends Query {
def pairs: Vector[KeyValue] = Vector.empty
override def render(writer: Writer): writer.type =
writer
}
final class Raw private[http4s] (value: String) extends Query {
private[this] var _pairs: Vector[KeyValue] = _
def pairs: Vector[KeyValue] = {
if (_pairs == null) {
_pairs = Query.parse(value)
}
_pairs
}
override def render(writer: Writer): writer.type =
writer.append(value)
override def equals(that: Any): Boolean =
that match {
case that: Query => that.toVector == toVector
case _ => false
}
override def hashCode: Int = 31 + pairs.##
}
final class Parsed private[http4s] (val pairs: Vector[KeyValue]) extends Query {
/** Render the Query as a `String`.
*
* Pairs are separated by '&' and keys are separated from values by '='
*/
override def render(writer: Writer): writer.type = {
var first = true
def encode(s: String) =
UriCoding.encode(
s,
spaceIsPlus = false,
charset = StandardCharsets.UTF_8,
toSkip = UriCoding.QueryNoEncode,
)
pairs.foreach {
case (n, None) =>
if (!first) writer.append('&')
else first = false
writer.append(encode(n))
case (n, Some(v)) =>
if (!first) writer.append('&')
else first = false
writer
.append(encode(n))
.append("=")
.append(encode(v))
}
writer
}
override def equals(that: Any): Boolean =
that match {
case that: Query => that.toVector == toVector
case _ => false
}
override def hashCode: Int = 31 + pairs.##
}
type KeyValue = (String, Option[String])
/** Represents the absence of a query string. */
val empty: Query = Query.Empty
/** Represents a query string with no keys or values: `?` */
val blank = new Query.Parsed(Vector("" -> None))
def apply(xs: (String, Option[String])*): Query =
if (xs.sizeIs == 0) Query.Empty
else new Query.Parsed(xs.toVector)
def fromVector(xs: Vector[(String, Option[String])]): Query =
if (xs.sizeIs == 0) Query.Empty
else new Query.Parsed(xs)
def fromPairs(xs: (String, String)*): Query =
if (xs.sizeIs == 0) Query.Empty
else
new Query.Parsed(
xs.foldLeft(Vector.empty[KeyValue]) { case (m, (k, s)) =>
m :+ (k -> Some(s))
}
)
/** Generate a [[Query]] from its `String` representation
*
* If parsing fails, the empty [[Query]] is returned
*/
def unsafeFromString(query: String): Query =
if (query.isEmpty) new Query.Parsed(Vector("" -> None))
else
QueryParser.parseQueryString(query) match {
case Right(query) => query
case Left(_) => Query.empty
}
@deprecated(message = "Use unsafeFromString instead", since = "0.22.0-M6")
def fromString(query: String): Query =
unsafeFromString(query)
/** Build a [[Query]] from the `Map` structure */
def fromMap(map: collection.Map[String, collection.Seq[String]]): Query =
new Query.Parsed(map.foldLeft(Vector.empty[KeyValue]) {
case (m, (k, Seq())) => m :+ (k -> None)
case (m, (k, vs)) => vs.foldLeft(m) { case (m, v) => m :+ (k -> Some(v)) }
})
private def parse(query: String): Vector[KeyValue] =
if (query.isEmpty) blank.toVector
else
QueryParser.parseQueryStringVector(query) match {
case Right(query) => query
case Left(_) => Vector.empty
}
/** query = *( pchar / "/" / "?" )
*
* These are illegal, but common in the wild. We will be
* "conservative in our sending behavior and liberal in our
* receiving behavior", and encode them.
*/
private[http4s] lazy val parser: Parser0[Query] = {
import cats.parse.Parser.charIn
import Rfc3986.pchar
pchar.orElse(charIn("/?[]")).rep0.string.map(pchars => new Query.Raw(pchars))
}
implicit val catsInstancesForHttp4sQuery: Hash[Query] with Order[Query] with Show[Query] =
new Hash[Query] with Order[Query] with Show[Query] {
override def hash(x: Query): Int =
x.hashCode
override def compare(x: Query, y: Query): Int =
x.toVector.compare(y.toVector)
override def show(a: Query): String =
a.renderString
}
}