Wednesday, October 29, 2014

Irony grammars

Recently I had task to create domain specific language. Looking for some open source implementation to speed up start I found Irony project .

Here's how I created my grammar.

Creating grammar with Irony


Start by creating class that inherits Irony.Parsing.Grammar.

 public class ATestGrammar : Grammar

You can add some extra information about language which you are creating by Language attribute:

[Language("ATest", "1.0", "Automated tests description")]
public class ATestGrammar : Grammar

We have added name, version and description of our language. This information is displayed in Irony.GrammarExplorer tool included in download.

Next, let's create class constructor where we will describe our grammar

public ATestGrammar() : base(false)
{
}

By calling base(false) we are indicating that our grammar is not case sensitive.

If you want to create extra information for Irony.GrammarExplorer (or your implementation) you can add

GrammarComments = "Some text";

Irony Grammar Comments


Let's define single line and delimited comments :

var SingleLineComment = new CommentTerminal("SingleLineComment", "//", "\r", "\n", "\u2085", "\u2028", "\u2029");
var DelimitedComment = new CommentTerminal("DelimitedComment", "/*", "*/");

NonGrammarTerminals.Add(SingleLineComment);
NonGrammarTerminals.Add(DelimitedComment);

Irony Grammar Elements


Elements are describing grammar structure. In our example we have following elements

var testcase = new NonTerminal("Case");
var id = new NonTerminal("TestCaseId");
var description = new NonTerminal("TestCaseDescription");
var steplist = new NonTerminal("CaseSteps");
var step = new NonTerminal("CaseStep");
var stepIn = new NonTerminal("CaseStepStart");
var call = new NonTerminal("CaseStepCall");
var callArguments = new NonTerminal("CaseStepCallArguments");
var stepDescription = new NonTerminal("CaseStepCallDescription");
var stepReturnValue = new NonTerminal("CaseStepCallReturnValue");
var stepExpectedValue = new NonTerminal("CaseStepCallExcpectedValue");

which are non-terminal (meaning there are children elements).

We must define root non-terminal element of our grammar:

this.Root = testcase;

And we have following terminal elements

var caseId = new StringLiteral("CaseId","\"", StringOptions.None);
var caseDescription = new StringLiteral("CaseDescription", "\"", StringOptions.None);
var callMethod = new StringLiteral("StepCallMethod", "\"", StringOptions.None);
var callMethodArguments = new StringLiteral("StepCallMethodArguments", "'", StringOptions.None);
var callDescription = new StringLiteral("CallDescription", "\"", StringOptions.None);
var callReturnValue = new StringLiteral("CallReturnValue", "\"", StringOptions.None);
var callExpectedValue = new StringLiteral("CallExpectedValue", "\"", StringOptions.None);

Those are elements which do not have children and which will hold values parsed using our grammar.

Irony Grammar Terms


We can treat terms as keywords. Let's define few:

var COLON = ToTerm(":");
var ID = ToTerm("id");
var DESCRIPTION = ToTerm("Description");
var STEP = ToTerm("step");
var CALL = ToTerm("Call");
var CALLARGUMENTS = ToTerm("CallArguments");
var LEFTPARENTIS = ToTerm("(");
var RIGHTPARENTIS = ToTerm(")");
var SAVERETURNVALUEAS = ToTerm("SaveReturnValueAs");
var EXPECTEDVALUE = ToTerm("ExpectedValue");

Irony Grammar Rules


Now we need to define actual grammar rules:

testcase.Rule = id + description + steplist;
id.Rule = ID + COLON + caseId;
description.Rule = DESCRIPTION + COLON + caseDescription;
steplist.Rule = MakePlusRule(steplist, step);
step.Rule = stepIn + call + callArguments + stepDescription + stepReturnValue + stepExpectedValue;
stepIn.Rule = STEP + COLON;
call.Rule = CALL + COLON + callMethod;
callArguments.Rule = Empty | CALLARGUMENTS + COLON + LEFTPARENTIS + callMethodArguments + RIGHTPARENTIS;
stepDescription.Rule = DESCRIPTION + COLON + callDescription;
stepReturnValue.Rule = Empty | SAVERETURNVALUEAS + COLON + callReturnValue;
stepExpectedValue.Rule = Empty | EXPECTEDVALUE + COLON + callExpectedValue;

Ok, so here's how we are reading our rules:

testcase.Rule = id + description + steplist;

Test Case have 3 elements: Id, Description and Step list

id.Rule = ID + COLON + caseId;

Id element have 3 elements: keyword id, keyword : and actual value. Example:

Id: "CheckService"

steplist.Rule = MakePlusRule(steplist, step);

Step list element have multiple Step elements.

callArguments.Rule = Empty | CALLARGUMENTS + COLON + LEFTPARENTIS + callMethodArguments + RIGHTPARENTIS;

Call arguments is either empty or in form

CallArguments: ('"ServiceName", "MachineName" , "Username" , "Password"')

DSL example source file


// Author: Milan Bundalo
// Date created: 10.9.2014
// Test case example
Id: "CheckService"
Description: "Check if service can be restarted."
Step:
Call: "StopService"
CallArguments: ('"Service1", "LIGHTNING", "User1", "Password"')
Description: "Stop Service1 on server Lightning."
                        ExpectedValue: "0"
Step:
Call: "Wait"
CallArguments: ('360000')
Description: "Wait 6*60*1000 milliseconds."
Step:
Call: "StartService"
CallArguments: ('"Service1" , "LIGHTNING", "User1","Password"')
Description: "Start Service1 on server Lightning."
                        ExpectedValue: "0"

Parsing Irony Grammar


Let's create console application to parse DSL example source file. In main method we need to add:

var grammar = new ATestGrammar.ATestGrammar();
var parser = new Parser(grammar);
var specification = System.IO.File.ReadAllText(args[0]);
var result = parser.Parse(specification);
if (result.HasErrors())
    {
        Console.WriteLine(String.Format("{0} at line {1} column {2}", result.ParserMessages[0].Message, result.ParserMessages[0].Location.Line, result.ParserMessages[0].Location.Column));
                throw new ArgumentException("Configuration is not in required format!");
      }

var testcase = result.Root;

Reading parsed Irony Grammar Nodes


Here are few examples  how to position node

var idInfo = testcase.ChildNodes.SingleOrDefault(s => s.Term.Name == "TestCaseId");

var description = testcase.ChildNodes.SingleOrDefault(s => s.Term.Name == "TestCaseDescription");

var caseSteps = testcase.ChildNodes.SingleOrDefault(s => s.Term.Name == "CaseSteps");

and how to read values

var testCaseId = idInfo.ChildNodes.SingleOrDefault(s => s.Term.Name == "CaseId").Token.ValueString;

var testCaseDescription = description.ChildNodes.SingleOrDefault(s => s.Term.Name == "CaseDescription").Token.ValueString;

No comments:

Post a Comment