Skip to content

Commit

Permalink
[api] Completed work on the parser
Browse files Browse the repository at this point in the history
  • Loading branch information
indexzero committed Aug 29, 2010
1 parent 37dc738 commit 6a7f1b0
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 19 deletions.
24 changes: 24 additions & 0 deletions examples/simple.ast
@@ -0,0 +1,24 @@
{
'1': {
name: 'Addition',
description: "In order to avoid silly mistakes\n As a math idiot\n I want to be told the sum of two numbers",
scenarios: [
{
outline: true,
name: 'Add two numbers',
breakdown: [
{ '1': ['Given', 'I have entered 50 into the calculator'] },
{ '2': ['And', 'I have entered 70 into the calculator'] },
{ '3': ['When', 'I press add'] },
{ '4': ['Then', 'the result should be 120 on the screen'] }
],
examples: {
// Remark: Only valid if outline === true
"header0" : ['value1', 'value2', 'value3']
"header1" : ['value1', 'value2', 'value3']
"header2" : ['value1', 'value2', 'value3']
}
}
]
}
}
125 changes: 110 additions & 15 deletions lib/kyuri/parser.js
Expand Up @@ -13,6 +13,33 @@ var Parser = function () {

};

//
// Helper method to get the last feature
// (i.e. the current feature we are building the ast for)
//
var getLastFeature = function (ast) {
var features = Object.keys(ast);
return ast[features[features.length - 1]];
};

//
// Helper method to get the last scenario
// (i.e. the current scenario we are building the ast for)
//
var getLastScenario = function (ast) {
var feature = getLastFeature(ast);
return feature.scenarios[feature.scenarios.length - 1];
};

//
// Helper method to get the last step
// (i.e. the current step we are building the ast for)
//
var getLastStep = function (ast) {
var scenario = getLastScenario(ast);
return scenario.breakdown[scenario.breakdown.length - 1];
}

