AMBARI-20252. Add ability to use brace expansion in the dependent keys(onechiporenko)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/bcbc597b Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/bcbc597b Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/bcbc597b Branch: refs/heads/branch-feature-AMBARI-12556 Commit: bcbc597b7bfa79ae884e2f82eab1f6f6fce022be Parents: 890996f Author: Oleg Nechiporenko <onechipore...@apache.org> Authored: Wed Mar 1 12:13:56 2017 +0200 Committer: Oleg Nechiporenko <onechipore...@apache.org> Committed: Wed Mar 1 12:13:56 2017 +0200 ---------------------------------------------------------------------- ambari-web/app/utils/ember_reopen.js | 105 ++++++++++++++++++++++ ambari-web/test/utils/ember_reopen_test.js | 112 ++++++++++++++++++++++++ 2 files changed, 217 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/bcbc597b/ambari-web/app/utils/ember_reopen.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/ember_reopen.js b/ambari-web/app/utils/ember_reopen.js index c7f93af..ff55b30 100644 --- a/ambari-web/app/utils/ember_reopen.js +++ b/ambari-web/app/utils/ember_reopen.js @@ -16,6 +16,11 @@ * limitations under the License. */ +const {assert, warn, EXTEND_PROTOTYPES} = Ember; + +const END_WITH_EACH_REGEX = /\.@each$/; +const DEEP_EACH_REGEX = /\.@each\.[^.]+\./; + /** Merge the contents of two objects together into the first object. @@ -350,3 +355,103 @@ Ember.Router.reopen({ } } }); + +function expandProperties(pattern, callback) { + assert('A computed property key must be a string', typeof pattern === 'string'); + assert( + 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"', + pattern.indexOf(' ') === -1 + ); + + let unbalancedNestedError = `Brace expanded properties have to be balanced and cannot be nested, pattern: ${pattern}`; + let properties = [pattern]; + + // Iterating backward over the pattern makes dealing with indices easier. + let bookmark; + let inside = false; + for (let i = pattern.length; i > 0; --i) { + let current = pattern[i - 1]; + + switch (current) { + // Closing curly brace will be the first character of the brace expansion we encounter. + // Bookmark its index so long as we're not already inside a brace expansion. + case '}': + if (!inside) { + bookmark = i - 1; + inside = true; + } else { + assert(unbalancedNestedError, false); + } + break; + // Opening curly brace will be the last character of the brace expansion we encounter. + // Apply the brace expansion so long as we've already seen a closing curly brace. + case '{': + if (inside) { + let expansion = pattern.slice(i, bookmark).split(','); + // Iterating backward allows us to push new properties w/out affecting our "cursor". + for (let j = properties.length; j > 0; --j) { + // Extract the unexpanded property from the array. + let property = properties.splice(j - 1, 1)[0]; + // Iterate over the expansion, pushing the newly formed properties onto the array. + for (let k = 0; k < expansion.length; ++k) { + properties.push(property.slice(0, i - 1) + + expansion[k] + + property.slice(bookmark + 1)); + } + } + inside = false; + } else { + assert(unbalancedNestedError, false); + } + break; + } + } + if (inside) { + assert(unbalancedNestedError, false); + } + + for (let i = 0; i < properties.length; i++) { + callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]')); + } +} + +Ember.ComputedProperty.prototype.property = function () { + let args = []; + + function addArg(property) { + warn( + `Dependent keys containing @each only work one level deep. ` + + `You used the key "${property}" which is invalid. ` + + `Please create an intermediary computed property.`, + DEEP_EACH_REGEX.test(property) === false + ); + args.push(property); + } + + for (let i = 0; i < arguments.length; i++) { + expandProperties(arguments[i], addArg); + } + + this._dependentKeys = args.uniq(); + return this; +} + +Ember.observer = function(func, ...paths) { + let args = []; + for (let i = 0; i < paths.length; i++) { + expandProperties(paths[i], property => args.push(property)); + } + func.__ember_observes__ = args.uniq(); + return func; +}; + +if(EXTEND_PROTOTYPES) { + Function.prototype.observes = function () { + let args = []; + for (let i = 0; i < arguments.length; i++) { + expandProperties(arguments[i], property => args.push(property)); + } + this.__ember_observes__ = args.uniq(); + return this; + }; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/bcbc597b/ambari-web/test/utils/ember_reopen_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/utils/ember_reopen_test.js b/ambari-web/test/utils/ember_reopen_test.js index aa50a50..06e89be 100644 --- a/ambari-web/test/utils/ember_reopen_test.js +++ b/ambari-web/test/utils/ember_reopen_test.js @@ -135,4 +135,116 @@ describe('Ember functionality extension', function () { }); }); }); + + describe('#expand_properties', function () { + + var body; + var tests = [ + { + keys: ['a', 'b'], + expandedKeys: ['a', 'b'] + }, + { + keys: ['a.b', 'b'], + expandedKeys: ['a.b', 'b'] + }, + { + keys: ['a', 'a.b'], + expandedKeys: ['a', 'a.b'] + }, + { + keys: ['a.b', 'a.b'], + expandedKeys: ['a.b'] + }, + { + keys: ['a.b', 'b.c'], + expandedKeys: ['a.b', 'b.c'] + }, + { + keys: ['a.{b,c}'], + expandedKeys: ['a.b', 'a.c'] + }, + { + keys: ['{a,b}.c'], + expandedKeys: ['a.c', 'b.c'] + }, + { + keys: ['a.{b,c,d}'], + expandedKeys: ['a.b', 'a.c', 'a.d'] + }, + { + keys: ['a.@each.{b,c}'], + expandedKeys: ['a.@each.b', 'a.@each.c'] + }, + { + keys: ['a.{b.[],c}'], + expandedKeys: ['a.b.[]', 'a.c'] + }, + { + keys: ['a.{b,c.[]}'], + expandedKeys: ['a.b', 'a.c.[]'] + }, + { + keys: ['a.{b,c.[],b}'], + expandedKeys: ['a.b', 'a.c.[]'] + }, + { + keys: ['{a,b}.{c,d}'], + expandedKeys: ['a.d', 'b.d', 'a.c', 'b.c'] + } + ]; + + beforeEach(function () { + body = function () {}; + }); + + describe('#computed properties', function() { + + describe('#Ember.computed', function () { + tests.forEach(function(test) { + it(JSON.stringify(test.keys), function () { + var args = test.keys.slice(0); + args.push(body); + var cp = Ember.computed.apply(null, args); + expect(cp._dependentKeys).to.be.eql(test.expandedKeys); + }); + }); + }); + + describe('#function(){}.property', function () { + tests.forEach(function(test) { + it(JSON.stringify(test.keys), function () { + var cp = body.property.apply(null, test.keys); + expect(cp._dependentKeys).to.be.eql(test.expandedKeys); + }); + }); + }); + + }); + + describe('#observers', function () { + + describe('#Ember.observer', function () { + tests.forEach(function(test) { + it(JSON.stringify(test.keys), function () { + var args = [body].concat(test.keys); + var obs = Ember.observer.apply(null, args); + expect(obs.__ember_observes__).to.be.eql(test.expandedKeys); + }); + }); + }); + + describe('#function(){}.observes', function () { + tests.forEach(function(test) { + it(JSON.stringify(test.keys), function () { + var obs = body.observes.apply(body, test.keys); + expect(obs.__ember_observes__).to.be.eql(test.expandedKeys); + }); + }); + }); + + }); + + }); + });