From fbb3f05b6ffd9d132cb643c831867b272b95dc0e Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 21 Oct 2018 17:07:29 +0200 Subject: [PATCH] Add URLSearchParams (#1049) --- BUILD.gn | 3 +- js/globals.ts | 3 + js/unit_tests.ts | 1 + js/url_search_params.ts | 207 +++++++++++++++++++++++++++++++++++ js/url_search_params_test.ts | 114 +++++++++++++++++++ tools/http_benchmark.py | 6 +- 6 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 js/url_search_params.ts create mode 100644 js/url_search_params_test.ts diff --git a/BUILD.gn b/BUILD.gn index cac34120f6..dc2d90e204 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -107,6 +107,7 @@ ts_sources = [ "js/trace.ts", "js/truncate.ts", "js/types.ts", + "js/url_search_params.ts", "js/util.ts", "js/v8_source_maps.ts", "js/write_file.ts", @@ -142,7 +143,7 @@ rust_executable("hyper_hello") { source_root = "tools/hyper_hello.rs" extern = [ "$rust_build:hyper", - "$rust_build:ring" + "$rust_build:ring", ] } diff --git a/js/globals.ts b/js/globals.ts index a67524f96e..e65fee9287 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -6,6 +6,7 @@ import { globalEval } from "./global_eval"; import { libdeno } from "./libdeno"; import * as textEncoding from "./text_encoding"; import * as timers from "./timers"; +import * as urlsearchparams from "./url_search_params"; // During the build process, augmentations to the variable `window` in this // file are tracked and created as part of default library that is built into @@ -33,6 +34,8 @@ window.TextDecoder = textEncoding.TextDecoder; window.atob = textEncoding.atob; window.btoa = textEncoding.btoa; +window.URLSearchParams = urlsearchparams.URLSearchParams; + window.fetch = fetch_.fetch; window.Headers = fetch_.DenoHeaders; diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 2f1a41f62a..35cffc7673 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -28,4 +28,5 @@ import "./truncate_test.ts"; import "./v8_source_maps_test.ts"; import "../website/app_test.js"; import "./metrics_test.ts"; +import "./url_search_params_test.ts"; import "./util_test.ts"; diff --git a/js/url_search_params.ts b/js/url_search_params.ts new file mode 100644 index 0000000000..cf005fc16b --- /dev/null +++ b/js/url_search_params.ts @@ -0,0 +1,207 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +export class URLSearchParams { + private params: Array<[string, string]> = []; + + constructor(init: string | string[][] | Record = "") { + if (typeof init === "string") { + // Overload: USVString + // If init is a string and starts with U+003F (?), + // remove the first code point from init. + if (init.charCodeAt(0) === 0x003f) { + init = init.slice(1); + } + + for (const pair of init.split("&")) { + // Empty params are ignored + if (pair.length === 0) { + continue; + } + const position = pair.indexOf("="); + const name = pair.slice(0, position === -1 ? pair.length : position); + const value = pair.slice(name.length + 1); + this.append(decodeURIComponent(name), decodeURIComponent(value)); + } + } else if (Array.isArray(init)) { + // Overload: sequence> + for (const tuple of init) { + this.append(tuple[0], tuple[1]); + } + } else if (Object(init) === init) { + // Overload: record + for (const key of Object.keys(init)) { + this.append(key, init[key]); + } + } + } + + /** Appends a specified key/value pair as a new search parameter. + * + * searchParams.append('name', 'first'); + * searchParams.append('name', 'second'); + */ + append(name: string, value: string): void { + this.params.push([name, value]); + } + + /** Deletes the given search parameter and its associated value, + * from the list of all search parameters. + * + * searchParams.delete('name'); + */ + delete(name: string): void { + let i = 0; + while (i < this.params.length) { + if (this.params[i][0] === name) { + this.params.splice(i, 1); + } else { + i++; + } + } + } + + /** Returns all the values associated with a given search parameter + * as an array. + * + * searchParams.getAll('name'); + */ + getAll(name: string): string[] { + const values = []; + for (const entry of this.params) { + if (entry[0] === name) { + values.push(entry[1]); + } + } + + return values; + } + + /** Returns the first value associated to the given search parameter. + * + * searchParams.get('name'); + */ + get(name: string): string | null { + for (const entry of this.params) { + if (entry[0] === name) { + return entry[1]; + } + } + + return null; + } + + /** Returns a Boolean that indicates whether a parameter with the + * specified name exists. + * + * searchParams.has('name'); + */ + has(name: string): boolean { + return this.params.some(entry => entry[0] === name); + } + + /** 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. + * + * searchParams.set('name', 'value'); + */ + set(name: string, value: string): void { + this.delete(name); + this.append(name, value); + } + + /** Sort all key/value pairs contained in this object in place and + * return undefined. The sort order is according to Unicode code + * points of the keys. + * + * searchParams.sort(); + */ + sort(): void { + this.params = this.params.sort( + (a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1) + ); + } + + /** Calls a function for each element contained in this object in + * place and return undefined. Optionally accepts an object to use + * as this when executing callback as second argument. + * + * searchParams.forEach((value, key, parent) => { + * console.log(value, key, parent); + * }); + * + */ + forEach( + callbackfn: (value: string, key: string, parent: URLSearchParams) => void, + // tslint:disable-next-line:no-any + thisArg?: any + ) { + if (typeof thisArg !== "undefined") { + callbackfn = callbackfn.bind(thisArg); + } + for (const [key, value] of this.entries()) { + callbackfn(value, key, this); + } + } + + /** Returns an iterator allowing to go through all keys contained + * in this object. + * + * for (const key of searchParams.keys()) { + * console.log(key); + * } + */ + *keys(): Iterable { + for (const entry of this.params) { + yield entry[0]; + } + } + + /** Returns an iterator allowing to go through all values contained + * in this object. + * + * for (const value of searchParams.values()) { + * console.log(value); + * } + */ + *values(): Iterable { + for (const entry of this.params) { + yield entry[1]; + } + } + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * for (const [key, value] of searchParams.entries()) { + * console.log(key, value); + * } + */ + *entries(): Iterable<[string, string]> { + yield* this.params; + } + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * for (const [key, value] of searchParams[Symbol.iterator]()) { + * console.log(key, value); + * } + */ + *[Symbol.iterator](): Iterable<[string, string]> { + yield* this.params; + } + + /** Returns a query string suitable for use in a URL. + * + * searchParams.toString(); + */ + toString(): string { + return this.params + .map( + tuple => + `${encodeURIComponent(tuple[0])}=${encodeURIComponent(tuple[1])}` + ) + .join("&"); + } +} diff --git a/js/url_search_params_test.ts b/js/url_search_params_test.ts new file mode 100644 index 0000000000..46e8103d93 --- /dev/null +++ b/js/url_search_params_test.ts @@ -0,0 +1,114 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import { test, assert, assertEqual } from "./test_util.ts"; + +test(function urlSearchParamsInitString() { + const init = "c=4&a=2&b=3&%C3%A1=1"; + const searchParams = new URLSearchParams(init); + assert( + init === searchParams.toString(), + "The init query string does not match" + ); +}); + +test(function urlSearchParamsInitIterable() { + const init = [["a", "54"], ["b", "true"]]; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.toString(), "a=54&b=true"); +}); + +test(function urlSearchParamsInitRecord() { + const init = { a: "54", b: "true" }; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.toString(), "a=54&b=true"); +}); + +test(function urlSearchParamsAppendSuccess() { + const searchParams = new URLSearchParams(); + searchParams.append("a", "true"); + assertEqual(searchParams.toString(), "a=true"); +}); + +test(function urlSearchParamsDeleteSuccess() { + const init = "a=54&b=true"; + const searchParams = new URLSearchParams(init); + searchParams.delete("b"); + assertEqual(searchParams.toString(), "a=54"); +}); + +test(function urlSearchParamsGetAllSuccess() { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.getAll("a"), ["54", "true"]); + assertEqual(searchParams.getAll("b"), ["true"]); + assertEqual(searchParams.getAll("c"), []); +}); + +test(function urlSearchParamsGetSuccess() { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.get("a"), "54"); + assertEqual(searchParams.get("b"), "true"); + assertEqual(searchParams.get("c"), null); +}); + +test(function urlSearchParamsHasSuccess() { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assert(searchParams.has("a")); + assert(searchParams.has("b")); + assert(!searchParams.has("c")); +}); + +test(function urlSearchParamsSetSuccess() { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + searchParams.set("a", "false"); + assertEqual(searchParams.toString(), "b=true&a=false"); +}); + +test(function urlSearchParamsSortSuccess() { + const init = "c=4&a=2&b=3&a=1"; + const searchParams = new URLSearchParams(init); + searchParams.sort(); + assertEqual(searchParams.toString(), "a=2&a=1&b=3&c=4"); +}); + +test(function urlSearchParamsForEachSuccess() { + const init = [["a", "54"], ["b", "true"]]; + const searchParams = new URLSearchParams(init); + let callNum = 0; + searchParams.forEach((value, key, parent) => { + assertEqual(searchParams, parent); + assertEqual(value, init[callNum][1]); + assertEqual(key, init[callNum][0]); + callNum++; + }); + assertEqual(callNum, init.length); +}); + +test(function urlSearchParamsMissingName() { + const init = "=4"; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.get(""), "4"); + assertEqual(searchParams.toString(), "=4"); +}); + +test(function urlSearchParamsMissingValue() { + const init = "4="; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.get("4"), ""); + assertEqual(searchParams.toString(), "4="); +}); + +test(function urlSearchParamsMissingEqualSign() { + const init = "4"; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.get("4"), ""); + assertEqual(searchParams.toString(), "4="); +}); + +test(function urlSearchParamsMissingPair() { + const init = "c=4&&a=54&"; + const searchParams = new URLSearchParams(init); + assertEqual(searchParams.toString(), "c=4&a=54"); +}); diff --git a/tools/http_benchmark.py b/tools/http_benchmark.py index b900043961..a039484b71 100755 --- a/tools/http_benchmark.py +++ b/tools/http_benchmark.py @@ -33,11 +33,7 @@ def http_benchmark(deno_exe, hyper_hello_exe): node_rps = node_http_benchmark() hyper_http_rps = hyper_http_benchmark(hyper_hello_exe) - return { - "deno": deno_rps, - "node": node_rps, - "hyper": hyper_http_rps - } + return {"deno": deno_rps, "node": node_rps, "hyper": hyper_http_rps} def run(server_cmd):