Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit 1dd8cbb

Browse files
Fixed #272: Better support for query params
1 parent 69d171f commit 1dd8cbb

3 files changed

Lines changed: 155 additions & 56 deletions

File tree

tyrian-tags/shared/src/main/scala/tyrian/LocationDetails.scala

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package tyrian
22

3+
import java.net.URLDecoder
4+
import java.nio.charset.StandardCharsets
5+
36
/** Represents the deconstructed parts of a url.
47
*
58
* @param hash
@@ -23,7 +26,7 @@ final case class LocationDetails(
2326
pathName: String,
2427
port: Option[String],
2528
protocol: Option[String],
26-
search: Option[String],
29+
search: LocationDetails.SearchParams,
2730
url: String
2831
):
2932
private val noSep = List("xmpp:", "data:", "mailto:")
@@ -86,58 +89,124 @@ object LocationDetails:
8689
case _ =>
8790
LocationPathDetails(path, None, None)
8891

92+
def parseQueryParams(rawQuery: String): SearchParams =
93+
val decoded = URLDecoder.decode(rawQuery, StandardCharsets.UTF_8.name())
94+
if decoded.startsWith("?") then
95+
val values = decoded.drop(1)
96+
SearchParams(
97+
values.split("&").toList.flatMap { param =>
98+
param.split("=", 2).toList match
99+
case key :: value :: Nil => List(QueryParam.Pair(key, value))
100+
case key :: Nil => List(QueryParam.KeyOnly(key))
101+
case _ => Nil
102+
}
103+
)
104+
else SearchParams.empty
105+
89106
def fromUrl(url: String): LocationDetails =
90107
url match
91108
case urlFileMatch(protocol, path) =>
92109
val p = parsePath(Option(path).getOrElse(""))
110+
val q = p.search.map(parseQueryParams).getOrElse(LocationDetails.SearchParams.empty)
93111

94112
LocationDetails(
95113
hash = p.hash,
96114
hostName = None,
97115
pathName = p.path,
98116
port = None,
99117
protocol = Option(protocol),
100-
search = p.search,
118+
search = q,
101119
url = url
102120
)
103121

104122
case urlDataMatch(protocol, path) =>
105123
val p = parsePath(Option(path).getOrElse(""))
124+
val q = p.search.map(parseQueryParams).getOrElse(LocationDetails.SearchParams.empty)
106125

107126
LocationDetails(
108127
hash = p.hash,
109128
hostName = None,
110129
pathName = p.path,
111130
port = None,
112131
protocol = Option(protocol),
113-
search = p.search,
132+
search = q,
114133
url = url
115134
)
116135

117136
case urlMatch(protocol, _, hostname, _, port, path) =>
118137
val p = parsePath(Option(path).getOrElse(""))
138+
val q = p.search.map(parseQueryParams).getOrElse(LocationDetails.SearchParams.empty)
119139

120140
LocationDetails(
121141
hash = p.hash,
122142
hostName = Option(hostname),
123143
pathName = p.path,
124144
port = Option(port),
125145
protocol = Option(protocol),
126-
search = p.search,
146+
search = q,
127147
url = url
128148
)
129149

130150
case pathOnly =>
131151
val p = parsePath(Option(pathOnly).getOrElse(""))
152+
val q = p.search.map(parseQueryParams).getOrElse(LocationDetails.SearchParams.empty)
132153

133154
LocationDetails(
134155
hash = p.hash,
135156
hostName = None,
136157
pathName = p.path,
137158
port = None,
138159
protocol = None,
139-
search = p.search,
160+
search = q,
140161
url = url
141162
)
142163

143164
final case class LocationPathDetails(path: String, search: Option[String], hash: Option[String])
165+
166+
final case class SearchParams(params: List[QueryParam]):
167+
def find(key: String): List[QueryParam] =
168+
params.filter(_.key == key)
169+
170+
def hasKey(key: String): Boolean =
171+
params.exists(_.key == key)
172+
173+
def keys: List[String] =
174+
params.map(_.key)
175+
176+
def toTuples: List[(String, String)] =
177+
params.map {
178+
case QueryParam.KeyOnly(_key) => (_key, "")
179+
case QueryParam.Pair(_key, value) => (_key, value)
180+
}
181+
182+
def valueOf(key: String): List[String] =
183+
find(key).flatMap(_.valueOpt.toList)
184+
185+
object SearchParams:
186+
val empty: SearchParams =
187+
SearchParams(Nil)
188+
189+
def apply(params: QueryParam*): SearchParams =
190+
SearchParams(params.toList)
191+
192+
enum QueryParam(val key: String) derives CanEqual:
193+
case KeyOnly(_key: String) extends QueryParam(_key)
194+
case Pair(_key: String, value: String) extends QueryParam(_key)
195+
196+
def valueOpt: Option[String] =
197+
this match
198+
case KeyOnly(_key) => None
199+
case Pair(_key, value) => Some(value)
200+
201+
def toTuple: (String, String) =
202+
this match
203+
case KeyOnly(_key) => (_key, "")
204+
case Pair(_key, value) => (_key, value)
205+
206+
object QueryParam:
207+
208+
def apply(key: String, value: String): QueryParam =
209+
QueryParam.Pair(key, value)
210+
211+
def apply(key: String): QueryParam =
212+
QueryParam.KeyOnly(key)

0 commit comments

Comments
 (0)