diff --git a/src/app/controllers/graphiteTarget.js b/src/app/controllers/graphiteTarget.js index 3e278d2259c..9ea62b4aab8 100644 --- a/src/app/controllers/graphiteTarget.js +++ b/src/app/controllers/graphiteTarget.js @@ -82,13 +82,24 @@ function (angular, _, config, graphiteFuncs, Parser) { $scope.segments = _.map(astNode.segments, function(segment) { return { + type: segment.type, val: segment.value, - html: segment.value === '*' ? '' : segment.value + html: getSegmentHtml(segment) }; }); } } + function getSegmentHtml(segment) { + if (segment.value === '*') { + return ''; + } + if (segment.type === 'template') { + return "[[" + segment.value + "]]"; + } + return segment.value; + } + function getSegmentPathUpTo(index) { var arr = $scope.segments.slice(0, index); diff --git a/src/app/services/graphite/lexer.js b/src/app/services/graphite/lexer.js index 482e7f7c635..00fd996859d 100644 --- a/src/app/services/graphite/lexer.js +++ b/src/app/services/graphite/lexer.js @@ -182,6 +182,7 @@ define([ match = this.scanIdentifier() || + this.scanTemplateSequence() || this.scanPunctuator() || this.scanNumericLiteral(); @@ -194,6 +195,26 @@ define([ return null; }, + scanTemplateSequence: function() { + if (this.peek() === '[' && this.peek(1) === '[') { + return { + type: 'templateStart', + value: '[[', + pos: this.char + }; + } + + if (this.peek() === ']' && this.peek(1) === ']') { + return { + type: 'templateEnd', + value: '[[', + pos: this.char + }; + } + + return null; + }, + /* * Extract a JavaScript identifier out of the next sequence of * characters or return 'null' if its not possible. In addition, diff --git a/src/app/services/graphite/parser.js b/src/app/services/graphite/parser.js index 1703aa7a4fa..bc72143c033 100644 --- a/src/app/services/graphite/parser.js +++ b/src/app/services/graphite/parser.js @@ -29,29 +29,61 @@ define([ } }, - metricExpression: function() { + metricSegment: function() { + if (this.match('identifier')) { + this.index++; + return { + type: 'segment', + value: this.tokens[this.index-1].value + }; + } + + if (!this.match('templateStart')) { + this.errorMark('Expected metric identifier'); + } + + this.index++; + if (!this.match('identifier')) { + this.errorMark('Expected identifier after templateStart'); + } + + var node = { + type: 'template', + value: this.tokens[this.index].value + }; + + this.index++; + + if (!this.match('templateEnd')) { + this.errorMark('Expected templateEnd'); + } + + this.index++; + return node; + }, + + metricExpression: function() { + if (!this.match('templateStart') && !this.match('identifier')) { return null; } var node = { type: 'metric', - segments: [{ - type: 'segment', - value: this.tokens[this.index].value - }] + segments: [] }; - this.index++; + node.segments.push(this.metricSegment()); - if (this.match('.')) { + while(this.match('.')) { this.index++; - var rest = this.metricExpression(); - if (!rest) { + + var segment = this.metricSegment(); + if (!segment) { this.errorMark('Expected metric identifier'); } - node.segments = node.segments.concat(rest.segments); + node.segments.push(segment); } return node; diff --git a/src/test/karma.conf.js b/src/test/karma.conf.js new file mode 100644 index 00000000000..e28da108300 --- /dev/null +++ b/src/test/karma.conf.js @@ -0,0 +1,27 @@ +module.exports = function(config) { + config.set({ + basePath: '../', + + frameworks: ['mocha', 'requirejs', 'expect'], + + // list of files / patterns to load in the browser + files: [ + 'test/test-main.js', + {pattern: 'app/**/*.js', included: false}, + {pattern: 'test/**/*.js', included: false} + ], + + // list of files to exclude + exclude: [ + ], + + reporters: ['progress'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + captureTimeout: 60000, + singleRun: false + }); +}; diff --git a/src/test/specs/lexer-specs.js b/src/test/specs/lexer-specs.js index b933e16bedb..06bd63d35da 100644 --- a/src/test/specs/lexer-specs.js +++ b/src/test/specs/lexer-specs.js @@ -30,6 +30,15 @@ define([ expect(tokens[tokens.length - 1].value).to.be(')'); }); + it('should tokenize metric with template parameter', function() { + var lexer = new Lexer("metric.[[server]].test"); + var tokens = lexer.tokenize(); + expect(tokens[2].type).to.be('templateStart'); + expect(tokens[3].type).to.be('identifier'); + expect(tokens[3].value).to.be('server'); + expect(tokens[4].type).to.be('templateEnd'); + }); + it('should handle error with unterminated string', function() { var lexer = new Lexer("alias(metric, 'asd)"); var tokens = lexer.tokenize(); diff --git a/src/test/specs/parser-specs.js b/src/test/specs/parser-specs.js index 8a3e0b497a7..c218e42bc10 100644 --- a/src/test/specs/parser-specs.js +++ b/src/test/specs/parser-specs.js @@ -54,6 +54,15 @@ define([ expect(rootNode.params[1].type).to.be('metric'); }); + it('function with templated series', function() { + var parser = new Parser("sum(test.[[server]].count)"); + var rootNode = parser.getAst(); + + expect(rootNode.message).to.be(undefined) + expect(rootNode.params[0].type).to.be('metric'); + expect(rootNode.params[0].segments[1].type).to.be('template'); + }); + it('invalid metric expression', function() { var parser = new Parser('metric.test.*.asd.'); var rootNode = parser.getAst(); diff --git a/src/test/test-main.js b/src/test/test-main.js new file mode 100644 index 00000000000..3b48c825c04 --- /dev/null +++ b/src/test/test-main.js @@ -0,0 +1,10 @@ +require.config({ + baseUrl:'base' +}); + +require([ + 'test/specs/lexer-specs', + 'test/specs/parser-specs', +], function () { + window.__karma__.start(); +}); \ No newline at end of file diff --git a/tasks/options/karma.js b/tasks/options/karma.js new file mode 100644 index 00000000000..f91c9772267 --- /dev/null +++ b/tasks/options/karma.js @@ -0,0 +1,9 @@ +module.exports = function(config) { + return { + unit: { + configFile: 'src/test/karma.conf.js', + singleRun: false, + browsers: ['Chrome'] + } + }; +}; \ No newline at end of file