-
Notifications
You must be signed in to change notification settings - Fork 352
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Using JSURL for data serialization in links
- Loading branch information
Danko Kozar
committed
Apr 1, 2020
1 parent
f8e4291
commit 0539129
Showing
11 changed files
with
363 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { serialize, deserialize } from './URLSerializer' | ||
import { State } from './state' | ||
import { AllParams } from '../../../algorithms/types/Param.types' | ||
|
||
const SCENARIOS = ['CHE-Basel-Landschaft', 'CHE-Basel-Stadt'] | ||
|
||
const INITIAL_STATE: State = { | ||
scenarios: SCENARIOS, | ||
current: 'CHE-Basel-Stadt', | ||
data: { | ||
population: { | ||
ICUBeds: 80, | ||
cases: 'CHE-Basel-Stadt', | ||
country: 'Switzerland', | ||
hospitalBeds: 698, | ||
importsPerDay: 0.1, | ||
populationServed: 195000, | ||
suspectedCasesToday: 10, | ||
}, | ||
epidemiological: { | ||
infectiousPeriod: 3, | ||
latencyTime: 5, | ||
lengthHospitalStay: 4, | ||
lengthICUStay: 14, | ||
overflowSeverity: 2, | ||
peakMonth: 0, | ||
r0: 2, | ||
seasonalForcing: 0.2, | ||
}, | ||
containment: { | ||
reduction: [ | ||
{ t: new Date('2020-01-31T00:00:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-02-23T18:40:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-03-18T13:20:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-04-11T08:00:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-05-05T02:40:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-05-28T21:20:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-06-21T16:00:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-07-15T10:40:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-08-08T05:20:00.000Z'), y: 1 }, | ||
{ t: new Date('2020-09-01T00:00:00.000Z'), y: 1 }, | ||
], | ||
numberPoints: 10, | ||
}, | ||
simulation: { | ||
simulationTimeRange: { tMin: new Date('2020-01-31T00:00:00.000Z'), tMax: new Date('2020-09-01T00:00:00.000Z') }, | ||
numberStochasticRuns: 0, | ||
}, | ||
}, | ||
} | ||
|
||
const PARAMS: AllParams = { | ||
population: { | ||
ICUBeds: 80, | ||
cases: 'CHE-Basel-Stadt', | ||
country: 'Switzerland', | ||
hospitalBeds: 698, | ||
importsPerDay: 0.1, | ||
populationServed: 195000, | ||
suspectedCasesToday: 10, | ||
}, | ||
epidemiological: { | ||
infectiousPeriod: 3, | ||
latencyTime: 5, | ||
lengthHospitalStay: 4, | ||
lengthICUStay: 14, | ||
overflowSeverity: 2, | ||
peakMonth: 0, | ||
r0: 2, | ||
seasonalForcing: 0.2, | ||
}, | ||
simulation: { | ||
simulationTimeRange: { tMax: new Date('2020-09-01T00:00:00.000Z'), tMin: new Date('2020-01-31T00:00:00.000Z') }, | ||
numberStochasticRuns: 0, | ||
}, | ||
containment: { | ||
reduction: [ | ||
{ y: 1, t: new Date('2020-01-31T00:00:00.000Z') }, | ||
{ y: 1, t: new Date('2020-02-23T18:40:00.000Z') }, | ||
{ y: 1, t: new Date('2020-03-18T13:20:00.000Z') }, | ||
{ y: 1, t: new Date('2020-04-11T08:00:00.000Z') }, | ||
{ y: 1, t: new Date('2020-05-05T02:40:00.000Z') }, | ||
{ y: 1, t: new Date('2020-05-28T21:20:00.000Z') }, | ||
{ y: 1, t: new Date('2020-06-21T16:00:00.000Z') }, | ||
{ y: 1, t: new Date('2020-07-15T10:40:00.000Z') }, | ||
{ y: 1, t: new Date('2020-08-08T05:20:00.000Z') }, | ||
{ y: 0.5, t: new Date('2020-09-01T00:00:00.000Z') }, // last slider moved to 0.5 | ||
], | ||
numberPoints: 10, | ||
}, | ||
current: 'CHE-Basel-Stadt', | ||
} | ||
|
||
const SERIALIZED_STRING = | ||
"~(population~(ICUBeds~80~cases~'CHE-Basel-Stadt~country~'Switzerland~hospitalBeds~698~importsPerDay~0.1~populationServed~195000~suspectedCasesToday~10)~epidemiological~(infectiousPeriod~3~latencyTime~5~lengthHospitalStay~4~lengthICUStay~14~overflowSeverity~2~peakMonth~0~r0~2~seasonalForcing~0.2)~simulation~(simulationTimeRange~(tMin~1580428800000~tMax~1598918400000)~numberStochasticRuns~0)~containment~(reduction~(~(y~1~t~1580428800000)~(y~1~t~1582483200000)~(y~1~t~1584537600000)~(y~1~t~1586592000000)~(y~1~t~1588646400000)~(y~1~t~1590700800000)~(y~1~t~1592755200000)~(y~1~t~1594809600000)~(y~1~t~1596864000000)~(y~0.5~t~1598918400000))~numberPoints~10)~current~'CHE-Basel-Stadt)" | ||
|
||
describe('URLSerializer', () => { | ||
it('serialize', () => { | ||
expect(serialize(INITIAL_STATE, PARAMS)).toBe(SERIALIZED_STRING) | ||
}) | ||
|
||
it('deserialize', () => { | ||
expect(deserialize(INITIAL_STATE, SERIALIZED_STRING)).toEqual({ | ||
current: 'CHE-Basel-Stadt', | ||
scenarios: INITIAL_STATE.scenarios, | ||
data: { | ||
containment: PARAMS.containment, | ||
epidemiological: PARAMS.epidemiological, | ||
population: PARAMS.population, | ||
simulation: PARAMS.simulation, | ||
}, | ||
}) | ||
}) | ||
|
||
it('throws', () => { | ||
expect(() => { | ||
deserialize(INITIAL_STATE, 'some random string') | ||
}).toThrow('URLSerializer: Error while parsing URL') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,53 @@ | ||
import type { AllParams } from '../../../algorithms/types/Param.types' | ||
|
||
import { State } from './state' | ||
|
||
/* | ||
Quick and dirty helper to serialize/deserialize parameters within the URL, | ||
so people can share/save it and keep their parameters | ||
This could have been done inside a redux middleware, but with some refacto. | ||
We use JSON.stringify to serialize things out. It's not the most optimized way at all, | ||
but it works, and it's simple enough. Note that Date object is serialized as a string, | ||
so some extra work is needed during deserialization. | ||
*/ | ||
|
||
export async function serializeScenarioToURL(scenarioState: State, params: AllParams) { | ||
const toSave = { | ||
import jsUrl from './serialization/jsUrl' | ||
import encodeURIComponent from './serialization/encodeURIComponent' | ||
|
||
/** | ||
* Serializes using JSURL (new format) | ||
*/ | ||
export function serialize(state: State, params: AllParams) { | ||
return jsUrl.serialize({ | ||
...params, | ||
current: scenarioState.current, | ||
containment: scenarioState.data.containment.reduction, | ||
} | ||
|
||
window.history.pushState('', '', `?${encodeURIComponent(JSON.stringify(toSave))}`) | ||
current: state.current, | ||
}) | ||
} | ||
|
||
export function deserializeScenarioFromURL(initState: State): State { | ||
if (window.location.search) { | ||
try { | ||
/* | ||
We deserialise the URL by removing the first char ('?'), and applying JSON.parse | ||
*/ | ||
const obj = JSON.parse(decodeURIComponent(window.location.search.slice(1))) | ||
|
||
// Be careful of dates object that have been serialized to string | ||
|
||
// safe to mutate here | ||
obj.simulation.simulationTimeRange.tMin = new Date(obj.simulation.simulationTimeRange.tMin) | ||
obj.simulation.simulationTimeRange.tMax = new Date(obj.simulation.simulationTimeRange.tMax) | ||
/** | ||
* Deserializes using: | ||
* 1. JSURL (new format) | ||
* 2. encodeURIComponent (old format for backward compatibility) | ||
*/ | ||
export function deserialize(state: State, queryString: string): State { | ||
let obj: AllParams | ||
|
||
const containmentDataReduction = obj.containment.map((c: { t: string; y: number }) => ({ | ||
y: c.y, | ||
t: new Date(c.t), | ||
})) | ||
if (queryString) { | ||
try { | ||
// 1. Trying to deserialize as JSURL | ||
obj = jsUrl.deserialize(queryString) | ||
} catch (error) { | ||
try { | ||
// 2. This is not JSURL, the old (URL encoded) format | ||
obj = encodeURIComponent.deserialize(queryString) | ||
} catch (error) { | ||
// 3. None of the two formats | ||
throw new Error('URLSerializer: Error while parsing URL') | ||
} | ||
} | ||
|
||
if (obj) { | ||
return { | ||
...initState, | ||
...state, | ||
current: obj.current, | ||
data: { | ||
population: initState.data.population, | ||
containment: { | ||
reduction: containmentDataReduction, | ||
numberPoints: containmentDataReduction.length, | ||
}, | ||
epidemiological: initState.data.epidemiological, | ||
containment: obj.containment, | ||
epidemiological: obj.epidemiological, | ||
population: obj.population, | ||
simulation: obj.simulation, | ||
}, | ||
} | ||
} catch (error) { | ||
console.error('Error while parsing URL :', error.message) | ||
} | ||
} | ||
return initState | ||
|
||
return state | ||
} |
44 changes: 44 additions & 0 deletions
44
src/components/Main/state/serialization/encodeURIComponent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import type { AllParams } from '../../../../algorithms/types/Param.types' | ||
|
||
const serialize = (params: AllParams): string => { | ||
return encodeURIComponent(JSON.stringify(params)) | ||
} | ||
|
||
const deserialize = (queryString: string): AllParams => { | ||
try { | ||
const obj = JSON.parse(decodeURIComponent(queryString)) | ||
|
||
const simulationTimeRangeOriginal = obj.simulation.simulationTimeRange | ||
const simulationTimeRangeConverted = { | ||
tMin: new Date(simulationTimeRangeOriginal.tMin), | ||
tMax: new Date(simulationTimeRangeOriginal.tMax), | ||
} | ||
|
||
// NOTE: Error in original parser. It should be: obj.containment | ||
const containmentOriginal = obj.containment | ||
const containmentConverted = { | ||
...containmentOriginal, | ||
// NOTE: Error in original parser. It should be: containmentOriginal.reduction.map | ||
reduction: containmentOriginal.map((c: { t: number; y: number }) => ({ | ||
y: c.y, | ||
t: new Date(c.t), | ||
})), | ||
} | ||
|
||
return { | ||
...obj, | ||
containment: containmentConverted, | ||
simulation: { | ||
...obj.simulation, | ||
simulationTimeRange: simulationTimeRangeConverted, | ||
}, | ||
} | ||
} catch (error) { | ||
throw new Error('encodeURIComponent: Error while parsing URL') | ||
} | ||
} | ||
|
||
export default { | ||
serialize, | ||
deserialize, | ||
} |
Oops, something went wrong.