-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #54 from GodHermit/add-export-and-import-features
Add export and import features
- Loading branch information
Showing
9 changed files
with
334 additions
and
92 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,150 @@ | ||
import { StateCreator } from 'zustand'; | ||
import { StoreType } from '..'; | ||
import replacer from '../utilities/replacer'; | ||
import reviver from '../utilities/reviver'; | ||
|
||
export interface Utilities { | ||
/** | ||
* Reset the store to its initial state | ||
*/ | ||
resetAll: () => void; | ||
/** | ||
* Export the store as JSON | ||
* @param downloadFile Whether to download the JSON file | ||
* @returns The store as JSON string | ||
*/ | ||
exportJSON: (downloadFile?: boolean) => string; | ||
/** | ||
* Import JSON into the store | ||
* @param json The JSON string to import (If not provided, the user will be prompted to select a file) | ||
*/ | ||
importJSON: (json?: string) => void; | ||
} | ||
|
||
export const createUtilitiesSlice: StateCreator< | ||
StoreType, | ||
[['zustand/persist', unknown]], | ||
[], | ||
Utilities | ||
> = (set, get, store) => ({ | ||
resetAll: () => { | ||
store.persist.clearStorage(); | ||
window.location.reload(); | ||
}, | ||
exportJSON: (downloadFile = false) => { | ||
try { | ||
// Convert the store to JSON | ||
const res = JSON.stringify(get(), replacer); | ||
|
||
// If the file needs to be downloaded | ||
if (downloadFile) { | ||
// Generate a Blob from the JSON string | ||
const file = new Blob([res], { type: 'application/json' }); | ||
|
||
// Create a temporary URL for the Blob | ||
const url = URL.createObjectURL(file); | ||
|
||
// Create a temporary <a> element to download the file | ||
const el = document.createElement('a'); | ||
el.href = url; | ||
el.download = 'turing-machine.json'; | ||
el.click(); | ||
|
||
// Revoke the temporary URL | ||
URL.revokeObjectURL(url); | ||
// Remove the temporary <a> element | ||
el.remove(); | ||
} | ||
|
||
return res; | ||
} catch (error) { | ||
// Add the error to the logs | ||
set(s => ({ | ||
registers: { | ||
...s.registers, | ||
logs: [ | ||
...s.registers.logs, | ||
error as Error, | ||
], | ||
}, | ||
})); | ||
|
||
return ''; | ||
} | ||
}, | ||
importJSON: (json) => { | ||
try { | ||
if (json !== undefined) { | ||
// Parse the JSON | ||
const parsed = JSON.parse(json, reviver); | ||
|
||
// Set the store to the parsed JSON | ||
set(parsed); | ||
|
||
return; | ||
} | ||
|
||
// Create a temporary <input> element to select the file | ||
const el = document.createElement('input'); | ||
el.type = 'file'; | ||
el.accept = 'application/json'; | ||
el.onchange = () => { | ||
if (el.files === null) return; | ||
|
||
// Read the file | ||
const reader = new FileReader(); | ||
reader.onload = () => { | ||
try { | ||
if (typeof reader.result !== 'string') return; | ||
|
||
// Parse the JSON | ||
const parsed = JSON.parse(reader.result, reviver); | ||
|
||
// Check if keys are valid | ||
Object | ||
.keys(parsed) | ||
.forEach(key => { | ||
// If the key is not in the store | ||
if (!(key in get())) { | ||
throw new Error(`Import failed: Invalid key "${key}"`); | ||
} | ||
}); | ||
|
||
// Set the store to the parsed JSON | ||
set(parsed); | ||
} catch (error) { | ||
// Add the error to the logs | ||
set(s => ({ | ||
registers: { | ||
...s.registers, | ||
logs: [ | ||
...s.registers.logs, | ||
error as Error, | ||
], | ||
}, | ||
})); | ||
} | ||
}; | ||
reader.readAsText(el.files[0]); | ||
|
||
// Remove the temporary <input> element | ||
el.remove(); | ||
}; | ||
|
||
// Click the temporary <input> element | ||
el.click(); | ||
|
||
} catch (error) { | ||
// Add the error to the logs | ||
set(s => ({ | ||
registers: { | ||
...s.registers, | ||
logs: [ | ||
...s.registers.logs, | ||
error as Error, | ||
], | ||
}, | ||
})); | ||
} | ||
}, | ||
}); |
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,57 @@ | ||
import replacer from './replacer'; | ||
|
||
describe('function replacer(key, value)', () => { | ||
|
||
context('key = "states"', () => { | ||
it('should replace states Map with array of entries', () => { | ||
const stateMap = new Map().set('!', '!').set(0, 'q0').set(1, 'q1'); | ||
const stateArray = [...stateMap.entries()]; | ||
|
||
expect(replacer('states', stateMap)).to.deep.equal(stateArray); | ||
}); | ||
}); | ||
|
||
context('key = "logs"', () => { | ||
it('should replace logs array with array of plain objects', () => { | ||
const logs = [ | ||
new Error('Error 1'), | ||
{ | ||
'tapeValue': '', | ||
'stateIndex': 0, | ||
'headPosition': 0, | ||
'step': 0, | ||
'symbol': 'λ', | ||
'stateName': 'q0', | ||
'instruction': { | ||
'stateIndex': 0, | ||
'symbol': 'λ', | ||
'move': 'R', | ||
'newSymbol': 'λ', | ||
'newStateIndex': 0, | ||
'stateName': 'q0', | ||
'newStateName': 'q0' | ||
}, | ||
'isFinalCondition': false | ||
} | ||
]; | ||
const plainLogs = logs.map(log => { | ||
if (log instanceof Error) { | ||
return { | ||
type: 'error', | ||
message: log.message, | ||
cause: log.cause, | ||
}; | ||
} | ||
return log; | ||
}); | ||
|
||
expect(replacer('logs', logs)).to.deep.equal(plainLogs); | ||
}); | ||
}); | ||
|
||
context('value does not need to be replaced', () => { | ||
it('should return value as is', () => { | ||
expect(replacer('key', 'value')).to.equal('value'); | ||
}); | ||
}); | ||
}); |
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,34 @@ | ||
import { StoreType } from '..'; | ||
|
||
/** | ||
* Custom replacer function for JSON.stringify | ||
* @param key | ||
* @param value | ||
* @returns | ||
*/ | ||
export default function replacer(key: string, value: unknown) { | ||
switch (key) { | ||
case 'states': | ||
// Convert Map to array of entries | ||
return [...(value as StoreType['registers']['states']).entries()]; | ||
|
||
case 'logs': | ||
// Convert instances of Error to plain objects | ||
return (value as StoreType['registers']['logs']) | ||
.map(log => { | ||
if (log instanceof Error) { | ||
return { | ||
type: 'error', | ||
message: log.message, | ||
cause: log.cause, | ||
}; | ||
} | ||
|
||
return log; | ||
}); | ||
|
||
default: | ||
// Return value as is | ||
return value; | ||
} | ||
} |
Oops, something went wrong.