123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- /*
- * Permission to use, copy, modify, and/or distribute this software for
- * any purpose with or without fee is hereby granted.
- *
- * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
- * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
- * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
- * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
- * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
- * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
- ///
- /// Provides read and write access to the query portion of a URL string.
- ///
- /// Authors: Mio
- /// Date: December 03, 2023
- /// Homepage: https://codeberg.org/supercell/mlib
- /// License: 0BSD
- /// Standards: https://url.spec.whatwg.org/#urlsearchparams
- /// Version: 2023.12.03
- ///
- /// History:
- /// 2023.12.03 is the initial version.
- ///
- module mlib.search_params;
- enum SearchParamsVersionYear = 2023;
- enum SearchParamsVersionMonth = 12;
- enum SearchParamsVersionDay = 3;
- enum SearchParamsVersion =
- SearchParamsVersionYear * 10_000 +
- SearchParamsVersionMonth * 100 +
- SearchParamsVersionDay;
- private string buildVersionString() {
- import std.format : format;
- return format!"%04d.%02d.%02d"(SearchParamsVersionYear,
- SearchParamsVersionMonth, SearchParamsVersionDay);
- }
- enum SearchParamsVersionString = buildVersionString();
- private struct pair
- {
- string name;
- string value;
- }
- private pair[] parse(string input) @trusted pure
- {
- import std.array : replace, split;
- import std.string : indexOf;
- import std.uri : decodeComponent;
- auto sequences = split(input, '&');
- pair[] output;
- foreach(bytes; sequences) {
- string name;
- string value;
- if (bytes.length == 0) {
- continue;
- }
- if (auto pos = bytes.indexOf("=")) {
- name = bytes[0..pos];
- value = bytes[pos + 1 .. $];
- }
- name = name.replace("+", " ");
- value = value.replace("+", " ");
- output ~= pair(decodeComponent(name), decodeComponent(value));
- }
- return output;
- }
- /// https://infra.spec.whatwg.org/#c0-control
- private enum C0ControlSet = [
- '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005',
- '\u0006', '\u0007', '\u0008', '\u0009', '\u000A', '\u000B',
- '\u000C', '\u000D', '\u000E', '\u000F', '\u0010', '\u0011',
- '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017',
- '\u0018', '\u0019', '\u001A', '\u001B', '\u001C', '\u001D',
- '\u001E', '\u001F'
- ];
- /// https://url.spec.whatwg.org/#query-percent-encode-set
- /// NOTE: Manually need to check for > U+007E (~)
- private enum QueryPercentEncodeSet = C0ControlSet ~ [
- '\u0020', '\u0022', '\u0023', '\u003C', '\u003E'
- ];
- private enum PathPercentEncodeSet = QueryPercentEncodeSet ~ [
- '\u003F', '\u0060', '\u007B', '\u007D'
- ];
- /// https://url.spec.whatwg.org/#userinfo-percent-encode-set
- private enum UserInfoPercentEncodeSet = PathPercentEncodeSet ~ [
- '\u002F', '\u003A', '\u003B', '\u003D', '\u0040', '\u005B',
- '\u005C', '\u005D', '\u005E', '\u007C'
- ];
- /// https://url.spec.whatwg.org/#component-percent-encode-set
- private enum ComponentPercentEncodeSet = UserInfoPercentEncodeSet ~ [
- '\u0024', '\u0026', '\u0027', '\u0028', '\u0029', '\u002A', '\u002B',
- '\u002C'
- ];
- /// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
- private enum PercentEncodeSet = ComponentPercentEncodeSet ~ [
- '\u0021', '\u0027', '\u0028', '\u0029', '\u007E'
- ];
- pure @safe
- private string percentEncode(string input, char[] encodeSet, bool spaceAsPlus)
- {
- import std.algorithm.searching : canFind;
- import std.outbuffer : OutBuffer;
- auto output = new OutBuffer();
- foreach(char c; input ) {
- if (c == ' ' && spaceAsPlus) {
- output.write('+');
- continue;
- }
- if (canFind(encodeSet, c)) {
- output.writef("%%%02X", c);
- } else if (c > '\u007E') {
- output.writef("%%%02X", c);
- } else {
- output.write(c);
- }
- }
- return output.toString();
- }
- ///
- /// The `URLSearchParams` API provides read and write methods to work
- /// with the query string of a URL.
- ///
- /// Standard: https://url.spec.whatwg.org/#urlsearchparams
- ///
- public class URLSearchParams
- {
- package(mlib):
- private pair[] list;
- ///
- /// Parse the *init* string as a query string, and use it to instantiate
- /// a new `URLSearchParams` object. A leading `?`, if present, is
- /// ignored.
- ///
- public this(string init = "") @trusted pure
- {
- if (init.length > 0) {
- list = init[0] == '?' ? parse(init[1..$]) : parse(init);
- }
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("user=abc&query=xyz");
- assert(params.get("user") == "abc");
- assert(params.toString() == "user=abc&query=xyz",
- params.toString());
- params = new URLSearchParams("?user=abc&query=xyz");
- assert(params.get("user") == "abc");
- assert(params.toString() == "user=abc&query=xyz");
- }
- ///
- /// Copy the query string from an existing `URLSearchParams`
- /// in to a new instance.
- ///
- /// Each instance maintains it's own list of paramters.
- ///
- public this(URLSearchParams other) @trusted pure
- {
- this(other.toString());
- }
- ///
- @safe pure unittest
- {
- auto params = new URLSearchParams("query[]=apple");
- assert(
- params.toString() == "query%5B%5D=apple",
- params.toString());
- auto newParams = new URLSearchParams(params);
- newParams.append("query[]", "the fruit");
- assert(
- newParams.toString() == "query%5B%5D=apple&query%5B%5D=the+fruit",
- newParams.toString());
- }
- ///
- /// The number of search parameter entries.
- ///
- public size_t length() @trusted pure const
- {
- return list.length;
- }
- public alias size = length;
- ///
- /// Append the specified key/value pair as a new search parameter.
- ///
- /// As shown in the example below, if the same key is appended multiple
- /// times it will appear in the parameter string multiple times for
- /// each value.
- ///
- /// Params:
- /// name = The name of the parameter to append
- /// value = The value of the parameter to append
- ///
- public void append(string name, string value) @trusted pure
- {
- this.list ~= pair(name, value);
- // NOTE: We diverge from the spec here, since we don't
- // keep track of a URL object. So no need to 'update'.
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("foo=1&bar=2");
- // Add a second foo parameter.
- params.append("foo", "4");
- assert(params.toString() == "foo=1&bar=2&foo=4");
- }
- ///
- /// Removes the specified parameters and their associated
- /// value(s) from the list of search parameters.
- ///
- /// A parameter name and optional value are used to match
- /// parameters. If only a parameter name is specified, then
- /// all search parameters that match the name are removed,
- /// along with their associated values. If both a parameter
- /// name and value are specified, then all search parameters
- /// that match both the parameter name and value are removed.
- ///
- /// NOTE: This method is called `delete()` on the specification,
- /// however, `delete` is a keyword in D.
- ///
- /// Params:
- /// name = The name of the parameter to be removed
- ///
- public void remove(string name) @trusted pure
- {
- import std.algorithm.iteration : filter;
- import std.array : array;
- this.list = filter!(p => p.name != name)(this.list).array;
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("foo=1&bar=2&foo=3");
- assert(params.toString() == "foo=1&bar=2&foo=3", params.toString());
- params.remove("foo");
- assert(params.toString() == "bar=2", params.toString());
- }
- ///
- /// Removes the specified parameters and their associated
- /// value(s) from the list of search parameters.
- ///
- /// A parameter name and optional value are used to match
- /// parameters. If only a parameter name is specified, then
- /// all search parameters that match the name are removed,
- /// along with their associated values. If both a parameter
- /// name and value are specified, then all search parameters
- /// that match both the parameter name and value are removed.
- ///
- /// NOTE: This method is called `delete()` on the specification,
- /// however, `delete` is a keyword in D.
- ///
- /// Params:
- /// name = The name of the parameter to be removed
- /// value = The value that parameters must match, along with
- /// the given name, to be removed.
- ///
- public void remove(string name, string value) @trusted pure
- {
- import std.algorithm.iteration : filter;
- import std.array : array;
- this.list = filter!(p => pair(name, value) != p)(this.list).array;
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("foo=1&bar=2&foo=3&foo=1");
- assert(params.toString() == "foo=1&bar=2&foo=3&foo=1");
- params.remove("foo", "1");
- assert(params.toString() == "bar=2&foo=3", params.toString);
- }
- ///
- /// Returns the value of the first name-value pair whose name
- /// is *name*. If there are no such pairs, `null` is returned.
- ///
- public string get(string name) @trusted pure const
- {
- foreach(p; list) {
- if (p.name == name) {
- return p.value;
- }
- }
- return null;
- }
- ///
- unittest
- {
- import std.conv : to;
- const params = new URLSearchParams("name=JohnDoe&age=0");
- const name = params.get("name");
- const age = to!int(params.get("age"));
- assert(name == "JohnDoe");
- assert(age == 0);
- assert(params.get("address") is null);
- }
- ///
- /// Retrieve all the values associated with a given search
- /// parameter *name* as an array.
- ///
- /// Params:
- /// name = The name of the parameter to return.
- ///
- /// Returns:
- /// An array of strings, which may be empty if no values
- /// for a given parameter are found.
- ///
- public string[] getAll(string name) @trusted pure const
- {
- import std.algorithm.iteration : each;
- string[] values;
- this.list.each!((p) {
- if (p.name == name) {
- values ~= p.value;
- }
- });
- return values;
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("foo=1&bar=2");
- params.append("foo", "4");
- assert(params.getAll("foo") == ["1", "4"]);
- }
- ///
- /// Indicates whether the specified *name* is in the search
- /// parameters.
- ///
- /// A parameter *name* and optional value are used to match paramters.
- /// If only a paramter name is specified, then the method will return
- /// `true` if any paramters in the query string match the name, and
- /// `false` otherwise. If both parameter name and value are specified,
- /// then the method will return `true` if a parameter matches both the
- /// name and value.
- ///
- /// Params:
- /// name = The name of the paramter to match.
- ///
- /// Returns:
- /// A boolean value indicating the precense of *name*.
- ///
- public bool has(string name) @trusted pure const
- {
- import std.algorithm.searching : any;
- return this.list.any!(p => p.name == name);
- }
- ///
- @trusted pure unittest
- {
- const params = new URLSearchParams("foo=1&bar=2&foo=3");
- assert(params.has("bar"));
- assert(false == params.has("bark"));
- assert(params.has("foo"));
- }
- ///
- /// Indicates whether the specified *name* is in the search
- /// parameters.
- ///
- /// A parameter *name* and optional value are used to match paramters.
- /// If only a paramter name is specified, then the method will return
- /// `true` if any paramters in the query string match the name, and
- /// `false` otherwise. If both parameter name and value are specified,
- /// then the method will return `true` if a parameter matches both the
- /// name and value.
- ///
- /// Params:
- /// name = The name of the paramter to match.
- /// value = The value of the paramter, along with the given name,
- /// to match.
- ///
- /// Returns:
- /// A boolean value indicating the precense of *name*.
- ///
- public bool has(string name, string value) @trusted pure const
- {
- import std.algorithm.searching : canFind;
- return this.list.canFind!(p => pair(name, value) == p);
- }
- ///
- @trusted pure unittest
- {
- const params = new URLSearchParams("foo=1&bar=2&foo=3");
- assert(false == params.has("bar", "1"));
- assert(params.has("bar", "2"));
- assert(false == params.has("foo", "4"));
- }
- ///
- /// Returns a range allowing iteration through all keys
- /// contained in this object. The keys are all strings.
- ///
- public auto keys() @trusted pure const
- {
- import std.algorithm.iteration : map;
- return map!(x => x.name)(this.list);
- }
- ///
- @trusted pure unittest
- {
- import std.string : startsWith;
- import std.outbuffer : OutBuffer;
- const searchParams = new URLSearchParams("key=value1&key2=value2");
- auto buffer = new OutBuffer();
- foreach(key; searchParams.keys()) {
- assert(key.startsWith("key"));
- buffer.writef("%s\n", key);
- }
- assert(buffer.toString() == "key\nkey2\n");
- }
- ///
- /// Sets the value associated with a given search parameter
- /// to the given value.
- ///
- /// If there were several matching values, this method deletes
- /// the others. If the search parameter doesn't exist, this
- /// method creates it.
- ///
- /// Params:
- /// name = The name of the parameter to set.
- /// value = The value of the parameter to set.
- ///
- public void set(string name, string value) @trusted pure
- {
- import std.algorithm.mutation : remove;
- bool found = false;
- for (auto i = 0; i < this.list.length;) {
- const current = list[i];
- if (current.name == name) {
- if (!found) {
- list[i].value = value;
- found = true;
- i += 1;
- } else {
- list = list.remove(i);
- // Do not increment.
- // Will keep the same index next iteration.
- }
- } else {
- i += 1;
- }
- }
- if (!found) {
- append(name, value);
- }
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("foo=1&bar=2&foo=3");
- assert(params.toString() == "foo=1&bar=2&foo=3");
- params.set("foo", "4");
- params.set("baz", "1");
- assert(params.toString() == "foo=4&bar=2&baz=1");
- }
- ///
- /// Sorts all existing name-value pairs in-place by their
- /// names.
- ///
- /// Sorting is done with a [stable sorting algorithm][1], so
- /// relative order between name-value pairs with the same name
- /// is preserved.
- ///
- /// [1]: https://dlang.org/phobos/std_algorithm_mutation.html#.SwapStrategy
- public void sort() @trusted pure
- {
- import std.algorithm.mutation : SwapStrategy;
- import std.algorithm.sorting : sort;
- this.list.sort!((p1, p2) => p1.name < p2.name, SwapStrategy.stable);
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("c=4&a=2&b=3&a=1");
- params.sort();
- // note that a=2 comes before a=1, same as input string
- assert(params.toString() == "a=2&a=1&b=3&c=4");
- }
- ///
- /// Returns a query string suitable for use in a URL.
- ///
- /// Characters are percent-encoded where necessary.
- ///
- override public string toString() const @trusted pure
- {
- import std.outbuffer : OutBuffer;
- auto output = new OutBuffer();
- bool appended = false;
- foreach(p; this.list) {
- const name = percentEncode(p.name, PercentEncodeSet, true);
- const value = percentEncode(p.value, PercentEncodeSet, true);
- if (appended) {
- output.write('&');
- }
- output.writef("%s=%s", name, value);
- appended = true;
- }
- return output.toString();
- }
- ///
- @trusted pure unittest
- {
- auto params = new URLSearchParams("query[]=abc&type=search");
- params.append("sort", "date");
- params.sort();
- assert(params.toString() == "query%5B%5D=abc&sort=date&type=search",
- params.toString());
- }
- ///
- /// Returns a range allowing iteration through all values
- /// contained in this object. The values are all strings.
- ///
- public auto values() @trusted pure const
- {
- import std.algorithm.iteration : map;
- return this.list.map!(x => x.value);
- }
- ///
- @trusted pure unittest
- {
- import std.outbuffer : OutBuffer;
- import std.string : startsWith;
- const params = new URLSearchParams("key1=value1&key2=value2");
- auto buffer = new OutBuffer();
- foreach(value; params.values()) {
- assert(value.startsWith("value"));
- buffer.writef("%s\n", value);
- }
- assert(buffer.toString() == "value1\nvalue2\n");
- }
- public int opApply(scope int delegate(string name, string value) dg)
- {
- foreach(pair p; this.list) {
- int res = dg(p.name, p.value);
- if (res)
- return res;
- }
- return 0;
- }
- unittest
- {
- auto params = new URLSearchParams("foo=bar");
- params.append("baz", "qux");
- foreach(key, value; params) {
- assert(params.get(key) == value);
- }
- }
- }
- ///
- @safe pure unittest
- {
- auto params = new URLSearchParams("abc=123");
- assert(params.get("abc") == "123");
- params.append("abc", "xyz");
- assert(params.toString() == "abc=123&abc=xyz");
- params.remove("abc");
- params.set("a", "b");
- assert(params.toString() == "a=b");
- auto newParams = new URLSearchParams(params);
- newParams.append("a", "c");
- assert(params.toString() == "a=b");
- assert(newParams.toString() == "a=b&a=c");
- }
|