Skip to content

Commit 61cc341

Browse files
caitpgoderbauer
authored andcommitted
feat(http): serialize search parameters from request options
- Extends URLSearchParams API to include operations for combining different URLSearchParams objects: These new methods include: setAll(otherParams): performs `this.set(key, values[0])` for each key/value-list pair in `otherParams` appendAll(otherParams): performs `this.append(key, values)` for each key/value-list pair in `otherParams` replaceAll(otherParams): for each key/value-list pair in `otherParams`, replaces current set of values for `key` with a copy of the list of values. - RequestOptions do not merge search params automatically (because there are multiple ways to do this). Instead, they replace any existing `search` field if `search` is provided. Explicit merging is required if merging is desirable. - Some extra test coverage added. Closes angular#2417 Closes angular#3020
1 parent caed98d commit 61cc341

File tree

7 files changed

+210
-18
lines changed

7 files changed

+210
-18
lines changed

modules/angular2/src/http/base_request_options.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {CONST_EXPR, CONST, isPresent} from 'angular2/src/facade/lang';
1+
import {CONST_EXPR, CONST, isPresent, isString} from 'angular2/src/facade/lang';
22
import {Headers} from './headers';
33
import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums';
44
import {IRequestOptions} from './interfaces';
55
import {Injectable} from 'angular2/di';
6+
import {URLSearchParams} from './url_search_params';
67

