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.
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";
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);
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.
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");
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"')
// 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"
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;
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;
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