Skip to content

Commit

Permalink
feat(http): add basic http service
Browse files Browse the repository at this point in the history
This implementation only works in JavaScript, while the Observable transpilation
story gets worked out. Right now, the service just makes a simple request,
and returns an Observable of Response.

Additional functionality will be captured in separate issues.

Fixes #2028
  • Loading branch information
jeffbcross committed Jun 9, 2015
1 parent 363b9ba commit 2156810
Show file tree
Hide file tree
Showing 35 changed files with 1,054 additions and 2 deletions.
10 changes: 10 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,16 @@ gulp.task('!build/change_detect.dart', function(done) {
proc.stdout.pipe(dartStream);
});

// ------------
// additional tasks for building examples
gulp.task('build.http.example', function() {
//Copy over people.json used in http example
return gulp.src('modules/examples/src/http/people.json')
.pipe(gulp.dest(CONFIG.dest.js.prod.es5 + '/examples/src/http/'))
.pipe(gulp.dest(CONFIG.dest.js.dev.es5 + '/examples/src/http/'))
.pipe(gulp.dest(CONFIG.dest.js.dart2js + '/examples/src/http/'));
});

// ------------
// angular material testing rules
gulp.task('build.css.material', function() {
Expand Down
11 changes: 11 additions & 0 deletions modules/angular2/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {bind, Binding} from 'angular2/di';
import {Http, HttpFactory} from './src/http/http';
import {XHRBackend} from 'angular2/src/http/backends/xhr_backend';
import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr';

export {Http};
export var httpInjectables: List<any> = [
XHRBackend,
bind(BrowserXHR).toValue(BrowserXHR),
bind(Http).toFactory(HttpFactory, [XHRBackend])
];
2 changes: 1 addition & 1 deletion modules/angular2/src/facade/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,5 @@ export class EventEmitter extends Observable {

throw(error) { this._subject.onError(error); }

return (value) { this._subject.onCompleted(); }
return (value?) { this._subject.onCompleted(); }
}
1 change: 1 addition & 0 deletions modules/angular2/src/facade/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export class ListWrapper {
l.sort();
}
}
static toString<T>(l: List<T>): string { return l.toString(); }
}