78
/**
89
* Creates a request options object similar to the `RequestInit` description
@@ -33,14 +34,19 @@ export class RequestOptions implements IRequestOptions {
3334
credentials: RequestCredentialsOpts;
3435
cache: RequestCacheOpts;
3536
url: string;
36-
constructor({method, headers, body, mode, credentials, cache, url}: IRequestOptions = {}) {
37+
search: URLSearchParams;
38+
constructor({method, headers, body, mode, credentials, cache, url, search}:
39+
IRequestOptions = {}) {
3740
this.method = isPresent(method) ? method : null;
3841
this.headers = isPresent(headers) ? headers : null;
3942
this.body = isPresent(body) ? body : null;
4043
this.mode = isPresent(mode) ? mode : null;
4144
this.credentials = isPresent(credentials) ? credentials : null;
4245
this.cache = isPresent(cache) ? cache : null;
4346
this.url = isPresent(url) ? url : null;
47+
this.search = isPresent(search) ? (isString(search) ? new URLSearchParams(<string>(search)) :
48+
<URLSearchParams>(search)) :
49+
null;
4450
}
4551

4652
/**
@@ -56,7 +62,11 @@ export class RequestOptions implements IRequestOptions {
5662
credentials: isPresent(options) && isPresent(options.credentials) ? options.credentials :
5763
this.credentials,
5864
cache: isPresent(options) && isPresent(options.cache) ? options.cache : this.cache,
59-
url: isPresent(options) && isPresent(options.url) ? options.url : this.url
65+
url: isPresent(options) && isPresent(options.url) ? options.url : this.url,
66+
search: isPresent(options) && isPresent(options.search) ?
67+
(isString(options.search) ? new URLSearchParams(<string>(options.search)) :
68+
(<URLSearchParams>(options.search)).clone()) :
69+
this.search
6070
});
6171
}
6272
}

modules/angular2/src/http/http.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
1717
newOptions = newOptions.merge(new RequestOptions({
1818
method: providedOpts.method,
1919
url: providedOpts.url,
20+
search: providedOpts.search,
2021
headers: providedOpts.headers,
2122
body: providedOpts.body,
2223
mode: providedOpts.mode,

modules/angular2/src/http/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {Headers} from './headers';
1212
import {BaseException} from 'angular2/src/facade/lang';
1313
import {EventEmitter} from 'angular2/src/facade/async';
1414
import {Request} from './static_request';
15+
import {URLSearchParamsUnionFixer, URLSearchParams} from './url_search_params';
16+
17+
// Work around Dartanalyzer problem :(
18+
const URLSearchParams_UnionFixer = URLSearchParamsUnionFixer;
1519

1620
/**
1721
* Abstract class from which real backends are derived.
@@ -41,6 +45,7 @@ export class Connection {
4145
export interface IRequestOptions {
4246
url?: string;
4347
method?: RequestMethods;
48+
search?: string | URLSearchParams;
4449
headers?: Headers;
4550
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
4651
body?: string;

modules/angular2/src/http/static_request.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
RegExpWrapper,
77
CONST_EXPR,
88
isPresent,
9-
isJsObject
9+
isJsObject,
10+
StringWrapper
1011
} from 'angular2/src/facade/lang';
1112

1213
// TODO(jeffbcross): properly implement body accessors
@@ -39,7 +40,19 @@ export class Request {
3940
cache: RequestCacheOpts;
4041
constructor(requestOptions: RequestOptions) {
4142
// TODO: assert that url is present
43+
let url = requestOptions.url;
4244
this.url = requestOptions.url;
45+
if (isPresent(requestOptions.search)) {
46+
let search = requestOptions.search.toString();
47+
if (search.length > 0) {
48+
let prefix = '?';
49+
if (StringWrapper.contains(this.url, '?')) {
50+
prefix = (this.url[this.url.length - 1] == '&') ? '' : '&';
51+
}
52+
// TODO: just delete search-query-looking string in url?
53+
this.url = url + prefix + search;
54+
}
55+
}
4356
this._body = requestOptions.body;
4457
this.method = requestOptions.method;
4558
// TODO(jeffbcross): implement behavior

modules/angular2/src/http/url_search_params.ts

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
1+
import {CONST_EXPR, isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
22
import {
33
Map,
44
MapWrapper,
@@ -7,28 +7,42 @@ import {
77
isListLikeIterable
88
} from 'angular2/src/facade/collection';
99

10-
function paramParser(rawParams: string): Map<string, List<string>> {
10+
function paramParser(rawParams: string = ''): Map<string, List<string>> {
1111
var map: Map<string, List<string>> = new Map();
12-
var params: List<string> = StringWrapper.split(rawParams, new RegExp('&'));
13-
ListWrapper.forEach(params, (param: string) => {
14-
var split: List<string> = StringWrapper.split(param, new RegExp('='));
15-
var key = ListWrapper.get(split, 0);
16-
var val = ListWrapper.get(split, 1);
17-
var list = isPresent(map.get(key)) ? map.get(key) : [];
18-
list.push(val);
19-
map.set(key, list);
20-
});
12+
if (rawParams.length > 0) {
13+
var params: List<string> = StringWrapper.split(rawParams, new RegExp('&'));
14+
ListWrapper.forEach(params, (param: string) => {
15+
var split: List<string> = StringWrapper.split(param, new RegExp('='));
16+
var key = ListWrapper.get(split, 0);
17+
var val = ListWrapper.get(split, 1);
18+
var list = isPresent(map.get(key)) ? map.get(key) : [];
19+
list.push(val);
20+
map.set(key, list);
21+
});
22+
}
2123
return map;
2224
}
2325

26+
// TODO(caitp): This really should not be needed. Issue with ts2dart.
27+
export const URLSearchParamsUnionFixer: string = CONST_EXPR("UnionFixer");
28+
2429
/**
2530
* Map-like representation of url search parameters, based on
26-
* [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams) in the url living standard.
27-
*
31+
* [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams) in the url living standard,
32+
* with several extensions for merging URLSearchParams objects:
33+
* - setAll()
34+
* - appendAll()
35+
* - replaceAll()
2836
*/
2937
export class URLSearchParams {
3038
paramsMap: Map<string, List<string>>;
31-
constructor(public rawParams: string) { this.paramsMap = paramParser(rawParams); }
39+
constructor(public rawParams: string = '') { this.paramsMap = paramParser(rawParams); }
40+
41+
clone(): URLSearchParams {
42+
var clone = new URLSearchParams();
43+
clone.appendAll(this);
44+
return clone;
45+
}
3246

3347
has(param: string): boolean { return this.paramsMap.has(param); }
3448

@@ -46,13 +60,75 @@ export class URLSearchParams {
4660
return isPresent(mapParam) ? mapParam : [];
4761
}
4862

63+
set(param: string, val: string) {
64+
var mapParam = this.paramsMap.get(param);
65+
var list = isPresent(mapParam) ? mapParam : [];
66+
ListWrapper.clear(list);
67+
list.push(val);
68+
this.paramsMap.set(param, list);
69+
}
70+
71+
// A merge operation
72+
// For each name-values pair in `searchParams`, perform `set(name, values[0])`
73+
//
74+
// E.g: "a=[1,2,3], c=[8]" + "a=[4,5,6], b=[7]" = "a=[4], c=[8], b=[7]"
75+
//
76+
// TODO(@caitp): document this better
77+
setAll(searchParams: URLSearchParams) {
78+
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
79+
var mapParam = this.paramsMap.get(param);
80+
var list = isPresent(mapParam) ? mapParam : [];
81+
ListWrapper.clear(list);
82+
list.push(value[0]);
83+
this.paramsMap.set(param, list);
84+
});
85+
}
86+
4987
append(param: string, val: string): void {
5088
var mapParam = this.paramsMap.get(param);
5189
var list = isPresent(mapParam) ? mapParam : [];
5290
list.push(val);
5391
this.paramsMap.set(param, list);
5492
}
5593

