search_params.d 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /*
  2. * Permission to use, copy, modify, and/or distribute this software for
  3. * any purpose with or without fee is hereby granted.
  4. *
  5. * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
  6. * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
  7. * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
  8. * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
  9. * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  10. * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  11. * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  12. */
  13. ///
  14. /// Provides read and write access to the query portion of a URL string.
  15. ///
  16. /// Authors: Mio
  17. /// Date: December 03, 2023
  18. /// Homepage: https://codeberg.org/supercell/mlib
  19. /// License: 0BSD
  20. /// Standards: https://url.spec.whatwg.org/#urlsearchparams
  21. /// Version: 2023.12.03
  22. ///
  23. /// History:
  24. /// 2023.12.03 is the initial version.
  25. ///
  26. module mlib.search_params;
  27. enum SearchParamsVersionYear = 2023;
  28. enum SearchParamsVersionMonth = 12;
  29. enum SearchParamsVersionDay = 3;
  30. enum SearchParamsVersion =
  31. SearchParamsVersionYear * 10_000 +
  32. SearchParamsVersionMonth * 100 +
  33. SearchParamsVersionDay;
  34. private string buildVersionString() {
  35. import std.format : format;
  36. return format!"%04d.%02d.%02d"(SearchParamsVersionYear,
  37. SearchParamsVersionMonth, SearchParamsVersionDay);
  38. }
  39. enum SearchParamsVersionString = buildVersionString();
  40. private struct pair
  41. {
  42. string name;
  43. string value;
  44. }
  45. private pair[] parse(string input) @trusted pure
  46. {
  47. import std.array : replace, split;
  48. import std.string : indexOf;
  49. import std.uri : decodeComponent;
  50. auto sequences = split(input, '&');
  51. pair[] output;
  52. foreach(bytes; sequences) {
  53. string name;
  54. string value;
  55. if (bytes.length == 0) {
  56. continue;
  57. }
  58. if (auto pos = bytes.indexOf("=")) {
  59. name = bytes[0..pos];
  60. value = bytes[pos + 1 .. $];
  61. }
  62. name = name.replace("+", " ");
  63. value = value.replace("+", " ");
  64. output ~= pair(decodeComponent(name), decodeComponent(value));
  65. }
  66. return output;
  67. }
  68. /// https://infra.spec.whatwg.org/#c0-control
  69. private enum C0ControlSet = [
  70. '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005',
  71. '\u0006', '\u0007', '\u0008', '\u0009', '\u000A', '\u000B',
  72. '\u000C', '\u000D', '\u000E', '\u000F', '\u0010', '\u0011',
  73. '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017',
  74. '\u0018', '\u0019', '\u001A', '\u001B', '\u001C', '\u001D',
  75. '\u001E', '\u001F'
  76. ];
  77. /// https://url.spec.whatwg.org/#query-percent-encode-set
  78. /// NOTE: Manually need to check for > U+007E (~)
  79. private enum QueryPercentEncodeSet = C0ControlSet ~ [
  80. '\u0020', '\u0022', '\u0023', '\u003C', '\u003E'
  81. ];
  82. private enum PathPercentEncodeSet = QueryPercentEncodeSet ~ [
  83. '\u003F', '\u0060', '\u007B', '\u007D'
  84. ];
  85. /// https://url.spec.whatwg.org/#userinfo-percent-encode-set
  86. private enum UserInfoPercentEncodeSet = PathPercentEncodeSet ~ [
  87. '\u002F', '\u003A', '\u003B', '\u003D', '\u0040', '\u005B',
  88. '\u005C', '\u005D', '\u005E', '\u007C'
  89. ];
  90. /// https://url.spec.whatwg.org/#component-percent-encode-set
  91. private enum ComponentPercentEncodeSet = UserInfoPercentEncodeSet ~ [
  92. '\u0024', '\u0026', '\u0027', '\u0028', '\u0029', '\u002A', '\u002B',
  93. '\u002C'
  94. ];
  95. /// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
  96. private enum PercentEncodeSet = ComponentPercentEncodeSet ~ [
  97. '\u0021', '\u0027', '\u0028', '\u0029', '\u007E'
  98. ];
  99. pure @safe
  100. private string percentEncode(string input, char[] encodeSet, bool spaceAsPlus)
  101. {
  102. import std.algorithm.searching : canFind;
  103. import std.outbuffer : OutBuffer;
  104. auto output = new OutBuffer();
  105. foreach(char c; input ) {
  106. if (c == ' ' && spaceAsPlus) {
  107. output.write('+');
  108. continue;
  109. }
  110. if (canFind(encodeSet, c)) {
  111. output.writef("%%%02X", c);
  112. } else if (c > '\u007E') {
  113. output.writef("%%%02X", c);
  114. } else {
  115. output.write(c);
  116. }
  117. }
  118. return output.toString();
  119. }
  120. ///
  121. /// The `URLSearchParams` API provides read and write methods to work
  122. /// with the query string of a URL.
  123. ///
  124. /// Standard: https://url.spec.whatwg.org/#urlsearchparams
  125. ///
  126. public class URLSearchParams
  127. {
  128. package(mlib):
  129. private pair[] list;
  130. ///
  131. /// Parse the *init* string as a query string, and use it to instantiate
  132. /// a new `URLSearchParams` object. A leading `?`, if present, is
  133. /// ignored.
  134. ///
  135. public this(string init = "") @trusted pure
  136. {
  137. if (init.length > 0) {
  138. list = init[0] == '?' ? parse(init[1..$]) : parse(init);
  139. }
  140. }
  141. ///
  142. @trusted pure unittest
  143. {
  144. auto params = new URLSearchParams("user=abc&query=xyz");
  145. assert(params.get("user") == "abc");
  146. assert(params.toString() == "user=abc&query=xyz",
  147. params.toString());
  148. params = new URLSearchParams("?user=abc&query=xyz");
  149. assert(params.get("user") == "abc");
  150. assert(params.toString() == "user=abc&query=xyz");
  151. }
  152. ///
  153. /// Copy the query string from an existing `URLSearchParams`
  154. /// in to a new instance.
  155. ///
  156. /// Each instance maintains it's own list of paramters.
  157. ///
  158. public this(URLSearchParams other) @trusted pure
  159. {
  160. this(other.toString());
  161. }
  162. ///
  163. @safe pure unittest
  164. {
  165. auto params = new URLSearchParams("query[]=apple");
  166. assert(
  167. params.toString() == "query%5B%5D=apple",
  168. params.toString());
  169. auto newParams = new URLSearchParams(params);
  170. newParams.append("query[]", "the fruit");
  171. assert(
  172. newParams.toString() == "query%5B%5D=apple&query%5B%5D=the+fruit",
  173. newParams.toString());
  174. }
  175. ///
  176. /// The number of search parameter entries.
  177. ///
  178. public size_t length() @trusted pure const
  179. {
  180. return list.length;
  181. }
  182. public alias size = length;
  183. ///
  184. /// Append the specified key/value pair as a new search parameter.
  185. ///
  186. /// As shown in the example below, if the same key is appended multiple
  187. /// times it will appear in the parameter string multiple times for
  188. /// each value.
  189. ///
  190. /// Params:
  191. /// name = The name of the parameter to append
  192. /// value = The value of the parameter to append
  193. ///
  194. public void append(string name, string value) @trusted pure
  195. {
  196. this.list ~= pair(name, value);
  197. // NOTE: We diverge from the spec here, since we don't
  198. // keep track of a URL object. So no need to 'update'.
  199. }
  200. ///
  201. @trusted pure unittest
  202. {
  203. auto params = new URLSearchParams("foo=1&bar=2");
  204. // Add a second foo parameter.
  205. params.append("foo", "4");
  206. assert(params.toString() == "foo=1&bar=2&foo=4");
  207. }
  208. ///
  209. /// Removes the specified parameters and their associated
  210. /// value(s) from the list of search parameters.
  211. ///
  212. /// A parameter name and optional value are used to match
  213. /// parameters. If only a parameter name is specified, then
  214. /// all search parameters that match the name are removed,
  215. /// along with their associated values. If both a parameter
  216. /// name and value are specified, then all search parameters
  217. /// that match both the parameter name and value are removed.
  218. ///
  219. /// NOTE: This method is called `delete()` on the specification,
  220. /// however, `delete` is a keyword in D.
  221. ///
  222. /// Params:
  223. /// name = The name of the parameter to be removed
  224. ///
  225. public void remove(string name) @trusted pure
  226. {
  227. import std.algorithm.iteration : filter;
  228. import std.array : array;
  229. this.list = filter!(p => p.name != name)(this.list).array;
  230. }
  231. ///
  232. @trusted pure unittest
  233. {
  234. auto params = new URLSearchParams("foo=1&bar=2&foo=3");
  235. assert(params.toString() == "foo=1&bar=2&foo=3", params.toString());
  236. params.remove("foo");
  237. assert(params.toString() == "bar=2", params.toString());
  238. }
  239. ///
  240. /// Removes the specified parameters and their associated
  241. /// value(s) from the list of search parameters.
  242. ///
  243. /// A parameter name and optional value are used to match
  244. /// parameters. If only a parameter name is specified, then
  245. /// all search parameters that match the name are removed,
  246. /// along with their associated values. If both a parameter
  247. /// name and value are specified, then all search parameters
  248. /// that match both the parameter name and value are removed.
  249. ///
  250. /// NOTE: This method is called `delete()` on the specification,
  251. /// however, `delete` is a keyword in D.
  252. ///
  253. /// Params:
  254. /// name = The name of the parameter to be removed
  255. /// value = The value that parameters must match, along with
  256. /// the given name, to be removed.
  257. ///
  258. public void remove(string name, string value) @trusted pure
  259. {
  260. import std.algorithm.iteration : filter;
  261. import std.array : array;
  262. this.list = filter!(p => pair(name, value) != p)(this.list).array;
  263. }
  264. ///
  265. @trusted pure unittest
  266. {
  267. auto params = new URLSearchParams("foo=1&bar=2&foo=3&foo=1");
  268. assert(params.toString() == "foo=1&bar=2&foo=3&foo=1");
  269. params.remove("foo", "1");
  270. assert(params.toString() == "bar=2&foo=3", params.toString);
  271. }
  272. ///
  273. /// Returns the value of the first name-value pair whose name
  274. /// is *name*. If there are no such pairs, `null` is returned.
  275. ///
  276. public string get(string name) @trusted pure const
  277. {
  278. foreach(p; list) {
  279. if (p.name == name) {
  280. return p.value;
  281. }
  282. }
  283. return null;
  284. }
  285. ///
  286. unittest
  287. {
  288. import std.conv : to;
  289. const params = new URLSearchParams("name=JohnDoe&age=0");
  290. const name = params.get("name");
  291. const age = to!int(params.get("age"));
  292. assert(name == "JohnDoe");
  293. assert(age == 0);
  294. assert(params.get("address") is null);
  295. }
  296. ///
  297. /// Retrieve all the values associated with a given search
  298. /// parameter *name* as an array.
  299. ///
  300. /// Params:
  301. /// name = The name of the parameter to return.
  302. ///
  303. /// Returns:
  304. /// An array of strings, which may be empty if no values
  305. /// for a given parameter are found.
  306. ///
  307. public string[] getAll(string name) @trusted pure const
  308. {
  309. import std.algorithm.iteration : each;
  310. string[] values;
  311. this.list.each!((p) {
  312. if (p.name == name) {
  313. values ~= p.value;
  314. }
  315. });
  316. return values;
  317. }
  318. ///
  319. @trusted pure unittest
  320. {
  321. auto params = new URLSearchParams("foo=1&bar=2");
  322. params.append("foo", "4");
  323. assert(params.getAll("foo") == ["1", "4"]);
  324. }
  325. ///
  326. /// Indicates whether the specified *name* is in the search
  327. /// parameters.
  328. ///
  329. /// A parameter *name* and optional value are used to match paramters.
  330. /// If only a paramter name is specified, then the method will return
  331. /// `true` if any paramters in the query string match the name, and
  332. /// `false` otherwise. If both parameter name and value are specified,
  333. /// then the method will return `true` if a parameter matches both the
  334. /// name and value.
  335. ///
  336. /// Params:
  337. /// name = The name of the paramter to match.
  338. ///
  339. /// Returns:
  340. /// A boolean value indicating the precense of *name*.
  341. ///
  342. public bool has(string name) @trusted pure const
  343. {
  344. import std.algorithm.searching : any;
  345. return this.list.any!(p => p.name == name);
  346. }
  347. ///
  348. @trusted pure unittest
  349. {
  350. const params = new URLSearchParams("foo=1&bar=2&foo=3");
  351. assert(params.has("bar"));
  352. assert(false == params.has("bark"));
  353. assert(params.has("foo"));
  354. }
  355. ///
  356. /// Indicates whether the specified *name* is in the search
  357. /// parameters.
  358. ///
  359. /// A parameter *name* and optional value are used to match paramters.
  360. /// If only a paramter name is specified, then the method will return
  361. /// `true` if any paramters in the query string match the name, and
  362. /// `false` otherwise. If both parameter name and value are specified,
  363. /// then the method will return `true` if a parameter matches both the
  364. /// name and value.
  365. ///
  366. /// Params:
  367. /// name = The name of the paramter to match.
  368. /// value = The value of the paramter, along with the given name,
  369. /// to match.
  370. ///
  371. /// Returns:
  372. /// A boolean value indicating the precense of *name*.
  373. ///
  374. public bool has(string name, string value) @trusted pure const
  375. {
  376. import std.algorithm.searching : canFind;
  377. return this.list.canFind!(p => pair(name, value) == p);
  378. }
  379. ///
  380. @trusted pure unittest
  381. {
  382. const params = new URLSearchParams("foo=1&bar=2&foo=3");
  383. assert(false == params.has("bar", "1"));
  384. assert(params.has("bar", "2"));
  385. assert(false == params.has("foo", "4"));
  386. }
  387. ///
  388. /// Returns a range allowing iteration through all keys
  389. /// contained in this object. The keys are all strings.
  390. ///
  391. public auto keys() @trusted pure const
  392. {
  393. import std.algorithm.iteration : map;
  394. return map!(x => x.name)(this.list);
  395. }
  396. ///
  397. @trusted pure unittest
  398. {
  399. import std.string : startsWith;
  400. import std.outbuffer : OutBuffer;
  401. const searchParams = new URLSearchParams("key=value1&key2=value2");
  402. auto buffer = new OutBuffer();
  403. foreach(key; searchParams.keys()) {
  404. assert(key.startsWith("key"));
  405. buffer.writef("%s\n", key);
  406. }
  407. assert(buffer.toString() == "key\nkey2\n");
  408. }
  409. ///
  410. /// Sets the value associated with a given search parameter
  411. /// to the given value.
  412. ///
  413. /// If there were several matching values, this method deletes
  414. /// the others. If the search parameter doesn't exist, this
  415. /// method creates it.
  416. ///
  417. /// Params:
  418. /// name = The name of the parameter to set.
  419. /// value = The value of the parameter to set.
  420. ///
  421. public void set(string name, string value) @trusted pure
  422. {
  423. import std.algorithm.mutation : remove;
  424. bool found = false;
  425. for (auto i = 0; i < this.list.length;) {
  426. const current = list[i];
  427. if (current.name == name) {
  428. if (!found) {
  429. list[i].value = value;
  430. found = true;
  431. i += 1;
  432. } else {
  433. list = list.remove(i);
  434. // Do not increment.
  435. // Will keep the same index next iteration.
  436. }
  437. } else {
  438. i += 1;
  439. }
  440. }
  441. if (!found) {
  442. append(name, value);
  443. }
  444. }
  445. ///
  446. @trusted pure unittest
  447. {
  448. auto params = new URLSearchParams("foo=1&bar=2&foo=3");
  449. assert(params.toString() == "foo=1&bar=2&foo=3");
  450. params.set("foo", "4");
  451. params.set("baz", "1");
  452. assert(params.toString() == "foo=4&bar=2&baz=1");
  453. }
  454. ///
  455. /// Sorts all existing name-value pairs in-place by their
  456. /// names.
  457. ///
  458. /// Sorting is done with a [stable sorting algorithm][1], so
  459. /// relative order between name-value pairs with the same name
  460. /// is preserved.
  461. ///
  462. /// [1]: https://dlang.org/phobos/std_algorithm_mutation.html#.SwapStrategy
  463. public void sort() @trusted pure
  464. {
  465. import std.algorithm.mutation : SwapStrategy;
  466. import std.algorithm.sorting : sort;
  467. this.list.sort!((p1, p2) => p1.name < p2.name, SwapStrategy.stable);
  468. }
  469. ///
  470. @trusted pure unittest
  471. {
  472. auto params = new URLSearchParams("c=4&a=2&b=3&a=1");
  473. params.sort();
  474. // note that a=2 comes before a=1, same as input string
  475. assert(params.toString() == "a=2&a=1&b=3&c=4");
  476. }
  477. ///
  478. /// Returns a query string suitable for use in a URL.
  479. ///
  480. /// Characters are percent-encoded where necessary.
  481. ///
  482. override public string toString() const @trusted pure
  483. {
  484. import std.outbuffer : OutBuffer;
  485. auto output = new OutBuffer();
  486. bool appended = false;
  487. foreach(p; this.list) {
  488. const name = percentEncode(p.name, PercentEncodeSet, true);
  489. const value = percentEncode(p.value, PercentEncodeSet, true);
  490. if (appended) {
  491. output.write('&');
  492. }
  493. output.writef("%s=%s", name, value);
  494. appended = true;
  495. }
  496. return output.toString();
  497. }
  498. ///
  499. @trusted pure unittest
  500. {
  501. auto params = new URLSearchParams("query[]=abc&type=search");
  502. params.append("sort", "date");
  503. params.sort();
  504. assert(params.toString() == "query%5B%5D=abc&sort=date&type=search",
  505. params.toString());
  506. }
  507. ///
  508. /// Returns a range allowing iteration through all values
  509. /// contained in this object. The values are all strings.
  510. ///
  511. public auto values() @trusted pure const
  512. {
  513. import std.algorithm.iteration : map;
  514. return this.list.map!(x => x.value);
  515. }
  516. ///
  517. @trusted pure unittest
  518. {
  519. import std.outbuffer : OutBuffer;
  520. import std.string : startsWith;
  521. const params = new URLSearchParams("key1=value1&key2=value2");
  522. auto buffer = new OutBuffer();
  523. foreach(value; params.values()) {
  524. assert(value.startsWith("value"));
  525. buffer.writef("%s\n", value);
  526. }
  527. assert(buffer.toString() == "value1\nvalue2\n");
  528. }
  529. public int opApply(scope int delegate(string name, string value) dg)
  530. {
  531. foreach(pair p; this.list) {
  532. int res = dg(p.name, p.value);
  533. if (res)
  534. return res;
  535. }
  536. return 0;
  537. }
  538. unittest
  539. {
  540. auto params = new URLSearchParams("foo=bar");
  541. params.append("baz", "qux");
  542. foreach(key, value; params) {
  543. assert(params.get(key) == value);
  544. }
  545. }
  546. }
  547. ///
  548. @safe pure unittest
  549. {
  550. auto params = new URLSearchParams("abc=123");
  551. assert(params.get("abc") == "123");
  552. params.append("abc", "xyz");
  553. assert(params.toString() == "abc=123&abc=xyz");
  554. params.remove("abc");
  555. params.set("a", "b");
  556. assert(params.toString() == "a=b");
  557. auto newParams = new URLSearchParams(params);
  558. newParams.append("a", "c");
  559. assert(params.toString() == "a=b");
  560. assert(newParams.toString() == "a=b&a=c");
  561. }