export function isListLikeIterable(obj): boolean {
Expand Down
9 changes: 9 additions & 0 deletions modules/angular2/src/http/backends/browser_xhr.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
library angular2.src.http.backends.browser_xhr;

/// import 'dart:html' show HttpRequest;
/// import 'package:angular2/di.dart';
/// @Injectable()
/// class BrowserXHR {
/// factory BrowserXHR() => new HttpRequest();
/// }
9 changes: 9 additions & 0 deletions modules/angular2/src/http/backends/browser_xhr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare var window;

import {Injectable} from 'angular2/di';

// Make sure not to evaluate this in a non-browser environment!
@Injectable()
export class BrowserXHR {
constructor() { return <any>(new window.XMLHttpRequest()); }
}
106 changes: 106 additions & 0 deletions modules/angular2/src/http/backends/mock_backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {Injectable} from 'angular2/di';
import {Request} from 'angular2/src/http/static_request';
import {Response} from 'angular2/src/http/static_response';
import {ReadyStates} from 'angular2/src/http/enums';
import * as Rx from 'rx';

/**
* Connection represents a request and response for an underlying transport, like XHR or mock.
* The mock implementation contains helper methods to respond to connections within tests.
* API subject to change and expand.
**/
export class Connection {
/**
* Observer to call on download progress, if provided in config.
**/
downloadObserver: Rx.Observer<Response>;

/**
* TODO
* Name `readyState` should change to be more generic, and states could be made to be more
* descriptive than XHR states.
**/

readyState: ReadyStates;
request: Request;
response: Rx.Subject<Response>;

constructor(req: Request) {
// State
if (Rx.hasOwnProperty('default')) {
this.response = new ((<any>Rx).default.Rx.Subject)();
} else {
this.response = new Rx.Subject<Response>();
}

this.readyState = ReadyStates.OPEN;
this.request = req;
this.dispose = this.dispose.bind(this);
}

dispose() {
if (this.readyState !== ReadyStates.DONE) {
this.readyState = ReadyStates.CANCELLED;
}
}

/**
* Called after a connection has been established.
**/
mockRespond(res: Response) {
if (this.readyState >= ReadyStates.DONE) {
throw new Error('Connection has already been resolved');
}
this.readyState = ReadyStates.DONE;
this.response.onNext(res);
this.response.onCompleted();
}

mockDownload(res: Response) {
this.downloadObserver.onNext(res);
if (res.bytesLoaded === res.totalBytes) {
this.downloadObserver.onCompleted();
}
}

mockError(err?) {
// Matches XHR semantics
this.readyState = ReadyStates.DONE;
this.response.onError(err);
this.response.onCompleted();
}
}

@Injectable()
export class MockBackend {
connections: Rx.Subject<Connection>;
connectionsArray: Array<Connection>;
pendingConnections: Rx.Observable<Connection>;
constructor() {
this.connectionsArray = [];
if (Rx.hasOwnProperty('default')) {
this.connections = new (<any>Rx).default.Rx.Subject();
} else {
this.connections = new Rx.Subject<Connection>();
}
this.connections.subscribe(connection => this.connectionsArray.push(connection));
this.pendingConnections = this.connections.filter((c) => c.readyState < ReadyStates.DONE);
}

verifyNoPendingRequests() {
let pending = 0;
this.pendingConnections.subscribe((c) => pending++);
if (pending > 0) throw new Error(`${pending} pending connections to be resolved`);
}

resolveAllConnections() { this.connections.subscribe((c) => c.readyState = 4); }

createConnection(req: Request) {
if (!req || !(req instanceof Request)) {
throw new Error(`createConnection requires an instance of Request, got ${req}`);
}
let connection = new Connection(req);
this.connections.onNext(connection);
return connection;
}
}
40 changes: 40 additions & 0 deletions modules/angular2/src/http/backends/xhr_backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {ConnectionBackend, Connection} from '../interfaces';
import {ReadyStates, RequestMethods} from '../enums';
import {Request} from '../static_request';
import {Response} from '../static_response';
import {Inject} from 'angular2/di';
import {Injectable} from 'angular2/di';
import {BrowserXHR} from './browser_xhr';
import * as Rx from 'rx';

export class XHRConnection implements Connection {
request: Request;
response: Rx.Subject<Response>;
readyState: ReadyStates;
private _xhr;
constructor(req: Request, NativeConstruct: any) {
this.request = req;
if (Rx.hasOwnProperty('default')) {
this.response = new (<any>Rx).default.Rx.Subject();
} else {
this.response = new Rx.Subject<Response>();
}
this._xhr = new NativeConstruct();
this._xhr.open(RequestMethods[req.method], req.url);
this._xhr.addEventListener(
'load',
() => {this.response.onNext(new Response(this._xhr.response || this._xhr.responseText))});
// TODO(jeffbcross): make this more dynamic based on body type
this._xhr.send(this.request.text());
}

dispose(): void { this._xhr.abort(); }
}

@Injectable()
export class XHRBackend implements ConnectionBackend {
constructor(private _NativeConstruct: BrowserXHR) {}
createConnection(request: Request): XHRConnection {
return new XHRConnection(request, this._NativeConstruct);
}
}
21 changes: 21 additions & 0 deletions modules/angular2/src/http/base_request_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {CONST_EXPR, CONST} from 'angular2/src/facade/lang';
import {Headers} from './headers';
import {URLSearchParams} from './url_search_params';
import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums';
import {RequestOptions} from './interfaces';
import {Injectable} from 'angular2/di';

@Injectable()
export class BaseRequestOptions implements RequestOptions {
method: RequestMethods;
headers: Headers;
body: URLSearchParams | FormData | string;
mode: RequestModesOpts;
credentials: RequestCredentialsOpts;
cache: RequestCacheOpts;

constructor() {
this.method = RequestMethods.GET;
this.mode = RequestModesOpts.Cors;
}
}
23 changes: 23 additions & 0 deletions modules/angular2/src/http/base_response_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Headers} from './headers';
import {ResponseTypes} from './enums';
import {ResponseOptions} from './interfaces';

