diff --git a/red/runtime/locales/en-US/runtime.json b/red/runtime/locales/en-US/runtime.json index 6e9f057ae..f7a4b8a3d 100644 --- a/red/runtime/locales/en-US/runtime.json +++ b/red/runtime/locales/en-US/runtime.json @@ -155,5 +155,16 @@ "readme": "### About\n\nThis is your project's README.md file. It helps users understand what your\nproject does, how to use it and anything else they may need to know." } } + }, + + "context": { + "error-module-not-loaded": "'__module__' could not be loaded", + "error-module-not-defined": "'module' is not defined in '__storage__' of settings.contextStorage", + "error-invalid-default-module": "Invalid storage '__storage__' is specified as a default storage", + "error-key-zero-length": "Invalid property expression: zero-length", + "error-unexpected-space-character": "Invalid property expression: unexpected ' ' at position __index__", + "error-empty-key": "Invalid property expression: key is empty", + "error-use-undefined-storage": "Undefined storage '__storage__' is specified" } + } diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index 2a00d40d3..31721e659 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -16,6 +16,7 @@ var clone = require("clone"); var util = require("../../util"); +var log = require("../../log"); var settings; var contexts = {}; @@ -55,7 +56,7 @@ function load() { try{ plugin = require("./"+plugins[pluginName].module); }catch(err){ - throw new Error(plugins[pluginName].module + " could not be loaded"); + throw new Error(log._("context.error-module-not-loaded", {module:plugins[pluginName].module})); } } else { plugin = plugins[pluginName].module; @@ -63,14 +64,14 @@ function load() { plugin.init(config); externalContexts[pluginName] = plugin; }else{ - throw new Error("module is not defined in settings.contextStorage." + pluginName ); + throw new Error(log._("context.error-module-not-defined", {storage:pluginName})); } } if(alias){ if(externalContexts.hasOwnProperty(alias)){ externalContexts["default"] = externalContexts[alias]; }else{ - throw new Error("default is invalid. module name=" + plugins["default"]) + throw new Error(log._("context.error-invalid-default-module", {storage:plugins["default"]})); } } } else { @@ -86,31 +87,52 @@ function copySettings(config, settings){ }); } -function parseKey(key){ - if(!key){ - return null; +function parseStorage(key) { + if (!key || key.charAt(0) !== '$') { + return ""; + } else { + var endOfStorageName = key.indexOf("."); + if (endOfStorageName == -1) { + endOfStorageName = key.length; + } + return key.substring(1,endOfStorageName)||"default"; } - var keyPath = {storage: "", key: ""}; - var index_$ = key.indexOf("$"); - var index_dot = key.indexOf(".", 1); - if(index_$===0&&index_dot) { - keyPath.storage = key.substring(1,index_dot)||"default"; - keyPath.key = key.substring(index_dot+1); +} + +function parseKey(key) { + if (!key) { + throw new Error(log._("context.error-key-zero-length")); + } + var indexSpace = key.indexOf(" "); + if (indexSpace != -1) { + throw new Error(log._("context.error-unexpected-space-character", {index:indexSpace})); + } + var keyPath = { storage: "", key: "" }; + var indexDot = key.indexOf("."); + // The key of "$file" should be treated as a key without persistable context. + if (indexDot != -1) { + keyPath.storage = parseStorage(key); + } + if (keyPath.storage) { + keyPath.key = key.substring(indexDot + 1); } else { keyPath.key = key; } + if(!keyPath.key) { + throw new Error(log._("context.error-empty-key")); + } return keyPath; } -function getContextStorage(keyPath) { - if (noContextStorage || !keyPath.storage) { +function getContextStorage(storage) { + if (noContextStorage || !storage) { return externalContexts["_"]; - } else if (externalContexts.hasOwnProperty(keyPath.storage)) { - return externalContexts[keyPath.storage]; + } else if (externalContexts.hasOwnProperty(storage)) { + return externalContexts[storage]; } else if (externalContexts.hasOwnProperty("default")) { return externalContexts["default"]; } else { - var contextError = new Error(keyPath.storage + " is not defined in contextStorage on settings.js"); + var contextError = new Error(log._("context.error-use-undefined-storage", {storage:storage})); contextError.name = "ContextError"; throw contextError; } @@ -122,21 +144,19 @@ function createContext(id,seed) { obj.get = function(key) { var keyPath = parseKey(key); - var context = getContextStorage(keyPath); + var context = getContextStorage(keyPath.storage); return context.get(keyPath.key, scope); }; obj.set = function(key, value) { var keyPath = parseKey(key); - var context = getContextStorage(keyPath); + var context = getContextStorage(keyPath.storage); return context.set(keyPath.key, value, scope); }; - obj.keys = function() { + obj.keys = function(storage) { //TODO: discuss about keys() behavior - var keys = []; - for(var plugin in externalContexts){ - keys.concat(externalContexts[plugin].keys(scope)); - } - return keys; + var storageName = parseStorage(storage); + var context = getContextStorage(storageName); + return context.keys(scope); }; if(id === "global"){ externalContexts["_"].setGlobalContext(seed); diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index e339de7e1..bc9e6aff6 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -163,21 +163,6 @@ describe('context', function() { config:{} } }; - var contextDefaultStorage={ - default: "test", - test:{ - module: { - init: function() { - return true; - }, - get: stubGet, - set: stubSet, - keys: stubKeys, - delete: stubDelete - }, - config:{} - } - }; afterEach(function(done) { stubGet.reset(); @@ -193,7 +178,68 @@ describe('context', function() { context = Context.get("1","flow"); } + describe('key name',function() { + var memoryStorage = { + memory: { + module: "memory" + } + }; + beforeEach(function() { + Context.init({contextStorage:memoryStorage}); + Context.load(); + context = Context.get("1","flow"); + }); + afterEach(function() { + Context.clean({allNodes:{}}); + }); + it('should work correctly with the valid key name',function() { + context.set("$memory.azAZ09$_","valid"); + context.get("$memory.azAZ09$_").should.eql("valid"); + context.set("$memory.a.b","ab"); + context.get("$memory.a.b").should.eql("ab"); + }); + it('should treat the key name without dot as a normal context',function() { + context.set("$memory","normal"); + context.get("$memory").should.eql("normal"); + }); + it('should fail when specifying invalid characters',function() { + (function() { + context.set("$memory.a.-","invalid1"); + }).should.throw(); + (function() { + context.set("$memory.'abc","invalid2"); + }).should.throw(); + }); + it('should fail when specifying unnecesary space characters for key name',function() { + (function() { + context.set("$ memory.space","space1"); + }).should.throw(); + (function() { + context.set("$memory .space","space2"); + }).should.throw(); + (function() { + context.set("$memory. space","space3"); + }).should.throw(); + }); + }); + describe('if external context storage exists',function() { + var contextDefaultStorage={ + default: "test", + test:{ + module: { + init: function() { + return true; + }, + get: stubGet, + set: stubSet, + keys: stubKeys, + delete: stubDelete + }, + config:{} + } + }; + it('should store local property to external context storage',function() { initializeContext(); should.not.exist(context.get("$test.foo")); @@ -224,7 +270,7 @@ describe('context', function() { stubSet.called.should.be.true(); stubKeys.called.should.be.true(); }); - it('should store data on default context', function() { + it('should store data when non-existent context storage was specified', function() { Context.init({contextStorage:contextDefaultStorage}); Context.load(); context = Context.get("1","flow"); @@ -236,12 +282,39 @@ describe('context', function() { stubSet.called.should.be.true(); stubKeys.called.should.be.true(); }); + it('should use the default context', function() { + Context.init({contextStorage:contextDefaultStorage}); + Context.load(); + context = Context.get("1","flow"); + should.not.exist(context.get("$default.foo")); + context.set("$default.foo","default"); + context.get("$default.foo"); + context.keys("$default"); + stubGet.called.should.be.true(); + stubSet.called.should.be.true(); + stubKeys.called.should.be.true(); + }); + it('should use the alias of default context', function() { + Context.init({contextStorage:contextDefaultStorage}); + Context.load(); + context = Context.get("1","flow"); + should.not.exist(context.get("$.foo")); + context.set("$.foo","alias"); + context.get("$.foo"); + context.keys("$"); + stubGet.called.should.be.true(); + stubSet.called.should.be.true(); + stubKeys.called.should.be.true(); + }); it('should load memory module', function(done) { Context.init({ contextStorage: { _: {}}}); try { Context.load(); - context.set("_.foo","test"); - context.get("$_.foo"); + context.set("$_.foo","mem1"); + context.get("$_.foo").should.eql("mem1"); + var keys = context.keys("$_"); + keys.should.have.length(1); + keys[0].should.eql("foo"); done(); } catch (err) { done(err); @@ -256,6 +329,27 @@ describe('context', function() { done(err); } }); + it('should accept special storage name', function(done) { + Context.init({ + contextStorage:{ + "#%&":{module:"memory"}, + \u3042:{module:"memory"}, + 1:{module:"localfilesystem"}, + } + }); + try { + Context.load(); + context.set("$#%&.sign","sign1"); + context.get("$#%&.sign").should.eql("sign1"); + context.set("$\u3042.file2","file2"); + context.get("$\u3042.file2").should.eql("file2"); + context.set("$1.num","num3"); + context.get("$1.num").should.eql("num3"); + done(); + } catch (err) { + done(err); + } + }); }); describe('if external context storage does not exist',function() { @@ -285,18 +379,11 @@ describe('context', function() { } } }); - it('should fail when using invalid default context', function(done) { + it('should fail when using invalid default context', function() { Context.init({contextStorage:{default:"noexist"}}); - try { + (function() { Context.load(); - try { - should.fail(null, null, "An error was not thrown using undefined storage for flow context"); - } catch (err) { - done(err); - } - } catch (err) { - done(); - } + }).should.throw(); }); it('should store data on memory when contextStorage is not defined', function() { Context.init({}); @@ -309,31 +396,17 @@ describe('context', function() { context.global.set("$nonexist.key1", "val3"); context.global.get("$nonexist.key1").should.eql("val3"); }); - it('should fail for the storage with no module', function(done) { + it('should fail for the storage with no module', function() { Context.init({ contextStorage: { test: {}}}); - try { + (function() { Context.load(); - try { - should.fail(null, null, "Should fail when no module was specified"); - } catch (err) { - done(err); - } - } catch (err) { - done(); - } + }).should.throw(); }); - it('should fail to load non-existent module', function(done) { + it('should fail to load non-existent module', function() { Context.init({contextStorage:{ file:{module:"nonexistent"} }}); - try { + (function() { Context.load(); - try { - should.fail(null, null, "Should fail to load non-existent module"); - } catch (err) { - done(err); - } - } catch (err) { - done(); - } + }).should.throw(); }); }); }); @@ -347,12 +420,6 @@ describe('context', function() { result.key.should.eql(expectedKey); }; - function returnModule(input, expectedModule) { - var result = parseKey(input); - result.storage.should.eql(expectedModule); - should(result.key).be.null(); - }; - it('should return module and key', function() { returnModuleAndKey("$test.aaa","test","aaa"); returnModuleAndKey("$test.aaa.bbb","test","aaa.bbb"); @@ -362,32 +429,48 @@ describe('context', function() { returnModuleAndKey("$test.$foo.$bar","test","$foo.$bar"); returnModuleAndKey("$test..foo","test",".foo"); returnModuleAndKey("$test..","test","."); + returnModuleAndKey("$te-_st.aaa","te-_st","aaa"); + returnModuleAndKey("$te{st.a2","te{st","a2"); + returnModuleAndKey("$te[st.a3","te[st","a3"); + returnModuleAndKey("$te'st.a4","te'st","a4"); + returnModuleAndKey("$te\"st.a5","te\"st","a5"); }); - // it('should return only module', function() { - // returnModule("$test","test",null); - // returnModule("$1","1",null); - // returnModule("$$test","$test",null); - // returnModule("$test.","test.",null); - // }); - it('should return module as default', function() { returnModuleAndKey("$default.foo","default","foo"); returnModuleAndKey("$.foo","default","foo"); - // returnModule("$default","default"); - // returnModule("$","default"); }); - it('should return null', function() { - var keyPath = parseKey("test.aaa"); - keyPath.storage.should.eql(""); - keyPath.key.should.eql("test.aaa"); + it('should return only keys', function() { + returnModuleAndKey("test.aaa", "", "test.aaa"); + returnModuleAndKey("test", "", "test"); + returnModuleAndKey("$test", "", "$test"); + }); - keyPath = parseKey("test"); - keyPath.storage.should.eql(""); - keyPath.key.should.eql("test"); + it('should fail with null key', function() { + (function() { + parseKey(""); + }).should.throw(); - should(parseKey(null)).be.null(); + (function() { + parseKey(null); + }).should.throw(); + }); + + it('should fail with space character', function() { + (function() { + parseKey(" $test"); + }).should.throw(); + + (function() { + parseKey("$test .a"); + }).should.throw(); + }); + + it('should fail with empty key', function() { + (function() { + parseKey("$test."); + }).should.throw(); }); });