URL
October 11, 2025 ยท View on GitHub
Create, manipulate and generate URLs.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/contact?theme=dark#twitter")!
assert link.host == "example.com"
assert link.path == "/contact"
}
Summary
About
I created this library to be able to parse URLs so that my router can react to the correct routes.
It can be used for a wide range of use cases, such as generating valid URLs given a set of basic information (like the scheme and the host) using a chain of methods.
Features
- Can parse an URL
- Can generate an URL
- Can chain methods to modify an URL
- Supports fragments, queries and ports
- Normalize URLs parts (uppercases scheme/host are always returned lowercase)
Installation
Using V installer
In your terminal, run this command:
v install khalyomede.url
Examples
- Parsing
- Generating
- Retrieving
- Comparing
- Casting
Parse an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com")!
}
URLs are automatically decoded when parsing them.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/contact?subject=Sales%20order")!
assert link.query["subject"] or { "" } == "Sales order"
}
Note that when the URL is not valid (meaning it does not have at least a scheme and a domain), you should handle this Error.
This means relatives URL are not permitted using this library (contrary to the built-in net.urllib module).
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("example.com/contact-us") or {
assert err.str() == "The scheme is missing."
panic(err)
}
}
You can perform fine grained error handling if you prefer.
module main
import khalyomede.url { Url, MissingScheme, MissingDomain, TraversingAboveRoot, BadlyEncodedPath, BadlyEncodedFragment, BadlyEncodedQuery }
fn main() {
link := Url.parse("contact-us") or {
error_message := match err {
MissingScheme { "The scheme is missing" }
MissingDomain { "The domain is missing" }
TraversingAboveRoot { "Trying to go above root" }
BadlyEncodedPath { "The path is not well encoded" }
BadlyEncodedFragment { "The fragment is not well encoded" }
BadlyEncodedQuery { "The query is not well encoded" }
}
panic(error_message + " (${err}).")
}
}
When an URL contains relative accessors (".", ".."), the absolute URL is resolved.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/user/1/../create")!
assert link.str() == "https://example.com/user/create"
}
Doubles slashes are automatically simplified.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/user//create")!
assert link.str() == "https://example.com/user/create"
}
This library automatically strips any ending slashes on paths (but not on the root path), for consistency.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/settings/")!
assert link.path == "/settings"
}
Lastly, you can get the originally parsed url.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("HTTPS://example.com")!
assert link.str() == "https://example.com"
assert link.original_url == "HTTPS://example.com"
}
Create a new URL
You need to specify at least the scheme and domain when creating a new URL.
module main
import khalyomede.url { Url, Https }
fn main() {
link := Url{
scheme: Https{}
host: "example.com"
}
}
You are free to specify more information if you want.
module main
import khalyomede.url { Url, Https }
fn main() {
link := Url{
scheme: Https{}
host: "example.com"
segments: ["contact-us"]
port: 8080 // port is a ?u16
query: {
"lang": "fr"
"theme": "dark"
}
fragment: "whatsapp"
}
assert link.str() == "https://example.com:8080/contact-us?lang=fr&theme=dark#whatsapp"
}
The Url struct is immutable. If you want to change one component of your url, use this syntax:
module main
import khalyomede.url { Url, Https }
fn main() {
mut link := Url{
scheme: Https{}
host: "example.com"
}
link = Url{
...link
query: {
"lang": "es"
}
}
assert link.query["lang"] or { "en" } == "es"
}
Get the scheme from an URL
You get a Scheme type (which is a sum type of all possible schemes). You can also convert it to a string.
module main
import khalyomede.url { Url, Https }
fn main() {
link := Url.parse("https://example.com")!
assert link.scheme == Https{}
assert link.scheme.str() == "https"
}
Note that when the scheme is not listed among the common ones (Http{}, Https{}, ...), the struct will be Other{}
module main
import khalyomede.url { Url, Other }
fn main() {
link := Url.parse("facebook://user/johndoe")!
assert link.scheme == Other{ value: "facebook" }
assert link.scheme.str() == "facebook"
}
You can match over all different schemes if you need it:
module main
import khalyomede.url { Url, Http, Https, Ftp, Ftps, Ssh, Git, File, Other }
fn main() {
link := Url.parse("facebook://user/johndoe")!
message := match link.scheme {
Http { "Url is HTTP" }
Https { "Url is HTTPS" }
Ftp { "Url is FTP" }
Ftps { "Url is FTPS" }
Ssh { "Url is SSH" }
Git { "Url is Git" }
File { "Url is file" }
Other { "Url is other (${link.scheme.value})" }
}
assert message == "Url is other (facebook)"
}
Get the port from an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("http://localhost:1234")!
assert link.port or { 80 } == 1234
}
Note that when there is no port, the value will be the one provided as a default when handling the Option value.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com")!
assert link.port or { 80 } == 80
}
Get the host from an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com")!
assert link.host == "example.com"
}
Get the path from an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/settings/notifications")!
assert link.path == "/settings/notifications"
assert link.segments == ["settings", "notifications"]
}
Note that query strings and fragments are ignored.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/contact?lang=fr#slack")!
assert link.path == "/contact"
}
Also, when the path is empty (root of the domain), the returned path will be "/". Keep in mind the path always starts with a leading slash.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com")!
assert link.path == "/"
assert link.segments == []
}
Get a query string by key from an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com?lang=fr")!
assert link.query["lang"] or { "en" } == "fr"
}
Note that if there is duplicate keys, only the last value will be preserved during parsing.
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com?lang=fr&lang=en")!
assert link.query["lang"] or { "en" } == "en"
}
This library does not support multi-level queries (for example, "filter[name]=john") since it has not been standardized.
Instead, we advise you use JSON for complex/multi-level values.
module main
import khalyomede.url { Url }
import json
struct Filter {
name ?string
}
fn main() {
link := Url.parse('https://example.com/users?filter={"name":"john"}')!
raw_filter := link.query["filter"] or { '{}' }
filter := json.decode(Filter, raw_filter)!
assert filter.name or { "" } == "john"
}
Get the raw original query string
module main
import khalyomede.url { Url }
import net.urllib { query_unescape }
fn main() {
link := Url.parse("https://example.com?search=V%20lang")!
assert link.raw_query == "search=V%20lang"
assert query_unescape(link.raw_query)! == "search=V lang"
}
Get the fragment from an URL
module main
import khalyomede.url { Url }
fn main() {
link := Url.parse("https://example.com/contact#whatsapp")!
assert link.fragment == "whatsapp"
}
Compare two URLs are equivalent
Just parse your two URLs and cast them to string then perform equality.
module main
import khalyomede.url { Url }
fn main() {
first_url := Url.parse("HTTPS://example.com")!
second_url := Url.parse("https://example.com")!
assert first_url.str() == second_url.str()
}
This performs a normalized comparison since both URLs are converted to lowercase and paths are normalized.
Default ports are also stripped. For example, HTTPS 443 port is automatically stripped, to allow for comparison.
module main
import khalyomede.url { Url }
fn main() {
first_url := Url.parse("https://example.com:443")!
second_url := Url.parse("https://example.com")!
assert first_url.str() == second_url.str()
}
Rendering your Url as a string
URLs are automatically encoded when casted to string.
module main
import khalyomede.url { Url, Https }
fn main() {
link := Url{
scheme: Https{}
host: "example.com"
segments: ["contact"]
query: {
"subject": "Sales order"
}
}
assert link.str() == "https://example.com/contact?subject=Sales+order"
}