11package 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