-
Notifications
You must be signed in to change notification settings - Fork 787
/
UrlForm.scala
181 lines (153 loc) · 6.33 KB
/
UrlForm.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
/*
* 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.Eq
import cats.Monoid
import cats.data.Chain
import cats.effect.Concurrent
import cats.syntax.all._
import org.http4s.Charset.`UTF-8`
import org.http4s.headers._
import org.http4s.internal.CollectionCompat
import org.http4s.parser._
import scala.io.Codec
class UrlForm private (val values: Map[String, Chain[String]]) extends AnyVal {
override def toString: String = values.toString()
def get(key: String): Chain[String] =
this.getOrElse(key, Chain.empty[String])
def getOrElse(key: String, default: => Chain[String]): Chain[String] =
values.getOrElse(key, default)
def getFirst(key: String): Option[String] =
values.get(key).flatMap(_.uncons).map { case (s, _) => s }
def getFirstOrElse(key: String, default: => String): String =
this.getFirst(key).getOrElse(default)
def +(kv: (String, String)): UrlForm = {
val newValues = values.get(kv._1).fold(Chain(kv._2))(_ :+ kv._2)
UrlForm(values.updated(kv._1, newValues))
}
/** @param key name of the field
* @param value value of the field
* @param ev evidence of the existence of `QueryParamEncoder[T]`
* @return `UrlForm` updated with `key` and `value` pair if key does not exist in `values`. Otherwise `value` will be added to the existing entry.
*/
def updateFormField[T](key: String, value: T)(implicit ev: QueryParamEncoder[T]): UrlForm =
this + (key -> ev.encode(value).value)
/** @param key name of the field
* @param value optional value of the field
* @param ev evidence of the existence of `QueryParamEncoder[T]`
* @return `UrlForm` updated as it is updated with `updateFormField(key, v)` if `value` is `Some(v)`, otherwise it is unaltered
*/
def updateFormField[T](key: String, value: Option[T])(implicit
ev: QueryParamEncoder[T]
): UrlForm =
value.fold(this)(updateFormField(key, _))
/** @param key name of the field
* @param vals a Chain of values for the field
* @param ev evidence of the existence of `QueryParamEncoder[T]`
* @return `UrlForm` updated with `key` and `vals` if key does not exist in `values`, otherwise `vals` will be appended to the existing entry. If `vals` is empty, `UrlForm` will remain as is
*/
def updateFormFields[T](key: String, vals: Chain[T])(implicit ev: QueryParamEncoder[T]): UrlForm =
vals.foldLeft(this)(_.updateFormField(key, _)(ev))
/** Same as `updateFormField(key, value)` */
def +?[T: QueryParamEncoder](key: String, value: T): UrlForm =
updateFormField(key, value)
/** Same as `updateParamEncoder`(key, value) */
def +?[T: QueryParamEncoder](key: String, value: Option[T]): UrlForm =
updateFormField(key, value)
/** Same as `updatedParamEncoders`(key, vals) */
def ++?[T: QueryParamEncoder](key: String, vals: Chain[T]): UrlForm =
updateFormFields(key, vals)
}
object UrlForm {
val empty: UrlForm = new UrlForm(Map.empty)
def apply(values: Map[String, Chain[String]]): UrlForm =
// value "" -> Chain() is just noise and it is not maintain during encoding round trip
if (values.get("").fold(false)(_.isEmpty)) new UrlForm(values - "")
else new UrlForm(values)
def single(key: String, value: String): UrlForm =
new UrlForm(Map(key -> Chain.one(value)))
def apply(values: (String, String)*): UrlForm =
values match {
case h +: tail =>
tail.foldLeft(single(h._1, h._2))(_ + _)
case _ => empty
}
def fromChain(values: Chain[(String, String)]): UrlForm =
values.knownSize match {
case 0 => empty
case 1 =>
val h = values.headOption.get
single(h._1, h._2)
case _ =>
values.foldLeft(empty)(_ + _)
}
implicit def entityEncoder[F[_]](implicit charset: Charset = `UTF-8`): EntityEncoder[F, UrlForm] =
EntityEncoder
.stringEncoder[F]
.contramap[UrlForm](encodeString(charset))
.withContentType(`Content-Type`(MediaType.application.`x-www-form-urlencoded`, charset))
implicit def entityDecoder[F[_]](implicit
F: Concurrent[F],
defaultCharset: Charset = `UTF-8`,
): EntityDecoder[F, UrlForm] =
EntityDecoder.decodeBy(MediaType.application.`x-www-form-urlencoded`) { m =>
DecodeResult(
EntityDecoder
.decodeText(m)
.map(decodeString(m.charset.getOrElse(defaultCharset)))
)
}
implicit val eqInstance: Eq[UrlForm] = Eq.instance { (x: UrlForm, y: UrlForm) =>
x.values === y.values
}
implicit val monoidInstance: Monoid[UrlForm] = new Monoid[UrlForm] {
override def empty: UrlForm = UrlForm.empty
override def combine(x: UrlForm, y: UrlForm): UrlForm =
UrlForm(x.values |+| y.values)
}
/** Attempt to decode the `String` to a [[UrlForm]] */
def decodeString(
charset: Charset
)(urlForm: String): Either[MalformedMessageBodyFailure, UrlForm] =
QueryParser
.parseQueryString(urlForm.replace("+", "%20"), new Codec(charset.nioCharset))
.map(q => UrlForm(CollectionCompat.mapValues(q.multiParams)(Chain.fromSeq)))
.leftMap { parseFailure =>
MalformedMessageBodyFailure(parseFailure.message, None)
}
/** Encode the [[UrlForm]] into a `String` using the provided `Charset` */
def encodeString(charset: Charset)(urlForm: UrlForm): String = {
def encode(s: String): String =
Uri.encode(s, charset.nioCharset, spaceIsPlus = true, toSkip = Uri.Unreserved)
val sb = new StringBuilder(urlForm.values.size * 20)
urlForm.values.foreach { case (k, vs) =>
if (sb.nonEmpty) sb.append('&')
val encodedKey = encode(k)
if (vs.isEmpty) sb.append(encodedKey)
else {
var first = true
vs.map { v =>
if (!first) sb.append('&')
else first = false
sb.append(encodedKey)
.append('=')
.append(encode(v))
}
}
}
sb.result()
}
}