var _states = {
start: {
transitions: {
Expand All @@ -28,17 +55,23 @@ var _states = {
},

feature: {
parent: 'start',
transitions: {
'SENTENCE': {
value: '*',
next: 'featureHeader'
next: 'featureHeader',
build: function (ast, token) {
var id = Object.keys(ast).length + 1;
ast[id.toString()] = {
name: token[1],
description: '',
scenarios: []
};
}
}
}
},

featureHeader: {
parent: 'feature',
transitions: {
'TERMINATOR': {
value: '*',
Expand All @@ -54,7 +87,6 @@ var _states = {
},

featureDescription: {
parent: 'feature',
transitions: {
'TERMINATOR': {
value: '*',
Expand All @@ -64,28 +96,51 @@ var _states = {
'SENTENCE': {
value: '*',
next: 'featureDescription',
last: ['TERMINATOR', 'INDENT']
last: ['TERMINATOR', 'INDENT'],
build: function (ast, token) {
var feature = getLastFeature(ast);
feature.description += token[1] + '\n';
}
},
'SCENARIO': {
value: 'SCENARIO',
next: 'scenario',
last: ['TERMINATOR', 'OUTDENT']
last: ['TERMINATOR', 'OUTDENT'],
build: function (ast, token) {
var feature = getLastFeature(ast);
feature.scenarios.push({
outline: false,
breakdown: [],
});
}
},
'SCENARIO_OUTLINE': {
value: 'SCENARIO_OUTLINE',
next: 'scenario',
last: ['TERMINATOR', 'OUTDENT']
last: ['TERMINATOR', 'OUTDENT'],
build: function (ast, token) {
var feature = getLastFeature(ast);
feature.scenarios.push({
outline: true,
breakdown: [],
hasExamples: false,
examples: {}
});
}
}
}
},

scenario: {
parent: 'feature',
transitions: {
'SENTENCE': {
value: '*',
next: 'scenarioHeader',
last: ['SCENARIO', 'SCENARIO_OUTLINE']
last: ['SCENARIO', 'SCENARIO_OUTLINE'],
build: function (ast, token) {
var scenario = getLastScenario(ast);
scenario.name = token[1];
}
}
}
},
Expand All @@ -110,12 +165,21 @@ var _states = {
'OPERATOR': {
value: '*',
next: 'stepBody',
last: ['INDENT', 'TERMINATOR']
last: ['INDENT', 'TERMINATOR'],
build: function (ast, token) {
var scenario = getLastScenario(ast),
id = scenario.breakdown.length + 1,
step = {};

step[id.toString()] = [token[1]];
scenario.breakdown.push(step);
}
},
'EXAMPLES': {
value: 'EXAMPLES',
next: 'examples',
last: 'TERMINATOR'
// Remark: Could in 'build' here to see outline === true and throw, good idea?
},
'OUTDENT': {
value: 1,
Expand All @@ -135,7 +199,13 @@ var _states = {
'SENTENCE': {
value: '*',
next: 'stepBody',
last: 'OPERATOR'
last: 'OPERATOR',
build: function (ast, token) {
var scenario = getLastScenario(ast),
step = getLastStep(ast);

step[scenario.breakdown.length].push(token[1]);
}
},
'TERMINATOR': {
value: '*',
Expand Down Expand Up @@ -170,7 +240,23 @@ var _states = {
'EXAMPLE_ROW': {
value: '*',
next: 'exampleRows',
last: ['INDENT', 'TERMINATOR']
last: ['INDENT', 'TERMINATOR'],
build: function (ast, token) {
var scenario = getLastScenario(ast);
if(!scenario.hasExamples) {
for (var i = 0; i < token[1].length; i++) {
scenario.examples[token[1][i]] = [];
}

scenario.exampleVariables = token[1];
scenario.hasExamples = true;
}
else {
for (var i = 0; i < token[1].length; i++) {
scenario.examples[scenario.exampleVariables[i]].push(token[1][i]);
}
}
}
},
'TERMINATOR': {
value: '*',
Expand All @@ -194,6 +280,7 @@ var _states = {
Parser.prototype = {
parse: function (tokens) {
this.tokens = tokens;
this.ast = {};
this.states = Object.create(_states);
this.current = this.states['start'];
this.last = null;
Expand All @@ -204,6 +291,8 @@ Parser.prototype = {
this.checkToken(token);
this.last = token;
}

return this.ast;
},

checkToken: function (token) {
Expand All @@ -217,12 +306,18 @@ Parser.prototype = {
},

checkTransition: function (token, transition) {
// If the transition value matches the current token and the last token
// was expected in our state machine, enter that state. Otherwise
// throw an error for an unexpected value.
if ((transition.value === '*' || transition.value === token[1])
&& this.checkLast(transition)) {
// If the transition value matches the current token and the last token
// was expected in our state machine, enter that state
// If we need to modify the ast in some way for this
// transition, do so now.
if (typeof transition.build === 'function') {
transition.build(this.ast, token);
}

this.current = this.states[transition.next];
//sys.puts('Transition to: ' + transition.next);
}
else {
throw new Error('Unexpected value "' + token[1] + '" for token "' + token[0] + '" at line ' + token[2]);
Expand Down
22 changes: 18 additions & 4 deletions test/parser-test.js
Expand Up @@ -13,8 +13,22 @@ var kyuri = require('kyuri'),
path = require('path'),
vows = require('vows'),
assert = require('assert'),
eyes = require('eyes');

inspect = require('eyes').inspector({
styles: {
all: 'cyan', // Overall style applied to everything
label: 'underline', // Inspection labels, like 'array' in `array: [1, 2, 3]`
other: 'inverted', // Objects which don't have a literal representation, such as functions
key: 'bold', // The keys in object literals, like 'a' in `{a: 1}`

special: 'grey', // null, undefined...
string: 'green',
number: 'magenta',
bool: 'blue', // true false
regexp: 'green', // /\d+/
},
maxLength: 4096
});

var readAllLines = function (filename) {
return function () {
fs.readFile(filename, encoding = 'ascii', this.callback);
Expand All @@ -27,14 +41,14 @@ vows.describe('kyuri/parser').addBatch({
topic: readAllLines(path.join(__dirname, '..', 'examples', 'simple.feature')),
"should parse correctly": function (err, data) {
assert.isNotNull(data.toString());
eyes.inspect(kyuri.parse(data.toString()));
inspect(kyuri.parse(data.toString()));
}
},
"parsing complex.feature": {
topic: readAllLines(path.join(__dirname, '..', 'examples', 'complex.feature')),
"should parse correctly": function (err, data) {
assert.isNotNull(data.toString());
eyes.inspect(kyuri.parse(data.toString()));
inspect(kyuri.parse(data.toString()));
}
}
}
Expand Down

0 comments on commit 6a7f1b0

Please sign in to comment.