Skip to content

Commit

Permalink
Merge pull request #54 from GodHermit/add-export-and-import-features
Browse files Browse the repository at this point in the history
Add export and import features
  • Loading branch information
GodHermit authored Sep 2, 2023
2 parents 3af00c0 + dabf609 commit 0f3d296
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 92 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "turing-machine",
"version": "0.4.0",
"version": "0.5.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
90 changes: 7 additions & 83 deletions src/_store/index.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,30 @@
import TuringMachine from '@/lib/turingMachine';
import { StateMap, StateMapKey } from '@/lib/turingMachine/types';
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { MachineSlice, createMachineSlice } from './slices/machineSlice';
import { RegistersSlice, createRegistersSlice } from './slices/registersSlice';
import { TapeSettingsSlice, createTapeSettingsSlice } from './slices/tapeSettingsSlice';

type StoreUtils = {
resetAll: () => void;
};
import { Utilities, createUtilitiesSlice } from './slices/utilitiesSlice';
import replacer from './utilities/replacer';
import reviver from './utilities/reviver';

export type StoreType = MachineSlice &
TapeSettingsSlice &
RegistersSlice &
StoreUtils;
Utilities;

export const useStore = create<StoreType>()(
persist(
(...a) => ({
...createMachineSlice(...a),
...createTapeSettingsSlice(...a),
...createRegistersSlice(...a),
resetAll: () => {
const [set, get, store] = a;

store.persist.clearStorage();
window.location.reload();
}
...createUtilitiesSlice(...a),
}),
{
name: 'turing-machine',
storage: createJSONStorage(() => localStorage, {
reviver: (key, value) => {
switch (key) {
case 'machine':
// Convert object to instance of TuringMachine
const newMachine = new TuringMachine(
(value as any).input,
(value as any).instructions,
(value as any).options
);
newMachine.setCurrentCondition((value as any).current);

return newMachine;

case 'states':
// Convert array of entries to Map
return (value as Array<[StateMapKey, string]>)
.reduce((acc, [key, value]) => {
acc.set(key, value);
return acc;
}, new Map<StateMapKey, string>());

case 'logs':
// Convert objects with type 'error' to instances of Error
return (value as Array<any>)
.map(log => {
if (log.type === 'error') {
return new Error(log.message, {
cause: log.cause,
});
}

return log;
});
case 'blankSymbol':
if (value !== TuringMachine.BLANK_SYMBOL) {
TuringMachine.setBlankSymbol(value as string);
}
return value;

default:
// Return value as is
return value;
}
},
replacer: (key, value) => {
switch (key) {
case 'states':
// Convert Map to array of entries
return [...(value as StateMap).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;
}
}
reviver,
replacer,
})
}
)
Expand Down
150 changes: 150 additions & 0 deletions src/_store/slices/utilitiesSlice.ts
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,
],
},
}));
}
},
});
57 changes: 57 additions & 0 deletions src/_store/utilities/replacer.cy.ts
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');
});
});
});
34 changes: 34 additions & 0 deletions src/_store/utilities/replacer.ts
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;
}
}
Loading

0 comments on commit 0f3d296

Please sign in to comment.