From 44e6014d3c10267d22c7a76f0b5a5c66caca4fcd Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 10 Jul 2015 10:32:43 +0300 Subject: [PATCH 01/10] initial commit --- CrossPlatformModules.csproj | 6 + fetch/README.md | 17 + fetch/body.ts | 100 ++++++ fetch/fetch.ts | 677 ++++++++++++++++++++++++++++++++++++ fetch/headers.ts | 341 ++++++++++++++++++ fetch/package.json | 28 ++ fetch/webidl.d.ts | 14 + 7 files changed, 1183 insertions(+) create mode 100644 fetch/README.md create mode 100644 fetch/body.ts create mode 100644 fetch/fetch.ts create mode 100644 fetch/headers.ts create mode 100644 fetch/package.json create mode 100644 fetch/webidl.d.ts diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index b262c50d3..52a85dee5 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -288,6 +288,10 @@ + + + + file-name-resolver.d.ts @@ -1670,6 +1674,8 @@ PreserveNewest + + diff --git a/fetch/README.md b/fetch/README.md new file mode 100644 index 000000000..fb27fc900 --- /dev/null +++ b/fetch/README.md @@ -0,0 +1,17 @@ +# Isomorphic Fetch Implementation + +## status + +WIP + +## motivation + +implementation of [fetch API](https://fetch.spec.whatwg.org/) in pure javascript. +polyfill for browser, and implemnt for node.js. +make network http access isomorphic. + + +## License + +The MIT License (MIT) +Copyright (c) 2013 Jxck diff --git a/fetch/body.ts b/fetch/body.ts new file mode 100644 index 000000000..1070006b5 --- /dev/null +++ b/fetch/body.ts @@ -0,0 +1,100 @@ +// https://fetch.spec.whatwg.org/#json +type object = JSON; + +type body = Object; // byte stream + +// https://fetch.spec.whatwg.org/#bodyinit +type BodyInit = Blob | BufferSource | FormData | URLSearchParams | USVString + +// https://fetch.spec.whatwg.org/#body +interface IBody { + // readonly property + bodyUsed: boolean; + + // method + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + text(): Promise; +}; + +// https://fetch.spec.whatwg.org/#body +class Body implements IBody { + private _bodyUsed: boolean; + private _body: body; + private _usedFlag: boolean; + private _mimeType: string; + + get bodyUsed(): boolean { + return this._bodyUsed; + } + + get body(): body { + return this._body; + } + + get usedFlag(): boolean { + return this._usedFlag; + } + + get mimeType(): string { + return this._mimeType; + } + + consumeBody(_type: string): any { + // step 1 + var p = new Promise((resolve, reject) => { + // step 2 + if (this._bodyUsed == true) { + return reject(new TypeError("body was already used")); + } + + // step 3 + this._bodyUsed = true; + + // step 3-1 + var stream = this._body; + + // step 3-2 + if (stream == null) { + stream = []; + } + + // step 3-3 + // TODO: Let bytes be the result of reading from stream until it returns end-of-stream. + var bytes; + + // step 4 + // TODO: implement me + switch(_type) { + case "ArrayBuffer": + case "Blob": + case "FormData": + case "JSON": + case "text": + } + }); + return p; + } + + arrayBuffer(): Promise { + return this.consumeBody("ArrayBuffer"); + } + + blob(): Promise { + return this.consumeBody("Blob"); + } + + formData(): Promise { + return this.consumeBody("FormData"); + } + + json(): Promise { + return this.consumeBody("JSON"); + } + + text(): Promise { + return this.consumeBody("text"); + } +} diff --git a/fetch/fetch.ts b/fetch/fetch.ts new file mode 100644 index 000000000..c2dbc6f75 --- /dev/null +++ b/fetch/fetch.ts @@ -0,0 +1,677 @@ +/// +/// +/// + +// http://heycam.github.io/webidl/#common-BufferSource +class BufferSource { +} + +// https://url.spec.whatwg.org/#urlsearchparams +class URLSearchParams { +} + +// https://fetch.spec.whatwg.org/#concept-method +enum MethodEnum { + OPTIONS, + GET, + HEAD, + POST, + PUT, + DELETE, + TRACE, + CONNECT, +} + +// https://fetch.spec.whatwg.org/#simple-method +enum SimpleMethodEnum { + GET, + HEAD, + POST +} + +function isSimpleMethod(method: ByteString): boolean { + if (SimpleMethodEnum[method] !== undefined) { + return true; + } + return false; +} + +// https://fetch.spec.whatwg.org/#forbidden-method +enum ForbiddenMethodEnum { + CONNECT, + TRACE, + TRACK +} + +function isForbiddenMethod(method: ByteString): boolean { + if (ForbiddenMethodEnum[method] !== undefined) { + return true; + } + return false +} + +// https://fetch.spec.whatwg.org/#requestcontext +type RequestContext = string; +enum RequestContextEnum { + "audio", "beacon", + "cspreport", "download", + "embed", "eventsource", + "favicon", "fetch", + "font", "form", + "frame", "hyperlink", + "iframe", "image", + "imageset", "import", + "internal", "location", + "manifest", "object", + "ping", "plugin", + "prefetch", "script", + "serviceworker", "sharedworker", + "subresource", "style", + "track", "video", + "worker", "xmlhttprequest", + "xslt" +}; + +// https://fetch.spec.whatwg.org/#concept-request-context-frame-type +enum ContextFrameTypeEnum { + "auxiliary", + "top-level", + "nested", + "none" +} + +// https://fetch.spec.whatwg.org/#concept-request-mode +type RequestMode = string; +enum RequestModeEnum { + "same-origin", + "no-cors", + "cors" +}; + +// https://fetch.spec.whatwg.org/#concept-request-credentials-mode +type RequestCredentials = string; +enum RequestCredentialsEnum { + "omit", + "same-origin", + "include" +}; + +// https://fetch.spec.whatwg.org/#concept-request-cache-mode +type RequestCache = string; +enum RequestCacheEnum { + "default", + "bypass", + "reload", + "revalidate", + "force-cache", + "offline" +}; + +// https://fetch.spec.whatwg.org/#concept-response-type +type ResponseType = string; +enum ResponseTypeEnum { + "basic", + "cors", + "default", + "error", + "opaque" +}; + +///////////////////////////// +/// Request +///////////////////////////// + +// https://fetch.spec.whatwg.org/#requestinfo +type RequestInfo = Request | USVString; + +// https://fetch.spec.whatwg.org/#request +interface IRequest extends IBody { + // readonly property + method: ByteString; + url: USVString; + headers: Headers; + context: RequestContext; + referrer: DOMString; + mode: RequestMode; + credentials: RequestCredentials; + cache: RequestCache; + + // method + clone(): IRequest; +}; + +// https://fetch.spec.whatwg.org/#requestinit +// dictionary RequestInit +interface RequestInit { + method: ByteString; + headers: HeadersInit; + body: BodyInit; + mode: RequestMode; + credentials: RequestCredentials; + cache: RequestCache; +}; + +type Client = Object; +type Referrer = Object; +type Context = Object; + +type request = { + method: string; + url: string; + headerList: Header[]; + unsafeRequestFlag: boolean; + body: body; + //TODO: client: Client; + context: Context; + //TODO: origin: string; + forceOriginHeaderFlag: boolean; + sameOriginDataURLFlag: boolean; + //TODO: referrer: Referrer; + mode: string; + credentialsMode: string; + cacheMode: string; +} + +class Request implements IRequest { + // readonly property on IRequest + private _method: ByteString; + private _url: USVString; + private _headers: Headers; + private _context: RequestContext; + private _referrer: DOMString; + private _mode: ByteString; + private _credentials: RequestCredentials; + private _cache: RequestCache; + + // readonly property on IBody + private _bodyUsed: boolean; + + // https://fetch.spec.whatwg.org/#dom-request-method + get method(): ByteString { + return this._method; + } + + // https://fetch.spec.whatwg.org/#dom-request-url + get url(): USVString { + return this._url; + } + + // https://fetch.spec.whatwg.org/#dom-request-headers + get headers(): Headers { + return this._headers; + } + + // https://fetch.spec.whatwg.org/#dom-request-context + get context(): RequestContext { + return this._context; + } + + // https://fetch.spec.whatwg.org/#dom-request-referrer + get referrer(): DOMString { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-request-mode + get mode(): RequestMode { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-request-credentials + get credentials(): RequestCredentials { + return this._credentials; + } + + // https://fetch.spec.whatwg.org/#dom-request-cache + get cache(): RequestCache { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed(): boolean { + return this._bodyUsed; + } + + // https://fetch.spec.whatwg.org/#dom-request-clone + // method on IRequest + public clone(): IRequest { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + // method on IBody + public arrayBuffer(): Promise { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + public blob(): Promise { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + public formData(): Promise { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-json + public json(): Promise { + return null; + } + + // https://fetch.spec.whatwg.org/#dom-body-text + public text(): Promise { + return null; + } + + // Request + public request: request; + public body: body; + public usedFlag: boolean; + public mimeType: string; + + // https://fetch.spec.whatwg.org/#dom-request + constructor(input: RequestInfo, init?: RequestInit) { + // can't detect class by instanceof + // if (input instanceof Request) { } + + var request: request; + // step 1 + if (typeof input === "object" && input.body !== null) { // Request + // step 1-1 + if (input.usedFlag) { + throw new TypeError("Request already used"); + } + // step 1-2 + input.usedFlag = true; + + // step 2 + request = input.request; + } else { + // step 2 + // new request otherwise + request = { + url: null, + method: MethodEnum[MethodEnum.GET], + headerList: [], + unsafeRequestFlag: false, + body: null, + //TODO: client: entry settings object, + //TODO: origin: entry settings object.origin, + forceOriginHeaderFlag: false, + sameOriginDataURLFlag: false, + referrer: null, + context: null, + mode: RequestModeEnum[RequestModeEnum["no-cors"]], + credentialsMode: RequestCredentialsEnum[RequestCredentialsEnum.omit], + cacheMode: RequestCacheEnum[RequestCacheEnum.default], + } + } + + // step 3 + request = { + url: request.url, + method: request.method, + headerList: request.headerList, + unsafeRequestFlag: true, + body: request.body, + //TODO: client: entry settings object, + //TODO: origin: entry settings object.origin, + forceOriginHeaderFlag: true, + sameOriginDataURLFlag: true, + //TODO: referrer : request.client, + context: 'fetch', + mode: request.mode, + credentialsMode: request.credentialsMode, + cacheMode: request.cacheMode + } + + // step 4, 5, 6 + var fallbackMode: RequestMode = null; + var fallbackCredentials: RequestCredentials = null; + var fallbackCache: RequestCache = null; + + //TODO: + function parseURL(url: string): string { + return url; + } + // step 7 + if (typeof input === "string") { + // step 7-1 + var parsedURL; + + try { + parsedURL = parseURL(input); + } catch(err) { + // step 7-2 + throw new TypeError(err); + } + + // step 7-3 + request.url = parsedURL; + + // step 7-4, 7-5, 7-6 + fallbackMode = RequestModeEnum[RequestModeEnum.cors]; + fallbackCredentials = RequestCredentialsEnum[RequestCredentialsEnum.omit]; + fallbackCache = RequestCacheEnum[RequestCacheEnum.default]; + } + + // step 8 + var mode = init.mode? init.mode: fallbackMode; + + // step 9 + if (mode !== null) request.mode = mode; + + // step 10 + var credentials = init.credentials? init.credentials: fallbackCredentials; + + // step 11 + if (credentials !== null) request.credentialsMode = credentials; + + // step 12 + var cache = init.cache? init.cache: fallbackCache; + + // step 13 + if (cache !== null) request.cacheMode = cache; + + // step 14 + if (init.method) { + var method = init.method; + + // step 14-1 + if(isForbiddenMethod(method)) { + throw new TypeError("forbidden method " + method); + } + + // step 14-2 + method = method.toUpperCase(); + + // step 14-3 + request.method = method; + } + + // step 15 + var r = this; + r.request = request; + r._headers = new Headers(); + + // step 16 + var headers = r.headers; + + // step 17 + if (init.headers) { + headers = init.headers; + } + + // step 18 + r.request.headerList = []; + + // step 19 + if (r.request.mode === "no-cors") { + // 19-1 + if (!isSimpleMethod(this.request.method)) { + throw new TypeError("not simple method" + method); + } + // 19-2 + r.headers.guard = "request-no-CORS"; + } + + // step 20 + r._headers = headers; + + // step 21 + if (init.body) { + // step 21-1 + var result = extract(init.body); + + // step 21-2 + r.request.body = result.stream; + + // step 21-3 + if (result.contentType !== null) { + var hasContentType = request.headerList.some((header) => { + return header.name === "Content-Type"; + }); + + if (!hasContentType) { + r._headers.append("Content-Type", result.contentType); + } + } + } + + // step 22 + // FIXME implement mime type extract + r.mimeType = null; + } +} + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extract(object: any): any { + // step 1 + var stream = []; + + // step 2 + var contentType = null; + + // step 3 + switch(object.constructor) { + // Blob + case Blob: + stream = object.contents; + + if (object.type) { + contentType = object.type; + } + case BufferSource: + // TODO: stream = copy(object); + case FormData: + // TODO: + case URLSearchParams: + stream = object.list.toString(); + contentType = "application/x-www-form-urlencoded;charset=UTF-8"; + case String: // USVString + // stream = encode(object); + contentType = "text/plain;charset=UTF-8"; + } + + // step 4 + return { stream: stream, contentType: contentType }; +} + + + +// https://fetch.spec.whatwg.org/#response +interface IResponse extends IBody { // Response implements Body; + // static Response error(); + // static Response redirect(USVString url, optional unsigned short status = 302); + + type: ResponseType; // readonly + url: USVString; // readonly + status: number; // readonly + statusText: ByteString; // readonly + headers: Headers; // readonly + // Response clone(); +}; + +// https://fetch.spec.whatwg.org/#responseinit +class ResponseInit { + status: number = 200; + statusText: ByteString = "OK"; + headers: HeadersInit; +}; + +class response { + type: string; + terminationReason: string; + url: string; + status: number; + statusMessage: string; + headerList: Header[]; + body: Body; + cacheState: string; + TLSState: string; + + constructor() { + this.type = "default"; + this.terminationReason = "timeout"; + this.url = null; + this.status = 200; + this.statusMessage = "OK"; + this.headerList = []; + this.body = null; + this.cacheState = "none"; + this.TLSState = "unauthenticated"; + } +} + +// https://fetch.spec.whatwg.org/#response +class Response implements IResponse { + // implements body + _bodyUsed: boolean; + + _type: ResponseType; // readonly + _url: USVString; // readonly + _status: number; // readonly + _statusText: ByteString; // readonly + _headers: Headers; // readonly + + _response: response; + + // https://fetch.spec.whatwg.org/#dom-response + // [Constructor(optional BodyInit body, optional ResponseInit init), Exposed=(Window,Worker)] + constructor(body?: BodyInit, init?: ResponseInit) { + if (init !== undefined) { + // step 1 + if (init.status < 200 && init.status > 599) { + throw new RangeError("status is not in the range 200 to 599"); + } + + // step 2 + if (init.statusText.indexOf("\r") < 0 || init.statusText.indexOf("\n") < 0) { + throw new TypeError("Invalid Reason-Phrase token production"); + } + } + + // step 3 + var r = this; + r._response = new response(); + r._headers = new Headers(); + + if (init !== undefined) { + // step 4 + r._response.status = init.status; + // step 5 + r._response.statusMessage = init.statusText; + } + + // step 6 + if (init.headers) { + // step 6-1 + r._response.headerList = []; + + // step 6-2 + // TODO: implements fill + // r._response.headerList =; + } + + // step 7 + if (body) { + // step 7-1 + var extracted = extract(body); + var stream = extracted.stream; + var contentType = extracted.contentType; + + // step 7-2 + r._response.body = stream; + + // step 7-3 + if (contentType !== null) { + var hasContentType = r._response.headerList.some((header) => { + return header.name === "Content-Type"; + }); + + if (!hasContentType) { + // TODO: append + // r._response.headerList.append(Header("Content-Type", contentType)); + } + } + + // step 8 + // TODO: extracting MIME type + + // step 9 + // TODO: TLS state + + // step 10 + return r; + } + } + + get bodyUsed(): boolean { + return this._bodyUsed; + } + + get type(): ResponseType { + return this._type; + } + + get url(): USVString { + return this._url; + } + + get status(): number { + return this._status; + } + + get statusText(): ByteString { + return this._statusText; + } + + get headers(): Headers { + return this._headers; + } + + arrayBuffer(): Promise { + return null; + } + + blob(): Promise { + return null; + } + + formData(): Promise { + return null; + } + + json(): Promise { + return null; + } + + text(): Promise { + return null; + } +} + + +// https://fetch.spec.whatwg.org/#globalfetch +// Window implements GlobalFetch; +interface Window { + fetch(input: RequestInfo, init?: RequestInit): Promise; +}; + +// WorkerGlobalScope implements GlobalFetch; +this.fetch = function(input: RequestInfo, init?: RequestInit): Promise { + // step 1 + var p = new Promise((resolve, reject) => { + try { + // step 2 + var r = (new Request(input, init)).request; + } catch(e) { + reject(e); + } + }); + + // step 4 + return p +} + +// WorkerGlobalScope implements GlobalFetch; diff --git a/fetch/headers.ts b/fetch/headers.ts new file mode 100644 index 000000000..33e3d2bf6 --- /dev/null +++ b/fetch/headers.ts @@ -0,0 +1,341 @@ +/// + +// https://fetch.spec.whatwg.org/#forbidden-header-name +var ForbiddenHeaderName = { + "Accept-Charset": "Accept-Charset", + "Accept-Encoding": "Accept-Encoding", + "Access-Control-Request-Headers": "Access-Control-Request-Headers", + "Access-Control-Request-Method": "Access-Control-Request-Method", + "Connection": "Connection", + "Content-Length": "Content-Length", + "Cookie": "Cookie", + "Cookie2": "Cookie2", + "Date": "Date", + "DNT": "DNT", + "Expect": "Expect", + "Host": "Host", + "Keep-Alive": "Keep-Alive", + "Origin": "Origin", + "Referer": "Referer", + "TE": "TE", + "Trailer": "Trailer", + "Transfer-Encoding": "Transfer-Encoding", + "Upgrade": "Upgrade", + "User-Agent": "User-Agent", + "Via": "Via" +}; +function isForbiddenHeaderName(name: ByteString): boolean { + if (ForbiddenHeaderName[name] !== undefined) { + return true; + } + var reg = /^(Proxy\-|Sec\-)/ + if (reg.exec(name)) { + return true; + } + + return false; +} + +// https://fetch.spec.whatwg.org/#forbidden-response-header-name +var ForbiddenResponseHeaderName = { + "Set-Cookie": "Set-Cookie", + "Set-Cookie2": "Set-Cookie2" +} +function isForbiddenResponseHeaderName(name: ByteString): boolean { + return ForbiddenResponseHeaderName[name] !== undefined; +} + +// https://fetch.spec.whatwg.org/#simple-header +var SimpleHeaderName = { + "Accept": "Accept", + "Accept-Language": "Accept-Language", + "Content-Language": "Content-Language" +} +var SimpleHeaderValue = { + "application/x-www-form-urlencoded": "application/x-www-form-urlencoded", + "multipart/form-data": "multipart/form-data", + "text/plain": "text/plain" +} +function isSimpleHeader(name, value: ByteString): boolean { + if (SimpleHeaderName[name] !== undefined) { + return true; + } + + if (name === "Content-Type") { + // TODO: parse value https://fetch.spec.whatwg.org/#concept-header-parse + if (SimpleHeaderValue[value] !== undefined) { + return true; + } + } + + return false; +} + +// https://fetch.spec.whatwg.org/#headersinit +// typedef (Headers or sequence> or OpenEndedDictionary) HeadersInit; +type HeadersInit = Headers | ByteString[][] | OpenEndedDictionary; + +// https://fetch.spec.whatwg.org/#headers +interface IHeaders { + append(name, value: ByteString): void; + delete(name: ByteString): void; + get(name: ByteString): ByteString; + getAll(name: ByteString): ByteString[]; + has(name: ByteString): boolean; + set(name, value: ByteString): void; + // iterable; +}; + +// https://fetch.spec.whatwg.org/#concept-header +class Header { + name: ByteString; + value: ByteString; + constructor(name, value: ByteString) { + // TODO: validation + this.name = name; + this.value = value; + } +} + +var Guard = { + "immutable": "immutable", + "request": "request", + "request-no-CORS": "request-no-CORS", + "response": "response", + "none": "none", +} + +function copy(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +// https://fetch.spec.whatwg.org/#headers +class Headers implements IHeaders{ + public headerList: Header[] = []; + public guard: string = Guard.none; + + // https://fetch.spec.whatwg.org/#dom-headers + constructor(init?: HeadersInit) { + // step 1 + var headers = this; // new Headers object + + // step 2 + if (init !== undefined) { + this.fill(headers, init); + } + + // step 3 + return headers; + } + + // https://fetch.spec.whatwg.org/#concept-headers-fill + private fill(headers: Headers, object: HeadersInit) { + // step 1 Headers + if (object instanceof Headers) { + var headerListCopy: Header[] = copy(object.headerList); + headerListCopy.forEach((header: Header) => { + headers.append(header.name, header.value); + }); + return; + } + + // step 2 ByteString[][] + if (Array.isArray(object)) { + var headerSequence = object; + headerSequence.forEach((header) => { + if(header.length !== 2) { + throw new TypeError("init for Headers was incorrect BytesString Sequence"); + } + headers.append(header[0], header[1]); + }); + return; + } + + // step 3 OpenEndedDictionary + if (typeof object === "object") { + Object.keys(object).forEach((key) => { + headers.append(key.toString(), object[key]); + }); + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append(name, value: ByteString): void { + // https://fetch.spec.whatwg.org/#concept-headers-append + // step 1 + if (!name || !value) { + // TODO name/value validation + throw new TypeError("invalid name/value"); + } + + // step 2, 3, 4, 5 + switch(this.guard) { + case Guard.immutable: + throw new TypeError("operation to immutable headers"); + case Guard.request: + if (isForbiddenHeaderName(name)) { + return; + } + case Guard["request-no-CORS"]: + if (!isSimpleHeader(name, value)) { + return; + } + case Guard.response: + if (isForbiddenResponseHeaderName(name)) { + return; + } + } + + // step 6 + name = name.toLowerCase(); + this.headerList.push(new Header(name, value)); + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete(name: ByteString): void { + // step 1 + if (!name) { + throw new TypeError("invalid name"); + } + + // step 2, 3, 4, 5 + switch(this.guard) { + case Guard.immutable: + throw new TypeError("operation to immutable headers"); + case Guard.request: + if (isForbiddenHeaderName(name)) { + return; + } + case Guard["request-no-CORS"]: + if (!isSimpleHeader(name, "invalid")) { + return; + } + case Guard.response: + if (isForbiddenResponseHeaderName(name)) { + return; + } + } + + name = name.toLowerCase(); + // step 6 + this.headerList = this.headerList.filter((header: Header) => { + return header.name !== name; + }); + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get(name: ByteString) :ByteString { + // step 1 + if (!name) { + throw new TypeError("invalid name"); + } + + // step 2 + var value: ByteString = null; + this.headerList.forEach((header: Header) => { + if (header.name === name) { + value = header.value; + return; + } + }); + + return value; + } + + // https://fetch.spec.whatwg.org/#dom-headers-getall + getAll(name: ByteString) :ByteString[] { + // step 1 + if (!name) { + throw new TypeError("invalid name"); + } + + // step 2 + var result: ByteString[] = this.headerList.reduce((acc: ByteString[], header: Header) => { + if (header.name === name) { + acc.push(header.value); + } + return acc; + }, []); + + return result; + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has(name: ByteString) :boolean { + // step 1 + if (!name) { + throw new TypeError("invalid name"); + } + + // step 2 + return this.headerList.some((header: Header) => { + return header.name === name; + }); + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set(name, value: ByteString): void { + // step 1 + if (!name || !value) { + throw new TypeError("invalid name/value"); + } + + switch(this.guard) { + // step 2 + case Guard.immutable: + throw new TypeError("operation to immutable headers"); + // step 3 + case Guard.request: + if (isForbiddenHeaderName(name)) { + return; + } + // step 4 + case Guard["request-no-CORS"]: + if (!isSimpleHeader(name, "invalid")) { + return; + } + // step 5 + case Guard.response: + if (isForbiddenResponseHeaderName(name)) { + return; + } + } + + // step 6 + // see https://fetch.spec.whatwg.org/#concept-header-list-set + + // step 6-1 + name = name.toLowerCase(); + + // find the all indexes of headers whos key is supplyed key + var indexes: number[] = this.headerList.reduce((acc: number[], header: Header, index: number) => { + if (header.name === name) { + acc.push(index); + } + return acc; + }, []); + + // count of existing headers + var len = indexes.length; + + // step 6-3 + // if there are no key + if (len === 0) { + // append to last and return + return this.append(name, value); + } + + // step 6-2 + // remove the headers in indexes from the last(because splice chenges index) + // and change first header value + indexes.reverse().forEach((e, i: number) => { + if(i === len - 1) { + // only replace first entry + this.headerList[e].value = value; + } else { + // remove duplicate from last + this.headerList.splice(e, 1); + } + }); + } +} diff --git a/fetch/package.json b/fetch/package.json new file mode 100644 index 000000000..f19e3419b --- /dev/null +++ b/fetch/package.json @@ -0,0 +1,28 @@ +{ + "name": "fetch-standard", + "author": "Jxck", + "license": "MIT", + "version": "0.0.0", + "description": "implementaion of https://fetch.spec.whatwg.org/", + "homepage": "https://github.com/Jxck/fetch", + "bugs": { + "url": "https://github.com/Jxck/fetch/issues" + }, + "keywords": [ + "fetch", + "whatwg" + ], + "repository": { + "type": "git", + "url": "https://github.com/Jxck/fetch" + }, + "main": "fetch.ts", + "scripts": { + "clean": "rm *.js", + "build": "tsc fetch.ts --target ES5", + "test": "npm run build && node fetch.js" + }, + "devDependencies": { + "typescript": "^1.4.1" + } +} diff --git a/fetch/webidl.d.ts b/fetch/webidl.d.ts new file mode 100644 index 000000000..62bae8bca --- /dev/null +++ b/fetch/webidl.d.ts @@ -0,0 +1,14 @@ +// http://heycam.github.io/webidl/#idl-ByteString +declare type ByteString = string; + +// http://heycam.github.io/webidl/#idl-USVString +declare type USVString = string; + +// http://heycam.github.io/webidl/#idl-DOMString +declare type DOMString = string; + +// see: https://fetch.spec.whatwg.org/#headersinit +declare type OpenEndedDictionary = Object; + +declare class FormData { } +declare class Blob { } \ No newline at end of file From 417f0d472117a181263677d0711ce428b87169de Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 10 Jul 2015 13:43:58 +0300 Subject: [PATCH 02/10] FormData & Blog moved to declarations --- declarations.d.ts | 28 ++++++++++++++++++++++++++++ fetch/webidl.d.ts | 5 +---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index 91eb4ca7d..1e459b3a8 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -28,6 +28,34 @@ declare var console: Console; declare var global; declare var require; +//XMLHttpRequest-related +interface FormData { + append(name: any, value: any, blobName?: string): void; +} + +declare var FormData: { + prototype: FormData; + new (): FormData; +} + +interface Blob { + size: number; + type: string; + msClose(): void; + msDetachStream(): any; + slice(start?: number, end?: number, contentType?: string): Blob; +} + +declare var Blob: { + prototype: Blob; + new (blobParts?: any[], options?: BlobPropertyBag): Blob; +} + +interface BlobPropertyBag { + type?: string; + endings?: string; +} + // Global functions declare function Deprecated(target: Object, key?: string | symbol, value?: any): void; diff --git a/fetch/webidl.d.ts b/fetch/webidl.d.ts index 62bae8bca..eab04ca20 100644 --- a/fetch/webidl.d.ts +++ b/fetch/webidl.d.ts @@ -8,7 +8,4 @@ declare type USVString = string; declare type DOMString = string; // see: https://fetch.spec.whatwg.org/#headersinit -declare type OpenEndedDictionary = Object; - -declare class FormData { } -declare class Blob { } \ No newline at end of file +declare type OpenEndedDictionary = Object; \ No newline at end of file From ff4f9d49c81087ae4d9bc21bd3318a1ac4c933d0 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 10 Jul 2015 15:34:39 +0300 Subject: [PATCH 03/10] duplicates removed --- declarations.d.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index 1e459b3a8..91eb4ca7d 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -28,34 +28,6 @@ declare var console: Console; declare var global; declare var require; -//XMLHttpRequest-related -interface FormData { - append(name: any, value: any, blobName?: string): void; -} - -declare var FormData: { - prototype: FormData; - new (): FormData; -} - -interface Blob { - size: number; - type: string; - msClose(): void; - msDetachStream(): any; - slice(start?: number, end?: number, contentType?: string): Blob; -} - -declare var Blob: { - prototype: Blob; - new (blobParts?: any[], options?: BlobPropertyBag): Blob; -} - -interface BlobPropertyBag { - type?: string; - endings?: string; -} - // Global functions declare function Deprecated(target: Object, key?: string | symbol, value?: any): void; From 273a1ad31fa26e42d391463d5b33c16755c7e795 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 10 Jul 2015 16:47:54 +0300 Subject: [PATCH 04/10] replace current fetch with github fetch --- CrossPlatformModules.csproj | 6 +- fetch/body.ts | 100 ------ fetch/fetch.d.ts | 87 +++++ fetch/fetch.ts | 677 ------------------------------------ fetch/headers.ts | 341 ------------------ fetch/package.json | 30 +- fetch/webidl.d.ts | 11 - gruntfile.js | 1 + 8 files changed, 92 insertions(+), 1161 deletions(-) delete mode 100644 fetch/body.ts create mode 100644 fetch/fetch.d.ts delete mode 100644 fetch/fetch.ts delete mode 100644 fetch/headers.ts delete mode 100644 fetch/webidl.d.ts diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index 52a85dee5..bf4f8b10b 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -288,10 +288,7 @@ - - - - + file-name-resolver.d.ts @@ -813,6 +810,7 @@ + PreserveNewest diff --git a/fetch/body.ts b/fetch/body.ts deleted file mode 100644 index 1070006b5..000000000 --- a/fetch/body.ts +++ /dev/null @@ -1,100 +0,0 @@ -// https://fetch.spec.whatwg.org/#json -type object = JSON; - -type body = Object; // byte stream - -// https://fetch.spec.whatwg.org/#bodyinit -type BodyInit = Blob | BufferSource | FormData | URLSearchParams | USVString - -// https://fetch.spec.whatwg.org/#body -interface IBody { - // readonly property - bodyUsed: boolean; - - // method - arrayBuffer(): Promise; - blob(): Promise; - formData(): Promise; - json(): Promise; - text(): Promise; -}; - -// https://fetch.spec.whatwg.org/#body -class Body implements IBody { - private _bodyUsed: boolean; - private _body: body; - private _usedFlag: boolean; - private _mimeType: string; - - get bodyUsed(): boolean { - return this._bodyUsed; - } - - get body(): body { - return this._body; - } - - get usedFlag(): boolean { - return this._usedFlag; - } - - get mimeType(): string { - return this._mimeType; - } - - consumeBody(_type: string): any { - // step 1 - var p = new Promise((resolve, reject) => { - // step 2 - if (this._bodyUsed == true) { - return reject(new TypeError("body was already used")); - } - - // step 3 - this._bodyUsed = true; - - // step 3-1 - var stream = this._body; - - // step 3-2 - if (stream == null) { - stream = []; - } - - // step 3-3 - // TODO: Let bytes be the result of reading from stream until it returns end-of-stream. - var bytes; - - // step 4 - // TODO: implement me - switch(_type) { - case "ArrayBuffer": - case "Blob": - case "FormData": - case "JSON": - case "text": - } - }); - return p; - } - - arrayBuffer(): Promise { - return this.consumeBody("ArrayBuffer"); - } - - blob(): Promise { - return this.consumeBody("Blob"); - } - - formData(): Promise { - return this.consumeBody("FormData"); - } - - json(): Promise { - return this.consumeBody("JSON"); - } - - text(): Promise { - return this.consumeBody("text"); - } -} diff --git a/fetch/fetch.d.ts b/fetch/fetch.d.ts new file mode 100644 index 000000000..8be44d6c1 --- /dev/null +++ b/fetch/fetch.d.ts @@ -0,0 +1,87 @@ +// Type definitions for fetch API +// Project: https://github.com/github/fetch +// Definitions by: Ryan Graham +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare module "fetch" { + + class Request { + constructor(input: string|Request, init?: RequestInit); + method: string; + url: string; + headers: Headers; + context: RequestContext; + referrer: string; + mode: RequestMode; + credentials: RequestCredentials; + cache: RequestCache; + } + + interface RequestInit { + method?: string; + headers?: HeaderInit|{ [index: string]: string }; + body?: BodyInit; + mode?: RequestMode; + credentials?: RequestCredentials; + cache?: RequestCache; + } + + enum RequestContext { + "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", + "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", + "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", + "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", + "xmlhttprequest", "xslt" + } + + enum RequestMode { "same-origin", "no-cors", "cors" } + enum RequestCredentials { "omit", "same-origin", "include" } + enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } + + class Headers { + append(name: string, value: string): void; + delete(name: string): void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; + } + + class Body { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + text(): Promise; + } + + class Response extends Body { + constructor(body?: BodyInit, init?: ResponseInit); + error(): Response; + redirect(url: string, status: number): Response; + type: ResponseType; + url: string; + status: number; + ok: boolean; + statusText: string; + headers: Headers; + clone(): Response; + } + + enum ResponseType { "basic", "cors", "default", "error", "opaque" } + + class ResponseInit { + status: number; + statusText: string; + headers: HeaderInit; + } + + type HeaderInit = Headers|Array; + type BodyInit = Blob|FormData|string; + type RequestInfo = Request|string; + + function fetch(url: string, init?: RequestInit): Promise; +} \ No newline at end of file diff --git a/fetch/fetch.ts b/fetch/fetch.ts deleted file mode 100644 index c2dbc6f75..000000000 --- a/fetch/fetch.ts +++ /dev/null @@ -1,677 +0,0 @@ -/// -/// -/// - -// http://heycam.github.io/webidl/#common-BufferSource -class BufferSource { -} - -// https://url.spec.whatwg.org/#urlsearchparams -class URLSearchParams { -} - -// https://fetch.spec.whatwg.org/#concept-method -enum MethodEnum { - OPTIONS, - GET, - HEAD, - POST, - PUT, - DELETE, - TRACE, - CONNECT, -} - -// https://fetch.spec.whatwg.org/#simple-method -enum SimpleMethodEnum { - GET, - HEAD, - POST -} - -function isSimpleMethod(method: ByteString): boolean { - if (SimpleMethodEnum[method] !== undefined) { - return true; - } - return false; -} - -// https://fetch.spec.whatwg.org/#forbidden-method -enum ForbiddenMethodEnum { - CONNECT, - TRACE, - TRACK -} - -function isForbiddenMethod(method: ByteString): boolean { - if (ForbiddenMethodEnum[method] !== undefined) { - return true; - } - return false -} - -// https://fetch.spec.whatwg.org/#requestcontext -type RequestContext = string; -enum RequestContextEnum { - "audio", "beacon", - "cspreport", "download", - "embed", "eventsource", - "favicon", "fetch", - "font", "form", - "frame", "hyperlink", - "iframe", "image", - "imageset", "import", - "internal", "location", - "manifest", "object", - "ping", "plugin", - "prefetch", "script", - "serviceworker", "sharedworker", - "subresource", "style", - "track", "video", - "worker", "xmlhttprequest", - "xslt" -}; - -// https://fetch.spec.whatwg.org/#concept-request-context-frame-type -enum ContextFrameTypeEnum { - "auxiliary", - "top-level", - "nested", - "none" -} - -// https://fetch.spec.whatwg.org/#concept-request-mode -type RequestMode = string; -enum RequestModeEnum { - "same-origin", - "no-cors", - "cors" -}; - -// https://fetch.spec.whatwg.org/#concept-request-credentials-mode -type RequestCredentials = string; -enum RequestCredentialsEnum { - "omit", - "same-origin", - "include" -}; - -// https://fetch.spec.whatwg.org/#concept-request-cache-mode -type RequestCache = string; -enum RequestCacheEnum { - "default", - "bypass", - "reload", - "revalidate", - "force-cache", - "offline" -}; - -// https://fetch.spec.whatwg.org/#concept-response-type -type ResponseType = string; -enum ResponseTypeEnum { - "basic", - "cors", - "default", - "error", - "opaque" -}; - -///////////////////////////// -/// Request -///////////////////////////// - -// https://fetch.spec.whatwg.org/#requestinfo -type RequestInfo = Request | USVString; - -// https://fetch.spec.whatwg.org/#request -interface IRequest extends IBody { - // readonly property - method: ByteString; - url: USVString; - headers: Headers; - context: RequestContext; - referrer: DOMString; - mode: RequestMode; - credentials: RequestCredentials; - cache: RequestCache; - - // method - clone(): IRequest; -}; - -// https://fetch.spec.whatwg.org/#requestinit -// dictionary RequestInit -interface RequestInit { - method: ByteString; - headers: HeadersInit; - body: BodyInit; - mode: RequestMode; - credentials: RequestCredentials; - cache: RequestCache; -}; - -type Client = Object; -type Referrer = Object; -type Context = Object; - -type request = { - method: string; - url: string; - headerList: Header[]; - unsafeRequestFlag: boolean; - body: body; - //TODO: client: Client; - context: Context; - //TODO: origin: string; - forceOriginHeaderFlag: boolean; - sameOriginDataURLFlag: boolean; - //TODO: referrer: Referrer; - mode: string; - credentialsMode: string; - cacheMode: string; -} - -class Request implements IRequest { - // readonly property on IRequest - private _method: ByteString; - private _url: USVString; - private _headers: Headers; - private _context: RequestContext; - private _referrer: DOMString; - private _mode: ByteString; - private _credentials: RequestCredentials; - private _cache: RequestCache; - - // readonly property on IBody - private _bodyUsed: boolean; - - // https://fetch.spec.whatwg.org/#dom-request-method - get method(): ByteString { - return this._method; - } - - // https://fetch.spec.whatwg.org/#dom-request-url - get url(): USVString { - return this._url; - } - - // https://fetch.spec.whatwg.org/#dom-request-headers - get headers(): Headers { - return this._headers; - } - - // https://fetch.spec.whatwg.org/#dom-request-context - get context(): RequestContext { - return this._context; - } - - // https://fetch.spec.whatwg.org/#dom-request-referrer - get referrer(): DOMString { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-request-mode - get mode(): RequestMode { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-request-credentials - get credentials(): RequestCredentials { - return this._credentials; - } - - // https://fetch.spec.whatwg.org/#dom-request-cache - get cache(): RequestCache { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-bodyused - get bodyUsed(): boolean { - return this._bodyUsed; - } - - // https://fetch.spec.whatwg.org/#dom-request-clone - // method on IRequest - public clone(): IRequest { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-arraybuffer - // method on IBody - public arrayBuffer(): Promise { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-blob - public blob(): Promise { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-formdata - public formData(): Promise { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-json - public json(): Promise { - return null; - } - - // https://fetch.spec.whatwg.org/#dom-body-text - public text(): Promise { - return null; - } - - // Request - public request: request; - public body: body; - public usedFlag: boolean; - public mimeType: string; - - // https://fetch.spec.whatwg.org/#dom-request - constructor(input: RequestInfo, init?: RequestInit) { - // can't detect class by instanceof - // if (input instanceof Request) { } - - var request: request; - // step 1 - if (typeof input === "object" && input.body !== null) { // Request - // step 1-1 - if (input.usedFlag) { - throw new TypeError("Request already used"); - } - // step 1-2 - input.usedFlag = true; - - // step 2 - request = input.request; - } else { - // step 2 - // new request otherwise - request = { - url: null, - method: MethodEnum[MethodEnum.GET], - headerList: [], - unsafeRequestFlag: false, - body: null, - //TODO: client: entry settings object, - //TODO: origin: entry settings object.origin, - forceOriginHeaderFlag: false, - sameOriginDataURLFlag: false, - referrer: null, - context: null, - mode: RequestModeEnum[RequestModeEnum["no-cors"]], - credentialsMode: RequestCredentialsEnum[RequestCredentialsEnum.omit], - cacheMode: RequestCacheEnum[RequestCacheEnum.default], - } - } - - // step 3 - request = { - url: request.url, - method: request.method, - headerList: request.headerList, - unsafeRequestFlag: true, - body: request.body, - //TODO: client: entry settings object, - //TODO: origin: entry settings object.origin, - forceOriginHeaderFlag: true, - sameOriginDataURLFlag: true, - //TODO: referrer : request.client, - context: 'fetch', - mode: request.mode, - credentialsMode: request.credentialsMode, - cacheMode: request.cacheMode - } - - // step 4, 5, 6 - var fallbackMode: RequestMode = null; - var fallbackCredentials: RequestCredentials = null; - var fallbackCache: RequestCache = null; - - //TODO: - function parseURL(url: string): string { - return url; - } - // step 7 - if (typeof input === "string") { - // step 7-1 - var parsedURL; - - try { - parsedURL = parseURL(input); - } catch(err) { - // step 7-2 - throw new TypeError(err); - } - - // step 7-3 - request.url = parsedURL; - - // step 7-4, 7-5, 7-6 - fallbackMode = RequestModeEnum[RequestModeEnum.cors]; - fallbackCredentials = RequestCredentialsEnum[RequestCredentialsEnum.omit]; - fallbackCache = RequestCacheEnum[RequestCacheEnum.default]; - } - - // step 8 - var mode = init.mode? init.mode: fallbackMode; - - // step 9 - if (mode !== null) request.mode = mode; - - // step 10 - var credentials = init.credentials? init.credentials: fallbackCredentials; - - // step 11 - if (credentials !== null) request.credentialsMode = credentials; - - // step 12 - var cache = init.cache? init.cache: fallbackCache; - - // step 13 - if (cache !== null) request.cacheMode = cache; - - // step 14 - if (init.method) { - var method = init.method; - - // step 14-1 - if(isForbiddenMethod(method)) { - throw new TypeError("forbidden method " + method); - } - - // step 14-2 - method = method.toUpperCase(); - - // step 14-3 - request.method = method; - } - - // step 15 - var r = this; - r.request = request; - r._headers = new Headers(); - - // step 16 - var headers = r.headers; - - // step 17 - if (init.headers) { - headers = init.headers; - } - - // step 18 - r.request.headerList = []; - - // step 19 - if (r.request.mode === "no-cors") { - // 19-1 - if (!isSimpleMethod(this.request.method)) { - throw new TypeError("not simple method" + method); - } - // 19-2 - r.headers.guard = "request-no-CORS"; - } - - // step 20 - r._headers = headers; - - // step 21 - if (init.body) { - // step 21-1 - var result = extract(init.body); - - // step 21-2 - r.request.body = result.stream; - - // step 21-3 - if (result.contentType !== null) { - var hasContentType = request.headerList.some((header) => { - return header.name === "Content-Type"; - }); - - if (!hasContentType) { - r._headers.append("Content-Type", result.contentType); - } - } - } - - // step 22 - // FIXME implement mime type extract - r.mimeType = null; - } -} - -// https://fetch.spec.whatwg.org/#concept-bodyinit-extract -function extract(object: any): any { - // step 1 - var stream = []; - - // step 2 - var contentType = null; - - // step 3 - switch(object.constructor) { - // Blob - case Blob: - stream = object.contents; - - if (object.type) { - contentType = object.type; - } - case BufferSource: - // TODO: stream = copy(object); - case FormData: - // TODO: - case URLSearchParams: - stream = object.list.toString(); - contentType = "application/x-www-form-urlencoded;charset=UTF-8"; - case String: // USVString - // stream = encode(object); - contentType = "text/plain;charset=UTF-8"; - } - - // step 4 - return { stream: stream, contentType: contentType }; -} - - - -// https://fetch.spec.whatwg.org/#response -interface IResponse extends IBody { // Response implements Body; - // static Response error(); - // static Response redirect(USVString url, optional unsigned short status = 302); - - type: ResponseType; // readonly - url: USVString; // readonly - status: number; // readonly - statusText: ByteString; // readonly - headers: Headers; // readonly - // Response clone(); -}; - -// https://fetch.spec.whatwg.org/#responseinit -class ResponseInit { - status: number = 200; - statusText: ByteString = "OK"; - headers: HeadersInit; -}; - -class response { - type: string; - terminationReason: string; - url: string; - status: number; - statusMessage: string; - headerList: Header[]; - body: Body; - cacheState: string; - TLSState: string; - - constructor() { - this.type = "default"; - this.terminationReason = "timeout"; - this.url = null; - this.status = 200; - this.statusMessage = "OK"; - this.headerList = []; - this.body = null; - this.cacheState = "none"; - this.TLSState = "unauthenticated"; - } -} - -// https://fetch.spec.whatwg.org/#response -class Response implements IResponse { - // implements body - _bodyUsed: boolean; - - _type: ResponseType; // readonly - _url: USVString; // readonly - _status: number; // readonly - _statusText: ByteString; // readonly - _headers: Headers; // readonly - - _response: response; - - // https://fetch.spec.whatwg.org/#dom-response - // [Constructor(optional BodyInit body, optional ResponseInit init), Exposed=(Window,Worker)] - constructor(body?: BodyInit, init?: ResponseInit) { - if (init !== undefined) { - // step 1 - if (init.status < 200 && init.status > 599) { - throw new RangeError("status is not in the range 200 to 599"); - } - - // step 2 - if (init.statusText.indexOf("\r") < 0 || init.statusText.indexOf("\n") < 0) { - throw new TypeError("Invalid Reason-Phrase token production"); - } - } - - // step 3 - var r = this; - r._response = new response(); - r._headers = new Headers(); - - if (init !== undefined) { - // step 4 - r._response.status = init.status; - // step 5 - r._response.statusMessage = init.statusText; - } - - // step 6 - if (init.headers) { - // step 6-1 - r._response.headerList = []; - - // step 6-2 - // TODO: implements fill - // r._response.headerList =; - } - - // step 7 - if (body) { - // step 7-1 - var extracted = extract(body); - var stream = extracted.stream; - var contentType = extracted.contentType; - - // step 7-2 - r._response.body = stream; - - // step 7-3 - if (contentType !== null) { - var hasContentType = r._response.headerList.some((header) => { - return header.name === "Content-Type"; - }); - - if (!hasContentType) { - // TODO: append - // r._response.headerList.append(Header("Content-Type", contentType)); - } - } - - // step 8 - // TODO: extracting MIME type - - // step 9 - // TODO: TLS state - - // step 10 - return r; - } - } - - get bodyUsed(): boolean { - return this._bodyUsed; - } - - get type(): ResponseType { - return this._type; - } - - get url(): USVString { - return this._url; - } - - get status(): number { - return this._status; - } - - get statusText(): ByteString { - return this._statusText; - } - - get headers(): Headers { - return this._headers; - } - - arrayBuffer(): Promise { - return null; - } - - blob(): Promise { - return null; - } - - formData(): Promise { - return null; - } - - json(): Promise { - return null; - } - - text(): Promise { - return null; - } -} - - -// https://fetch.spec.whatwg.org/#globalfetch -// Window implements GlobalFetch; -interface Window { - fetch(input: RequestInfo, init?: RequestInit): Promise; -}; - -// WorkerGlobalScope implements GlobalFetch; -this.fetch = function(input: RequestInfo, init?: RequestInit): Promise { - // step 1 - var p = new Promise((resolve, reject) => { - try { - // step 2 - var r = (new Request(input, init)).request; - } catch(e) { - reject(e); - } - }); - - // step 4 - return p -} - -// WorkerGlobalScope implements GlobalFetch; diff --git a/fetch/headers.ts b/fetch/headers.ts deleted file mode 100644 index 33e3d2bf6..000000000 --- a/fetch/headers.ts +++ /dev/null @@ -1,341 +0,0 @@ -/// - -// https://fetch.spec.whatwg.org/#forbidden-header-name -var ForbiddenHeaderName = { - "Accept-Charset": "Accept-Charset", - "Accept-Encoding": "Accept-Encoding", - "Access-Control-Request-Headers": "Access-Control-Request-Headers", - "Access-Control-Request-Method": "Access-Control-Request-Method", - "Connection": "Connection", - "Content-Length": "Content-Length", - "Cookie": "Cookie", - "Cookie2": "Cookie2", - "Date": "Date", - "DNT": "DNT", - "Expect": "Expect", - "Host": "Host", - "Keep-Alive": "Keep-Alive", - "Origin": "Origin", - "Referer": "Referer", - "TE": "TE", - "Trailer": "Trailer", - "Transfer-Encoding": "Transfer-Encoding", - "Upgrade": "Upgrade", - "User-Agent": "User-Agent", - "Via": "Via" -}; -function isForbiddenHeaderName(name: ByteString): boolean { - if (ForbiddenHeaderName[name] !== undefined) { - return true; - } - var reg = /^(Proxy\-|Sec\-)/ - if (reg.exec(name)) { - return true; - } - - return false; -} - -// https://fetch.spec.whatwg.org/#forbidden-response-header-name -var ForbiddenResponseHeaderName = { - "Set-Cookie": "Set-Cookie", - "Set-Cookie2": "Set-Cookie2" -} -function isForbiddenResponseHeaderName(name: ByteString): boolean { - return ForbiddenResponseHeaderName[name] !== undefined; -} - -// https://fetch.spec.whatwg.org/#simple-header -var SimpleHeaderName = { - "Accept": "Accept", - "Accept-Language": "Accept-Language", - "Content-Language": "Content-Language" -} -var SimpleHeaderValue = { - "application/x-www-form-urlencoded": "application/x-www-form-urlencoded", - "multipart/form-data": "multipart/form-data", - "text/plain": "text/plain" -} -function isSimpleHeader(name, value: ByteString): boolean { - if (SimpleHeaderName[name] !== undefined) { - return true; - } - - if (name === "Content-Type") { - // TODO: parse value https://fetch.spec.whatwg.org/#concept-header-parse - if (SimpleHeaderValue[value] !== undefined) { - return true; - } - } - - return false; -} - -// https://fetch.spec.whatwg.org/#headersinit -// typedef (Headers or sequence> or OpenEndedDictionary) HeadersInit; -type HeadersInit = Headers | ByteString[][] | OpenEndedDictionary; - -// https://fetch.spec.whatwg.org/#headers -interface IHeaders { - append(name, value: ByteString): void; - delete(name: ByteString): void; - get(name: ByteString): ByteString; - getAll(name: ByteString): ByteString[]; - has(name: ByteString): boolean; - set(name, value: ByteString): void; - // iterable; -}; - -// https://fetch.spec.whatwg.org/#concept-header -class Header { - name: ByteString; - value: ByteString; - constructor(name, value: ByteString) { - // TODO: validation - this.name = name; - this.value = value; - } -} - -var Guard = { - "immutable": "immutable", - "request": "request", - "request-no-CORS": "request-no-CORS", - "response": "response", - "none": "none", -} - -function copy(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - -// https://fetch.spec.whatwg.org/#headers -class Headers implements IHeaders{ - public headerList: Header[] = []; - public guard: string = Guard.none; - - // https://fetch.spec.whatwg.org/#dom-headers - constructor(init?: HeadersInit) { - // step 1 - var headers = this; // new Headers object - - // step 2 - if (init !== undefined) { - this.fill(headers, init); - } - - // step 3 - return headers; - } - - // https://fetch.spec.whatwg.org/#concept-headers-fill - private fill(headers: Headers, object: HeadersInit) { - // step 1 Headers - if (object instanceof Headers) { - var headerListCopy: Header[] = copy(object.headerList); - headerListCopy.forEach((header: Header) => { - headers.append(header.name, header.value); - }); - return; - } - - // step 2 ByteString[][] - if (Array.isArray(object)) { - var headerSequence = object; - headerSequence.forEach((header) => { - if(header.length !== 2) { - throw new TypeError("init for Headers was incorrect BytesString Sequence"); - } - headers.append(header[0], header[1]); - }); - return; - } - - // step 3 OpenEndedDictionary - if (typeof object === "object") { - Object.keys(object).forEach((key) => { - headers.append(key.toString(), object[key]); - }); - } - } - - // https://fetch.spec.whatwg.org/#dom-headers-append - append(name, value: ByteString): void { - // https://fetch.spec.whatwg.org/#concept-headers-append - // step 1 - if (!name || !value) { - // TODO name/value validation - throw new TypeError("invalid name/value"); - } - - // step 2, 3, 4, 5 - switch(this.guard) { - case Guard.immutable: - throw new TypeError("operation to immutable headers"); - case Guard.request: - if (isForbiddenHeaderName(name)) { - return; - } - case Guard["request-no-CORS"]: - if (!isSimpleHeader(name, value)) { - return; - } - case Guard.response: - if (isForbiddenResponseHeaderName(name)) { - return; - } - } - - // step 6 - name = name.toLowerCase(); - this.headerList.push(new Header(name, value)); - } - - // https://fetch.spec.whatwg.org/#dom-headers-delete - delete(name: ByteString): void { - // step 1 - if (!name) { - throw new TypeError("invalid name"); - } - - // step 2, 3, 4, 5 - switch(this.guard) { - case Guard.immutable: - throw new TypeError("operation to immutable headers"); - case Guard.request: - if (isForbiddenHeaderName(name)) { - return; - } - case Guard["request-no-CORS"]: - if (!isSimpleHeader(name, "invalid")) { - return; - } - case Guard.response: - if (isForbiddenResponseHeaderName(name)) { - return; - } - } - - name = name.toLowerCase(); - // step 6 - this.headerList = this.headerList.filter((header: Header) => { - return header.name !== name; - }); - } - - // https://fetch.spec.whatwg.org/#dom-headers-get - get(name: ByteString) :ByteString { - // step 1 - if (!name) { - throw new TypeError("invalid name"); - } - - // step 2 - var value: ByteString = null; - this.headerList.forEach((header: Header) => { - if (header.name === name) { - value = header.value; - return; - } - }); - - return value; - } - - // https://fetch.spec.whatwg.org/#dom-headers-getall - getAll(name: ByteString) :ByteString[] { - // step 1 - if (!name) { - throw new TypeError("invalid name"); - } - - // step 2 - var result: ByteString[] = this.headerList.reduce((acc: ByteString[], header: Header) => { - if (header.name === name) { - acc.push(header.value); - } - return acc; - }, []); - - return result; - } - - // https://fetch.spec.whatwg.org/#dom-headers-has - has(name: ByteString) :boolean { - // step 1 - if (!name) { - throw new TypeError("invalid name"); - } - - // step 2 - return this.headerList.some((header: Header) => { - return header.name === name; - }); - } - - // https://fetch.spec.whatwg.org/#dom-headers-set - set(name, value: ByteString): void { - // step 1 - if (!name || !value) { - throw new TypeError("invalid name/value"); - } - - switch(this.guard) { - // step 2 - case Guard.immutable: - throw new TypeError("operation to immutable headers"); - // step 3 - case Guard.request: - if (isForbiddenHeaderName(name)) { - return; - } - // step 4 - case Guard["request-no-CORS"]: - if (!isSimpleHeader(name, "invalid")) { - return; - } - // step 5 - case Guard.response: - if (isForbiddenResponseHeaderName(name)) { - return; - } - } - - // step 6 - // see https://fetch.spec.whatwg.org/#concept-header-list-set - - // step 6-1 - name = name.toLowerCase(); - - // find the all indexes of headers whos key is supplyed key - var indexes: number[] = this.headerList.reduce((acc: number[], header: Header, index: number) => { - if (header.name === name) { - acc.push(index); - } - return acc; - }, []); - - // count of existing headers - var len = indexes.length; - - // step 6-3 - // if there are no key - if (len === 0) { - // append to last and return - return this.append(name, value); - } - - // step 6-2 - // remove the headers in indexes from the last(because splice chenges index) - // and change first header value - indexes.reverse().forEach((e, i: number) => { - if(i === len - 1) { - // only replace first entry - this.headerList[e].value = value; - } else { - // remove duplicate from last - this.headerList.splice(e, 1); - } - }); - } -} diff --git a/fetch/package.json b/fetch/package.json index f19e3419b..3ec180e48 100644 --- a/fetch/package.json +++ b/fetch/package.json @@ -1,28 +1,2 @@ -{ - "name": "fetch-standard", - "author": "Jxck", - "license": "MIT", - "version": "0.0.0", - "description": "implementaion of https://fetch.spec.whatwg.org/", - "homepage": "https://github.com/Jxck/fetch", - "bugs": { - "url": "https://github.com/Jxck/fetch/issues" - }, - "keywords": [ - "fetch", - "whatwg" - ], - "repository": { - "type": "git", - "url": "https://github.com/Jxck/fetch" - }, - "main": "fetch.ts", - "scripts": { - "clean": "rm *.js", - "build": "tsc fetch.ts --target ES5", - "test": "npm run build && node fetch.js" - }, - "devDependencies": { - "typescript": "^1.4.1" - } -} +{ "name" : "fetch", + "main" : "fetch.js" } \ No newline at end of file diff --git a/fetch/webidl.d.ts b/fetch/webidl.d.ts deleted file mode 100644 index eab04ca20..000000000 --- a/fetch/webidl.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// http://heycam.github.io/webidl/#idl-ByteString -declare type ByteString = string; - -// http://heycam.github.io/webidl/#idl-USVString -declare type USVString = string; - -// http://heycam.github.io/webidl/#idl-DOMString -declare type DOMString = string; - -// see: https://fetch.spec.whatwg.org/#headersinit -declare type OpenEndedDictionary = Object; \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 0311ffad2..e1fb7d72d 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -198,6 +198,7 @@ module.exports = function(grunt) { expand: true, src: [ "./js-libs/**/*.js", + "./fetch/**/*.js", ], dest: "<%= localCfg.outModulesDir %>/", cwd: localCfg.srcDir From d8758f98010e961a33353a78fbf7fa72f888cda2 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 10 Jul 2015 17:22:15 +0300 Subject: [PATCH 05/10] fetch.js added --- .gitignore | 1 + fetch/fetch.js | 333 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 fetch/fetch.js diff --git a/.gitignore b/.gitignore index 59fa716a4..0d730935c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ *.js !gruntfile.js !js-libs/**/*.* +!fetch/**/*.* !apps/TelerikNEXT/lib/**/*.* CrossPlatformModules.sln.ide/ *.suo diff --git a/fetch/fetch.js b/fetch/fetch.js new file mode 100644 index 000000000..3b0ec8ce1 --- /dev/null +++ b/fetch/fetch.js @@ -0,0 +1,333 @@ +(function () { + 'use strict'; + + var self = exports; + + if (self.fetch) { + return + } + + function normalizeName(name) { + if (typeof name !== 'string') { + name = name.toString(); + } + if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = value.toString(); + } + return value + } + + function Headers(headers) { + this.map = {} + + if (headers instanceof Headers) { + headers.forEach(function (value, name) { + this.append(name, value) + }, this) + + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function (name) { + this.append(name, headers[name]) + }, this) + } + } + + Headers.prototype.append = function (name, value) { + name = normalizeName(name) + value = normalizeValue(value) + var list = this.map[name] + if (!list) { + list = [] + this.map[name] = list + } + list.push(value) + } + + Headers.prototype['delete'] = function (name) { + delete this.map[normalizeName(name)] + } + + Headers.prototype.get = function (name) { + var values = this.map[normalizeName(name)] + return values ? values[0] : null + } + + Headers.prototype.getAll = function (name) { + return this.map[normalizeName(name)] || [] + } + + Headers.prototype.has = function (name) { + return this.map.hasOwnProperty(normalizeName(name)) + } + + Headers.prototype.set = function (name, value) { + this.map[normalizeName(name)] = [normalizeValue(value)] + } + + Headers.prototype.forEach = function (callback, thisArg) { + Object.getOwnPropertyNames(this.map).forEach(function (name) { + this.map[name].forEach(function (value) { + callback.call(thisArg, value, name, this) + }, this) + }, this) + } + + function consumed(body) { + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true + } + + function fileReaderReady(reader) { + return new Promise(function (resolve, reject) { + reader.onload = function () { + resolve(reader.result) + } + reader.onerror = function () { + reject(reader.error) + } + }) + } + + function readBlobAsArrayBuffer(blob) { + var reader = new FileReader() + reader.readAsArrayBuffer(blob) + return fileReaderReady(reader) + } + + function readBlobAsText(blob) { + var reader = new FileReader() + reader.readAsText(blob) + return fileReaderReady(reader) + } + + var support = { + blob: 'FileReader' in self && 'Blob' in self && (function () { + try { + new Blob(); + return true + } catch (e) { + return false + } + })(), + formData: 'FormData' in self + } + + function Body() { + this.bodyUsed = false + + + this._initBody = function (body) { + this._bodyInit = body + if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (!body) { + this._bodyText = '' + } else { + throw new Error('unsupported BodyInit type') + } + } + + if (support.blob) { + this.blob = function () { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + } + + this.arrayBuffer = function () { + return this.blob().then(readBlobAsArrayBuffer) + } + + this.text = function () { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + } + } else { + this.text = function () { + var rejected = consumed(this) + return rejected ? rejected : Promise.resolve(this._bodyText) + } + } + + if (support.formData) { + this.formData = function () { + return this.text().then(decode) + } + } + + this.json = function () { + return this.text().then(JSON.parse) + } + + return this + } + + // HTTP methods whose capitalization should be normalized + var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + + function normalizeMethod(method) { + var upcased = method.toUpperCase() + return (methods.indexOf(upcased) > -1) ? upcased : method + } + + function Request(url, options) { + options = options || {} + this.url = url + + this.credentials = options.credentials || 'omit' + this.headers = new Headers(options.headers) + this.method = normalizeMethod(options.method || 'GET') + this.mode = options.mode || null + this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(options.body) + } + + function decode(body) { + var form = new FormData() + body.trim().split('&').forEach(function (bytes) { + if (bytes) { + var split = bytes.split('=') + var name = split.shift().replace(/\+/g, ' ') + var value = split.join('=').replace(/\+/g, ' ') + form.append(decodeURIComponent(name), decodeURIComponent(value)) + } + }) + return form + } + + function headers(xhr) { + var head = new Headers() + var pairs = xhr.getAllResponseHeaders().trim().split('\n') + pairs.forEach(function (header) { + var split = header.trim().split(':') + var key = split.shift().trim() + var value = split.join(':').trim() + head.append(key, value) + }) + return head + } + + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this._initBody(bodyInit) + this.type = 'default' + this.url = null + this.status = options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText + this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) + this.url = options.url || '' + } + + Body.call(Response.prototype) + + self.Headers = Headers; + self.Request = Request; + self.Response = Response; + + self.fetch = function (input, init) { + // TODO: Request constructor should accept input, init + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) + } + + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest() + + function responseURL() { + if ('responseURL' in xhr) { + return xhr.responseURL + } + + // Avoid security warnings on getResponseHeader when not allowed by CORS + if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { + return xhr.getResponseHeader('X-Request-URL') + } + + return; + } + + xhr.onload = function () { + var status = (xhr.status === 1223) ? 204 : xhr.status + if (status < 100 || status > 599) { + reject(new TypeError('Network request failed')) + return + } + var options = { + status: status, + statusText: xhr.statusText, + headers: headers(xhr), + url: responseURL() + } + //var body = 'response' in xhr ? xhr.response : xhr.responseText; + resolve(new Response(xhr.responseText, options)) + } + + xhr.onerror = function () { + reject(new TypeError('Network request failed')) + } + + xhr.open(request.method, request.url, true) + + if (request.credentials === 'include') { + xhr.withCredentials = true + } + + if ('responseType' in xhr && support.blob) { + xhr.responseType = 'blob' + } + + request.headers.forEach(function (value, name) { + xhr.setRequestHeader(name, value) + }) + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) + }) + } + self.fetch.polyfill = true + +})(); \ No newline at end of file From fd329798d108ccad3473a9f4994e9efbb0f90612 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Mon, 13 Jul 2015 17:25:55 +0300 Subject: [PATCH 06/10] fetch tests added --- CrossPlatformModules.csproj | 1 + apps/tests/fetch-tests.ts | 273 ++++++++++++++++++++++++++++++++++++ apps/tests/testRunner.ts | 1 + 3 files changed, 275 insertions(+) create mode 100644 apps/tests/fetch-tests.ts diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index bf4f8b10b..454f62a88 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -171,6 +171,7 @@ + diff --git a/apps/tests/fetch-tests.ts b/apps/tests/fetch-tests.ts new file mode 100644 index 000000000..945d06932 --- /dev/null +++ b/apps/tests/fetch-tests.ts @@ -0,0 +1,273 @@ +/* tslint:disable:no-unused-variable */ +import TKUnit = require("./TKUnit"); +import fetchModule = require("fetch"); +import types = require("utils/types"); + +// +// # Fetch module +// Using fetch methods requires to load "fetch" module. +// ``` JavaScript +// var fetch = require("fetch"); +// ``` +// + +export var test_fetch_defined = function () { + TKUnit.assert(types.isDefined((fetchModule.fetch)), "Method fetch() should be defined!"); +}; + +export var test_fetch = function (done: (err: Error, res?: string) => void) { + var result; + + fetchModule.fetch("https://httpbin.org/get").then(function (r) { + TKUnit.assert(r instanceof fetchModule.Response, "Result from fetch() should be valid Response object! Actual result is: " + result); + done(null); + }, function (e) { + done(e); + }); +}; + +export var test_fetch_text = function (done: (err: Error, res?: string) => void) { + var result; + + // + // ### Get string from URL + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(r => { return r.text(); }).then(function (r) { + //// Argument (r) is string! + // + TKUnit.assert(types.isString(r), "Result from text() should be string! Actual result is: " + r); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; + +export var test_fetch_json = function (done: (err: Error, res?: string) => void) { + var result; + + // + // ### Get JSON from URL + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(r => { return r.json(); }).then(function (r) { + //// Argument (r) is JSON object! + // + TKUnit.assert(types.isString(JSON.stringify(r)), "Result from json() should be JSON object! Actual result is: " + r); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; +/* +export var test_fetch_blob = function (done: (err: Error, res?: string) => void) { + var result; + + // + // ### Get Blob from URL + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(r => { return r.blob(); }).then(function (r) { + //// Argument (r) is Blob object! + // + TKUnit.assert(r instanceof Blob, "Result from blob() should be Blob object! Actual result is: " + r); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; + +export var test_fetch_arrayBuffer = function (done: (err: Error, res?: string) => void) { + var result; + + // + // ### Get ArrayBuffer from URL + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(r => { return r.arrayBuffer(); }).then(function (r) { + //// Argument (r) is ArrayBuffer object! + // + TKUnit.assert(r instanceof ArrayBuffer, "Result from arrayBuffer() should be ArrayBuffer object! Actual result is: " + r); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; + +export var test_fetch_formData = function (done: (err: Error, res?: string) => void) { + var result; + + // + // ### Get FormData from URL + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(r => { return r.formData(); }).then(function (r) { + //// Argument (r) is FormData object! + // + TKUnit.assert(r instanceof FormData, "Result from formData() should be FormData object! Actual result is: " + r); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; +*/ +export var test_fetch_fail_invalid_url = function (done) { + var result; + var completed: boolean; + var isReady = function () { return completed; } + + fetchModule.fetch("hgfttp://httpbin.org/get").catch(function (e) { + completed = true; + result = e; + done(null) + }); +}; + +export var test_fetch_response_status = function (done) { + + // + // ### Get response status code + // ``` fetch + fetchModule.fetch("https://httpbin.org/get").then(function (response) { + //// Argument (response) is Response! + var statusCode = response.status; + // + try { + TKUnit.assert(types.isDefined(statusCode), "response.status should be defined! Actual result is: " + statusCode); + done(null); + } + catch (err) { + done(err); + } + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; + +export var test_fetch_response_headers = function (done) { + + // + // ### Get response headers + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/get").then(function (response) { + //// Argument (response) is Response! + // var all = response.headers.getAll(); + // + try { + TKUnit.assert(types.isDefined(response.headers), "response.headers should be defined! Actual result is: " + response.headers); + done(null); + } + catch (err) { + done(err); + } + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; + +export var test_fetch_headers_sent = function (done) { + var result: fetchModule.Headers; + + fetchModule.fetch("https://httpbin.org/get", { + method: "GET", + headers: { "Content-Type": "application/json" } + }).then(function (response) { + result = response.headers; + try { + TKUnit.assert(result.get("Content-Type") === "application/json", "Headers not sent/received properly! Actual result is: " + result); + done(null); + } + catch (err) { + done(err); + } + }, function (e) { + done(e); + }); +}; + +export var test_fetch_post_form_data = function (done) { + fetchModule.fetch("https://httpbin.org/post", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "MyVariableOne=ValueOne&MyVariableTwo=ValueTwo" + }).then(r => { + // return r.formData(); Uncomment this when FormData is available! + return r.json(); + }).then(function (r) { + try { + TKUnit.assert(r.form["MyVariableOne"] === "ValueOne" && r.form["MyVariableTwo"] === "ValueTwo", "Content not sent/received properly! Actual result is: " + r.form); + done(null); + } + catch (err) { + done(err); + } + }, function (e) { + done(e); + }); +}; + +export var test_fetch_post_json = function (done) { + // + // ### Post JSON + // ``` JavaScript + fetchModule.fetch("https://httpbin.org/post", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ MyVariableOne: "ValueOne", MyVariableTwo: "ValueTwo" }) + }).then(r => { return r.json(); }).then(function (r) { + // + try { + TKUnit.assert(r.json["MyVariableOne"] === "ValueOne" && r.json["MyVariableTwo"] === "ValueTwo", "Content not sent/received properly! Actual result is: " + r.json); + done(null); + } + catch (err) { + done(err); + } + // + // console.log(result); + }, function (e) { + // + done(e); + // + // console.log("Error occurred " + e); + }); + // ``` + // +}; \ No newline at end of file diff --git a/apps/tests/testRunner.ts b/apps/tests/testRunner.ts index ce0f61ebc..d38d8f33e 100644 --- a/apps/tests/testRunner.ts +++ b/apps/tests/testRunner.ts @@ -36,6 +36,7 @@ allTests["STYLE-PROPERTIES"] = require("./ui/style/style-properties-tests"); allTests["SCROLL-VIEW"] = require("./ui/scroll-view/scroll-view-tests"); allTests["FILE SYSTEM"] = require("./file-system-tests"); allTests["HTTP"] = require("./http-tests"); +allTests["FETCH"] = require("./fetch-tests"); allTests["APPLICATION SETTINGS"] = require("./application-settings-tests"); allTests["IMAGE SOURCE"] = require("./image-source-tests"); allTests["TIMER"] = require("./timer-tests"); From 6cd62bbf6c2ece02a62766c3509daf8ff49dfda7 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Tue, 14 Jul 2015 10:25:55 +0300 Subject: [PATCH 07/10] tests improved --- apps/tests/fetch-tests.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/tests/fetch-tests.ts b/apps/tests/fetch-tests.ts index 945d06932..2553995db 100644 --- a/apps/tests/fetch-tests.ts +++ b/apps/tests/fetch-tests.ts @@ -17,13 +17,23 @@ export var test_fetch_defined = function () { export var test_fetch = function (done: (err: Error, res?: string) => void) { var result; - + // + // ### Get Response from URL + // ``` JavaScript fetchModule.fetch("https://httpbin.org/get").then(function (r) { + //// Argument (r) is Response! + // TKUnit.assert(r instanceof fetchModule.Response, "Result from fetch() should be valid Response object! Actual result is: " + result); done(null); + // }, function (e) { + //// Argument (e) is Error! + // done(e); + // }); + // ``` + // }; export var test_fetch_text = function (done: (err: Error, res?: string) => void) { @@ -32,7 +42,7 @@ export var test_fetch_text = function (done: (err: Error, res?: string) => void) // // ### Get string from URL // ``` JavaScript - fetchModule.fetch("https://httpbin.org/get").then(r => { return r.text(); }).then(function (r) { + fetchModule.fetch("https://httpbin.org/get").then(response => { return response.text(); }).then(function (r) { //// Argument (r) is string! // TKUnit.assert(types.isString(r), "Result from text() should be string! Actual result is: " + r); @@ -54,7 +64,7 @@ export var test_fetch_json = function (done: (err: Error, res?: string) => void) // // ### Get JSON from URL // ``` JavaScript - fetchModule.fetch("https://httpbin.org/get").then(r => { return r.json(); }).then(function (r) { + fetchModule.fetch("https://httpbin.org/get").then(response => { return response.json(); }).then(function (r) { //// Argument (r) is JSON object! // TKUnit.assert(types.isString(JSON.stringify(r)), "Result from json() should be JSON object! Actual result is: " + r); @@ -76,7 +86,7 @@ export var test_fetch_blob = function (done: (err: Error, res?: string) => void) // // ### Get Blob from URL // ``` JavaScript - fetchModule.fetch("https://httpbin.org/get").then(r => { return r.blob(); }).then(function (r) { + fetchModule.fetch("https://httpbin.org/get").then(response => { return response.blob(); }).then(function (r) { //// Argument (r) is Blob object! // TKUnit.assert(r instanceof Blob, "Result from blob() should be Blob object! Actual result is: " + r); @@ -98,7 +108,7 @@ export var test_fetch_arrayBuffer = function (done: (err: Error, res?: string) = // // ### Get ArrayBuffer from URL // ``` JavaScript - fetchModule.fetch("https://httpbin.org/get").then(r => { return r.arrayBuffer(); }).then(function (r) { + fetchModule.fetch("https://httpbin.org/get").then(response => { return response.arrayBuffer(); }).then(function (r) { //// Argument (r) is ArrayBuffer object! // TKUnit.assert(r instanceof ArrayBuffer, "Result from arrayBuffer() should be ArrayBuffer object! Actual result is: " + r); @@ -120,7 +130,7 @@ export var test_fetch_formData = function (done: (err: Error, res?: string) => v // // ### Get FormData from URL // ``` JavaScript - fetchModule.fetch("https://httpbin.org/get").then(r => { return r.formData(); }).then(function (r) { + fetchModule.fetch("https://httpbin.org/get").then(response => { return response.formData(); }).then(function (r) { //// Argument (r) is FormData object! // TKUnit.assert(r instanceof FormData, "Result from formData() should be FormData object! Actual result is: " + r); @@ -137,13 +147,11 @@ export var test_fetch_formData = function (done: (err: Error, res?: string) => v }; */ export var test_fetch_fail_invalid_url = function (done) { - var result; var completed: boolean; var isReady = function () { return completed; } fetchModule.fetch("hgfttp://httpbin.org/get").catch(function (e) { completed = true; - result = e; done(null) }); }; @@ -151,7 +159,7 @@ export var test_fetch_fail_invalid_url = function (done) { export var test_fetch_response_status = function (done) { // - // ### Get response status code + // ### Get Response status // ``` fetch fetchModule.fetch("https://httpbin.org/get").then(function (response) { //// Argument (response) is Response! From 1ea4a0af0d7021c2483187cd7c8e9b62f6763003 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 17 Jul 2015 09:51:43 +0300 Subject: [PATCH 08/10] unsupported methods temporary commented --- fetch/fetch.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fetch/fetch.d.ts b/fetch/fetch.d.ts index 8be44d6c1..6f6ff076e 100644 --- a/fetch/fetch.d.ts +++ b/fetch/fetch.d.ts @@ -51,9 +51,11 @@ declare module "fetch" { class Body { bodyUsed: boolean; +/* arrayBuffer(): Promise; blob(): Promise; formData(): Promise; +*/ json(): Promise; text(): Promise; } From fc697e5ec2e7ed648b85cd4bc1e945effde11cba Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 17 Jul 2015 10:25:06 +0300 Subject: [PATCH 09/10] lint error fixed --- fetch/fetch.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/fetch/fetch.d.ts b/fetch/fetch.d.ts index 6f6ff076e..67745571e 100644 --- a/fetch/fetch.d.ts +++ b/fetch/fetch.d.ts @@ -85,5 +85,6 @@ declare module "fetch" { type BodyInit = Blob|FormData|string; type RequestInfo = Request|string; + /* tslint:disable */ function fetch(url: string, init?: RequestInit): Promise; } \ No newline at end of file From 053d7ddab3119e48777f9ccc786491c2d6dce701 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 17 Jul 2015 10:35:11 +0300 Subject: [PATCH 10/10] LICENSE file added --- fetch/LICENSE | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fetch/LICENSE diff --git a/fetch/LICENSE b/fetch/LICENSE new file mode 100644 index 000000000..6065c75b7 --- /dev/null +++ b/fetch/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014-2015 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file