mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 12:43:11 +10:00
Add URL param encoding/decoding helpers
This commit is contained in:
parent
f75cb14a8a
commit
f138fcc0bc
2 changed files with 287 additions and 3 deletions
|
|
@ -1,4 +1,166 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import queryString from 'query-string';
|
||||
import { types } from './sdkLoader';
|
||||
|
||||
export const createSlug = (str) =>
|
||||
encodeURIComponent(str.toLowerCase().split(' ').join('-'))
|
||||
const { LatLng, LatLngBounds } = types;
|
||||
|
||||
export const createSlug = str => encodeURIComponent(str.toLowerCase().split(' ').join('-'));
|
||||
|
||||
/**
|
||||
* Parse float from a string
|
||||
*
|
||||
* @param {String} str - string to parse
|
||||
*
|
||||
* @return {Number|null} number parsed from the string, null if not a number
|
||||
*/
|
||||
export const parseFloatNum = str => {
|
||||
const num = window.parseFloat(str);
|
||||
return window.isNaN(num) ? null : num;
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode a location to use in a URL
|
||||
*
|
||||
* @param {LatLng} location - location instance to encode
|
||||
*
|
||||
* @return {String} location coordinates separated by a comma
|
||||
*/
|
||||
export const encodeLatLng = location => `${location.lat},${location.lng}`;
|
||||
|
||||
/**
|
||||
* Decode a location from a string
|
||||
*
|
||||
* @param {String} str - string encoded with `encodeLatLng`
|
||||
*
|
||||
* @return {LatLng|null} location instance, null if could not parse
|
||||
*/
|
||||
export const decodeLatLng = str => {
|
||||
const parts = str.split(',');
|
||||
if (parts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
const lat = parseFloatNum(parts[0]);
|
||||
const lng = parseFloatNum(parts[1]);
|
||||
if (lat === null || lng === null) {
|
||||
return null;
|
||||
}
|
||||
return new LatLng(lat, lng);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode a location bounds to use in a URL
|
||||
*
|
||||
* @param {LatLngBounds} bounds - bounds instance to encode
|
||||
*
|
||||
* @return {String} bounds coordinates separated by a comma
|
||||
*/
|
||||
export const encodeLatLngBounds = bounds => `${encodeLatLng(bounds.ne)},${encodeLatLng(bounds.sw)}`;
|
||||
|
||||
/**
|
||||
* Decode a location bounds from a string
|
||||
*
|
||||
* @param {String} str - string encoded with `encodeLatLngBounds`
|
||||
*
|
||||
* @return {LatLngBounds|null} location bounds instance, null if could not parse
|
||||
*/
|
||||
export const decodeLatLngBounds = str => {
|
||||
const parts = str.split(',');
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
const ne = decodeLatLng(`${parts[0]},${parts[1]}`);
|
||||
const sw = decodeLatLng(`${parts[2]},${parts[3]}`);
|
||||
if (ne === null || sw === null) {
|
||||
return null;
|
||||
}
|
||||
return new LatLngBounds(ne, sw);
|
||||
};
|
||||
|
||||
// Serialise SDK types in given object values into strings
|
||||
const serialiseSdkTypes = obj =>
|
||||
Object.keys(obj).reduce(
|
||||
(result, key) => {
|
||||
const val = obj[key];
|
||||
/* eslint-disable no-param-reassign */
|
||||
if (val instanceof LatLngBounds) {
|
||||
result[key] = encodeLatLngBounds(val);
|
||||
} else if (val instanceof LatLng) {
|
||||
result[key] = encodeLatLng(val);
|
||||
} else {
|
||||
result[key] = val;
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
/**
|
||||
* Serialise given object into a string that can be used in a
|
||||
* URL. Encode SDK types into a format that can be parsed with `parse`
|
||||
* defined below.
|
||||
*
|
||||
* @param {Object} params - object with strings/numbers/booleans or
|
||||
* SDK types as values
|
||||
*
|
||||
* @return {String} query string with sorted keys and serialised
|
||||
* values, `undefined` and `null` values are removed
|
||||
*/
|
||||
export const stringify = params => {
|
||||
const serialised = serialiseSdkTypes(params);
|
||||
const cleaned = Object.keys(serialised).reduce(
|
||||
(result, key) => {
|
||||
const val = serialised[key];
|
||||
/* eslint-disable no-param-reassign */
|
||||
if (val !== null) {
|
||||
result[key] = val;
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return queryString.stringify(cleaned);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a URL search query. Converts numeric values into numbers,
|
||||
* 'true' and 'false' as booleans, and serialised LatLng and
|
||||
* LatLngBounds into respective instances based on given options.
|
||||
*
|
||||
* @param {String} search - query string to parse, optionally with a
|
||||
* leading '?' or '#' character
|
||||
*
|
||||
* @param {Object} options - Options for parsing:
|
||||
*
|
||||
* - latlng {Array<String} keys to parse as LatLng instances, null if
|
||||
* not able to parse
|
||||
* - latlngBounds {Array<String} keys to parse as LatLngBounds
|
||||
* instances, null if not able to parse
|
||||
*
|
||||
* @return {Object} key/value pairs parsed from the given String
|
||||
*/
|
||||
export const parse = (search, options = {}) => {
|
||||
const { latlng = [], latlngBounds = [] } = options;
|
||||
const params = queryString.parse(search);
|
||||
return Object.keys(params).reduce(
|
||||
(result, key) => {
|
||||
const val = params[key];
|
||||
/* eslint-disable no-param-reassign */
|
||||
if (latlng.includes(key)) {
|
||||
result[key] = decodeLatLng(val);
|
||||
} else if (latlngBounds.includes(key)) {
|
||||
result[key] = decodeLatLngBounds(val);
|
||||
} else if (val === 'true') {
|
||||
result[key] = true;
|
||||
} else if (val === 'false') {
|
||||
result[key] = false;
|
||||
} else {
|
||||
const num = parseFloatNum(val);
|
||||
result[key] = num === null ? val : num;
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
122
src/util/urlHelpers.test.js
Normal file
122
src/util/urlHelpers.test.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { types } from './sdkLoader';
|
||||
import {
|
||||
parseFloatNum,
|
||||
encodeLatLng,
|
||||
decodeLatLng,
|
||||
encodeLatLngBounds,
|
||||
decodeLatLngBounds,
|
||||
stringify,
|
||||
parse,
|
||||
} from './urlHelpers';
|
||||
|
||||
const { LatLng, LatLngBounds } = types;
|
||||
|
||||
const SPACE = encodeURIComponent(' ');
|
||||
const COMMA = encodeURIComponent(',');
|
||||
|
||||
describe('urlHelpers', () => {
|
||||
describe('parseFloatNum()', () => {
|
||||
it('handles empty value', () => {
|
||||
expect(parseFloatNum('')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles non-numeric value', () => {
|
||||
expect(parseFloatNum('abc')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles int value with surrounding whitespace', () => {
|
||||
expect(parseFloatNum(' 123 \t')).toEqual(123);
|
||||
});
|
||||
|
||||
it('handles float value', () => {
|
||||
expect(parseFloatNum('123.01')).toBeCloseTo(123.01, 2);
|
||||
});
|
||||
|
||||
it('handles trailing chars', () => {
|
||||
expect(parseFloatNum('123abc')).toEqual(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LatLng serialisation', () => {
|
||||
it('encodes and decodes', () => {
|
||||
const location = new LatLng(40, 60);
|
||||
expect(decodeLatLng(encodeLatLng(location))).toEqual(location);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LatLngBounds serialisation', () => {
|
||||
it('encodes and decodes', () => {
|
||||
const bounds = new LatLngBounds(new LatLng(50, 70), new LatLng(30, 50));
|
||||
expect(decodeLatLngBounds(encodeLatLngBounds(bounds))).toEqual(bounds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringify()', () => {
|
||||
it('handles empty params', () => {
|
||||
expect(stringify({})).toEqual('');
|
||||
});
|
||||
|
||||
it('sorts params', () => {
|
||||
const params = { b: 'B', c: 'C', a: 'A' };
|
||||
expect(stringify(params)).toEqual('a=A&b=B&c=C');
|
||||
});
|
||||
|
||||
it('encodes values', () => {
|
||||
const params = {
|
||||
space: 'A and b',
|
||||
num: 123,
|
||||
bool: true,
|
||||
undef: undefined,
|
||||
nil: null,
|
||||
};
|
||||
expect(stringify(params)).toEqual(`bool=true&num=123&space=A${SPACE}and${SPACE}b`);
|
||||
});
|
||||
|
||||
it('encodes SDK types', () => {
|
||||
const params = {
|
||||
origin: new LatLng(40, 60),
|
||||
bounds: new LatLngBounds(new LatLng(50, 70), new LatLng(30, 50)),
|
||||
};
|
||||
const origin = `40${COMMA}60`;
|
||||
const bounds = `50${COMMA}70${COMMA}30${COMMA}50`;
|
||||
expect(stringify(params)).toEqual(`bounds=${bounds}&origin=${origin}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse()', () => {
|
||||
it('handles empty string', () => {
|
||||
expect(parse('')).toEqual({});
|
||||
});
|
||||
|
||||
it('handles question mark', () => {
|
||||
expect(parse('?')).toEqual({});
|
||||
});
|
||||
|
||||
it('decodes values', () => {
|
||||
const search = `bool1=true&bool2=false&num1=123&num2=-1.01&space=A${SPACE}and${SPACE}b`;
|
||||
expect(parse(search)).toEqual({
|
||||
space: 'A and b',
|
||||
num1: 123,
|
||||
num2: -1.01,
|
||||
bool2: false,
|
||||
bool1: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes SDK types', () => {
|
||||
const origin = `40${COMMA}60`;
|
||||
const bounds = `50${COMMA}70${COMMA}30${COMMA}50`;
|
||||
const search = `bounds=${bounds}&origin=${origin}&invalid=a,10&badBounds=true`;
|
||||
const options = {
|
||||
latlng: ['origin', 'invalid'],
|
||||
latlngBounds: ['bounds', 'badBounds'],
|
||||
};
|
||||
expect(parse(search, options)).toEqual({
|
||||
origin: new LatLng(40, 60),
|
||||
invalid: null,
|
||||
bounds: new LatLngBounds(new LatLng(50, 70), new LatLng(30, 50)),
|
||||
badBounds: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue