feat(http): refactor library to work in dart

Mostly internal refactoring needed to make ts2dart and DartAnalyzer happy.

Fixes #2415
This commit is contained in:
Jeff Cross 2015-06-19 12:14:12 -07:00
parent fa7da0ca5d
commit 55bf0e554f
26 changed files with 424 additions and 379 deletions

View File

@ -1 +0,0 @@
library angular2.http;

View File

@ -10,20 +10,24 @@ import {Http, HttpFactory} from './src/http/http';
import {XHRBackend, XHRConnection} from 'angular2/src/http/backends/xhr_backend'; import {XHRBackend, XHRConnection} from 'angular2/src/http/backends/xhr_backend';
import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr'; import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr';
import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options'; import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options';
import {ConnectionBackend} from 'angular2/src/http/interfaces';
export {MockConnection, MockBackend} from 'angular2/src/http/backends/mock_backend'; export {MockConnection, MockBackend} from 'angular2/src/http/backends/mock_backend';
export {Request} from 'angular2/src/http/static_request'; export {Request} from 'angular2/src/http/static_request';
export {Response} from 'angular2/src/http/static_response'; export {Response} from 'angular2/src/http/static_response';
export {Http, XHRBackend, XHRConnection, BaseRequestOptions, RequestOptions, HttpFactory};
export { export {
IHttp, IHttp,
IRequestOptions, IRequestOptions,
IRequest,
IResponse, IResponse,
Connection, Connection,
ConnectionBackend ConnectionBackend
} from 'angular2/src/http/interfaces'; } from 'angular2/src/http/interfaces';
export {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options';
export {XHRBackend, XHRConnection} from 'angular2/src/http/backends/xhr_backend';
export {Http, HttpFactory} from './src/http/http';
export {Headers} from 'angular2/src/http/headers'; export {Headers} from 'angular2/src/http/headers';
export * from 'angular2/src/http/enums'; export * from 'angular2/src/http/enums';
@ -47,8 +51,9 @@ export {URLSearchParams} from 'angular2/src/http/url_search_params';
* *
*/ */
export var httpInjectables: List<any> = [ export var httpInjectables: List<any> = [
bind(BrowserXHR) bind(ConnectionBackend)
.toValue(BrowserXHR), .toClass(XHRBackend),
BrowserXHR,
XHRBackend, XHRBackend,
BaseRequestOptions, BaseRequestOptions,
bind(HttpFactory).toFactory(HttpFactory, [XHRBackend, BaseRequestOptions]), bind(HttpFactory).toFactory(HttpFactory, [XHRBackend, BaseRequestOptions]),

View File

@ -11,6 +11,8 @@ class Math {
static double random() => _random.nextDouble(); static double random() => _random.nextDouble();
} }
int ENUM_INDEX(value) => value.index;
class CONST { class CONST {
const CONST(); const CONST();
} }

View File

@ -31,6 +31,11 @@ _global.assert = function assert(condition) {
_global['assert'].call(condition); _global['assert'].call(condition);
} }
}; };
export function ENUM_INDEX(value: int): int {
return value;
}
// This function is needed only to properly support Dart's const expressions // This function is needed only to properly support Dart's const expressions
// see https://github.com/angular/ts2dart/pull/151 for more info // see https://github.com/angular/ts2dart/pull/151 for more info
export function CONST_EXPR<T>(expr: T): T { export function CONST_EXPR<T>(expr: T): T {

View File

@ -1,9 +1,11 @@
library angular2.src.http.backends.browser_xhr; library angular2.src.http.backends.browser_xhr;
/// import 'dart:html' show HttpRequest; import 'dart:html' show HttpRequest;
/// import 'package:angular2/di.dart'; import 'package:angular2/di.dart';
/// @Injectable() @Injectable()
/// class BrowserXHR { class BrowserXHR {
/// factory BrowserXHR() => new HttpRequest(); HttpRequest build() {
/// } return new HttpRequest();
}
}

View File

@ -5,5 +5,6 @@ import {Injectable} from 'angular2/di';
// Make sure not to evaluate this in a non-browser environment! // Make sure not to evaluate this in a non-browser environment!
@Injectable() @Injectable()
export class BrowserXHR { export class BrowserXHR {
constructor() { return <any>(new window.XMLHttpRequest()); } constructor() {}
build(): any { return <any>(new window.XMLHttpRequest()); }
} }

View File

@ -3,7 +3,9 @@ import {Request} from 'angular2/src/http/static_request';
import {Response} from 'angular2/src/http/static_response'; import {Response} from 'angular2/src/http/static_response';
import {ReadyStates} from 'angular2/src/http/enums'; import {ReadyStates} from 'angular2/src/http/enums';
import {Connection, ConnectionBackend} from 'angular2/src/http/interfaces'; import {Connection, ConnectionBackend} from 'angular2/src/http/interfaces';
import * as Rx from 'rx'; import {ObservableWrapper, EventEmitter} from 'angular2/src/facade/async';
import {isPresent} from 'angular2/src/facade/lang';
import {IMPLEMENTS, BaseException} from 'angular2/src/facade/lang';
/** /**
* *
@ -14,7 +16,8 @@ import * as Rx from 'rx';
* {@link MockBackend} in order to mock responses to requests. * {@link MockBackend} in order to mock responses to requests.
* *
**/ **/
export class MockConnection implements Connection { @IMPLEMENTS(Connection)
export class MockConnection {
// TODO Name `readyState` should change to be more generic, and states could be made to be more // TODO Name `readyState` should change to be more generic, and states could be made to be more
// descriptive than XHR states. // descriptive than XHR states.
/** /**
@ -33,18 +36,12 @@ export class MockConnection implements Connection {
* Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md) * Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md)
* of {@link Response}. Can be subscribed to in order to be notified when a response is available. * of {@link Response}. Can be subscribed to in order to be notified when a response is available.
*/ */
response: Rx.Subject<Response>; response: EventEmitter;
constructor(req: Request) { constructor(req: Request) {
if (Rx.hasOwnProperty('default')) { this.response = new EventEmitter();
this.response = new ((<any>Rx).default.Rx.Subject)();
} else {
this.response = new Rx.Subject<Response>();
}
this.readyState = ReadyStates.OPEN; this.readyState = ReadyStates.OPEN;
this.request = req; this.request = req;
this.dispose = this.dispose.bind(this);
} }
/** /**
@ -71,12 +68,12 @@ export class MockConnection implements Connection {
* *
*/ */
mockRespond(res: Response) { mockRespond(res: Response) {
if (this.readyState >= ReadyStates.DONE) { if (this.readyState === ReadyStates.DONE || this.readyState === ReadyStates.CANCELLED) {
throw new Error('Connection has already been resolved'); throw new BaseException('Connection has already been resolved');
} }
this.readyState = ReadyStates.DONE; this.readyState = ReadyStates.DONE;
this.response.onNext(res); ObservableWrapper.callNext(this.response, res);
this.response.onCompleted(); ObservableWrapper.callReturn(this.response);
} }
/** /**
@ -100,8 +97,8 @@ export class MockConnection implements Connection {
mockError(err?) { mockError(err?) {
// Matches XHR semantics // Matches XHR semantics
this.readyState = ReadyStates.DONE; this.readyState = ReadyStates.DONE;
this.response.onError(err); ObservableWrapper.callThrow(this.response, err);
this.response.onCompleted(); ObservableWrapper.callReturn(this.response);
} }
} }
@ -137,7 +134,8 @@ export class MockConnection implements Connection {
* This method only exists in the mock implementation, not in real Backends. * This method only exists in the mock implementation, not in real Backends.
**/ **/
@Injectable() @Injectable()
export class MockBackend implements ConnectionBackend { @IMPLEMENTS(ConnectionBackend)
export class MockBackend {
/** /**
* [RxJS * [RxJS
* Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md) * Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md)
@ -171,7 +169,7 @@ export class MockBackend implements ConnectionBackend {
* *
* This property only exists in the mock implementation, not in real Backends. * This property only exists in the mock implementation, not in real Backends.
*/ */
connections: Rx.Subject<MockConnection>; connections: EventEmitter; //<MockConnection>
/** /**
* An array representation of `connections`. This array will be updated with each connection that * An array representation of `connections`. This array will be updated with each connection that
@ -188,20 +186,14 @@ export class MockBackend implements ConnectionBackend {
* *
* This property only exists in the mock implementation, not in real Backends. * This property only exists in the mock implementation, not in real Backends.
*/ */
pendingConnections: Rx.Observable<MockConnection>; pendingConnections: EventEmitter; //<MockConnection>
constructor() { constructor() {
var Observable;
this.connectionsArray = []; this.connectionsArray = [];
if (Rx.hasOwnProperty('default')) { this.connections = new EventEmitter();
this.connections = new (<any>Rx).default.Rx.Subject(); ObservableWrapper.subscribe(this.connections,
Observable = (<any>Rx).default.Rx.Observable; connection => this.connectionsArray.push(connection));
} else { this.pendingConnections = new EventEmitter();
this.connections = new Rx.Subject<MockConnection>(); // Observable.fromArray(this.connectionsArray).filter((c) => c.readyState < ReadyStates.DONE);
Observable = Rx.Observable;
}
this.connections.subscribe(connection => this.connectionsArray.push(connection));
this.pendingConnections =
Observable.fromArray(this.connectionsArray).filter((c) => c.readyState < ReadyStates.DONE);
} }
/** /**
@ -211,8 +203,8 @@ export class MockBackend implements ConnectionBackend {
*/ */
verifyNoPendingRequests() { verifyNoPendingRequests() {
let pending = 0; let pending = 0;
this.pendingConnections.subscribe((c) => pending++); ObservableWrapper.subscribe(this.pendingConnections, c => pending++);
if (pending > 0) throw new Error(`${pending} pending connections to be resolved`); if (pending > 0) throw new BaseException(`${pending} pending connections to be resolved`);
} }
/** /**
@ -221,7 +213,7 @@ export class MockBackend implements ConnectionBackend {
* *
* This method only exists in the mock implementation, not in real Backends. * This method only exists in the mock implementation, not in real Backends.
*/ */
resolveAllConnections() { this.connections.subscribe((c) => c.readyState = 4); } resolveAllConnections() { ObservableWrapper.subscribe(this.connections, c => c.readyState = 4); }
/** /**
* Creates a new {@link MockConnection}. This is equivalent to calling `new * Creates a new {@link MockConnection}. This is equivalent to calling `new
@ -229,12 +221,12 @@ export class MockBackend implements ConnectionBackend {
* observable of this `MockBackend` instance. This method will usually only be used by tests * observable of this `MockBackend` instance. This method will usually only be used by tests
* against the framework itself, not by end-users. * against the framework itself, not by end-users.
*/ */
createConnection(req: Request): Connection { createConnection(req: Request) {
if (!req || !(req instanceof Request)) { if (!isPresent(req) || !(req instanceof Request)) {
throw new Error(`createConnection requires an instance of Request, got ${req}`); throw new BaseException(`createConnection requires an instance of Request, got ${req}`);
} }
let connection = new MockConnection(req); let connection = new MockConnection(req);
this.connections.onNext(connection); ObservableWrapper.callNext(this.connections, connection);
return connection; return connection;
} }
} }

View File

@ -1,11 +1,11 @@
import {ConnectionBackend, Connection} from '../interfaces'; import {ConnectionBackend, Connection} from '../interfaces';
import {ReadyStates, RequestMethods} from '../enums'; import {ReadyStates, RequestMethods, RequestMethodsMap} from '../enums';
import {Request} from '../static_request'; import {Request} from '../static_request';
import {Response} from '../static_response'; import {Response} from '../static_response';
import {Inject} from 'angular2/di';
import {Injectable} from 'angular2/di'; import {Injectable} from 'angular2/di';
import {BrowserXHR} from './browser_xhr'; import {BrowserXHR} from './browser_xhr';
import * as Rx from 'rx'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {isPresent, ENUM_INDEX} from 'angular2/src/facade/lang';
/** /**
* Creates connections using `XMLHttpRequest`. Given a fully-qualified * Creates connections using `XMLHttpRequest`. Given a fully-qualified
@ -22,22 +22,24 @@ export class XHRConnection implements Connection {
* [Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md) * [Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md)
* which emits a single {@link Response} value on load event of `XMLHttpRequest`. * which emits a single {@link Response} value on load event of `XMLHttpRequest`.
*/ */
response: Rx.Subject<Response>; response: EventEmitter; //<Response>;
readyState: ReadyStates; readyState: ReadyStates;
private _xhr; private _xhr;
constructor(req: Request, NativeConstruct: any) { constructor(req: Request, browserXHR: BrowserXHR) {
// TODO: get rid of this when enum lookups are available in ts2dart
// https://github.com/angular/ts2dart/issues/221
var requestMethodsMap = new RequestMethodsMap();
this.request = req; this.request = req;
if (Rx.hasOwnProperty('default')) { this.response = new EventEmitter();
this.response = new (<any>Rx).default.Rx.Subject(); this._xhr = browserXHR.build();
} else {
this.response = new Rx.Subject<Response>();
}
this._xhr = new NativeConstruct();
// TODO(jeffbcross): implement error listening/propagation // TODO(jeffbcross): implement error listening/propagation
this._xhr.open(RequestMethods[req.method], req.url); this._xhr.open(requestMethodsMap.getMethod(ENUM_INDEX(req.method)), req.url);
this._xhr.addEventListener( this._xhr.addEventListener(
'load', 'load',
() => {this.response.onNext(new Response(this._xhr.response || this._xhr.responseText))}); (_) => {ObservableWrapper.callNext(
this.response, new Response({
body: isPresent(this._xhr.response) ? this._xhr.response : this._xhr.responseText
}))});
// TODO(jeffbcross): make this more dynamic based on body type // TODO(jeffbcross): make this more dynamic based on body type
this._xhr.send(this.request.text()); this._xhr.send(this.request.text());
} }
@ -76,8 +78,8 @@ export class XHRConnection implements Connection {
**/ **/
@Injectable() @Injectable()
export class XHRBackend implements ConnectionBackend { export class XHRBackend implements ConnectionBackend {
constructor(private _NativeConstruct: BrowserXHR) {} constructor(private _browserXHR: BrowserXHR) {}
createConnection(request: Request): XHRConnection { createConnection(request: Request): XHRConnection {
return new XHRConnection(request, this._NativeConstruct); return new XHRConnection(request, this._browserXHR);
} }
} }

View File

@ -1,11 +1,11 @@
import {CONST_EXPR, CONST, isPresent} from 'angular2/src/facade/lang'; import {CONST_EXPR, CONST, isPresent} from 'angular2/src/facade/lang';
import {Headers} from './headers'; import {Headers} from './headers';
import {URLSearchParams} from './url_search_params';
import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums'; import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums';
import {IRequestOptions} from './interfaces'; import {IRequestOptions} from './interfaces';
import {Injectable} from 'angular2/di'; import {Injectable} from 'angular2/di';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {ListWrapper, StringMapWrapper, StringMap} from 'angular2/src/facade/collection';
const INITIALIZER = CONST_EXPR({});
/** /**
* Creates a request options object with default properties as described in the [Fetch * Creates a request options object with default properties as described in the [Fetch
* Spec](https://fetch.spec.whatwg.org/#requestinit) to be optionally provided when instantiating a * Spec](https://fetch.spec.whatwg.org/#requestinit) to be optionally provided when instantiating a
@ -28,28 +28,49 @@ export class RequestOptions implements IRequestOptions {
/** /**
* Body to be used when creating the request. * Body to be used when creating the request.
*/ */
body: URLSearchParams | FormData | Blob | string; // TODO: support FormData, Blob, URLSearchParams, JSON
body: string;
mode: RequestModesOpts = RequestModesOpts.Cors; mode: RequestModesOpts = RequestModesOpts.Cors;
credentials: RequestCredentialsOpts; credentials: RequestCredentialsOpts;
cache: RequestCacheOpts; cache: RequestCacheOpts;
constructor({method, headers, body, mode, credentials, cache}: IRequestOptions = { url: string;
method: RequestMethods.GET, constructor({method, headers, body, mode, credentials, cache, url}: IRequestOptions = {}) {
mode: RequestModesOpts.Cors this.method = isPresent(method) ? method : RequestMethods.GET;
}) {
this.method = method;
this.headers = headers; this.headers = headers;
this.body = body; this.body = body;
this.mode = mode; this.mode = isPresent(mode) ? mode : RequestModesOpts.Cors;
this.credentials = credentials; this.credentials = credentials;
this.cache = cache; this.cache = cache;
this.url = url;
} }
/** /**
* Creates a copy of the `RequestOptions` instance, using the optional input as values to override * Creates a copy of the `RequestOptions` instance, using the optional input as values to override
* existing values. * existing values.
*/ */
merge(opts: IRequestOptions = {}): RequestOptions { merge({url = null, method = null, headers = null, body = null, mode = null, credentials = null,
return new RequestOptions(StringMapWrapper.merge(this, opts)); cache = null}: any = {}): RequestOptions {
return new RequestOptions({
method: isPresent(method) ? method : this.method,
headers: isPresent(headers) ? headers : this.headers,
body: isPresent(body) ? body : this.body,
mode: isPresent(mode) ? mode : this.mode,
credentials: isPresent(credentials) ? credentials : this.credentials,
cache: isPresent(cache) ? cache : this.cache,
url: isPresent(url) ? url : this.url
});
}
static fromStringWrapper(map: StringMap<string, any>): RequestOptions {
return new RequestOptions({
method: StringMapWrapper.get(map, 'method'),
headers: StringMapWrapper.get(map, 'headers'),
body: StringMapWrapper.get(map, 'body'),
mode: StringMapWrapper.get(map, 'mode'),
credentials: StringMapWrapper.get(map, 'credentials'),
cache: StringMapWrapper.get(map, 'cache'),
url:<string>StringMapWrapper.get(map, 'url')
})
} }
} }

View File

@ -3,21 +3,19 @@ import {ResponseTypes} from './enums';
import {ResponseOptions} from './interfaces'; import {ResponseOptions} from './interfaces';
export class BaseResponseOptions implements ResponseOptions { export class BaseResponseOptions implements ResponseOptions {
body: string | Object | ArrayBuffer | JSON | FormData | Blob;
status: number; status: number;
headers: Headers | Object; headers: Headers;
statusText: string; statusText: string;
type: ResponseTypes; type: ResponseTypes;
url: string; url: string;
constructor({status = 200, statusText = 'Ok', type = ResponseTypes.Default, constructor() {
headers = new Headers(), url = ''}: ResponseOptions = {}) { this.status = 200;
this.status = status; this.statusText = 'Ok';
this.statusText = statusText; this.type = ResponseTypes.Default;
this.type = type; this.headers = new Headers();
this.headers = headers;
this.url = url;
} }
} }
;
export var baseResponseOptions = Object.freeze(new BaseResponseOptions()); export var baseResponseOptions = new BaseResponseOptions();

View File

@ -1,3 +1,5 @@
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
export enum RequestModesOpts { export enum RequestModesOpts {
Cors, Cors,
NoCors, NoCors,
@ -29,6 +31,14 @@ export enum RequestMethods {
PATCH PATCH
} }
// TODO: Remove this when enum lookups are available in ts2dart
// https://github.com/angular/ts2dart/issues/221
export class RequestMethodsMap {
private _methods: List<string>;
constructor() { this._methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH']; }
getMethod(method: int): string { return this._methods[method]; }
}
export enum ReadyStates { export enum ReadyStates {
UNSENT, UNSENT,
OPEN, OPEN,

View File

@ -11,7 +11,8 @@ import {
List, List,
Map, Map,
MapWrapper, MapWrapper,
ListWrapper ListWrapper,
StringMap
} from 'angular2/src/facade/collection'; } from 'angular2/src/facade/collection';
/** /**
@ -21,15 +22,15 @@ import {
*/ */
export class Headers { export class Headers {
_headersMap: Map<string, List<string>>; _headersMap: Map<string, List<string>>;
constructor(headers?: Headers | Object) { constructor(headers?: Headers | StringMap<string, any>) {
if (isBlank(headers)) { if (isBlank(headers)) {
this._headersMap = new Map(); this._headersMap = new Map();
return; return;
} }
if (isPresent((<Headers>headers)._headersMap)) { if (headers instanceof Headers) {
this._headersMap = (<Headers>headers)._headersMap; this._headersMap = (<Headers>headers)._headersMap;
} else if (isJsObject(headers)) { } else if (headers instanceof StringMap) {
this._headersMap = MapWrapper.createFromStringMap(headers); this._headersMap = MapWrapper.createFromStringMap(headers);
MapWrapper.forEach(this._headersMap, (v, k) => { MapWrapper.forEach(this._headersMap, (v, k) => {
if (!isListLikeIterable(v)) { if (!isListLikeIterable(v)) {
@ -42,7 +43,8 @@ export class Headers {
} }
append(name: string, value: string): void { append(name: string, value: string): void {
var list = this._headersMap.get(name) || []; var mapName = this._headersMap.get(name);
var list = isListLikeIterable(mapName) ? mapName : [];
list.push(value); list.push(value);
this._headersMap.set(name, list); this._headersMap.set(name, list);
} }
@ -57,13 +59,19 @@ export class Headers {
keys(): List<string> { return MapWrapper.keys(this._headersMap); } keys(): List<string> { return MapWrapper.keys(this._headersMap); }
// TODO: this implementation seems wrong. create list then check if it's iterable?
set(header: string, value: string | List<string>): void { set(header: string, value: string | List<string>): void {
var list = []; var list = [];
if (!isListLikeIterable(value)) { var isDart = false;
list.push(value); // Dart hack
if (list.toString().length === 2) {
isDart = true;
}
if (isListLikeIterable(value)) {
var pushValue = (<List<string>>value).toString();
if (isDart) pushValue = pushValue.substring(1, pushValue.length - 1);
list.push(pushValue);
} else { } else {
list.push(ListWrapper.toString((<List<string>>value))); list.push(value);
} }
this._headersMap.set(header, list); this._headersMap.set(header, list);
@ -71,7 +79,10 @@ export class Headers {
values(): List<List<string>> { return MapWrapper.values(this._headersMap); } values(): List<List<string>> { return MapWrapper.values(this._headersMap); }
getAll(header: string): Array<string> { return this._headersMap.get(header) || []; } getAll(header: string): Array<string> {
var headers = this._headersMap.get(header);
return isListLikeIterable(headers) ? headers : [];
}
entries() { throw new BaseException('"entries" method is not implemented on Headers class'); } entries() { throw new BaseException('"entries" method is not implemented on Headers class'); }
} }

View File

@ -1,24 +1,25 @@
/// <reference path="../../typings/rx/rx.d.ts" /> import {isString, isPresent, isBlank} from 'angular2/src/facade/lang';
import {Injectable} from 'angular2/src/di/decorators'; import {Injectable} from 'angular2/src/di/decorators';
import {IRequestOptions, Connection, IHttp} from './interfaces'; import {IRequestOptions, Connection, ConnectionBackend} from './interfaces';
import {Request} from './static_request'; import {Request} from './static_request';
import {Response} from './static_response'; import {BaseRequestOptions, RequestOptions} from './base_request_options';
import {XHRBackend} from './backends/xhr_backend';
import {BaseRequestOptions} from './base_request_options';
import {RequestMethods} from './enums'; import {RequestMethods} from './enums';
import {URLSearchParams} from './url_search_params'; import {EventEmitter} from 'angular2/src/facade/async';
import * as Rx from 'rx';
function httpRequest(backend: XHRBackend, request: Request): Rx.Observable<Response> { function httpRequest(backend: ConnectionBackend, request: Request): EventEmitter {
return <Rx.Observable<Response>>(Observable.create(observer => { return backend.createConnection(request).response;
var connection: Connection = backend.createConnection(request); }
var internalSubscription = connection.response.subscribe(observer);
return () => { function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
internalSubscription.dispose(); var newOptions = defaultOpts;
connection.dispose(); if (isPresent(providedOpts)) {
}; newOptions = newOptions.merge(providedOpts);
})); }
if (isPresent(method)) {
return newOptions.merge({method: method, url: url});
} else {
return newOptions.merge({url: url});
}
} }
/** /**
@ -72,7 +73,7 @@ function httpRequest(backend: XHRBackend, request: Request): Rx.Observable<Respo
**/ **/
@Injectable() @Injectable()
export class Http { export class Http {
constructor(private _backend: XHRBackend, private _defaultOptions: BaseRequestOptions) {} constructor(private _backend: ConnectionBackend, private _defaultOptions: BaseRequestOptions) {}
/** /**
* Performs any type of http request. First argument is required, and can either be a url or * Performs any type of http request. First argument is required, and can either be a url or
@ -80,77 +81,70 @@ export class Http {
* object can be provided as the 2nd argument. The options object will be merged with the values * object can be provided as the 2nd argument. The options object will be merged with the values
* of {@link BaseRequestOptions} before performing the request. * of {@link BaseRequestOptions} before performing the request.
*/ */
request(url: string | Request, options?: IRequestOptions): Rx.Observable<Response> { request(url: string | Request, options?: IRequestOptions): EventEmitter {
if (typeof url === 'string') { var responseObservable: EventEmitter;
return httpRequest(this._backend, new Request(url, this._defaultOptions.merge(options))); if (isString(url)) {
responseObservable = httpRequest(
this._backend,
new Request(mergeOptions(this._defaultOptions, options, RequestMethods.GET, url)));
} else if (url instanceof Request) { } else if (url instanceof Request) {
return httpRequest(this._backend, url); responseObservable = httpRequest(this._backend, url);
} }
return responseObservable;
} }
/** /**
* Performs a request with `get` http method. * Performs a request with `get` http method.
*/ */
get(url: string, options?: IRequestOptions): Rx.Observable<Response> { get(url: string, options?: IRequestOptions) {
return httpRequest(this._backend, new Request(url, this._defaultOptions.merge(options) return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options,
.merge({method: RequestMethods.GET}))); RequestMethods.GET, url)));
} }
/** /**
* Performs a request with `post` http method. * Performs a request with `post` http method.
*/ */
post(url: string, body: URLSearchParams | FormData | Blob | string, post(url: string, body: string, options?: IRequestOptions) {
options?: IRequestOptions): Rx.Observable<Response> {
return httpRequest(this._backend, return httpRequest(this._backend,
new Request(url, this._defaultOptions.merge(options) new Request(mergeOptions(this._defaultOptions.merge({body: body}), options,
RequestMethods.POST, url)));
.merge({body: body, method: RequestMethods.POST})));
} }
/** /**
* Performs a request with `put` http method. * Performs a request with `put` http method.
*/ */
put(url: string, body: URLSearchParams | FormData | Blob | string, put(url: string, body: string, options?: IRequestOptions) {
options?: IRequestOptions): Rx.Observable<Response> {
return httpRequest(this._backend, return httpRequest(this._backend,
new Request(url, this._defaultOptions.merge(options) new Request(mergeOptions(this._defaultOptions.merge({body: body}), options,
.merge({body: body, method: RequestMethods.PUT}))); RequestMethods.PUT, url)));
} }
/** /**
* Performs a request with `delete` http method. * Performs a request with `delete` http method.
*/ */
delete (url: string, options?: IRequestOptions): Rx.Observable<Response> { delete (url: string, options?: IRequestOptions) {
return httpRequest(this._backend, new Request(url, this._defaultOptions.merge(options).merge( return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options,
{method: RequestMethods.DELETE}))); RequestMethods.DELETE, url)));
} }
/** /**
* Performs a request with `patch` http method. * Performs a request with `patch` http method.
*/ */
patch(url: string, body: URLSearchParams | FormData | Blob | string, patch(url: string, body: string, options?: IRequestOptions) {
options?: IRequestOptions): Rx.Observable<Response> {
return httpRequest(this._backend, return httpRequest(this._backend,
new Request(url, this._defaultOptions.merge(options) new Request(mergeOptions(this._defaultOptions.merge({body: body}), options,
.merge({body: body, method: RequestMethods.PATCH}))); RequestMethods.PATCH, url)));
} }
/** /**
* Performs a request with `head` http method. * Performs a request with `head` http method.
*/ */
head(url: string, options?: IRequestOptions): Rx.Observable<Response> { head(url: string, options?: IRequestOptions) {
return httpRequest(this._backend, new Request(url, this._defaultOptions.merge(options) return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options,
.merge({method: RequestMethods.HEAD}))); RequestMethods.HEAD, url)));
} }
} }
var Observable;
if (Rx.hasOwnProperty('default')) {
Observable = (<any>Rx).default.Rx.Observable;
} else {
Observable = Rx.Observable;
}
/** /**
* *
* Alias to the `request` method of {@link Http}, for those who'd prefer a simple function instead * Alias to the `request` method of {@link Http}, for those who'd prefer a simple function instead
@ -174,10 +168,10 @@ if (Rx.hasOwnProperty('default')) {
* } * }
* ``` * ```
**/ **/
export function HttpFactory(backend: XHRBackend, defaultOptions: BaseRequestOptions): IHttp { export function HttpFactory(backend: ConnectionBackend, defaultOptions: BaseRequestOptions) {
return function(url: string | Request, options?: IRequestOptions) { return function(url: string | Request, options?: IRequestOptions) {
if (typeof url === 'string') { if (isString(url)) {
return httpRequest(backend, new Request(url, defaultOptions.merge(options))); return httpRequest(backend, new Request(mergeOptions(defaultOptions, options, null, url)));
} else if (url instanceof Request) { } else if (url instanceof Request) {
return httpRequest(backend, url); return httpRequest(backend, url);
} }

View File

@ -9,24 +9,36 @@ import {
ResponseTypes ResponseTypes
} from './enums'; } from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
import {URLSearchParams} from './url_search_params'; import {BaseException} from 'angular2/src/facade/lang';
import {EventEmitter} from 'angular2/src/facade/async';
import {Request} from './static_request';
export class ConnectionBackend {
constructor() {}
createConnection(request: any): Connection { throw new BaseException('Abstract!'); }
}
export class Connection {
readyState: ReadyStates;
request: Request;
response: EventEmitter; //<IResponse>;
dispose(): void { throw new BaseException('Abstract!'); }
}
export interface IRequestOptions { export interface IRequestOptions {
url?: string;
method?: RequestMethods; method?: RequestMethods;
headers?: Headers; headers?: Headers;
body?: URLSearchParams | FormData | Blob | string; // TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
body?: string;
mode?: RequestModesOpts; mode?: RequestModesOpts;
credentials?: RequestCredentialsOpts; credentials?: RequestCredentialsOpts;
cache?: RequestCacheOpts; cache?: RequestCacheOpts;
} }
export interface IRequest {
method: RequestMethods;
mode: RequestModesOpts;
credentials: RequestCredentialsOpts;
}
export interface ResponseOptions { export interface ResponseOptions {
// TODO: Support Blob, ArrayBuffer, JSON
body?: string | Object | FormData;
status?: number; status?: number;
statusText?: string; statusText?: string;
headers?: Headers | Object; headers?: Headers | Object;
@ -43,23 +55,12 @@ export interface IResponse {
url: string; url: string;
totalBytes: number; totalBytes: number;
bytesLoaded: number; bytesLoaded: number;
blob(): Blob; blob(): any; // TODO: Blob
arrayBuffer(): ArrayBuffer; arrayBuffer(): any; // TODO: ArrayBuffer
text(): string; text(): string;
json(): Object; json(): Object;
} }
export interface ConnectionBackend {
createConnection(observer: any, config: IRequest): Connection;
}
export interface Connection {
readyState: ReadyStates;
request: IRequest;
response: Rx.Subject<IResponse>;
dispose(): void;
}
/** /**
* Provides an interface to provide type information for {@link HttpFactory} when injecting. * Provides an interface to provide type information for {@link HttpFactory} when injecting.
* *
@ -83,4 +84,4 @@ export interface Connection {
*/ */
// Prefixed as IHttp because used in conjunction with Http class, but interface is callable // Prefixed as IHttp because used in conjunction with Http class, but interface is callable
// constructor(@Inject(Http) http:IHttp) // constructor(@Inject(Http) http:IHttp)
export interface IHttp { (url: string, options?: IRequestOptions): Rx.Observable<IResponse> } export interface IHttp { (url: string, options?: IRequestOptions): EventEmitter }

View File

@ -1,8 +1,9 @@
import {RequestMethods, RequestModesOpts, RequestCredentialsOpts} from './enums'; import {RequestMethods, RequestModesOpts, RequestCredentialsOpts, RequestCacheOpts} from './enums';
import {URLSearchParams} from './url_search_params'; import {RequestOptions} from './base_request_options';
import {IRequestOptions, IRequest} from './interfaces'; import {IRequestOptions} from './interfaces';
import {Headers} from './headers'; import {Headers} from './headers';
import {BaseException, RegExpWrapper} from 'angular2/src/facade/lang'; import {BaseException, RegExpWrapper, CONST_EXPR, isPresent} from 'angular2/src/facade/lang';
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
// TODO(jeffbcross): properly implement body accessors // TODO(jeffbcross): properly implement body accessors
/** /**
@ -13,7 +14,7 @@ import {BaseException, RegExpWrapper} from 'angular2/src/facade/lang';
* but is considered a static value whose body can be accessed many times. There are other * but is considered a static value whose body can be accessed many times. There are other
* differences in the implementation, but this is the most significant. * differences in the implementation, but this is the most significant.
*/ */
export class Request implements IRequest { export class Request {
/** /**
* Http method with which to perform the request. * Http method with which to perform the request.
* *
@ -27,22 +28,27 @@ export class Request implements IRequest {
* Spec](https://fetch.spec.whatwg.org/#headers-class). {@link Headers} class reference. * Spec](https://fetch.spec.whatwg.org/#headers-class). {@link Headers} class reference.
*/ */
headers: Headers; headers: Headers;
/** Url of the remote resource */
url: string;
// TODO: support URLSearchParams | FormData | Blob | ArrayBuffer
private _body: string;
cache: RequestCacheOpts;
// TODO(jeffbcross): determine way to add type to destructured args
constructor(options?: IRequestOptions) {
var requestOptions: RequestOptions = options instanceof
StringMap ? RequestOptions.fromStringWrapper(<StringMap<string, any>>options) :
<RequestOptions>options;
private _body: URLSearchParams | FormData | Blob | string; this.url = requestOptions.url;
this._body = requestOptions.body;
constructor(/** Url of the remote resource */ public url: string, this.method = requestOptions.method;
{body, method = RequestMethods.GET, mode = RequestModesOpts.Cors,
credentials = RequestCredentialsOpts.Omit,
headers = new Headers()}: IRequestOptions = {}) {
this._body = body;
this.method = method;
// Defaults to 'cors', consistent with browser
// TODO(jeffbcross): implement behavior // TODO(jeffbcross): implement behavior
this.mode = mode; this.mode = requestOptions.mode;
// Defaults to 'omit', consistent with browser // Defaults to 'omit', consistent with browser
// TODO(jeffbcross): implement behavior // TODO(jeffbcross): implement behavior
this.credentials = credentials; this.credentials = requestOptions.credentials;
this.headers = headers; this.headers = requestOptions.headers;
this.cache = requestOptions.cache;
} }
/** /**
@ -50,5 +56,5 @@ export class Request implements IRequest {
* empty * empty
* string. * string.
*/ */
text(): String { return this._body ? this._body.toString() : ''; } text(): String { return isPresent(this._body) ? this._body.toString() : ''; }
} }

View File

@ -1,7 +1,7 @@
import {IResponse, ResponseOptions} from './interfaces'; import {IResponse, ResponseOptions} from './interfaces';
import {ResponseTypes} from './enums'; import {ResponseTypes} from './enums';
import {baseResponseOptions} from './base_response_options'; import {baseResponseOptions} from './base_response_options';
import {BaseException, isJsObject, isString, global} from 'angular2/src/facade/lang'; import {BaseException, isJsObject, isString, isPresent, Json} from 'angular2/src/facade/lang';
import {Headers} from './headers'; import {Headers} from './headers';
// TODO: make this injectable so baseResponseOptions can be overridden, mostly for the benefit of // TODO: make this injectable so baseResponseOptions can be overridden, mostly for the benefit of
@ -72,34 +72,37 @@ export class Response implements IResponse {
* Spec](https://fetch.spec.whatwg.org/#headers-class). * Spec](https://fetch.spec.whatwg.org/#headers-class).
*/ */
headers: Headers; headers: Headers;
constructor(private _body?: string | Object | ArrayBuffer | JSON | FormData | Blob, // TODO: Support ArrayBuffer, JSON, FormData, Blob
{status, statusText, headers, type, url}: ResponseOptions = baseResponseOptions) { private _body: string | Object;
constructor({body, status, statusText, headers, type, url}: ResponseOptions = {}) {
if (isJsObject(headers)) { if (isJsObject(headers)) {
headers = new Headers(headers); headers = new Headers(headers);
} }
this.status = status; this._body = isPresent(body) ? body : baseResponseOptions.body;
this.statusText = statusText; this.status = isPresent(status) ? status : baseResponseOptions.status;
this.headers = <Headers>headers; this.statusText = isPresent(statusText) ? statusText : baseResponseOptions.statusText;
this.type = type; this.headers = isPresent(headers) ? <Headers>headers : baseResponseOptions.headers;
this.url = url; this.type = isPresent(type) ? type : baseResponseOptions.type;
this.url = isPresent(url) ? url : baseResponseOptions.url;
} }
/** /**
* Not yet implemented * Not yet implemented
*/ */
blob(): Blob { // TODO: Blob return type
throw new BaseException('"blob()" method not implemented on Response superclass'); blob(): any { throw new BaseException('"blob()" method not implemented on Response superclass'); }
}
/** /**
* Attempts to return body as parsed `JSON` object, or raises an exception. * Attempts to return body as parsed `JSON` object, or raises an exception.
*/ */
json(): JSON { json(): Object {
var jsonResponse;
if (isJsObject(this._body)) { if (isJsObject(this._body)) {
return <JSON>this._body; jsonResponse = this._body;
} else if (isString(this._body)) { } else if (isString(this._body)) {
return global.JSON.parse(<string>this._body); jsonResponse = Json.parse(<string>this._body);
} }
return jsonResponse;
} }
/** /**
@ -110,7 +113,8 @@ export class Response implements IResponse {
/** /**
* Not yet implemented * Not yet implemented
*/ */
arrayBuffer(): ArrayBuffer { // TODO: ArrayBuffer return type
arrayBuffer(): any {
throw new BaseException('"arrayBuffer()" method not implemented on Response superclass'); throw new BaseException('"arrayBuffer()" method not implemented on Response superclass');
} }
} }

View File

@ -1,14 +1,20 @@
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang'; import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; import {
Map,
MapWrapper,
List,
ListWrapper,
isListLikeIterable
} from 'angular2/src/facade/collection';
function paramParser(rawParams: string): Map<string, List<string>> { function paramParser(rawParams: string): Map<string, List<string>> {
var map: Map<string, List<string>> = new Map(); var map: Map<string, List<string>> = new Map();
var params: List<string> = StringWrapper.split(rawParams, '&'); var params: List<string> = StringWrapper.split(rawParams, new RegExp('&'));
ListWrapper.forEach(params, (param: string) => { ListWrapper.forEach(params, (param: string) => {
var split: List<string> = StringWrapper.split(param, '='); var split: List<string> = StringWrapper.split(param, new RegExp('='));
var key = ListWrapper.get(split, 0); var key = ListWrapper.get(split, 0);
var val = ListWrapper.get(split, 1); var val = ListWrapper.get(split, 1);
var list = map.get(key) || []; var list = isPresent(map.get(key)) ? map.get(key) : [];
list.push(val); list.push(val);
map.set(key, list); map.set(key, list);
}); });
@ -21,12 +27,23 @@ export class URLSearchParams {
has(param: string): boolean { return this.paramsMap.has(param); } has(param: string): boolean { return this.paramsMap.has(param); }
get(param: string): string { return ListWrapper.first(this.paramsMap.get(param)); } get(param: string): string {
var storedParam = this.paramsMap.get(param);
if (isListLikeIterable(storedParam)) {
return ListWrapper.first(storedParam);
} else {
return null;
}
}
getAll(param: string): List<string> { return this.paramsMap.get(param) || []; } getAll(param: string): List<string> {
var mapParam = this.paramsMap.get(param);
return isPresent(mapParam) ? mapParam : [];
}
append(param: string, val: string): void { append(param: string, val: string): void {
var list = this.paramsMap.get(param) || []; var mapParam = this.paramsMap.get(param);
var list = isPresent(mapParam) ? mapParam : [];
list.push(val); list.push(val);
this.paramsMap.set(param, list); this.paramsMap.set(param, list);
} }

View File

@ -14,13 +14,15 @@ import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr';
import {XHRConnection, XHRBackend} from 'angular2/src/http/backends/xhr_backend'; import {XHRConnection, XHRBackend} from 'angular2/src/http/backends/xhr_backend';
import {bind, Injector} from 'angular2/di'; import {bind, Injector} from 'angular2/di';
import {Request} from 'angular2/src/http/static_request'; import {Request} from 'angular2/src/http/static_request';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {RequestOptions} from 'angular2/src/http/base_request_options';
var abortSpy; var abortSpy;
var sendSpy; var sendSpy;
var openSpy; var openSpy;
var addEventListenerSpy; var addEventListenerSpy;
class MockBrowserXHR extends SpyObject { class MockBrowserXHR extends BrowserXHR {
abort: any; abort: any;
send: any; send: any;
open: any; open: any;
@ -29,24 +31,26 @@ class MockBrowserXHR extends SpyObject {
responseText: string; responseText: string;
constructor() { constructor() {
super(); super();
this.abort = abortSpy = this.spy('abort'); var spy = new SpyObject();
this.send = sendSpy = this.spy('send'); this.abort = abortSpy = spy.spy('abort');
this.open = openSpy = this.spy('open'); this.send = sendSpy = spy.spy('send');
this.addEventListener = addEventListenerSpy = this.spy('addEventListener'); this.open = openSpy = spy.spy('open');
this.addEventListener = addEventListenerSpy = spy.spy('addEventListener');
} }
build() { return new MockBrowserXHR(); }
} }
export function main() { export function main() {
describe('XHRBackend', () => { describe('XHRBackend', () => {
var backend; var backend;
var sampleRequest; var sampleRequest;
var constructSpy = new SpyObject();
beforeEach(() => { beforeEach(() => {
var injector = var injector =
Injector.resolveAndCreate([bind(BrowserXHR).toValue(MockBrowserXHR), XHRBackend]); Injector.resolveAndCreate([bind(BrowserXHR).toClass(MockBrowserXHR), XHRBackend]);
backend = injector.get(XHRBackend); backend = injector.get(XHRBackend);
sampleRequest = new Request('https://google.com'); sampleRequest = new Request(new RequestOptions({url: 'https://google.com'}));
}); });
it('should create a connection', it('should create a connection',
@ -55,22 +59,21 @@ export function main() {
describe('XHRConnection', () => { describe('XHRConnection', () => {
it('should call abort when disposed', () => { it('should call abort when disposed', () => {
var connection = new XHRConnection(sampleRequest, MockBrowserXHR); var connection = new XHRConnection(sampleRequest, new MockBrowserXHR());
connection.dispose(); connection.dispose();
expect(abortSpy).toHaveBeenCalled(); expect(abortSpy).toHaveBeenCalled();
}); });
it('should automatically call open with method and url', () => { it('should automatically call open with method and url', () => {
new XHRConnection(sampleRequest, MockBrowserXHR); new XHRConnection(sampleRequest, new MockBrowserXHR());
expect(openSpy).toHaveBeenCalledWith('GET', sampleRequest.url); expect(openSpy).toHaveBeenCalledWith('GET', sampleRequest.url);
}); });
it('should automatically call send on the backend with request body', () => { it('should automatically call send on the backend with request body', () => {
var body = 'Some body to love'; var body = 'Some body to love';
var request = new Request('https://google.com', {body: body}); new XHRConnection(new Request(new RequestOptions({body: body})), new MockBrowserXHR());
var connection = new XHRConnection(request, MockBrowserXHR);
expect(sendSpy).toHaveBeenCalledWith(body); expect(sendSpy).toHaveBeenCalledWith(body);
}); });
}); });

View File

@ -1,5 +1,5 @@
import {Headers} from 'angular2/src/http/headers'; import {Headers} from 'angular2/src/http/headers';
import {Map} from 'angular2/src/facade/collection'; import {Map, StringMapWrapper} from 'angular2/src/facade/collection';
import { import {
AsyncTestCompleter, AsyncTestCompleter,
beforeEach, beforeEach,
@ -17,27 +17,24 @@ export function main() {
it('should conform to spec', () => { it('should conform to spec', () => {
// Examples borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers // Examples borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers
// Spec at https://fetch.spec.whatwg.org/#dom-headers // Spec at https://fetch.spec.whatwg.org/#dom-headers
var myHeaders = new Headers(); // Currently empty var firstHeaders = new Headers(); // Currently empty
myHeaders.append('Content-Type', 'image/jpeg'); firstHeaders.append('Content-Type', 'image/jpeg');
expect(myHeaders.get('Content-Type')).toBe('image/jpeg'); expect(firstHeaders.get('Content-Type')).toBe('image/jpeg');
var httpHeaders = { var httpHeaders = StringMapWrapper.create();
'Content-Type': 'image/jpeg', StringMapWrapper.set(httpHeaders, 'Content-Type', 'image/jpeg');
'Accept-Charset': 'utf-8', StringMapWrapper.set(httpHeaders, 'Accept-Charset', 'utf-8');
'X-My-Custom-Header': 'Zeke are cool' StringMapWrapper.set(httpHeaders, 'X-My-Custom-Header', 'Zeke are cool');
}; var secondHeaders = new Headers(httpHeaders);
var myHeaders = new Headers(httpHeaders); var secondHeadersObj = new Headers(secondHeaders);
var secondHeadersObj = new Headers(myHeaders);
expect(secondHeadersObj.get('Content-Type')).toBe('image/jpeg'); expect(secondHeadersObj.get('Content-Type')).toBe('image/jpeg');
}); });
describe('initialization', () => { describe('initialization', () => {
it('should create a private headersMap map',
() => { expect(new Headers()._headersMap).toBeAnInstanceOf(Map); });
it('should merge values in provided dictionary', () => { it('should merge values in provided dictionary', () => {
var headers = new Headers({foo: 'bar'}); var map = StringMapWrapper.create();
StringMapWrapper.set(map, 'foo', 'bar');
var headers = new Headers(map);
expect(headers.get('foo')).toBe('bar'); expect(headers.get('foo')).toBe('bar');
expect(headers.getAll('foo')).toEqual(['bar']); expect(headers.getAll('foo')).toEqual(['bar']);
}); });
@ -46,7 +43,9 @@ export function main() {
describe('.set()', () => { describe('.set()', () => {
it('should clear all values and re-set for the provided key', () => { it('should clear all values and re-set for the provided key', () => {
var headers = new Headers({foo: 'bar'}); var map = StringMapWrapper.create();
StringMapWrapper.set(map, 'foo', 'bar');
var headers = new Headers(map);
expect(headers.get('foo')).toBe('bar'); expect(headers.get('foo')).toBe('bar');
expect(headers.getAll('foo')).toEqual(['bar']); expect(headers.getAll('foo')).toEqual(['bar']);
headers.set('foo', 'baz'); headers.set('foo', 'baz');
@ -57,9 +56,10 @@ export function main() {
it('should convert input array to string', () => { it('should convert input array to string', () => {
var headers = new Headers(); var headers = new Headers();
headers.set('foo', ['bar', 'baz']); var inputArr = ['bar', 'baz'];
expect(headers.get('foo')).toBe('bar,baz'); headers.set('foo', inputArr);
expect(headers.getAll('foo')).toEqual(['bar,baz']); expect(/bar, ?baz/g.test(headers.get('foo'))).toBe(true);
expect(/bar, ?baz/g.test(headers.getAll('foo')[0])).toBe(true);
}); });
}); });
}); });

View File

@ -1,5 +1,6 @@
import { import {
AsyncTestCompleter, AsyncTestCompleter,
afterEach,
beforeEach, beforeEach,
ddescribe, ddescribe,
describe, describe,
@ -11,13 +12,14 @@ import {
SpyObject SpyObject
} from 'angular2/test_lib'; } from 'angular2/test_lib';
import {Http, HttpFactory} from 'angular2/src/http/http'; import {Http, HttpFactory} from 'angular2/src/http/http';
import {XHRBackend} from 'angular2/src/http/backends/xhr_backend';
import {Injector, bind} from 'angular2/di'; import {Injector, bind} from 'angular2/di';
import {MockBackend} from 'angular2/src/http/backends/mock_backend'; import {MockBackend} from 'angular2/src/http/backends/mock_backend';
import {Response} from 'angular2/src/http/static_response'; import {Response} from 'angular2/src/http/static_response';
import {RequestMethods} from 'angular2/src/http/enums'; import {RequestMethods} from 'angular2/src/http/enums';
import {BaseRequestOptions} from 'angular2/src/http/base_request_options'; import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options';
import {Request} from 'angular2/src/http/static_request'; import {Request} from 'angular2/src/http/static_request';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {ConnectionBackend} from 'angular2/src/http/interfaces';
class SpyObserver extends SpyObject { class SpyObserver extends SpyObject {
onNext: Function; onNext: Function;
@ -34,20 +36,19 @@ class SpyObserver extends SpyObject {
export function main() { export function main() {
describe('http', () => { describe('http', () => {
var url = 'http://foo.bar'; var url = 'http://foo.bar';
var http; var http: Http;
var injector; var injector: Injector;
var backend: MockBackend; var backend: MockBackend;
var baseResponse; var baseResponse;
var sampleObserver;
var httpFactory; var httpFactory;
beforeEach(() => { beforeEach(() => {
injector = Injector.resolveAndCreate([ injector = Injector.resolveAndCreate([
BaseRequestOptions, BaseRequestOptions,
MockBackend, MockBackend,
bind(XHRBackend).toClass(MockBackend), bind(ConnectionBackend).toClass(MockBackend),
bind(HttpFactory).toFactory(HttpFactory, [MockBackend, BaseRequestOptions]), bind(HttpFactory).toFactory(HttpFactory, [MockBackend, BaseRequestOptions]),
bind(Http).toFactory( bind(Http).toFactory(
function(backend: XHRBackend, defaultOptions: BaseRequestOptions) { function(backend: ConnectionBackend, defaultOptions: BaseRequestOptions) {
return new Http(backend, defaultOptions); return new Http(backend, defaultOptions);
}, },
[MockBackend, BaseRequestOptions]) [MockBackend, BaseRequestOptions])
@ -55,224 +56,194 @@ export function main() {
http = injector.get(Http); http = injector.get(Http);
httpFactory = injector.get(HttpFactory); httpFactory = injector.get(HttpFactory);
backend = injector.get(MockBackend); backend = injector.get(MockBackend);
baseResponse = new Response('base response'); baseResponse = new Response({body: 'base response'});
sampleObserver = new SpyObserver();
}); });
afterEach(() => backend.verifyNoPendingRequests()); afterEach(() => backend.verifyNoPendingRequests());
describe('HttpFactory', () => { describe('HttpFactory', () => {
it('should return an Observable', () => { it('should return an Observable', () => {
expect(typeof httpFactory(url).subscribe).toBe('function'); expect(ObservableWrapper.isObservable(httpFactory(url))).toBe(true);
backend.resolveAllConnections(); backend.resolveAllConnections();
}); });
it('should perform a get request for given url if only passed a string', it('should perform a get request for given url if only passed a string',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var connection; ObservableWrapper.subscribe(backend.connections, c => c.mockRespond(baseResponse));
backend.connections.subscribe((c) => connection = c); ObservableWrapper.subscribe(httpFactory('http://basic.connection'), res => {
var subscription = httpFactory('http://basic.connection')
.subscribe(res => {
expect(res.text()).toBe('base response'); expect(res.text()).toBe('base response');
async.done(); async.done();
}); });
connection.mockRespond(baseResponse)
})); }));
it('should accept a fully-qualified request as its only parameter', () => { it('should accept a fully-qualified request as its only parameter',
var req = new Request('https://google.com'); inject([AsyncTestCompleter], (async) => {
backend.connections.subscribe(c => { var req = new Request(new RequestOptions({url: 'https://google.com'}));
ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.url).toBe('https://google.com'); expect(c.request.url).toBe('https://google.com');
c.mockRespond(new Response('Thank you')); c.mockRespond(new Response({body: 'Thank you'}));
});
httpFactory(req).subscribe(() => {});
});
it('should perform a get request for given url if passed a ConnectionConfig instance',
inject([AsyncTestCompleter], async => {
var connection;
backend.connections.subscribe((c) => connection = c);
httpFactory('http://basic.connection', {method: RequestMethods.GET})
.subscribe(res => {
expect(res.text()).toBe('base response');
async.done(); async.done();
}); });
connection.mockRespond(baseResponse) ObservableWrapper.subscribe(httpFactory(req), (res) => {});
})); }));
// TODO: make dart not complain about "argument type 'Map' cannot be assigned to the parameter
it('should perform a get request for given url if passed a dictionary', // type 'IRequestOptions'"
inject([AsyncTestCompleter], async => { // xit('should perform a get request for given url if passed a dictionary',
var connection; // inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => connection = c); // ObservableWrapper.subscribe(backend.connections, c => c.mockRespond(baseResponse));
httpFactory(url, {method: RequestMethods.GET}) // ObservableWrapper.subscribe(httpFactory(url, {method: RequestMethods.GET}), res => {
.subscribe(res => { // expect(res.text()).toBe('base response');
expect(res.text()).toBe('base response'); // async.done();
async.done(); // });
}); // }));
connection.mockRespond(baseResponse)
}));
}); });
describe('Http', () => { describe('Http', () => {
describe('.request()', () => { describe('.request()', () => {
it('should return an Observable', it('should return an Observable',
() => { expect(typeof http.request(url).subscribe).toBe('function'); }); () => { expect(ObservableWrapper.isObservable(http.request(url))).toBe(true); });
it('should accept a fully-qualified request as its only parameter', () => { it('should accept a fully-qualified request as its only parameter',
var req = new Request('https://google.com'); inject([AsyncTestCompleter], (async) => {
backend.connections.subscribe(c => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.url).toBe('https://google.com'); expect(c.request.url).toBe('https://google.com');
c.mockRespond(new Response('Thank you')); c.mockRespond(new Response({body: 'Thank you'}));
}); async.done();
http.request(req).subscribe(() => {});
});
}); });
ObservableWrapper.subscribe(
http.request(new Request(new RequestOptions({url: 'https://google.com'}))),
(res) => {});
}));
it('should perform a get request for given url if only passed a string', it('should perform a get request for given url if only passed a string',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var connection; ObservableWrapper.subscribe(backend.connections, c => c.mockRespond(baseResponse));
backend.connections.subscribe((c) => connection = c); ObservableWrapper.subscribe(http.request('http://basic.connection'), res => {
var subscription = http.request('http://basic.connection')
.subscribe(res => {
expect(res.text()).toBe('base response'); expect(res.text()).toBe('base response');
async.done(); async.done();
}); });
connection.mockRespond(baseResponse)
})); }));
// TODO: make dart not complain about "argument type 'Map' cannot be assigned to the
it('should perform a get request for given url if passed a ConnectionConfig instance', // parameter type 'IRequestOptions'"
inject([AsyncTestCompleter], async => { // xit('should perform a get request for given url if passed a dictionary',
var connection; // inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => connection = c); // ObservableWrapper.subscribe(backend.connections, c => c.mockRespond(baseResponse));
http.request('http://basic.connection', {method: RequestMethods.GET}) // ObservableWrapper.subscribe(http.request(url, {method: RequestMethods.GET}), res =>
.subscribe(res => { // {
expect(res.text()).toBe('base response'); // expect(res.text()).toBe('base response');
async.done(); // async.done();
// });
// }));
}); });
connection.mockRespond(baseResponse);
}));
it('should perform a get request for given url if passed a dictionary',
inject([AsyncTestCompleter], async => {
var connection;
backend.connections.subscribe((c) => connection = c);
http.request(url, {method: RequestMethods.GET})
.subscribe(res => {
expect(res.text()).toBe('base response');
async.done();
});
connection.mockRespond(baseResponse);
}));
describe('.get()', () => { describe('.get()', () => {
it('should perform a get request for given url', inject([AsyncTestCompleter], async => { it('should perform a get request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.GET); expect(c.request.method).toBe(RequestMethods.GET);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.get(url).subscribe(res => {}); ObservableWrapper.subscribe(http.get(url), res => {});
})); }));
}); });
describe('.post()', () => { describe('.post()', () => {
it('should perform a post request for given url', inject([AsyncTestCompleter], async => { it('should perform a post request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.POST); expect(c.request.method).toBe(RequestMethods.POST);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.post(url).subscribe(res => {}); ObservableWrapper.subscribe(http.post(url, 'post me'), res => {});
})); }));
it('should attach the provided body to the request', inject([AsyncTestCompleter], async => { it('should attach the provided body to the request', inject([AsyncTestCompleter], async => {
var body = 'this is my put body'; var body = 'this is my post body';
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.text()).toBe(body); expect(c.request.text()).toBe(body);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.post(url, body).subscribe(res => {}); ObservableWrapper.subscribe(http.post(url, body), res => {});
})); }));
}); });
describe('.put()', () => { describe('.put()', () => {
it('should perform a put request for given url', inject([AsyncTestCompleter], async => { it('should perform a put request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.PUT); expect(c.request.method).toBe(RequestMethods.PUT);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.put(url).subscribe(res => {}); ObservableWrapper.subscribe(http.put(url, 'put me'), res => {});
})); }));
it('should attach the provided body to the request', inject([AsyncTestCompleter], async => { it('should attach the provided body to the request', inject([AsyncTestCompleter], async => {
var body = 'this is my put body'; var body = 'this is my put body';
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.text()).toBe(body); expect(c.request.text()).toBe(body);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.put(url, body).subscribe(res => {}); ObservableWrapper.subscribe(http.put(url, body), res => {});
})); }));
}); });
describe('.delete()', () => { describe('.delete()', () => {
it('should perform a delete request for given url', inject([AsyncTestCompleter], async => { it('should perform a delete request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.DELETE); expect(c.request.method).toBe(RequestMethods.DELETE);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.delete(url).subscribe(res => {}); ObservableWrapper.subscribe(http.delete(url), res => {});
})); }));
}); });
describe('.patch()', () => { describe('.patch()', () => {
it('should perform a patch request for given url', inject([AsyncTestCompleter], async => { it('should perform a patch request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.PATCH); expect(c.request.method).toBe(RequestMethods.PATCH);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.patch(url).subscribe(res => {}); ObservableWrapper.subscribe(http.patch(url, 'this is my patch body'), res => {});
})); }));
it('should attach the provided body to the request', inject([AsyncTestCompleter], async => { it('should attach the provided body to the request', inject([AsyncTestCompleter], async => {
var body = 'this is my put body'; var body = 'this is my patch body';
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.text()).toBe(body); expect(c.request.text()).toBe(body);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.patch(url, body).subscribe(res => {}); ObservableWrapper.subscribe(http.patch(url, body), res => {});
})); }));
}); });
describe('.head()', () => { describe('.head()', () => {
it('should perform a head request for given url', inject([AsyncTestCompleter], async => { it('should perform a head request for given url', inject([AsyncTestCompleter], async => {
backend.connections.subscribe((c) => { ObservableWrapper.subscribe(backend.connections, c => {
expect(c.request.method).toBe(RequestMethods.HEAD); expect(c.request.method).toBe(RequestMethods.HEAD);
backend.resolveAllConnections(); backend.resolveAllConnections();
async.done(); async.done();
}); });
http.head(url).subscribe(res => {}); ObservableWrapper.subscribe(http.head(url), res => {});
})); }));
}); });
}); });

View File

@ -22,14 +22,14 @@ export function main() {
// Compliant with spec described at https://url.spec.whatwg.org/#urlsearchparams // Compliant with spec described at https://url.spec.whatwg.org/#urlsearchparams
expect(searchParams.has("topic")).toBe(true); expect(searchParams.has("topic")).toBe(true);
expect(searchParams.has("foo")).toBe(false); expect(searchParams.has("foo")).toBe(false);
expect(searchParams.get("topic")).toBe("api"); expect(searchParams.get("topic")).toEqual("api");
expect(searchParams.getAll("topic")).toEqual(["api"]); expect(searchParams.getAll("topic")).toEqual(["api"]);
expect(searchParams.get("foo")).toBe(null); expect(searchParams.get("foo")).toBe(null);
searchParams.append("topic", "webdev"); searchParams.append("topic", "webdev");
expect(searchParams.getAll("topic")).toEqual(["api", "webdev"]); expect(searchParams.getAll("topic")).toEqual(["api", "webdev"]);
expect(searchParams.toString()).toBe("q=URLUtils.searchParams&topic=api&topic=webdev"); expect(searchParams.toString()).toEqual("q=URLUtils.searchParams&topic=api&topic=webdev");
searchParams.delete("topic"); searchParams.delete("topic");
expect(searchParams.toString()).toBe("q=URLUtils.searchParams"); expect(searchParams.toString()).toEqual("q=URLUtils.searchParams");
}); });
}); });
} }

View File

@ -0,0 +1,5 @@
library examples.e2e_test.http.http_spec;
main() {
}

View File

@ -1,4 +1,5 @@
import {bootstrap, Component, View, NgFor, Inject} from 'angular2/angular2'; import {bootstrap, Component, View, NgFor, Inject} from 'angular2/angular2';
import {ObservableWrapper} from 'angular2/src/facade/async';
import {Http, httpInjectables} from 'angular2/http'; import {Http, httpInjectables} from 'angular2/http';
@Component({selector: 'http-app'}) @Component({selector: 'http-app'})
@ -8,7 +9,7 @@ import {Http, httpInjectables} from 'angular2/http';
<h1>people</h1> <h1>people</h1>
<ul class="people"> <ul class="people">
<li *ng-for="#person of people"> <li *ng-for="#person of people">
hello, {{person.name}} hello, {{person['name']}}
</li> </li>
</ul> </ul>
` `
@ -16,6 +17,6 @@ import {Http, httpInjectables} from 'angular2/http';
export class HttpCmp { export class HttpCmp {
people: Object; people: Object;
constructor(http: Http) { constructor(http: Http) {
http.get('./people.json').map(res => res.json()).subscribe(people => this.people = people); ObservableWrapper.subscribe(http.get('./people.json'), res => this.people = res.json());
} }
} }

View File

@ -302,6 +302,7 @@ function define(classOrName, check) {
return cls; return cls;
} }
var assert: any = function(value) { var assert: any = function(value) {
return { return {
is: function is(...types) { is: function is(...types) {

View File

@ -5,8 +5,6 @@ config.baseUrl = 'http://localhost:8002/';
config.exclude.push( config.exclude.push(
'dist/js/cjs/examples/e2e_test/sourcemap/sourcemap_spec.js', 'dist/js/cjs/examples/e2e_test/sourcemap/sourcemap_spec.js',
//TODO(jeffbcross): remove when http has been implemented for dart
'dist/js/cjs/examples/e2e_test/http/http_spec.js',
// TODO: remove this line when largetable dart has been added // TODO: remove this line when largetable dart has been added
'dist/js/cjs/benchmarks_external/e2e_test/largetable_perf.js', 'dist/js/cjs/benchmarks_external/e2e_test/largetable_perf.js',
'dist/js/cjs/benchmarks_external/e2e_test/polymer_tree_perf.js', 'dist/js/cjs/benchmarks_external/e2e_test/polymer_tree_perf.js',

View File

@ -44,16 +44,7 @@ function stripModulePrefix(relativePath: string): string {
function getSourceTree() { function getSourceTree() {
// Transpile everything in 'modules' except for rtts_assertions. // Transpile everything in 'modules' except for rtts_assertions.
var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart'], var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart'], ['rtts_assert/**/*']);
// TODO(jeffbcross): add http when lib supports dart
[
'rtts_assert/**/*',
'examples/e2e_test/http/**/*',
'examples/src/http/**/*',
'angular2/src/http/**/*',
'angular2/test/http/**/*',
'angular2/http.ts'
]);
var transpiled = ts2dart(tsInputTree, { var transpiled = ts2dart(tsInputTree, {
generateLibraryName: true, generateLibraryName: true,
generateSourceMap: false, generateSourceMap: false,
@ -107,6 +98,11 @@ function getHtmlSourcesTree() {
return mergeTrees([htmlSrcsTree, urlParamsToFormTree]); return mergeTrees([htmlSrcsTree, urlParamsToFormTree]);
} }
function getExamplesJsonTree() {
// Copy JSON files
return modulesFunnel(['examples/**/*.json']);
}
function getTemplatedPubspecsTree() { function getTemplatedPubspecsTree() {
// The JSON structure for templating pubspec.yaml files. // The JSON structure for templating pubspec.yaml files.
@ -154,7 +150,7 @@ function getDocsTree() {
module.exports = function makeDartTree(options: AngularBuilderOptions) { module.exports = function makeDartTree(options: AngularBuilderOptions) {
var dartSources = dartfmt(getSourceTree(), {dartSDK: options.dartSDK, logs: options.logs}); var dartSources = dartfmt(getSourceTree(), {dartSDK: options.dartSDK, logs: options.logs});
var sourceTree = mergeTrees([dartSources, getHtmlSourcesTree()]); var sourceTree = mergeTrees([dartSources, getHtmlSourcesTree(), getExamplesJsonTree()]);
sourceTree = fixDartFolderLayout(sourceTree); sourceTree = fixDartFolderLayout(sourceTree);
var dartTree = mergeTrees([sourceTree, getTemplatedPubspecsTree(), getDocsTree()]); var dartTree = mergeTrees([sourceTree, getTemplatedPubspecsTree(), getDocsTree()]);