Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Languages feature #12

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3,514 changes: 3,514 additions & 0 deletions gherkin-languages.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion lib/gherkin/elements/line.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
defmodule Gherkin.Elements.Line do
@moduledoc false

defstruct raw_text: "", text: "", line_number: nil

def get_line_name(line_text) do
[_, line_name] = String.split(line_text, ":", parts: 2)
line_name
end
end
120 changes: 120 additions & 0 deletions lib/gherkin/keywords.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
defmodule Gherkin.Keywords do
alias __MODULE__

@keywords_file_path Path.expand("../../gherkin-languages.json", __DIR__)

defstruct and: [],
background: [],
but: [],
examples: [],
feature: [],
given: [],
name: "",
native: "",
rule: [],
scenario: [],
scenario_outline: [],
then: [],
when: []

@doc """
Get keywords from loaded keywords for given language

Returns a Keywords struct with atom keys
"""
def get_keywords(language \\ "en") do
keywords_map = language
|> load_language_keywords()
|> atomize_map()

struct!(Keywords, keywords_map)
end

@doc """
Transform a given keyword struct into a list of keywords
And optionally filter it with given keys list

Returns a list of keyword strings
"""
def get_keywords_list(%Keywords{} = keywords, keys_list \\ nil) do
list = keywords
|> setup_list()
|> Enum.filter(fn ({_, keyword}) -> is_list(keyword) end)
filtered_list = if keys_list, do: Enum.filter(list, fn ({key, _}) -> Enum.member?(keys_list, key) end), else: list
cleanup_list(filtered_list)
end

def get_step_keywords_list(keywords) do
get_keywords_list(keywords, [:given, :when, :then, :and, :but])
end

def get_description_keywords_list(keywords) do
get_keywords_list(keywords, [:background, :feature, :examples, :scenario, :given, :rule, :then, :and, :but])
end

@doc """
Find a matched keyword from given keywords list inside given text

Returns a keyword string
"""
def find_keyword_from_list(text, keywords) do
Enum.find(
keywords,
fn keyword ->
String.match?(text, ~r/^\b(#{keyword})(\b|:)/)
end
)
end

@doc """
Check if given text contains on of the given keywords

Returns a boolean
"""
def match_keywords?(text, keywords) do
Enum.any?(
keywords,
fn keyword ->
String.match?(text, ~r/^\b(#{keyword})(\b|:)/)
end
)
end

defp setup_list(%Keywords{} = keywords) do
# Transform a Keywords struct into a map and filter keywords keys which aren't lists (eg. name or native)
keywords
|> Map.from_struct()
|> Enum.filter(fn ({_, keyword}) -> is_list(keyword) end)
end

defp cleanup_list(list) do
list
|> Enum.map(fn ({_, value}) -> value end)
|> List.flatten()
|> Enum.filter(fn value -> value != "* " end)
|> Enum.map(fn value -> String.trim(value) end)
end

defp load_language_keywords(language) do
case get_json(@keywords_file_path) do
%{^language => json_keywords} -> json_keywords
_ -> raise("Language \"#{language}\" is not included in gherkin-language.")
end
end

defp get_json(filename) do
with {:ok, body} <- File.read(filename),
{:ok, json} <- Jason.decode(body), do: json
end

defp atomize_map(map) do
for {key, val} <- map,
into: %{},
do: {
key
|> Macro.underscore()
|> String.to_atom(),
val
}
end
end
20 changes: 15 additions & 5 deletions lib/gherkin/parser.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
defmodule Gherkin.Parser do
@moduledoc false
alias Gherkin.Elements.Feature
alias Gherkin.Keywords
alias Gherkin.Parsers.FeatureParser

@doc """
Expand Down Expand Up @@ -37,8 +39,8 @@ defmodule Gherkin.Parser do
defp normalize_lines(lines) do
lines
|> Stream.map(&trim/1)
# Drop empty lines and comment lines as nothing will be done with them
|> Stream.filter(fn line -> line.text != "" and not String.starts_with?(line.text, "#") end)
# Drop empty lines and comment lines (but not language line) as nothing will be done with them
|> Stream.filter(fn line -> line.text != "" and not Regex.match?(~r/^#(?! language)/, line.text) end)
|> Enum.reduce([], &normalize_line/2)
|> Enum.reverse()
end
Expand All @@ -56,7 +58,7 @@ defmodule Gherkin.Parser do
defp normalize_line(%{text: ~s(""") <> _} = line, lines) do
indent_length = String.length(line.raw_text) - String.length(line.text)

{{:multiline, indent_length}, [line| lines]}
{{:multiline, indent_length}, [line | lines]}
end

# Line between opening/closing quotes for Doc String
Expand All @@ -66,9 +68,17 @@ defmodule Gherkin.Parser do
end

# Default processing
defp normalize_line(line, lines), do: [line| lines]
defp normalize_line(line, lines), do: [line | lines]

defp build_gherkin_document(lines, file) do
FeatureParser.build_feature(%Gherkin.Elements.Feature{file: file}, lines)
keywords = build_feature_keywords(List.first(lines))

FeatureParser.build_feature(%Feature{file: file}, lines, keywords)
end

defp build_feature_keywords(%{text: "# language: " <> language}) do
Keywords.get_keywords(language)
end

defp build_feature_keywords(_), do: Keywords.get_keywords()
end
6 changes: 3 additions & 3 deletions lib/gherkin/parsers/background_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ defmodule Gherkin.Parsers.BackgroundParser do
alias Gherkin.Parsers.DescriptionParser
alias Gherkin.Parsers.StepParser

def build_background(map, all_lines) do
{map, remaining_lines} = DescriptionParser.build_description(map, all_lines)
StepParser.build_background_steps(map, remaining_lines)
def build_background(map, all_lines, keywords) do
{map, remaining_lines} = DescriptionParser.build_description(map, all_lines, keywords)
StepParser.build_background_steps(map, remaining_lines, keywords)
end
end
13 changes: 7 additions & 6 deletions lib/gherkin/parsers/description_parser.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
defmodule Gherkin.Parsers.DescriptionParser do
@moduledoc false

def build_description(map, [line |lines] = all_lines) do
if starts_with_keyword?(line.text) do
alias Gherkin.Keywords

def build_description(map, [line | lines] = all_lines, keywords) do
if starts_with_keyword?(line.text, keywords) do
{map, all_lines}
else
build_description(%{map | description: map.description <> line.text <> "\n"}, lines)
build_description(%{map | description: map.description <> line.text <> "\n"}, lines, keywords)
end
end

@all_keywords ["@", "Feature", "Rule", "Background", "Example", "Scenario", ~s{"""}, "Given", "Then", "And", "But"]
defp starts_with_keyword?(line) do
String.starts_with?(line, @all_keywords)
def starts_with_keyword?(line, keywords) do
String.starts_with?(line, ["@", ~s{"""} | Keywords.get_description_keywords_list(keywords)])
end
end
98 changes: 66 additions & 32 deletions lib/gherkin/parsers/feature_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,82 @@ defmodule Gherkin.Parsers.FeatureParser do
alias Gherkin.Parsers.ScenarioParser
alias Gherkin.Parsers.RuleParser
alias Gherkin.Parsers.TagParser
import Gherkin.Keywords, only: [match_keywords?: 2]
import Gherkin.Elements.Line, only: [get_line_name: 1]

def build_feature(feature, []) do
def build_feature(feature, [], _keywords) do
%{feature | rules: Enum.reverse(feature.rules), scenarios: Enum.reverse(feature.scenarios)}
end

def build_feature(feature, all_lines) do
def build_feature(feature, all_lines, keywords) do
{tags, [line | remaining_lines]} = TagParser.process_tags(all_lines)

