From f6ce603e45cbf69f9f872950745021ecb5c6bdda Mon Sep 17 00:00:00 2001 From: Benedict Aas Date: Tue, 15 May 2018 11:59:36 +0100 Subject: [PATCH] feat(GUI): add convenience localstorage class (#2276) * feat(GUI): add convenience localstorage class We add a class `Storage` and accompanying helper methods that makes localStorage usage easier. Change-Type: patch Changelog-Entry: Add a convenience Storage class on top of localStorage. --- lib/gui/app/models/local-settings.js | 9 +- lib/gui/app/models/storage.js | 164 +++++++++++++++++++++++++++ tests/gui/models/storage.spec.js | 97 ++++++++++++++++ 3 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 lib/gui/app/models/storage.js create mode 100644 tests/gui/models/storage.spec.js diff --git a/lib/gui/app/models/local-settings.js b/lib/gui/app/models/local-settings.js index 87ee956f..b95e7990 100644 --- a/lib/gui/app/models/local-settings.js +++ b/lib/gui/app/models/local-settings.js @@ -21,6 +21,7 @@ const _ = require('lodash') const fs = require('fs') const path = require('path') const os = require('os') +const Storage = require('./storage') /** * @summary Local storage settings key @@ -28,6 +29,7 @@ const os = require('os') * @type {String} */ const LOCAL_STORAGE_SETTINGS_KEY = 'etcher-settings' +const settingsStorage = new Storage(LOCAL_STORAGE_SETTINGS_KEY) /** * @summary Local settings filename @@ -86,7 +88,7 @@ exports.readAll = () => { const workdirConfigPath = path.join(process.cwd(), RCFILE) const settings = {} return Bluebird.try(() => { - _.merge(settings, JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SETTINGS_KEY))) + _.merge(settings, settingsStorage.getAll()) }).return(readConfigFile(homeConfigPath)) .then((homeConfig) => { _.merge(settings, homeConfig) @@ -114,9 +116,8 @@ exports.readAll = () => { * }); */ exports.writeAll = (settings) => { - const INDENTATION_SPACES = 2 return Bluebird.try(() => { - window.localStorage.setItem(LOCAL_STORAGE_SETTINGS_KEY, JSON.stringify(settings, null, INDENTATION_SPACES)) + settingsStorage.setAll(settings) }) } @@ -137,6 +138,6 @@ exports.writeAll = (settings) => { */ exports.clear = () => { return Bluebird.try(() => { - window.localStorage.removeItem(LOCAL_STORAGE_SETTINGS_KEY) + settingsStorage.clearAll() }) } diff --git a/lib/gui/app/models/storage.js b/lib/gui/app/models/storage.js new file mode 100644 index 00000000..05a7b6d7 --- /dev/null +++ b/lib/gui/app/models/storage.js @@ -0,0 +1,164 @@ +/* + * Copyright 2018 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const INDENTATION_SPACES = 2 + +/** + * @summary Localstorage class and helper functions + * @class + * @public + */ +class Storage { + /** + * @function + * @public + * + * @param {String} superkey - superkey + * + * @example + * const potatoStorage = new Storage('potato') + */ + constructor (superkey) { + this.superkey = superkey + } + + /** + * @summary Get the whole object under the superkey + * @function + * @public + * + * @returns {Object} + * + * @example + * for (const key in potatoStorage.getAll()) { + * console.log(key) + * } + */ + getAll () { + try { + // JSON.parse(null) === null, so we fallback to {} + return JSON.parse(window.localStorage.getItem(this.superkey)) || {} + } catch (err) { + this.setAll({}) + throw err + } + } + + /** + * @summary Set the whole object under the superkey + * @function + * @public + * + * @param {Any} value - any valid JSON value + * + * @example + * potatoStorage.setAll({ + * location: 'somewhere', + * freshness: 100, + * edible: true + * }) + */ + setAll (value) { + window.localStorage.setItem(this.superkey, JSON.stringify(value, null, INDENTATION_SPACES)) + } + + /** + * @summary Clear the whole object under the superkey + * @function + * @public + * + * @example + * potatoStorage.clearAll() + */ + clearAll () { + window.localStorage.removeItem(this.superkey) + } + + /** + * @summary Get a stored value + * @function + * @public + * + * @param {String} key - object field key + * @param {Any} defaultValue - any valid JSON value + * @returns {Any} - the JSON parsed value + * + * @example + * potatoStorage.get('location', 'my farm') + */ + get (key, defaultValue) { + const value = this.getAll()[key] + + // eslint-disable-next-line no-undefined + if (value === undefined) { + return defaultValue + } + + return value + } + + /** + * @summary Modify a stored value + * @function + * @public + * + * @param {String} key - object field key + * @param {Function} func - function to apply to the value + * @param {Any} defaultValue - fallback value + * @returns {Any} - the value returned by the function applied above + * + * @example + * potatoStorage.modify('freshness', (freshness) => { + * return freshness + 1 + * }) + */ + modify (key, func, defaultValue) { + const obj = this.getAll() + + let result = null + // eslint-disable-next-line no-undefined + if (obj[key] === undefined) { + result = func(defaultValue) + } else { + result = func(obj[key]) + } + + // eslint-disable-next-line lodash/prefer-lodash-method + this.setAll(Object.assign(obj, { [key]: result })) + return result + } + + /** + * @summary Set a stored value + * @function + * @public + * + * @param {String} key - object field key + * @param {Any} value - value to set + * + * @example + * potatoStorage.set('edible', true) + */ + set (key, value) { + this.modify(key, () => { + return value + }) + } +} + +module.exports = Storage diff --git a/tests/gui/models/storage.spec.js b/tests/gui/models/storage.spec.js new file mode 100644 index 00000000..26b598ff --- /dev/null +++ b/tests/gui/models/storage.spec.js @@ -0,0 +1,97 @@ +/* + * Copyright 2018 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const _ = require('lodash') +const m = require('mochainon') +const Storage = require('../../../lib/gui/app/models/storage') + +describe('Browser: storage', function () { + beforeEach(function () { + this.testStorage = new Storage('test') + + this.superObject = { + fieldA: 1, + fieldB: 2 + } + + this.testStorage.setAll(this.superObject) + }) + + afterEach(function () { + this.testStorage.clearAll() + }) + + describe('.getAll()', function () { + it('should return the super-object', function () { + m.chai.expect(this.testStorage.getAll()).to.deep.equal(this.superObject) + }) + }) + + describe('.setAll()', function () { + it('should set the super-object', function () { + const superObject = { fieldC: 3, fieldD: 4 } + this.testStorage.setAll(superObject) + m.chai.expect(this.testStorage.getAll()).to.deep.equal(superObject) + }) + }) + + describe('.clearAll()', function () { + it('should remove the super-object', function () { + this.testStorage.clearAll() + m.chai.expect(this.testStorage.getAll()).to.deep.equal({}) + }) + }) + + describe('.get()', function () { + it('should retrieve the value', function () { + m.chai.expect(this.testStorage.get('fieldA')).to.equal(1) + }) + }) + + describe('.modify()', function () { + it('should change the value', function () { + this.testStorage.modify('fieldA', (fieldA) => { + return fieldA + 1 + }) + m.chai.expect(this.testStorage.get('fieldA')).to.equal(2) + }) + + it('should return a value', function () { + const value = this.testStorage.modify('fieldA', (fieldA) => { + return fieldA + 1 + }) + m.chai.expect(value).to.equal(2) + }) + + it('should use the fallback default value if field doesn\'t exist', function () { + const FALLBACK = 1.5 + m.chai.expect(this.testStorage.modify('fieldC', _.ceil, FALLBACK)).to.equal(2) + }) + + it('should be undefined if no fallback default value is given', function () { + m.chai.expect(this.testStorage.modify('fieldC', _.identity)).to.be.undefined + }) + }) + + describe('.set()', function () { + it('should set a value', function () { + this.testStorage.set('fieldC', 3) + m.chai.expect(this.testStorage.get('fieldC')).to.equal(3) + }) + }) +})