Skip to content

Commit

Permalink
feat(query-strings): move query string helpers from route-recognizer …
Browse files Browse the repository at this point in the history
…so they can be reused

This replace the implementation of buildQueryString with the one from route-recognizer to reduce code duplication. It and also includes route-recognizer's parseQueryString helper for convenience. Previously the only consumer of buildQueryString was http-client, which works fine with the new implementation.
  • Loading branch information
bryanrsmith committed Aug 28, 2015
1 parent d99c08e commit adccb98
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 53 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This library has **NO** external dependencies.
* [aurelia-templating](https://github.com/aurelia/templating)
* [aurelia-templating-router](https://github.com/aurelia/templating-router)
* [aurelia-router](https://github.com/aurelia/router)
* [aurelia-route-recognizer](https://github.com/aurelia/route-recognizer)
* [aurelia-loader](https://github.com/aurelia/loader)
* [aurelia-framework](https://github.com/aurelia/framework)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"aurelia-http-client",
"aurelia-templating",
"aurelia-router",
"aurelia-route-recognizer",
"aurelia-templating-router",
"aurelia-loader",
"aurelia-framework"
Expand Down
125 changes: 73 additions & 52 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,70 +86,91 @@ export function join(path1: string, path2: string): string {
return urlPrefix + url3.join('/').replace(/\:\//g, '://') + trailingSlash;
}

export function buildQueryString(a: Object, traditional?: boolean): string {
let s = [];

function add(key: string, value: any) {
// If value is a function, invoke it and return its value
let v = value;
if (typeof value === 'function') {
v = value();
} else if (value === null || value === undefined) {
v = '';

/**
* Generate a query string from an object.
*
* @param params Object containing the keys and values to be used.
* @returns The generated query string, excluding leading '?'.
*/
export function buildQueryString(params: Object): string {
let pairs = [];
let keys = Object.keys(params || {}).sort();
let encode = encodeURIComponent;
let encodeKey = k => encode(k).replace('%24', '$');

for (let i = 0, len = keys.length; i < len; i++) {
let key = keys[i];
let value = params[key];
if (value === null || value === undefined) {
continue;
}

s.push(encodeURIComponent(key) + '=' + encodeURIComponent(v));
if (Array.isArray(value)) {
let arrayKey = `${encodeKey(key)}[]`;
for (let j = 0, l = value.length; j < l; j++) {
pairs.push(`${arrayKey}=${encode(value[j])}`);
}
} else {
pairs.push(`${encodeKey(key)}=${encode(value)}`);
}
}

for (let prefix in a) {
_buildQueryString(prefix, a[prefix], traditional, add);
if (pairs.length === 0) {
return '';
}

// Return the resulting serialization
return s.join('&').replace(r20, '+');
return pairs.join('&');
}

function _buildQueryString(prefix: string, obj: any, traditional: boolean, add: (p: string, v: any) => void): void {
if (Array.isArray(obj)) {
// Serialize array item.
obj.forEach((v, i) => {
if (traditional || rbracket.test(prefix)) {
// Treat each array item as a scalar.
add(prefix, v);
} else {
// Item is non-scalar (array or object), encode its numeric index.
let innerPrefix = prefix + '[' + (typeof v === 'object' ? i : '') + ']';
_buildQueryString(innerPrefix, v, traditional, add);
}
});
} else if (!traditional && type(obj) === 'object') {
// Serialize object item.
for (let name in obj) {
_buildQueryString(prefix + '[' + name + ']', obj[name], traditional, add);
}
} else {
// Serialize scalar item.
add(prefix, obj);
/**
* Parse a query string.
*
* @param The query string to parse.
* @returns Object with keys and values mapped from the query string.
*/
export function parseQueryString(queryString: string): Object {
let queryParams = {};
if (!queryString || typeof queryString !== 'string') {
return queryParams;
}
}

const r20 = /%20/g;
const rbracket = /\[\]$/;
const class2type = {};
let query = queryString;
if (query.charAt(0) === '?') {
query = query.substr(1);
}

let pairs = query.split('&');
for (let i = 0; i < pairs.length; i++) {
let pair = pairs[i].split('=');
let key = decodeURIComponent(pair[0]);
let keyLength = key.length;
let isArray = false;
let value;

'Boolean Number String Function Array Date RegExp Object Error'
.split(' ')
.forEach((name) => {
class2type['[object ' + name + ']'] = name.toLowerCase();
});
if (!key) {
continue;
} else if (pair.length === 1) {
value = true;
} else {
//Handle arrays
if (keyLength > 2 && key.slice(keyLength - 2) === '[]') {
isArray = true;
key = key.slice(0, keyLength - 2);
if (!queryParams[key]) {
queryParams[key] = [];
}
}

function type(obj: any) {
if (obj === null || obj === undefined) {
return obj + '';
value = pair[1] ? decodeURIComponent(pair[1]) : '';
}

if (isArray) {
queryParams[key].push(value);
} else {
queryParams[key] = value;
}
}

// Support: Android<4.0 (functionish RegExp)
return typeof obj === 'object' || typeof obj === 'function'
? class2type[toString.call(obj)] || 'object'
: typeof obj;
return queryParams;
}
48 changes: 47 additions & 1 deletion test/path.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { relativeToFile, join } from '../src/index';
import { relativeToFile, join, parseQueryString, buildQueryString } from '../src/index';

describe('relativeToFile', () => {
it('can make a dot path relative to a simple file', () => {
Expand Down Expand Up @@ -178,3 +178,49 @@ describe('join', () => {
});
});

describe('query strings', () => {
it('should build query strings', () => {
let gen = buildQueryString;

expect(gen()).toBe('');
expect(gen(null)).toBe('');
expect(gen({})).toBe('');
expect(gen({ a: null })).toBe('');

expect(gen({ '': 'a' })).toBe('=a');
expect(gen({ a: 'b' })).toBe('a=b');
expect(gen({ a: 'b', c: 'd' })).toBe('a=b&c=d');
expect(gen({ a: 'b', c: null })).toBe('a=b');

expect(gen({ a: [ 'b', 'c' ]})).toBe('a[]=b&a[]=c');
expect(gen({ '&': [ 'b', 'c' ]})).toBe('%26[]=b&%26[]=c');

expect(gen({ a: '&' })).toBe('a=%26');
expect(gen({ '&': 'a' })).toBe('%26=a');
expect(gen({ a: true })).toBe('a=true');
expect(gen({ '$test': true })).toBe('$test=true');
});

it('should parse query strings', () => {
let parse = parseQueryString;

expect(parse('')).toEqual({});
expect(parse('=')).toEqual({});
expect(parse('&')).toEqual({});
expect(parse('?')).toEqual({});

expect(parse('a')).toEqual({ a: true });
expect(parse('a&b')).toEqual({ a: true, b: true });
expect(parse('a=')).toEqual({ a: '' });
expect(parse('a=&b=')).toEqual({ a: '', b: '' });

expect(parse('a=b')).toEqual({ a: 'b' });
expect(parse('a=b&c=d')).toEqual({ a: 'b', c: 'd' });
expect(parse('a=b&&c=d')).toEqual({ a: 'b', c: 'd' });
expect(parse('a=b&a=c')).toEqual({ a: 'c' });

expect(parse('a=%26')).toEqual({ a: '&' });
expect(parse('%26=a')).toEqual({ '&': 'a' });
expect(parse('%26[]=b&%26[]=c')).toEqual({ '&': [ 'b', 'c' ]});
});
});

0 comments on commit adccb98

Please sign in to comment.