case line do
%{text: "Feature: " <> name, line_number: line_number} = _ ->
updated_feature = %{feature | line: line_number, name: String.trim(name), tags: tags}
{updated_feature, remaining_lines} = DescriptionParser.build_description(updated_feature, remaining_lines)
build_feature(updated_feature, remaining_lines)
%{text: "Background:" <> _} = _ ->
{updated_feature, remaining_lines} = BackgroundParser.build_background(feature, remaining_lines)
build_feature(updated_feature, remaining_lines)
%{text: "Scenario:" <> scenario_name, line_number: line_number} = _ ->
build_scenario(scenario_name, line_number, tags, feature, remaining_lines)
%{text: "Example:" <> scenario_name, line_number: line_number} = _ ->
build_scenario(scenario_name, line_number, tags, feature, remaining_lines)
%{text: "Scenario Outline:" <> scenario_name, line_number: line_number} = _ ->
build_scenario_outline(scenario_name, line_number, tags, feature, remaining_lines)
%{text: "Scenario Template:" <> scenario_name, line_number: line_number} = _ ->
build_scenario_outline(scenario_name, line_number, tags, feature, remaining_lines)
%{text: "Rule:" <> rule_name, line_number: line_number} = _ ->
new_rule = %Gherkin.Elements.Rule{line: line_number, name: String.trim(rule_name), tags: tags}
{updated_rule, remaining_lines} = RuleParser.build_rule(new_rule, remaining_lines)
build_feature(%{feature | rules: [updated_rule | feature.rules]}, remaining_lines)
_ ->
raise("Unexpected line when building Feature: #{line.text}")

cond do
String.contains?(line.text, ["language"]) ->
build_feature(feature, remaining_lines, keywords)

match_keywords?(line.text, keywords.feature) ->
updated_feature = %{
feature |
line: line.line_number,
name: line.text
|> get_line_name()
|> String.trim(),
tags: tags
}
{updated_feature, remaining_lines} = DescriptionParser.build_description(
updated_feature,
remaining_lines,
keywords
)
build_feature(updated_feature, remaining_lines, keywords)

match_keywords?(line.text, keywords.background) ->
{updated_feature, remaining_lines} = BackgroundParser.build_background(feature, remaining_lines, keywords)
build_feature(updated_feature, remaining_lines, keywords)

match_keywords?(line.text, keywords.scenario_outline) ->
line.text
|> get_line_name()
|> build_scenario_outline(line.line_number, tags, feature, remaining_lines, keywords)

match_keywords?(line.text, keywords.scenario) ->
line.text
|> get_line_name()
|> build_scenario(line.line_number, tags, feature, remaining_lines, keywords)

match_keywords?(line.text, keywords.rule) ->
new_rule = %Gherkin.Elements.Rule{
line: line.line_number,
name: line.text
|> get_line_name()
|> String.trim(),
tags: tags
}
{updated_rule, remaining_lines} = RuleParser.build_rule(new_rule, remaining_lines, keywords)
build_feature(%{feature | rules: [updated_rule | feature.rules]}, remaining_lines, keywords)

true -> raise("Unexpected line when building Feature: #{line.text}")
end
end

defp build_scenario(scenario_name, line_number, tags, feature, remaining_lines) do
defp build_scenario(scenario_name, line_number, tags, feature, remaining_lines, keywords) do
new_scenario = %Gherkin.Elements.Scenario{line: line_number, name: String.trim(scenario_name), tags: tags}
{updated_scenario, remaining_lines} = ScenarioParser.build_scenario(new_scenario, remaining_lines)
build_feature(%{feature | scenarios: [updated_scenario | feature.scenarios]}, remaining_lines)
{updated_scenario, remaining_lines} = ScenarioParser.build_scenario(new_scenario, remaining_lines, keywords)
build_feature(%{feature | scenarios: [updated_scenario | feature.scenarios]}, remaining_lines, keywords)
end

defp build_scenario_outline(scenario_name, line_number, tags, feature, remaining_lines) do
new_scenario_outline = %Gherkin.Elements.ScenarioOutline{line: line_number, name: String.trim(scenario_name), tags: tags}
{updated_scenario_outline, remaining_lines} = ScenarioParser.build_scenario_outline(new_scenario_outline, remaining_lines)
build_feature(%{feature | scenarios: [updated_scenario_outline | feature.scenarios]}, remaining_lines)
defp build_scenario_outline(scenario_name, line_number, tags, feature, remaining_lines, keywords) do
new_scenario_outline = %Gherkin.Elements.ScenarioOutline{
line: line_number,
name: String.trim(scenario_name),
tags: tags
}
{updated_scenario_outline, remaining_lines} = ScenarioParser.build_scenario_outline(
new_scenario_outline,
remaining_lines,
keywords
)
build_feature(%{feature | scenarios: [updated_scenario_outline | feature.scenarios]}, remaining_lines, keywords)
end
end
Loading