A Gherkin parser for MoonBit with DOM, visitor, fold, and SAX-style APIs
Dependencies
moon add moonrockz/gherkinlet source = @gherkin.Source::from_string(
"Feature: Login\n Scenario: Success\n Given a user\n When they log in\n Then they see the dashboard",
)
let doc = @gherkin.parse!(source)
let feature = doc.feature.unwrap()
// feature.name == "Login"let doc = @gherkin.parse!(source)
// doc.feature, doc.comments — full treedoc.accept(my_visitor)let count = doc.fold(0, {
..@gherkin.GherkinFold::default(),
visit_step: @gherkin.continuing(fn(n, _) { n + 1 }),
})@gherkin.parse_with_handler!(source, my_handler)mise run build:component # produces _build/gherkin.component.wasmmoon run src/cmd/main -- path/to/file.featureecho "Feature: Test" | moon run src/cmd/main -- -///|
test "create a source from a string" {
let src = @gherkin.Source::from_string(
"Feature: Hello\n Scenario: World",
uri="test.feature",
)
@debug.debug_inspect(src.uri(), content="Some(\"test.feature\")")
@debug.debug_inspect(src.line_count(), content="2")
@debug.debug_inspect(src.line(1), content="Some(\"Feature: Hello\")")
}///|
test "parse a feature with scenarios and steps" {
let input =
#|Feature: Calculator
#| Scenario: Addition
#| Given two numbers
#| When I add them
#| Then I get the sum
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
let feature = doc.feature.unwrap()
@debug.debug_inspect(
feature.name,
content=(
#|"Calculator"
),
)
@debug.debug_inspect(
feature.language,
content=(
#|"en"
),
)
feature.children[0] is Scenario(s)
@debug.debug_inspect(
s.name,
content=(
#|"Addition"
),
)
@debug.debug_inspect(s.steps.length(), content="3")
@debug.debug_inspect(s.steps[0].keyword_type, content="Context")
@debug.debug_inspect(s.steps[1].keyword_type, content="Action")
@debug.debug_inspect(s.steps[2].keyword_type, content="Outcome")
}///|
test "parse a step with a data table" {
let input =
#|Feature: Users
#| Scenario: List users
#| Given users:
#| | name | age |
#| | Alice | 30 |
#| | Bob | 25 |
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
doc.feature.unwrap().children[0] is Scenario(s)
s.steps[0].argument is Some(DataTable(table))
@debug.debug_inspect(table.rows.length(), content="3")
@debug.debug_inspect(
table.rows[0].cells[0].value,
content=(
#|"name"
),
)
@debug.debug_inspect(
table.rows[1].cells[0].value,
content=(
#|"Alice"
),
)
@debug.debug_inspect(
table.rows[2].cells[1].value,
content=(
#|"25"
),
)
}///|
test "parse a step with a doc string" {
let input =
#|Feature: Payloads
#| Scenario: JSON body
#| Given a request body:
#| ```json
#| {"key": "value"}
#| ```
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
doc.feature.unwrap().children[0] is Scenario(s)
s.steps[0].argument is Some(DocString(ds))
@debug.debug_inspect(ds.media_type, content="Some(\"json\")")
assert_true(ds.content.contains("key"))
}///|
test "parse tags on features and scenarios" {
let input =
#|@smoke @regression
#|Feature: Tagged
#| @critical
#| Scenario: Important
#| Given a step
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
let feature = doc.feature.unwrap()
@debug.debug_inspect(feature.tags.length(), content="2")
@debug.debug_inspect(
feature.tags[0].name,
content=(
#|"@smoke"
),
)
feature.children[0] is Scenario(s)
@debug.debug_inspect(
s.tags[0].name,
content=(
#|"@critical"
),
)
}///|
test "serialize a document to JSON" {
let doc = @gherkin.parse(
@gherkin.Source::from_string(
"Feature: JSON\n Scenario: Test\n Given a step",
),
)
let text = doc.to_json().stringify()
assert_true(text.contains("JSON"))
assert_true(text.contains("Test"))
}///|
struct ScenarioCounter {
mut count : Int
}
///|
impl GherkinVisitor for ScenarioCounter with fn visit_scenario(self, _scenario) {
self.count 1
}
///|
test "count scenarios with a visitor" {
let input =
#|Feature: Counter
#| Scenario: First
#| Given a
#| Scenario: Second
#| Given b
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
let counter : ScenarioCounter = { count: 0, }
doc.accept(counter)
@debug.debug_inspect(counter.count, content="2")
}///|
test "count steps with fold" {
let input =
#|Feature: Fold
#| Scenario: One
#| Given step 1
#| When step 2
#| Scenario: Two
#| Then step 3
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
let step_count = doc.fold(0, {
..@gherkin.GherkinFold::default(),
visit_step: @gherkin.continuing(fn(n, _step) { n + 1 }),
})
@debug.debug_inspect(step_count, content="3")
}///|
let step_count = doc.fold(0, {
..GherkinFold::default(),
visit_scenario: fn(n, scenario) {
if scenario.tags.iter().any(fn(t) { t.name == "@skip" }) {
SkipChildren(n) // skip steps inside @skip scenarios
} else {
Continue(n)
}
},
visit_step: continuing(fn(n, _) { n + 1 }),
})///|
struct EventLog {
events : Array[String]
}
///|
impl GherkinHandler for EventLog with fn on_feature(self, event) {
self.events.push("feature:\{event.name}")
}
///|
impl GherkinHandler for EventLog with fn on_scenario(self, event) {
self.events.push("scenario:\{event.name}")
}
///|
impl GherkinHandler for EventLog with fn on_step(self, event) {
self.events.push("step:\{event.text}")
}
///|
test "log events with a handler" {
let input =
#|Feature: Events
#| Scenario: Example
#| Given a step
#| When an action
let logger : EventLog = { events: [], }
@gherkin.parse_with_handler(@gherkin.Source::from_string(input), logger)
@debug.debug_inspect(
logger.events[0],
content=(
#|"feature:Events"
),
)
@debug.debug_inspect(
logger.events[1],
content=(
#|"scenario:Example"
),
)
@debug.debug_inspect(
logger.events[2],
content=(
#|"step:a step"
),
)
@debug.debug_inspect(
logger.events[3],
content=(
#|"step:an action"
),
)
}///|
test "tokenize a source" {
let tokens = @gherkin.tokenize(
@gherkin.Source::from_string("Feature: Test\n Given a step"),
)
tokens[0] is FeatureLine(_, kw, name)
@debug.debug_inspect(
kw,
content=(
#|"Feature"
),
)
@debug.debug_inspect(
name,
content=(
#|"Test"
),
)
tokens[1] is StepLine(_, _, kt, text)
@debug.debug_inspect(kt, content="Context")
@debug.debug_inspect(
text,
content=(
#|"a step"
),
)
}///|
test "lazy iteration with Lexer" {
let lexer = @gherkin.Lexer::new(
@gherkin.Source::from_string("Given a\nWhen b\nThen c"),
)
let mut count = 0
for tok in lexer.iter() {
match tok {
StepLine(_, _, _, _) => count 1
_ => ()
}
}
@debug.debug_inspect(count, content="3")
}///|
test "handle parse errors" {
let input =
#|Feature: Tables
#| Scenario: Bad
#| Given a table:
#| | a | b |
#| | 1 | 2 | 3 |
let result = try {
let _ = @gherkin.parse(@gherkin.Source::from_string(input))
"ok"
} catch {
InconsistentTableCells(message~, ..) => message
_ => "other"
}
assert_true(result.contains("inconsistent cell count"))
}///|
test "parse French Gherkin" {
let input =
#|# language: fr
#|Fonctionnalité: Connexion
#| Scénario: Succès
#| Soit un utilisateur
let doc = @gherkin.parse(@gherkin.Source::from_string(input))
let feature = doc.feature.unwrap()
@debug.debug_inspect(
feature.language,
content=(
#|"fr"
),
)
@debug.debug_inspect(
feature.keyword,
content=(
#|"Fonctionnalité"
),
)
}pub(open) trait GherkinHandler {
fn on_document(Self) -> Unit = _
fn on_end_document(Self) -> Unit = _
fn on_feature(Self, FeatureEvent) -> Unit = _
fn on_end_feature(Self) -> Unit = _
fn on_rule(Self, RuleEvent) -> Unit = _
fn on_end_rule(Self) -> Unit = _
fn on_background(Self, BackgroundEvent) -> Unit = _
fn on_end_background(Self) -> Unit = _
fn on_scenario(Self, ScenarioEvent) -> Unit = _
fn on_end_scenario(Self) -> Unit = _
fn on_step(Self, StepEvent) -> Unit = _
fn on_examples(Self, ExamplesEvent) -> Unit = _
fn on_tag(Self, TagEvent) -> Unit = _
fn on_comment(Self, CommentEvent) -> Unit = _
fn on_doc_string(Self, DocStringEvent) -> Unit = _
fn on_data_table(Self, DataTableEvent) -> Unit = _
}pub(open) trait GherkinVisitor {
fn visit_document(Self, GherkinDocument) -> Unit = _
fn visit_feature(Self, Feature) -> Unit = _
fn visit_rule(Self, Rule) -> Unit = _
fn visit_background(Self, Background) -> Unit = _
fn visit_scenario(Self, Scenario) -> Unit = _
fn visit_step(Self, Step) -> Unit = _
fn visit_doc_string(Self, DocString) -> Unit = _
fn visit_data_table(Self, DataTable) -> Unit = _
fn visit_examples(Self, Examples) -> Unit = _
fn visit_tag(Self, Tag) -> Unit = _
fn visit_comment(Self, Comment) -> Unit = _
fn visit_table_row(Self, TableRow) -> Unit = _
}pub suberror ParseError {
UnexpectedToken(message~ : String, location~ : Location)
UnexpectedEof(message~ : String, location~ : Location)
InconsistentTableCells(message~ : String, location~ : Location)
CompositeError(errors~ : Array[ParseError])
}pub(all) enum GherkinEvent {
DocumentStart
DocumentEnd
FeatureStart(FeatureEvent)
FeatureEnd
RuleStart(RuleEvent)
RuleEnd
BackgroundStart(BackgroundEvent)
BackgroundEnd
ScenarioStart(ScenarioEvent)
ScenarioEnd
Step(StepEvent)
DocString(DocStringEvent)
DataTable(DataTableEvent)
Examples(ExamplesEvent)
Tag(TagEvent)
Comment(CommentEvent)
} derive(Eq, Debug)pub(all) struct GherkinFold[A] {
visit_document : (A, GherkinDocument) -> FoldAction[A]
visit_feature : (A, Feature) -> FoldAction[A]
visit_rule : (A, Rule) -> FoldAction[A]
visit_background : (A, Background) -> FoldAction[A]
visit_scenario : (A, Scenario) -> FoldAction[A]
visit_step : (A, Step) -> FoldAction[A]
visit_doc_string : (A, DocString) -> FoldAction[A]
visit_data_table : (A, DataTable) -> FoldAction[A]
visit_examples : (A, Examples) -> FoldAction[A]
visit_tag : (A, Tag) -> FoldAction[A]
visit_comment : (A, Comment) -> FoldAction[A]
visit_table_row : (A, TableRow) -> FoldAction[A]
}let count = doc.fold(0, { ..GherkinFold::default(),
visit_scenario: fn(n, _) { Continue(n + 1) }
})pub struct GherkinReader {
// private fields
}pub struct GherkinWriter {
// private fields
}let w = GherkinWriter::new()
parse_with_handler(source, w)
let output = w.to_string()impl GherkinHandler for GherkinWriterfn GherkinWriter::background(self : GherkinWriter, keyword : String, name? : String, description? : String) -> Unitfn GherkinWriter::doc_string(self : GherkinWriter, content : String, delimiter? : String, media_type? : String?) -> Unitfn GherkinWriter::examples(self : GherkinWriter, keyword : String, name? : String, description? : String, header? : Array[String], body? : Array[Array[String]]) -> Unitfn GherkinWriter::feature(self : GherkinWriter, keyword : String, name : String, language? : String, description? : String) -> Unitfn GherkinWriter::rule(self : GherkinWriter, keyword : String, name : String, description? : String) -> Unitfn GherkinWriter::scenario(self : GherkinWriter, keyword : String, name : String, description? : String) -> Unitpub(all) struct Step {
location : Location
keyword : String
keyword_type : KeywordType
text : String
id : String
argument : StepArgument?
} derive(Eq, ToJson, Debug, FromJson)pub(all) struct StepEvent {
location : Location
keyword : String
keyword_type : KeywordType
text : String
} derive(Eq, Debug)pub enum Token {
FeatureLine(Location, String, String)
RuleLine(Location, String, String)
BackgroundLine(Location, String, String)
ScenarioLine(Location, String, String, ScenarioKind)
ExamplesLine(Location, String, String)
StepLine(Location, String, KeywordType, String)
DocStringSeparator(Location, String, String?)
TableRow(Location, Array[String])
TagLine(Location, Array[String])
Comment(Location, String)
Language(Location, String)
Empty(Location)
Other(Location, String)
Eof(Location)
} derive(Eq, ToJson, Debug)let config = { ..WriteConfig::default(), indent: " " }fn classify_line(line : String, line_num : Int, state : LexerState, language? : String) -> (Token, LexerState)// Without continuing:
visit_scenario: fn(n, _) { Continue(n + 1) }
// With continuing:
visit_scenario: continuing(fn(n, _) { n + 1 })Install
Download zipA Gherkin parser for MoonBit with DOM, visitor, fold, and SAX-style APIs
Dependencies