Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web API: URLSearchParams #1049

Merged
merged 10 commits into from Oct 21, 2018
3 changes: 3 additions & 0 deletions js/globals.ts
Expand Up @@ -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 { URLSearchParams } from "./urlsearchparams";

// 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
Expand Down Expand Up @@ -33,6 +34,8 @@ window.TextDecoder = textEncoding.TextDecoder;
window.atob = textEncoding.atob;
window.btoa = textEncoding.btoa;

window.URLSearchParams = URLSearchParams;

window.fetch = fetch_.fetch;

window.Headers = fetch_.DenoHeaders;
Expand Down
1 change: 1 addition & 0 deletions js/unit_tests.ts
Expand Up @@ -28,4 +28,5 @@ import "./truncate_test.ts";
import "./v8_source_maps_test.ts";
import "../website/app_test.js";
import "./metrics_test.ts";
import "./urlsearchparams_test.ts";
import "./util_test.ts";
174 changes: 174 additions & 0 deletions js/urlsearchparams.ts
@@ -0,0 +1,174 @@
export class URLSearchParams {

private params: [string, string][] = [];

public constructor(init: string | Iterable<[string, string]> | Record<string, string> = '') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init may be undefined

var u = new URLSearchParams()
undefined
u instanceof URLSearchParams
true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The = at the end states it's optional. It defaults to '' to comply with WHATWG.

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));
}
// Is object
} else if (Object(init) === init) {
if (Symbol.iterator in init) {
// Overload: sequence<sequence<USVString>>
for (const tuple of init[Symbol.iterator]()) {
this.append(tuple[0], tuple[1]);
}
} else {
// Overload: record<USVString, USVString>
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');
*/
public append(name: string, value: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public is unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed this and all other occurrences in the next commit, thanks you 👍

this.params.push([name, value]);
}

/** Deletes the given search parameter and its associated value,
* from the list of all search parameters.
*
* searchParams.delete('name');
*/
public delete(name: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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');
*/
public getAll(name: string): string[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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');
*/
public get(name: string): string | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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');
*/
public has(name: string): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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');
*/
public set(name: string, value: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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();
*/
public sort(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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: string, key: string, parent: URLSearchParams) => {
* console.log(value, key, parent);
* });
*
*/
public forEach(callbackfn: (value: string, key: string, parent: URLSearchParams) => void, thisArg?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DItto

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);
*/
public *keys(): Iterable<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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);
*/
public *values(): Iterable<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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);
*/
public *entries(): Iterable<[string, string]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

yield* this.params;
}

/** Returns an iterator allowing to go through all key/value
* pairs contained in this object.
*
* for (const [key, value] of searchParams) console.log(key, value);
*/
public *[Symbol.iterator](): Iterable<[string, string]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

yield* this.params;
}

/** Returns a query string suitable for use in a URL.
*
* searchParams.toString();
*/
public toString(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DItto

return this.params.map((tuple) => `${encodeURIComponent(tuple[0])}=${encodeURIComponent(tuple[1])}`).join("&");
}
}
111 changes: 111 additions & 0 deletions js/urlsearchparams_test.ts
@@ -0,0 +1,111 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import { test, assert, assertEqual } from "./test_util.ts";

test(function initString() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you prefix call of the test function names with “url” or “urlSearchParams”
This is so we can easily filter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fixed in next commit 👍

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 initIterable() {
const init = [["a", "54"], ["b", "true"]];
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.toString(), "a=54&b=true");
});

test(function initRecord() {
const init = { a: '54', b: 'true' };
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.toString(), "a=54&b=true");
});

test(function appendSuccess() {
const searchParams = new URLSearchParams();
searchParams.append("a", "true");
assertEqual(searchParams.toString(), "a=true");
});

test(function deleteSuccess() {
const init = "a=54&b=true";
const searchParams = new URLSearchParams(init);
searchParams.delete("b");
assertEqual(searchParams.toString(), "a=true");
});

test(function getAllSuccess() {
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 getSuccess() {
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 hasSuccess() {
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 setSuccess() {
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 sortSuccess() {
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 forEachSuccess() {
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 missingName() {
const init = "=4";
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.get(""), "4");
assertEqual(searchParams.toString(), "=4");
});

test(function missingValue() {
const init = "4=";
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.get("4"), "");
assertEqual(searchParams.toString(), "4=");
});

test(function missingEqualSign() {
const init = "4";
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.get("4"), "");
assertEqual(searchParams.toString(), "4=");
});

test(function missingPair() {
const init = "c=4&&a=54&";
const searchParams = new URLSearchParams(init);
assertEqual(searchParams.toString(), "c=4&a=54");
});