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/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index b262c50d3..454f62a88 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -171,6 +171,7 @@ + @@ -288,6 +289,7 @@ + file-name-resolver.d.ts @@ -809,6 +811,7 @@ + PreserveNewest @@ -1670,6 +1673,8 @@ PreserveNewest + + diff --git a/apps/tests/fetch-tests.ts b/apps/tests/fetch-tests.ts new file mode 100644 index 000000000..2553995db --- /dev/null +++ b/apps/tests/fetch-tests.ts @@ -0,0 +1,281 @@ +/* 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; + // + // ### 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) { + var result; + + // + // ### Get string from URL + // ``` JavaScript + 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); + 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(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); + 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(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); + 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(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); + 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(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); + done(null); + // + }, function (e) { + //// Argument (e) is Error! + // + done(e); + // + }); + // ``` + // +}; +*/ +export var test_fetch_fail_invalid_url = function (done) { + var completed: boolean; + var isReady = function () { return completed; } + + fetchModule.fetch("hgfttp://httpbin.org/get").catch(function (e) { + completed = true; + done(null) + }); +}; + +export var test_fetch_response_status = function (done) { + + // + // ### Get Response status + // ``` 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"); 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 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/fetch.d.ts b/fetch/fetch.d.ts new file mode 100644 index 000000000..67745571e --- /dev/null +++ b/fetch/fetch.d.ts @@ -0,0 +1,90 @@ +// 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; + + /* tslint:disable */ + function fetch(url: string, init?: RequestInit): Promise; +} \ No newline at end of file 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 diff --git a/fetch/package.json b/fetch/package.json new file mode 100644 index 000000000..3ec180e48 --- /dev/null +++ b/fetch/package.json @@ -0,0 +1,2 @@ +{ "name" : "fetch", + "main" : "fetch.js" } \ 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