encoding/query
Reading and building the query string of a web address, such as q=hello&page=2.
import encoding.query
A query string is the key=value&key=value text after the ? in a web address, and the same format is the body of an HTML form posted as application/x-www-form-urlencoded. This package reads and writes that text: it undoes the percent escapes and + for spaces, and builds correctly escaped strings from your values.
Use it when you hold the query text alone: a form body, the raw_query of a request, a signed API call. When you have a whole address and want to change its parameters, encoding/url has SetParam, RemoveParam and KeepParams on top of this package. Inside a net/http handler, http.Query and http.Form read single values for you; take http.RawQuery and this package when you need repeated keys or want to rewrite the query.
Two ways to work
- On the text.
Get,GetAllandHastake the query string itself. Each call decodes the whole string again, which is fine for reading a handful of values. - On a
QueryMap.Decodeturns the text into a map you can change withSet,Add,RemoveandRetain, then write back withEncode. One key may have several values (tag=a&tag=b), so each key holds a list, and the map remembers the order in which keys first appeared.
What trips people up
- There is no getter on a
QueryMap. Read the text withGet, or read the map directly:m.data.has("tag")andm.data.get("tag").items. - Missing and empty look the same to
Get. Bothpage=and nopageat all return""; askHaswhen it matters. A bareflagcounts as present with an empty value, andEncodewrites it back asflag=. - Numbers are not checked.
"12abc".to_int()is 12 and"abc".to_int()is 0, so bound every number you read before using it. - Repeated keys are grouped.
a=1&b=2&a=3encodes back asa=1&a=3&b=2. That is the same data, but if a signature or cache key covers the exact text, sign what you send, not what you received. - Keys are case sensitive, and a leading
?is skipped byDecode.
Guide: read the parameters of a search page
A search page reads a few parameters, gives the missing ones a default, and converts the numbers. Get already decodes the value, so green+tea arrives as green tea. GetAll returns every value of a repeated key, which is how checkbox filters arrive.
import encoding.query
func main:
qs := "q=green+tea&brand=nik&brand=pars&min=20&page=3&limit=500"
words := query.Get(qs, "q")
mut page := 1
if query.Has(qs, "page"):
page = query.Get(qs, "page").to_int()
end
mut limit := 20
if query.Has(qs, "limit"):
limit = query.Get(qs, "limit").to_int()
end
if page < 1: page = 1 end
if limit < 1 or limit > 100: limit = 100 end
brands := query.GetAll(qs, "brand")
println "search for:", words
println "brands:", brands.len(), brands
println "min price:", query.Get(qs, "min").to_int()
println "rows", (page - 1) * limit + 1, "to", page * limit
endGet returns an empty string both for a missing key and for key=, so use Has when the difference matters, as the defaults for page and limit do here. Never trust the numbers: the visitor asked for 500 rows, and the handler caps limit at 100 and keeps page at 1 or more, so a bad value cannot make it read the whole table or a negative offset.
Guide: next and previous page links
Pagination links must keep every filter the visitor chose and change only the page number. Decode the current query into a QueryMap, Set the page (it replaces all values of that key, or adds the key when it is new) and Encode it again.
import encoding.query
import str
func page_link(current: str, page: int): str:
mut m := query.Decode(current)
query.Set(m, "page", str.FromInt(page))
ret "/products?" + query.Encode(m)
end
func main:
current := "category=books&sort=price&tag=new&tag=sale&page=2"
println "prev:", page_link(current, 1)
println "next:", page_link(current, 3)
println "first visit:", page_link("category=books", 2)
endSet keeps an existing key in its place, so the links differ from the address the visitor came from only in the page number, and a cache sees one address per page instead of one per parameter order.
Guide: HTML form bodies
A form sent with method="post" has the body type application/x-www-form-urlencoded, which is the same format as a query string. Decode it with the same functions. Spaces arrive as + and everything else outside plain ASCII as %XX bytes, Persian text included.
import encoding.query
func main:
body := "name=Sara+Ahmadi&city=%D8%B4%DB%8C%D8%B1%D8%A7%D8%B2¬e=50%25+off%3F&agree=on"
form := query.Decode(body)
println "fields:", query.Keys(form)
println "name:", query.Get(body, "name")
println "city:", query.Get(body, "city")
println "note:", query.Get(body, "note")
println "agreed:", query.Get(body, "agree") == "on"
mut reply := query.NewQueryMap()
query.Set(reply, "status", "saved ok")
println "form style:", query.Encode(reply)
println "URL style: ", query.EncodeWith(reply, query.QueryOptions { space_as_plus = false })
endEncode writes spaces as +, as forms do. Some APIs want %20 instead; EncodeWith with space_as_plus = false writes that.
Guide: keep only the parameters you know
Parameters you do not use still change the address, which splits a cache into many copies of the same page and lets anyone pass odd flags along. Retain keeps only a list of allowed keys; Remove drops one and says whether it was there.
import encoding.query
func main:
incoming := "q=lamp&page=2&utm_source=ad&debug=1&sort=new&callback=x"
mut m := query.Decode(incoming)
mut allowed := Vector {} as Vector<str>
allowed.push("q")
allowed.push("page")
allowed.push("sort")
query.Retain(m, allowed)
println "kept:", query.Encode(m)
println "removed sort:", query.Remove(m, "sort")
println "removed again:", query.Remove(m, "sort")
println "cache key:", query.Encode(m), "with", query.Size(m), "keys"
endText functions or a QueryMap
| Job | Use |
|---|---|
| Read one or two values once | Get, GetAll, Has on the text |
| Change a query and write it back | Decode, then Set, Add, Remove, Encode |
| Filter untrusted parameters | Retain |
| Change the query of a full URL | url.SetParam and the other functions of encoding/url |
Types
QueryOptions
struct QueryOptions| Name | Type | Description |
|---|---|---|
space_as_plus | bool = true | Form style, true by default: a space is written as + and a + is read as a space. When false, %20 is written and a + is read as a plus sign. |
import encoding.query
func main:
text := "q=hello&page=2&tag=a&tag=b&empty="
println query.Get(text, "q"), query.Get(text, "page"), query.Has(text, "empty"), query.Has(text, "x")
println query.GetAll(text, "tag")
mut m := query.Decode(text)
println query.Size(m), query.Keys(m)
query.Add(m, "tag", "c")
query.Set(m, "page", "3")
println query.Remove(m, "empty")
println query.Encode(m)
mut keep := Vector {} as Vector<str>
keep.push("q")
query.Retain(m, keep)
println query.Encode(m)
opts := query.QueryOptions {}
println query.EncodeWith(query.DecodeWith("b=2&a=1", opts), opts)
empty := query.NewQueryMap()
println query.Size(empty)
endQueryValues
struct QueryValues| Name | Type | Description |
|---|---|---|
items | Vector | The values, in order. |
The example for this item is shown with the ones above: encoding-query-Query.salam
QueryMap
struct QueryMap| Name | Type | Description |
|---|---|---|
data | HashMap | The values of each key; read it with data.has(key) and data.get(key).items. |
order | Vector | The keys in the order they first appeared. |
The example for this item is shown with the ones above: encoding-query-Query.salam
Functions
NewQueryMap
func NewQueryMap(): QueryMapThe example for this item is shown with the ones above: encoding-query-Query.salam
DecodeWith
func DecodeWith(qs: str, opts: QueryOptions): QueryMapThe example for this item is shown with the ones above: encoding-query-Query.salam
Decode
func Decode(qs: str): QueryMap+ are undone, a leading ? and empty pairs are skipped, and a key without = gets an empty value.The example for this item is shown with the ones above: encoding-query-Query.salam
EncodeWith
func EncodeWith(values: QueryMap, opts: QueryOptions): strThe example for this item is shown with the ones above: encoding-query-Query.salam
Encode
func Encode(values: QueryMap): str-_.~ is percent-encoded.The example for this item is shown with the ones above: encoding-query-Query.salam
GetAll
func GetAll(qs: str, key: str): Vector<str>The example for this item is shown with the ones above: encoding-query-Query.salam
Get
func Get(qs: str, key: str): strThe example for this item is shown with the ones above: encoding-query-Query.salam
Has
func Has(qs: str, key: str): boolThe example for this item is shown with the ones above: encoding-query-Query.salam
Size
func Size(m: QueryMap): intThe example for this item is shown with the ones above: encoding-query-Query.salam
Add
func Add(values &: QueryMap, key: str, val: str)The example for this item is shown with the ones above: encoding-query-Query.salam
Set
func Set(values &: QueryMap, key: str, val: str)The example for this item is shown with the ones above: encoding-query-Query.salam
Keys
func Keys(values: QueryMap): Vector<str>The example for this item is shown with the ones above: encoding-query-Query.salam
Remove
func Remove(values &: QueryMap, key: str): boolThe example for this item is shown with the ones above: encoding-query-Query.salam
Retain
func Retain(values &: QueryMap, keep: Vector<str>)The example for this item is shown with the ones above: encoding-query-Query.salam