sema-url

0.1.0

URL parsing, building, resolving, and query-string manipulation

$ sema pkg install sema-url
Readme Versions 1 Dependencies 0

sema-url

URL parsing, building, resolving, and query-string manipulation.

Install

sema pkg add sema-url

Quick start

(import "sema-url")

(define parsed (url/parse "https://api.example.com:8080/users?page=1&limit=10#results"))
(get parsed :scheme)   ; => "https"
(get parsed :host)     ; => "api.example.com"
(get parsed :port)     ; => 8080
(get parsed :path)     ; => "/users"

(url/query-parse (get parsed :query))
; => {"limit" "10" "page" "1"}

(url/build {:scheme "https" :host "example.com" :path "/search"
            :query (url/query-build {"q" "sema lang" "page" "1"})})
; => "https://example.com/search?page=1&q=sema%20lang"

(url/join "https://example.com/docs/intro" "../api/reference")
; => "https://example.com/api/reference"

API

Function Description
(url/parse str) Parse a URL into {:scheme :userinfo :host :port :path :query :fragment}
(url/build parts) Build a URL string from a parts map
(url/join base ref) Resolve a reference against a base URL, RFC 3986 style
(url/absolute? str) #t when the string starts with scheme://
(url/with-query url params) Replace a URL's query string from a params map
(url/query-parse str) Parse "a=1&b=2" into {"a" "1" "b" "2"}
(url/query-build map) Build an encoded query string from a map
(url/encode str) Percent-encode a string (UTF-8 aware)
(url/decode str) Percent-decode a string (UTF-8 aware)

url/parse

(url/parse "https://user:pw@example.com:8080/a/b?x=1#top")
; => {:scheme "https" :userinfo "user:pw" :host "example.com" :port 8080
;     :path "/a/b" :query "x=1" :fragment "top"}

Absent parts are nil; :path defaults to "/". :query and :fragment are returned raw (still encoded) — feed :query to url/query-parse to get a map. :port is an integer; a non-numeric port raises an error. URLs without a scheme parse too ((url/parse "localhost:3000/api") → host "localhost", port 3000).

Limitations: only hierarchical URLs are supported — non-// schemes like mailto: and IPv6 host literals ([::1]) are not.

url/build

(url/build {:scheme "https" :host "example.com" :port 8080 :path "/x" :query "a=1"})
; => "https://example.com:8080/x?a=1"

nil parts are omitted. :query and :fragment are emitted verbatim, so encode them first (e.g. with url/query-build). (url/build (url/parse u)) roundtrips.

url/join

(url/join "https://example.com/a/b" "c")           ; => "https://example.com/a/c"
(url/join "https://example.com/a/b/" "c")          ; => "https://example.com/a/b/c"
(url/join "https://example.com/a/b/" "../c")       ; => "https://example.com/a/c"
(url/join "https://example.com/a/b" "/c/d")        ; => "https://example.com/c/d"
(url/join "https://example.com/a" "https://o.io/") ; => "https://o.io/"
(url/join "https://example.com/a" "//cdn.io/x.js") ; => "https://cdn.io/x.js"

RFC 3986-style resolution: an absolute ref replaces the base entirely; an absolute path replaces the base's path; a relative path resolves against the directory of the base's path, normalizing . and .. segments. The base's query and fragment are dropped; the ref's are kept. An empty ref returns the base unchanged.

url/absolute?

(url/absolute? "https://example.com")  ; => #t
(url/absolute? "/a/b")                 ; => #f
(url/absolute? "example.com/a")        ; => #f

url/with-query

(url/with-query "https://example.com/s?old=1" {"q" "sema" "page" "2"})
; => "https://example.com/s?page=2&q=sema"

(url/with-query "https://example.com/s?old=1" {})
; => "https://example.com/s"

Replaces (never merges) the query; an empty map removes it. The fragment is preserved.

url/query-parse

(url/query-parse "q=sema+lang&page=1")  ; => {"page" "1" "q" "sema lang"}
(url/query-parse "debug")               ; => {"debug" ""}
(url/query-parse "")                    ; => {}

Keys and values are percent-decoded (+ becomes a space). A key without = gets the value "". Repeated keys keep the last value — if you need multi-value params, split the query string yourself.

url/query-build

(url/query-build {"q" "sema lang" "page" 1})  ; => "page=1&q=sema%20lang"

Keys and values are percent-encoded; non-string values go through str. Keys are emitted in sorted order, so output is deterministic.

url/encode

(url/encode "a b/c")  ; => "a%20b%2Fc"
(url/encode "café")   ; => "caf%C3%A9"

Percent-encodes everything outside RFC 3986 unreserved characters (A-Z a-z 0-9 - . _ ~). Non-ASCII characters are encoded as their UTF-8 bytes.

url/decode

(url/decode "caf%C3%A9")  ; => "café"
(url/decode "a+b")        ; => "a b"
(url/decode "100%")       ; => "100%"

Decodes %XX sequences as UTF-8 (upper- or lowercase hex) and + as a space. A % not followed by two hex digits is kept literally.

Testing

sema pkg add sema-test   # once
sema tests.sema

License

MIT

VersionSizePublished
0.1.0 6 KB 2026-07-07 21:28:20
No dependencies