94+
// A merge operation
95+
// For each name-values pair in `searchParams`, perform `append(name, value)`
96+
// for each value in `values`.
97+
//
98+
// E.g: "a=[1,2], c=[8]" + "a=[3,4], b=[7]" = "a=[1,2,3,4], c=[8], b=[7]"
99+
//
100+
// TODO(@caitp): document this better
101+
appendAll(searchParams: URLSearchParams) {
102+
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
103+
var mapParam = this.paramsMap.get(param);
104+
var list = isPresent(mapParam) ? mapParam : [];
105+
for (var i = 0; i < value.length; ++i) {
106+
list.push(value[i]);
107+
}
108+
this.paramsMap.set(param, list);
109+
});
110+
}
111+
112+
113+
// A merge operation
114+
// For each name-values pair in `searchParams`, perform `delete(name)`,
115+
// followed by `set(name, values)`
116+
//
117+
// E.g: "a=[1,2,3], c=[8]" + "a=[4,5,6], b=[7]" = "a=[4,5,6], c=[8], b=[7]"
118+
//
119+
// TODO(@caitp): document this better
120+
replaceAll(searchParams: URLSearchParams) {
121+
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
122+
var mapParam = this.paramsMap.get(param);
123+
var list = isPresent(mapParam) ? mapParam : [];
124+
ListWrapper.clear(list);
125+
for (var i = 0; i < value.length; ++i) {
126+
list.push(value[i]);
127+
}
128+
this.paramsMap.set(param, list);
129+
});
130+
}
131+
56132
toString(): string {
57133
var paramsList = [];
58134
MapWrapper.forEach(this.paramsMap, (values, k) => {

modules/angular2/test/http/http_spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {ResponseOptions} from 'angular2/src/http/base_response_options';
2121
import {Request} from 'angular2/src/http/static_request';
2222
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
2323
import {ConnectionBackend} from 'angular2/src/http/interfaces';
24+
import {URLSearchParams} from 'angular2/src/http/url_search_params';
2425

2526
class SpyObserver extends SpyObject {
2627
onNext: Function;
@@ -202,6 +203,47 @@ export function main() {
202203
ObservableWrapper.subscribe(http.head(url), res => {});
203204
}));
204205
});
206+
207+
208+
describe('searchParams', () => {
209+
it('should append search params to url', inject([AsyncTestCompleter], async => {
210+
var params = new URLSearchParams();
211+
params.append('q', 'puppies');
212+
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
213+
expect(c.request.url).toEqual('https://www.google.com?q=puppies');
214+
backend.resolveAllConnections();
215+
async.done();
216+
});
217+
ObservableWrapper.subscribe(
218+
http.get('https://www.google.com', new RequestOptions({search: params})),
219+
res => {});
220+
}));
221+
222+
223+
it('should append string search params to url', inject([AsyncTestCompleter], async => {
224+
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
225+
expect(c.request.url).toEqual('https://www.google.com?q=piggies');
226+
backend.resolveAllConnections();
227+
async.done();
228+
});
229+
ObservableWrapper.subscribe(
230+
http.get('https://www.google.com', new RequestOptions({search: 'q=piggies'})),
231+
res => {});
232+
}));
233+
234+
235+
it('should produce valid url when url already contains a query',
236+
inject([AsyncTestCompleter], async => {
237+
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
238+
expect(c.request.url).toEqual('https://www.google.com?q=angular&as_eq=1.x');
239+
backend.resolveAllConnections();
240+
async.done();
241+
});
242+
ObservableWrapper.subscribe(http.get('https://www.google.com?q=angular',
243+
new RequestOptions({search: 'as_eq=1.x'})),
244+
res => {});
245+
}));
246+
});
205247
});
206248
});
207249
}