export class BaseResponseOptions implements ResponseOptions {
status: number;
headers: Headers | Object;
statusText: string;
type: ResponseTypes;
url: string;

constructor({status = 200, statusText = 'Ok', type = ResponseTypes.Default,
headers = new Headers(), url = ''}: ResponseOptions = {}) {
this.status = status;
this.statusText = statusText;
this.type = type;
this.headers = headers;
this.url = url;
}
}
;

export var baseResponseOptions = Object.freeze(new BaseResponseOptions());
12 changes: 12 additions & 0 deletions modules/angular2/src/http/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

export enum RequestModesOpts { Cors, NoCors, SameOrigin };

export enum RequestCacheOpts { Default, NoStore, Reload, NoCache, ForceCache, OnlyIfCached };

export enum RequestCredentialsOpts { Omit, SameOrigin, Include };

export enum RequestMethods { GET, POST, PUT, DELETE, OPTIONS, HEAD };

export enum ReadyStates { UNSENT, OPEN, HEADERS_RECEIVED, LOADING, DONE, CANCELLED };

export enum ResponseTypes { Basic, Cors, Default, Error, Opaque }
79 changes: 79 additions & 0 deletions modules/angular2/src/http/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
isPresent,
isBlank,
isJsObject,
isType,
StringWrapper,
BaseException
} from 'angular2/src/facade/lang';
import {
isListLikeIterable,
List,
Map,
MapWrapper,
ListWrapper
} from 'angular2/src/facade/collection';

// (@jeffbcross): This is implemented mostly to spec, except that the entries method has been
// removed because it doesn't exist in dart, and it doesn't seem worth adding it to the facade.

export class Headers {
_headersMap: Map<string, List<string>>;
constructor(headers?: Headers | Object) {
if (isBlank(headers)) {
this._headersMap = MapWrapper.create();
return;
}

if (isPresent((<Headers>headers)._headersMap)) {
this._headersMap = (<Headers>headers)._headersMap;
} else if (isJsObject(headers)) {
this._headersMap = MapWrapper.createFromStringMap(headers);
MapWrapper.forEach(this._headersMap, (v, k) => {
if (!isListLikeIterable(v)) {
var list = ListWrapper.create();
ListWrapper.push(list, v);
MapWrapper.set(this._headersMap, k, list);
}
});
}
}

append(name: string, value: string): void {
var list = MapWrapper.get(this._headersMap, name) || ListWrapper.create();
ListWrapper.push(list, value);
MapWrapper.set(this._headersMap, name, list);
}

delete (name: string): void { MapWrapper.delete(this._headersMap, name); }

forEach(fn: Function) { return MapWrapper.forEach(this._headersMap, fn); }

get(header: string): string {
return ListWrapper.first(MapWrapper.get(this._headersMap, header));
}

has(header: string) { return MapWrapper.contains(this._headersMap, header); }

keys() { 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 {
var list = ListWrapper.create();
if (!isListLikeIterable(value)) {
ListWrapper.push(list, value);
} else {
ListWrapper.push(list, ListWrapper.toString((<List<string>>value)));
}

MapWrapper.set(this._headersMap, header, list);
}

values() { return MapWrapper.values(this._headersMap); }

getAll(header: string): Array<string> {
return MapWrapper.get(this._headersMap, header) || ListWrapper.create();
}

entries() { throw new BaseException('"entries" method is not implemented on Headers class'); }
}
Loading

2 comments on commit 2156810

@matsko
Copy link
Contributor

@matsko matsko commented on 2156810 Jun 9, 2015

Choose a reason for hiding this comment

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

+1 :)

@cironunes
Copy link
Member

Choose a reason for hiding this comment

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

<3

Please sign in to comment.