Packages

Parse and encode URL query strings

Current section

Files

Jump to
qs src qs.gleam
Raw

src/qs.gleam

import gleam/dict.{type Dict}
import gleam/list
import gleam/option.{None, Some}
import gleam/result
import gleam/string
import gleam/uri
pub type Query(v) =
Dict(String, v)
pub type QueryBasic =
Query(List(String))
@internal
pub type RawKeyValue =
#(String, String)
@internal
pub fn parse_key_value(segment: String) -> Result(RawKeyValue, String) {
segment
|> uri.percent_decode
|> result.unwrap(segment)
|> string.split_once(on: "=")
|> result.replace_error(
"Unable to parse "
|> string.append(segment),
)
}
/// Parse a query string.
/// Values that cannot be parsed are ignored.
///
/// ## Example
///
/// ```
/// "?color=red&tags=large&tags=wool"
/// |> qs.parse
///
/// ==
///
/// Ok(
/// dict.from_list(
/// [ #("color", ["red"]), #("tags", ["large", "wool"]) ]
/// )
/// )
/// ```
///
pub fn parse(qs: String) -> Result(QueryBasic, String) {
use key_values <- result.then(split_and_parse(qs))
list.fold(over: key_values, from: empty(), with: add_key_value)
|> Ok
}
@internal
pub fn split_and_parse(qs: String) -> Result(List(RawKeyValue), String) {
qs
|> string.replace("?", "")
|> string.split(on: "&")
|> list.map(parse_key_value)
|> result.values
|> Ok
}
fn add_key_value(query: QueryBasic, key_value: RawKeyValue) -> QueryBasic {
let #(key, value) = key_value
let updater = fn(res) {
case res {
Some(existing) -> list.append(existing, [value])
None -> [value]
}
}
dict.upsert(in: query, update: key, with: updater)
}
/// Serialize a query
///
/// ## Example
///
/// ```
/// [ #("color", ["red"]), #("tag", ["large", "wool"]) ]
/// |> qs.serialize
///
/// ==
///
/// "?color=red&tag=large&tag=wool"
/// ```
pub fn serialize(query: QueryBasic) -> String {
serialize_with(query, serialize_key_value)
}
@internal
pub fn serialize_with(query: Query(v), serialize_key_value: fn(#(String, v)) ->
List(String)) -> String {
query
|> dict.to_list
|> list.flat_map(serialize_key_value)
|> string.join("&")
|> add_question_mark
}
fn serialize_key_value(key_value: #(String, List(String))) -> List(String) {
let #(key, values) = key_value
list.map(values, fn(value) { join_key_value(key, value, "=") })
}
@internal
pub fn join_key_value(key: String, value: String, join: String) -> String {
uri.percent_encode(key) <> join <> uri.percent_encode(value)
}
fn add_question_mark(query: String) -> String {
"?"
|> string.append(query)
}
/// Make an empty Query
pub fn empty() -> Query(v) {
dict.new()
}
/// Get values from the query
pub fn get(query: Query(v), key: String) -> Result(v, String) {
let error = "Invalid key " <> key
dict.get(query, key)
|> result.replace_error(error)
}
/// Tell if the query has the given key
pub fn has_key(query: Query(b), key: String) -> Bool {
dict.has_key(query, key)
}
/// Insert a value in the query
/// Replaces existing values
pub fn insert(query: Query(b), key: String, value: b) {
dict.insert(query, key, value)
}
/// Merge two Querys.
/// Second query takes precedence.
pub fn merge(a: Query(a), b: Query(a)) {
dict.merge(a, b)
}
/// Delete a key from the query
pub fn delete(query: Query(a), key: String) {
dict.delete(query, key)
}