diff --git a/js/dom_types.ts b/js/dom_types.ts index e1fa235bb3..e3f75e4886 100644 --- a/js/dom_types.ts +++ b/js/dom_types.ts @@ -13,7 +13,8 @@ See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. *******************************************************************************/ -export type HeadersInit = string[][] | Record; +export type HeadersInit = Headers | string[][] | Record; +export type URLSearchParamsInit = string | string[][] | Record; type BodyInit = | Blob | BufferSource @@ -74,7 +75,7 @@ export interface ProgressEventInit extends EventInit { total?: number; } -interface URLSearchParams { +export interface URLSearchParams { /** * Appends a specified key/value pair as a new search parameter. */ @@ -101,7 +102,20 @@ interface URLSearchParams { * If there were several values, delete the others. */ set(name: string, value: string): void; + /** + * 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. + */ sort(): void; + /** + * Returns a query string suitable for use in a URL. + */ + toString(): string; + /** + * Iterates over each name-value pair in the query + * and invokes the given function. + */ forEach( callbackfn: (value: string, key: string, parent: URLSearchParams) => void, // tslint:disable-next-line:no-any diff --git a/js/fetch.ts b/js/fetch.ts index 48429ca19c..9cf117ed8f 100644 --- a/js/fetch.ts +++ b/js/fetch.ts @@ -22,85 +22,86 @@ import { import { TextDecoder } from "./text_encoding"; import { DenoBlob } from "./blob"; -interface Header { - name: string; - value: string; -} - +// ref: https://fetch.spec.whatwg.org/#dom-headers export class DenoHeaders implements Headers { - private readonly headerList: Header[] = []; + private headerMap: Map = new Map(); constructor(init?: HeadersInit) { - if (init) { - this._fill(init); + if (arguments.length === 0 || init === undefined) { + return; } - } - private _append(header: Header): void { - // TODO(qti3e) Check header based on the fetch spec. - this._appendToHeaderList(header); - } - - private _appendToHeaderList(header: Header): void { - const lowerCaseName = header.name.toLowerCase(); - for (let i = 0; i < this.headerList.length; ++i) { - if (this.headerList[i].name.toLowerCase() === lowerCaseName) { - header.name = this.headerList[i].name; - } - } - this.headerList.push(header); - } - - private _fill(init: HeadersInit): void { - if (Array.isArray(init)) { - for (let i = 0; i < init.length; ++i) { - const header = init[i]; - if (header.length !== 2) { + if (init instanceof DenoHeaders) { + // init is the instance of Header + init.forEach((value: string, name: string) => { + this.headerMap.set(name, value); + }); + } else if (Array.isArray(init)) { + // init is a sequence + init.forEach(item => { + if (item.length !== 2) { throw new TypeError("Failed to construct 'Headers': Invalid value"); } - this._append({ - name: header[0], - value: header[1] - }); - } + const [name, value] = this.normalizeParams(item[0], item[1]); + const v = this.headerMap.get(name); + const str = v ? `${v}, ${value}` : value; + this.headerMap.set(name, str); + }); + } else if (Object.prototype.toString.call(init) === "[object Object]") { + // init is a object + const names = Object.keys(init); + names.forEach(name => { + const value = (init as Record)[name]; + const [newname, newvalue] = this.normalizeParams(name, value); + this.headerMap.set(newname, newvalue); + }); } else { - for (const key in init) { - this._append({ - name: key, - value: init[key] - }); - } + throw new TypeError("Failed to construct 'Headers': Invalid value"); } } + private normalizeParams(name: string, value?: string): string[] { + name = String(name).toLowerCase(); + value = String(value).trim(); + return [name, value]; + } + append(name: string, value: string): void { - this._appendToHeaderList({ name, value }); + const [newname, newvalue] = this.normalizeParams(name, value); + const v = this.headerMap.get(newname); + const str = v ? `${v}, ${newvalue}` : newvalue; + this.headerMap.set(newname, str); } delete(name: string): void { - assert(false, "Implement me"); + const [newname] = this.normalizeParams(name); + this.headerMap.delete(newname); } + get(name: string): string | null { - for (const header of this.headerList) { - if (header.name.toLowerCase() === name.toLowerCase()) { - return header.value; - } - } - return null; + const [newname] = this.normalizeParams(name); + const value = this.headerMap.get(newname); + return value || null; } + has(name: string): boolean { - assert(false, "Implement me"); - return false; + const [newname] = this.normalizeParams(name); + return this.headerMap.has(newname); } + set(name: string, value: string): void { - assert(false, "Implement me"); + const [newname, newvalue] = this.normalizeParams(name, value); + this.headerMap.set(newname, newvalue); } + forEach( callbackfn: (value: string, key: string, parent: Headers) => void, // tslint:disable-next-line:no-any thisArg?: any ): void { - assert(false, "Implement me"); + this.headerMap.forEach((value, name) => { + callbackfn(value, name, this); + }); } } diff --git a/js/fetch_test.ts b/js/fetch_test.ts index 25d8c2ed01..9d76cb6ba4 100644 --- a/js/fetch_test.ts +++ b/js/fetch_test.ts @@ -43,3 +43,129 @@ testPerm({ net: true }, async function fetchBlob() { assertEqual(blob.type, headers.get("Content-Type")); assertEqual(blob.size, Number(headers.get("Content-Length"))); }); + +// Logic heavily copied from web-platform-tests, make +// sure pass mostly header basic test +/* tslint:disable-next-line:max-line-length */ +// ref: https://github.com/web-platform-tests/wpt/blob/7c50c216081d6ea3c9afe553ee7b64534020a1b2/fetch/api/headers/headers-basic.html +/* tslint:disable:no-unused-expression */ +test(function newHeaderTest() { + new Headers(); + new Headers(undefined); + new Headers({}); + try { + new Headers(null); + } catch (e) { + assertEqual(e.message, "Failed to construct 'Headers': Invalid value"); + } + + try { + const init = [["a", "b", "c"]]; + new Headers(init); + } catch (e) { + assertEqual(e.message, "Failed to construct 'Headers': Invalid value"); + } +}); + +const headerDict = { + "name1": "value1", + "name2": "value2", + "name3": "value3", + "name4": undefined, + "Content-Type": "value4" +}; +const headerSeq = []; +for (const name in headerDict) { + headerSeq.push([name, headerDict[name]]); +} + +test(function newHeaderWithSequence() { + const headers = new Headers(headerSeq); + for (const name in headerDict) { + assertEqual(headers.get(name), String(headerDict[name])); + } + assertEqual(headers.get("length"), null); +}); + +test(function newHeaderWithRecord() { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assertEqual(headers.get(name), String(headerDict[name])); + } +}); + +test(function newHeaderWithHeadersInstance() { + const headers = new Headers(headerDict); + const headers2 = new Headers(headers); + for (const name in headerDict) { + assertEqual(headers2.get(name), String(headerDict[name])); + } +}); + +test(function headerAppendSuccess() { + const headers = new Headers(); + for (const name in headerDict) { + headers.append(name, headerDict[name]); + assertEqual(headers.get(name), String(headerDict[name])); + } +}); + +test(function headerSetSuccess() { + const headers = new Headers(); + for (const name in headerDict) { + headers.set(name, headerDict[name]); + assertEqual(headers.get(name), String(headerDict[name])); + } +}); + +test(function headerHasSuccess() { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assert(headers.has(name), "headers has name " + name); + /* tslint:disable-next-line:max-line-length */ + assert(!headers.has("nameNotInHeaders"), "headers do not have header: nameNotInHeaders"); + } +}); + +test(function headerDeleteSuccess() { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assert(headers.has(name), "headers have a header: " + name); + headers.delete(name); + assert(!headers.has(name), "headers do not have anymore a header: " + name); + } +}); + +test(function headerGetSuccess() { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assertEqual(headers.get(name), String(headerDict[name])); + assertEqual(headers.get("nameNotInHeaders"), null); + } +}); + +const headerEntriesDict = { + "name1": "value1", + "Name2": "value2", + "name": "value3", + "content-Type": "value4", + "Content-Typ": "value5", + "Content-Types": "value6" +}; + +test(function headerForEachSuccess() { + const headers = new Headers(headerEntriesDict); + const keys = Object.keys(headerEntriesDict); + keys.forEach(key => { + const value = headerEntriesDict[key]; + const newkey = key.toLowerCase(); + headerEntriesDict[newkey] = value; + }); + let callNum = 0; + headers.forEach((value, key, container) => { + assertEqual(headers, container); + assertEqual(value, headerEntriesDict[key]); + callNum++; + }); + assertEqual(callNum, keys.length); +});