Skip to content

Commit 16ddce7

Browse files
authored
feat: Add a URL template utility. (#146)
* feat: Add a URL template utility.
1 parent f9d69e3 commit 16ddce7

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright @ 2025 - present 8x8, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.jitsi.utils
17+
18+
import java.net.URI
19+
20+
class TemplatedUrl(
21+
private val template: String,
22+
inMap: Map<String, String> = emptyMap(),
23+
private val requiredKeys: Set<String> = emptySet(),
24+
) {
25+
private val map = inMap.toMutableMap()
26+
27+
/** Saves the given key:value pair in the map. */
28+
fun set(key: String, value: String) {
29+
map[key] = value
30+
}
31+
32+
/** Resolve the template with the current map. */
33+
fun resolve() = doResolve(map)
34+
35+
/** Resolve the template with the current map and a new key:value pair (does not save the new pair). */
36+
fun resolve(key: String, value: String) = resolve(mapOf(key to value))
37+
38+
/** Resolve the template with the current map and a new map pair (does not save the new map). */
39+
fun resolve(newMap: Map<String, String>) = doResolve(map + newMap)
40+
41+
private fun doResolve(map: Map<String, String>): URI {
42+
if (!requiredKeys.all { it in map }) {
43+
throw IllegalArgumentException("Missing required keys: ${requiredKeys - map.keys}")
44+
}
45+
var resolvedUrl = template
46+
for ((key, value) in map) {
47+
resolvedUrl = resolvedUrl.replace("{{$key}}", value)
48+
}
49+
return URI(resolvedUrl)
50+
}
51+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright @ 2025 - present 8x8, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.jitsi.utils
17+
18+
import io.kotest.assertions.throwables.shouldThrow
19+
import io.kotest.core.spec.style.ShouldSpec
20+
import io.kotest.matchers.shouldBe
21+
import java.net.URI
22+
import java.net.URISyntaxException
23+
24+
class TemplatedUrlTest : ShouldSpec({
25+
context("TemplatedUrl") {
26+
should("resolve a simple template") {
27+
val template = "https://example.com/{{path}}"
28+
val templatedUrl = TemplatedUrl(template)
29+
templatedUrl.set("path", "test")
30+
31+
templatedUrl.resolve() shouldBe URI("https://example.com/test")
32+
}
33+
34+
should("resolve a template with multiple keys") {
35+
val template = "wss://{{host}}/{{path}}?param={{param}}"
36+
val templatedUrl = TemplatedUrl(template)
37+
templatedUrl.set("host", "example.com")
38+
templatedUrl.set("path", "api/resource")
39+
templatedUrl.set("param", "value")
40+
41+
templatedUrl.resolve() shouldBe URI("wss://example.com/api/resource?param=value")
42+
}
43+
44+
should("resolve a template with multiple keys when they are set()") {
45+
val template = "wss://{{host}}/{{path}}?param={{param}}"
46+
val templatedUrl = TemplatedUrl(template)
47+
templatedUrl.set("host", "example.com")
48+
templatedUrl.set("path", "api/resource")
49+
templatedUrl.set("param", "value")
50+
51+
templatedUrl.resolve(
52+
mapOf("host" to "example2.com", "path" to "api/resource2", "param" to "value2")
53+
) shouldBe URI("wss://example2.com/api/resource2?param=value2")
54+
}
55+
should("resolve a template with keys set in the constructor, with set() and in resolve()") {
56+
val template = "wss://{{host}}/{{path}}?param={{param}}"
57+
val templatedUrl = TemplatedUrl(template, mapOf("host" to "example.com"))
58+
templatedUrl.set("path", "api/resource")
59+
templatedUrl.set("param", "value")
60+
61+
templatedUrl.resolve(
62+
mapOf("param" to "value2")
63+
) shouldBe URI("wss://example.com/api/resource?param=value2")
64+
}
65+
66+
should("resolve with a new key-value pair without saving it") {
67+
val template = "https://example.com/{{path}}?param={{param}}"
68+
val templatedUrl = TemplatedUrl(template)
69+
templatedUrl.set("path", "api")
70+
templatedUrl.set("param", "permanent")
71+
72+
// Resolve with temporary param value
73+
templatedUrl.resolve("param", "temp") shouldBe URI("https://example.com/api?param=temp")
74+
75+
templatedUrl.resolve() shouldBe URI("https://example.com/api?param=permanent")
76+
}
77+
78+
should("throw an exception when not all requiredKeys are set") {
79+
val template = "https://example.com/{{path}}?param={{param}}"
80+
val templatedUrl = TemplatedUrl(template, requiredKeys = setOf("param", "required"))
81+
templatedUrl.set("path", "api")
82+
83+
shouldThrow<IllegalArgumentException> {
84+
templatedUrl.resolve()
85+
}
86+
shouldThrow<IllegalArgumentException> {
87+
// "required" is still not set
88+
templatedUrl.resolve("param", "value")
89+
}
90+
}
91+
92+
should("fail to resolve when some requiredKeys are not set") {
93+
val template = "https://{{host}}/{{path}}"
94+
val templatedUrl = TemplatedUrl(template, requiredKeys = setOf("host", "path"))
95+
96+
templatedUrl.set("host", "example.com")
97+
98+
shouldThrow<IllegalArgumentException> {
99+
templatedUrl.resolve()
100+
}
101+
}
102+
103+
should("throw an exception when the URI is invalid") {
104+
val templatedUrl = TemplatedUrl("https://example.com/{{}}")
105+
106+
shouldThrow<URISyntaxException> {
107+
templatedUrl.resolve()
108+
}
109+
}
110+
should("throw an exception when the values lead to an invalid URI ") {
111+
val templatedUrl = TemplatedUrl("https://example.com/{{path}}")
112+
113+
shouldThrow<URISyntaxException> {
114+
templatedUrl.resolve("path", "}")
115+
}
116+
}
117+
118+
should("handle keys that appear multiple times") {
119+
val template = "https://{{host}}/{{path}}/{{path}}"
120+
val templatedUrl = TemplatedUrl(template)
121+
templatedUrl.set("host", "example.com")
122+
templatedUrl.set("path", "resource")
123+
124+
templatedUrl.resolve() shouldBe URI("https://example.com/resource/resource")
125+
}
126+
should("not modify the map passed as a constructor parameter") {
127+
val m = mutableMapOf("key1" to "value1", "key2" to "value2")
128+
val templatedUrl = TemplatedUrl("https://example.com/{{key1}}/{{key2}}", m)
129+
templatedUrl.set("key1", "newValue1")
130+
templatedUrl.set("key2", "newValue2")
131+
m shouldBe mapOf("key1" to "value1", "key2" to "value2")
132+
}
133+
}
134+
})

0 commit comments

Comments
 (0)