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 #2417 Closes #3020
This commit is contained in:
parent
dfa5103b1d
commit
77d3668432
|
@ -1,8 +1,9 @@
|
||||||
import {CONST_EXPR, CONST, isPresent} from 'angular2/src/facade/lang';
|
import {CONST_EXPR, CONST, isPresent, isString} from 'angular2/src/facade/lang';
|
||||||
import {Headers} from './headers';
|
import {Headers} from './headers';
|
||||||
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 {URLSearchParams} from './url_search_params';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a request options object similar to the `RequestInit` description
|
* Creates a request options object similar to the `RequestInit` description
|
||||||
|
@ -33,7 +34,9 @@ export class RequestOptions implements IRequestOptions {
|
||||||
credentials: RequestCredentialsOpts;
|
credentials: RequestCredentialsOpts;
|
||||||
cache: RequestCacheOpts;
|
cache: RequestCacheOpts;
|
||||||
url: string;
|
url: string;
|
||||||
constructor({method, headers, body, mode, credentials, cache, url}: IRequestOptions = {}) {
|
search: URLSearchParams;
|
||||||
|
constructor({method, headers, body, mode, credentials, cache, url, search}:
|
||||||
|
IRequestOptions = {}) {
|
||||||
this.method = isPresent(method) ? method : null;
|
this.method = isPresent(method) ? method : null;
|
||||||
this.headers = isPresent(headers) ? headers : null;
|
this.headers = isPresent(headers) ? headers : null;
|
||||||
this.body = isPresent(body) ? body : null;
|
this.body = isPresent(body) ? body : null;
|
||||||
|
@ -41,6 +44,9 @@ export class RequestOptions implements IRequestOptions {
|
||||||
this.credentials = isPresent(credentials) ? credentials : null;
|
this.credentials = isPresent(credentials) ? credentials : null;
|
||||||
this.cache = isPresent(cache) ? cache : null;
|
this.cache = isPresent(cache) ? cache : null;
|
||||||
this.url = isPresent(url) ? url : null;
|
this.url = isPresent(url) ? url : null;
|
||||||
|
this.search = isPresent(search) ? (isString(search) ? new URLSearchParams(<string>(search)) :
|
||||||
|
<URLSearchParams>(search)) :
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +62,11 @@ export class RequestOptions implements IRequestOptions {
|
||||||
credentials: isPresent(options) && isPresent(options.credentials) ? options.credentials :
|
credentials: isPresent(options) && isPresent(options.credentials) ? options.credentials :
|
||||||
this.credentials,
|
this.credentials,
|
||||||
cache: isPresent(options) && isPresent(options.cache) ? options.cache : this.cache,
|
cache: isPresent(options) && isPresent(options.cache) ? options.cache : this.cache,
|
||||||
url: isPresent(options) && isPresent(options.url) ? options.url : this.url
|
url: isPresent(options) && isPresent(options.url) ? options.url : this.url,
|
||||||
|
search: isPresent(options) && isPresent(options.search) ?
|
||||||
|
(isString(options.search) ? new URLSearchParams(<string>(options.search)) :
|
||||||
|
(<URLSearchParams>(options.search)).clone()) :
|
||||||
|
this.search
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
|
||||||
newOptions = newOptions.merge(new RequestOptions({
|
newOptions = newOptions.merge(new RequestOptions({
|
||||||
method: providedOpts.method,
|
method: providedOpts.method,
|
||||||
url: providedOpts.url,
|
url: providedOpts.url,
|
||||||
|
search: providedOpts.search,
|
||||||
headers: providedOpts.headers,
|
headers: providedOpts.headers,
|
||||||
body: providedOpts.body,
|
body: providedOpts.body,
|
||||||
mode: providedOpts.mode,
|
mode: providedOpts.mode,
|
||||||
|
|
|
@ -12,6 +12,10 @@ import {Headers} from './headers';
|
||||||
import {BaseException} from 'angular2/src/facade/lang';
|
import {BaseException} from 'angular2/src/facade/lang';
|
||||||
import {EventEmitter} from 'angular2/src/facade/async';
|
import {EventEmitter} from 'angular2/src/facade/async';
|
||||||
import {Request} from './static_request';
|
import {Request} from './static_request';
|
||||||
|
import {URLSearchParamsUnionFixer, URLSearchParams} from './url_search_params';
|
||||||
|
|
||||||
|
// Work around Dartanalyzer problem :(
|
||||||
|
const URLSearchParams_UnionFixer = URLSearchParamsUnionFixer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class from which real backends are derived.
|
* Abstract class from which real backends are derived.
|
||||||
|
@ -41,6 +45,7 @@ export class Connection {
|
||||||
export interface IRequestOptions {
|
export interface IRequestOptions {
|
||||||
url?: string;
|
url?: string;
|
||||||
method?: RequestMethods;
|
method?: RequestMethods;
|
||||||
|
search?: string | URLSearchParams;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
|
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
|
||||||
body?: string;
|
body?: string;
|
||||||
|
|
|
@ -6,7 +6,8 @@ import {
|
||||||
RegExpWrapper,
|
RegExpWrapper,
|
||||||
CONST_EXPR,
|
CONST_EXPR,
|
||||||
isPresent,
|
isPresent,
|
||||||
isJsObject
|
isJsObject,
|
||||||
|
StringWrapper
|
||||||
} from 'angular2/src/facade/lang';
|
} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
// TODO(jeffbcross): properly implement body accessors
|
// TODO(jeffbcross): properly implement body accessors
|
||||||
|
@ -39,7 +40,19 @@ export class Request {
|
||||||
cache: RequestCacheOpts;
|
cache: RequestCacheOpts;
|
||||||
constructor(requestOptions: RequestOptions) {
|
constructor(requestOptions: RequestOptions) {
|
||||||
// TODO: assert that url is present
|
// TODO: assert that url is present
|
||||||
|
let url = requestOptions.url;
|
||||||
this.url = requestOptions.url;
|
this.url = requestOptions.url;
|
||||||
|
if (isPresent(requestOptions.search)) {
|
||||||
|
let search = requestOptions.search.toString();
|
||||||
|
if (search.length > 0) {
|
||||||
|
let prefix = '?';
|
||||||
|
if (StringWrapper.contains(this.url, '?')) {
|
||||||
|
prefix = (this.url[this.url.length - 1] == '&') ? '' : '&';
|
||||||
|
}
|
||||||
|
// TODO: just delete search-query-looking string in url?
|
||||||
|
this.url = url + prefix + search;
|
||||||
|
}
|
||||||
|
}
|
||||||
this._body = requestOptions.body;
|
this._body = requestOptions.body;
|
||||||
this.method = requestOptions.method;
|
this.method = requestOptions.method;
|
||||||
// TODO(jeffbcross): implement behavior
|
// TODO(jeffbcross): implement behavior
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
|
import {CONST_EXPR, isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
|
||||||
import {
|
import {
|
||||||
Map,
|
Map,
|
||||||
MapWrapper,
|
MapWrapper,
|
||||||
|
@ -7,8 +7,9 @@ import {
|
||||||
isListLikeIterable
|
isListLikeIterable
|
||||||
} from 'angular2/src/facade/collection';
|
} 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();
|
||||||
|
if (rawParams.length > 0) {
|
||||||
var params: List<string> = StringWrapper.split(rawParams, new RegExp('&'));
|
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, new RegExp('='));
|
var split: List<string> = StringWrapper.split(param, new RegExp('='));
|
||||||
|
@ -18,17 +19,30 @@ function paramParser(rawParams: string): Map<string, List<string>> {
|
||||||
list.push(val);
|
list.push(val);
|
||||||
map.set(key, list);
|
map.set(key, list);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(caitp): This really should not be needed. Issue with ts2dart.
|
||||||
|
export const URLSearchParamsUnionFixer: string = CONST_EXPR("UnionFixer");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map-like representation of url search parameters, based on
|
* Map-like representation of url search parameters, based on
|
||||||
* [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams) in the url living standard.
|
* [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams) in the url living standard,
|
||||||
*
|
* with several extensions for merging URLSearchParams objects:
|
||||||
|
* - setAll()
|
||||||
|
* - appendAll()
|
||||||
|
* - replaceAll()
|
||||||
*/
|
*/
|
||||||
export class URLSearchParams {
|
export class URLSearchParams {
|
||||||
paramsMap: Map<string, List<string>>;
|
paramsMap: Map<string, List<string>>;
|
||||||
constructor(public rawParams: string) { this.paramsMap = paramParser(rawParams); }
|
constructor(public rawParams: string = '') { this.paramsMap = paramParser(rawParams); }
|
||||||
|
|
||||||
|
clone(): URLSearchParams {
|
||||||
|
var clone = new URLSearchParams();
|
||||||
|
clone.appendAll(this);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
has(param: string): boolean { return this.paramsMap.has(param); }
|
has(param: string): boolean { return this.paramsMap.has(param); }
|
||||||
|
|
||||||
|
@ -46,6 +60,30 @@ export class URLSearchParams {
|
||||||
return isPresent(mapParam) ? mapParam : [];
|
return isPresent(mapParam) ? mapParam : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set(param: string, val: string) {
|
||||||
|
var mapParam = this.paramsMap.get(param);
|
||||||
|
var list = isPresent(mapParam) ? mapParam : [];
|
||||||
|
ListWrapper.clear(list);
|
||||||
|
list.push(val);
|
||||||
|
this.paramsMap.set(param, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A merge operation
|
||||||
|
// For each name-values pair in `searchParams`, perform `set(name, values[0])`
|
||||||
|
//
|
||||||
|
// E.g: "a=[1,2,3], c=[8]" + "a=[4,5,6], b=[7]" = "a=[4], c=[8], b=[7]"
|
||||||
|
//
|
||||||
|
// TODO(@caitp): document this better
|
||||||
|
setAll(searchParams: URLSearchParams) {
|
||||||
|
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
|
||||||
|
var mapParam = this.paramsMap.get(param);
|
||||||
|
var list = isPresent(mapParam) ? mapParam : [];
|
||||||
|
ListWrapper.clear(list);
|
||||||
|
list.push(value[0]);
|
||||||
|
this.paramsMap.set(param, list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
append(param: string, val: string): void {
|
append(param: string, val: string): void {
|
||||||
var mapParam = this.paramsMap.get(param);
|
var mapParam = this.paramsMap.get(param);
|
||||||
var list = isPresent(mapParam) ? mapParam : [];
|
var list = isPresent(mapParam) ? mapParam : [];
|
||||||
|
@ -53,6 +91,44 @@ export class URLSearchParams {
|
||||||
this.paramsMap.set(param, list);
|
this.paramsMap.set(param, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A merge operation
|
||||||
|
// For each name-values pair in `searchParams`, perform `append(name, value)`
|
||||||
|
// for each value in `values`.
|
||||||
|
//
|
||||||
|
// E.g: "a=[1,2], c=[8]" + "a=[3,4], b=[7]" = "a=[1,2,3,4], c=[8], b=[7]"
|
||||||
|
//
|
||||||
|
// TODO(@caitp): document this better
|
||||||
|
appendAll(searchParams: URLSearchParams) {
|
||||||
|
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
|
||||||
|
var mapParam = this.paramsMap.get(param);
|
||||||
|
var list = isPresent(mapParam) ? mapParam : [];
|
||||||
|
for (var i = 0; i < value.length; ++i) {
|
||||||
|
list.push(value[i]);
|
||||||
|
}
|
||||||
|
this.paramsMap.set(param, list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A merge operation
|
||||||
|
// For each name-values pair in `searchParams`, perform `delete(name)`,
|
||||||
|
// followed by `set(name, values)`
|
||||||
|
//
|
||||||
|
// E.g: "a=[1,2,3], c=[8]" + "a=[4,5,6], b=[7]" = "a=[4,5,6], c=[8], b=[7]"
|
||||||
|
//
|
||||||
|
// TODO(@caitp): document this better
|
||||||
|
replaceAll(searchParams: URLSearchParams) {
|
||||||
|
MapWrapper.forEach(searchParams.paramsMap, (value, param) => {
|
||||||
|
var mapParam = this.paramsMap.get(param);
|
||||||
|
var list = isPresent(mapParam) ? mapParam : [];
|
||||||
|
ListWrapper.clear(list);
|
||||||
|
for (var i = 0; i < value.length; ++i) {
|
||||||
|
list.push(value[i]);
|
||||||
|
}
|
||||||
|
this.paramsMap.set(param, list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
var paramsList = [];
|
var paramsList = [];
|
||||||
MapWrapper.forEach(this.paramsMap, (values, k) => {
|
MapWrapper.forEach(this.paramsMap, (values, k) => {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {ResponseOptions} from 'angular2/src/http/base_response_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 {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||||
import {ConnectionBackend} from 'angular2/src/http/interfaces';
|
import {ConnectionBackend} from 'angular2/src/http/interfaces';
|
||||||
|
import {URLSearchParams} from 'angular2/src/http/url_search_params';
|
||||||
|
|
||||||
class SpyObserver extends SpyObject {
|
class SpyObserver extends SpyObject {
|
||||||
onNext: Function;
|
onNext: Function;
|
||||||
|
@ -202,6 +203,47 @@ export function main() {
|
||||||
ObservableWrapper.subscribe(http.head(url), res => {});
|
ObservableWrapper.subscribe(http.head(url), res => {});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('searchParams', () => {
|
||||||
|
it('should append search params to url', inject([AsyncTestCompleter], async => {
|
||||||
|
var params = new URLSearchParams();
|
||||||
|
params.append('q', 'puppies');
|
||||||
|
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
|
||||||
|
expect(c.request.url).toEqual('https://www.google.com?q=puppies');
|
||||||
|
backend.resolveAllConnections();
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
ObservableWrapper.subscribe(
|
||||||
|
http.get('https://www.google.com', new RequestOptions({search: params})),
|
||||||
|
res => {});
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should append string search params to url', inject([AsyncTestCompleter], async => {
|
||||||
|
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
|
||||||
|
expect(c.request.url).toEqual('https://www.google.com?q=piggies');
|
||||||
|
backend.resolveAllConnections();
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
ObservableWrapper.subscribe(
|
||||||
|
http.get('https://www.google.com', new RequestOptions({search: 'q=piggies'})),
|
||||||
|
res => {});
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should produce valid url when url already contains a query',
|
||||||
|
inject([AsyncTestCompleter], async => {
|
||||||
|
ObservableWrapper.subscribe<MockConnection>(backend.connections, c => {
|
||||||
|
expect(c.request.url).toEqual('https://www.google.com?q=angular&as_eq=1.x');
|
||||||
|
backend.resolveAllConnections();
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
ObservableWrapper.subscribe(http.get('https://www.google.com?q=angular',
|
||||||
|
new RequestOptions({search: 'as_eq=1.x'})),
|
||||||
|
res => {});
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,51 @@ export function main() {
|
||||||
expect(searchParams.toString()).toEqual("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()).toEqual("q=URLUtils.searchParams");
|
expect(searchParams.toString()).toEqual("q=URLUtils.searchParams");
|
||||||
|
|
||||||
|
// Test default constructor
|
||||||
|
expect(new URLSearchParams().toString()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support map-like merging operation via setAll()', () => {
|
||||||
|
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
|
||||||
|
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
|
||||||
|
mapA.setAll(mapB);
|
||||||
|
expect(mapA.has('a')).toBe(true);
|
||||||
|
expect(mapA.has('b')).toBe(true);
|
||||||
|
expect(mapA.has('c')).toBe(true);
|
||||||
|
expect(mapA.getAll('a')).toEqual(['4']);
|
||||||
|
expect(mapA.getAll('b')).toEqual(['7']);
|
||||||
|
expect(mapA.getAll('c')).toEqual(['8']);
|
||||||
|
expect(mapA.toString()).toEqual('a=4&c=8&b=7');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support multimap-like merging operation via appendAll()', () => {
|
||||||
|
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
|
||||||
|
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
|
||||||
|
mapA.appendAll(mapB);
|
||||||
|
expect(mapA.has('a')).toBe(true);
|
||||||
|
expect(mapA.has('b')).toBe(true);
|
||||||
|
expect(mapA.has('c')).toBe(true);
|
||||||
|
expect(mapA.getAll('a')).toEqual(['1', '2', '3', '4', '5', '6']);
|
||||||
|
expect(mapA.getAll('b')).toEqual(['7']);
|
||||||
|
expect(mapA.getAll('c')).toEqual(['8']);
|
||||||
|
expect(mapA.toString()).toEqual('a=1&a=2&a=3&a=4&a=5&a=6&c=8&b=7');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support multimap-like merging operation via replaceAll()', () => {
|
||||||
|
var mapA = new URLSearchParams('a=1&a=2&a=3&c=8');
|
||||||
|
var mapB = new URLSearchParams('a=4&a=5&a=6&b=7');
|
||||||
|
mapA.replaceAll(mapB);
|
||||||
|
expect(mapA.has('a')).toBe(true);
|
||||||
|
expect(mapA.has('b')).toBe(true);
|
||||||
|
expect(mapA.has('c')).toBe(true);
|
||||||
|
expect(mapA.getAll('a')).toEqual(['4', '5', '6']);
|
||||||
|
expect(mapA.getAll('b')).toEqual(['7']);
|
||||||
|
expect(mapA.getAll('c')).toEqual(['8']);
|
||||||
|
expect(mapA.toString()).toEqual('a=4&a=5&a=6&c=8&b=7');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue