query.ha (2685B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use io; 5 use memio; 6 use strings; 7 8 export type query_decoder = struct { 9 tokenizer: strings::tokenizer, 10 bufs: (memio::stream, memio::stream), 11 }; 12 13 // Initializes a decoder for a query string. Use [[query_next]] to walk it. The 14 // caller must call [[query_finish]] once they're done using it. 15 export fn decodequery(q: const str) query_decoder = query_decoder { 16 tokenizer = strings::tokenize(q, "&"), 17 bufs = (memio::dynamic(), memio::dynamic()), 18 }; 19 20 // Frees resources associated with the [[query_decoder]]. 21 export fn query_finish(dec: *query_decoder) void = { 22 io::close(&dec.bufs.0)!; 23 io::close(&dec.bufs.1)!; 24 }; 25 26 // Retrieves the next (key, value) pair from the query. The return value is 27 // borrowed from the decoder and will be replaced on the next call, use 28 // [[strings::dup]] to extend its lifetime. 29 export fn query_next(dec: *query_decoder) ((str, str) | invalid | void) = { 30 const tok = match (strings::next_token(&dec.tokenizer)) { 31 case let s: str => 32 yield s; 33 case => return; 34 }; 35 36 const raw = strings::cut(tok, "="); 37 memio::reset(&dec.bufs.0); 38 percent_decode_static(&dec.bufs.0, raw.0)?; 39 memio::reset(&dec.bufs.1); 40 percent_decode_static(&dec.bufs.1, raw.1)?; 41 return ( 42 memio::string(&dec.bufs.0)!, 43 memio::string(&dec.bufs.1)!, 44 ); 45 }; 46 47 // Encodes (key, value) pairs into a URI query string. The result must be 48 // freed by the caller. 49 export fn encodequery(pairs: [](str, str)) str = { 50 const buf = memio::dynamic(); 51 for (let i = 0z; i < len(pairs); i += 1) { 52 const pair = pairs[i]; 53 if (i > 0) memio::appendrune(&buf, '&')!; 54 55 assert(len(pair.0) > 0); 56 percent_encode(&buf, pair.0, unres_query_frag)!; 57 if (len(pair.1) > 0) { 58 memio::appendrune(&buf, '=')!; 59 percent_encode(&buf, pair.1, unres_query_frag)!; 60 }; 61 }; 62 63 return memio::string(&buf)!; 64 }; 65 66 @test fn decodequery() void = { 67 const u = parse("https://sr.ht/projects?search=%23risc-v&sort=longest-active&quantity=100%25")!; 68 defer finish(&u); 69 70 const query = decodequery(u.query); 71 defer query_finish(&query); 72 const pair = query_next(&query)! as (str, str); 73 assert(pair.0 == "search"); 74 assert(pair.1 == "#risc-v"); 75 76 const pair = query_next(&query)! as (str, str); 77 assert(pair.0 == "sort"); 78 assert(pair.1 == "longest-active"); 79 80 const pair = query_next(&query)! as (str, str); 81 assert(pair.0 == "quantity"); 82 assert(pair.1 == "100%"); 83 }; 84 85 @test fn encodequery() void = { 86 const pairs = [ 87 ("search", "#risc-v"), 88 ("sort", "longest-active"), 89 ("quantity", "100%") 90 ]; 91 const encoded = encodequery(pairs); 92 defer free(encoded); 93 94 assert(encoded == "search=%23risc-v&sort=longest-active&quantity=100%25"); 95 };