modules/angular2/test/http/url_search_params_spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,51 @@ export function main() {
3030
expect(searchParams.toString()).toEqual("q=URLUtils.searchParams&topic=api&topic=webdev");
3131
searchParams.delete("topic");
3232
expect(searchParams.toString()).toEqual("q=URLUtils.searchParams");
33+
34+
// Test default constructor
35+
expect(new URLSearchParams().toString()).toBe("");
36+
});
37+
38+
39+
it('should support map-like merging operation via setAll()', () => {
40+
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
41+
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
42+
mapA.setAll(mapB);
43+
expect(mapA.has('a')).toBe(true);
44+
expect(mapA.has('b')).toBe(true);
45+
expect(mapA.has('c')).toBe(true);
46+
expect(mapA.getAll('a')).toEqual(['4']);
47+
expect(mapA.getAll('b')).toEqual(['7']);
48+
expect(mapA.getAll('c')).toEqual(['8']);
49+
expect(mapA.toString()).toEqual('a=4&c=8&b=7');
50+
});
51+
52+
53+
it('should support multimap-like merging operation via appendAll()', () => {
54+
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
55+
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
56+
mapA.appendAll(mapB);
57+
expect(mapA.has('a')).toBe(true);
58+
expect(mapA.has('b')).toBe(true);
59+
expect(mapA.has('c')).toBe(true);
60+
expect(mapA.getAll('a')).toEqual(['1', '2', '3', '4', '5', '6']);
61+
expect(mapA.getAll('b')).toEqual(['7']);
62+
expect(mapA.getAll('c')).toEqual(['8']);
63+
expect(mapA.toString()).toEqual('a=1&a=2&a=3&a=4&a=5&a=6&c=8&b=7');
64+
});
65+
66+
67+
it('should support multimap-like merging operation via replaceAll()', () => {
68+
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
69+
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
70+
mapA.replaceAll(mapB);
71+
expect(mapA.has('a')).toBe(true);
72+
expect(mapA.has('b')).toBe(true);
73+
expect(mapA.has('c')).toBe(true);
74+
expect(mapA.getAll('a')).toEqual(['4', '5', '6']);
75+
expect(mapA.getAll('b')).toEqual(['7']);
76+
expect(mapA.getAll('c')).toEqual(['8']);
77+
expect(mapA.toString()).toEqual('a=4&a=5&a=6&c=8&b=7');
3378
});
3479
});
3580
}

0 commit comments

Comments
 (0)