Current section

Files

Jump to
gurka src gurka_parser.erl
Raw

src/gurka_parser.erl

-module(gurka_parser).
-include("gurka.hrl").
-export([parse/1, tokens/1]).
-spec parse(File) -> {ok, Feature} | {error, Reason} when
File :: file:name(),
Feature :: feature(),
Reason :: file:posix() | badarg | terminated | system_limit.
parse(File) ->
case file:read_file(File) of
{ok, Binary} ->
{_, Lines} = lists:foldl(fun(Line, {Row, AccIn}) ->
{Row + 1, [{Row, Line, tokens(Line)} | AccIn]}
end, {1, []}, binary:split(Binary, [<<$\n>>, <<$\r>>], [global, trim])),
{ok, inherit_tags(compact(process(undefined, undefined, lists:reverse(Lines))))};
{error, Reason} ->
{error, Reason}
end.
-spec process(Phase, Action, Lines) -> Feature when
Phase :: phase(),
Action :: action(),
Lines :: lines(),
Feature :: feature().
process(_Phase, _Action, []) ->
[];
process(Phase, Action, [{_Row, _Line, []} | Lines]) ->
process(Phase, Action, Lines);
process(Phase, Action, [{Row, _Line, Tags = [<<$@, _/binary>> | _]} | Lines]) ->
[#step{phase = Phase, action = tags, tokens = Tags, row = Row} | process(Phase, Action, Lines)];
process(Phase, Action, [{Row, Line, [<<"|">> | Tokens]} | Lines]) ->
{Lines2, Table} = build_table([{Row, Line, [<<"|">> | Tokens]} | Lines], []),
[#step{phase = Phase, action = table, tokens = Table, row = Row} | process(Phase, Action, Lines2)];
process(Phase, Action, [{Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines]) ->
{Lines2, Docstring} = build_docstring(Lines, []),
[#step{phase = Phase, action = docstring, tokens = Docstring, row = Row} | process(Phase, Action, Lines2)];
process(Phase, Action, [{Row, _Line, [FirstToken | Tokens]} | Lines]) ->
case normalize_token(FirstToken) of
"and" ->
[#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)];
"but" ->
[#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)];
"given" ->
[#step{phase = Phase, action = given, tokens = Tokens, row = Row} | process(Phase, given, Lines)];
"when" ->
[#step{phase = Phase, action = 'when', tokens = Tokens, row = Row} | process(Phase, 'when', Lines)];
"then" ->
[#step{phase = Phase, action = then, tokens = Tokens, row = Row} | process(Phase, then, Lines)];
"examples" ->
[#step{phase = Phase, action = examples, tokens = Tokens, row = Row} | process(Phase, examples, Lines)];
"feature" ->
[#step{phase = feature, action = start, tokens = Tokens, row = Row} | process(feature, undefined, Lines)];
"background" ->
[#step{phase = background, action = start, tokens = Tokens, row = Row} | process(background, undefined, Lines)];
"scenario" ->
case normalize_token(hd(Tokens)) of
"outline" ->
[#step{phase = scenario_outline, action = start, tokens = tl(Tokens), row = Row} | process(scenario_outline, undefined, Lines)];
_ ->
[#step{phase = scenario, action = start, tokens = Tokens, row = Row} | process(scenario, undefined, Lines)]
end;
_ ->
[#step{phase = Phase, action = desc, tokens = [FirstToken | Tokens], row = Row} | process(Phase, Action, Lines)]
end.
compact(Feature) ->
compact(lists:reverse(Feature), []).
compact([], Acc) ->
Acc;
compact([#step{action = docstring, tokens = Docstring} | [Step | Steps]], Acc) ->
compact([Step#step{tokens = Step#step.tokens ++ [{docstring, Docstring}]} | Steps], Acc);
compact([#step{action = table, tokens = Table} | [Step = #step{action = examples} | Steps]], Acc) ->
compact([Step#step{tokens = Table} | Steps], Acc);
compact([#step{action = table, tokens = Table} | [Step | Steps]], Acc) ->
compact([Step#step{tokens = Step#step.tokens ++ [{table, Table}]} | Steps], Acc);
compact([Step = #step{phase = Phase, action = start} | [#step{action = tags, tokens = Tags} | Steps]], Acc) when Phase == feature; Phase == scenario; Phase == scenario_outline ->
compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc);
compact([Step = #step{phase = scenario_outline, action = examples} | [#step{action = tags, tokens = Tags} | Steps]], Acc) ->
compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc);
compact([#step{action = tags} | Steps], Acc) ->
compact(Steps, Acc);
compact([Step | Steps], Acc) ->
compact(Steps, [Step | Acc]).
inherit_tags(Feature) ->
inherit_tags(Feature, {[], []}).
inherit_tags([], _) ->
[];
inherit_tags([Step = #step{phase = feature, action = start} | Steps], _) ->
FeatureTags = proplists:get_value(tags, Step#step.meta, []),
NewMeta = [{merged_tags, FeatureTags} | Step#step.meta],
[Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, []})];
inherit_tags([Step = #step{phase = Phase, action = start} | Steps], {FeatureTags, _}) when Phase == scenario; Phase == scenario_outline ->
ScenarioTags = proplists:get_value(tags, Step#step.meta, []),
NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags} | Step#step.meta],
[Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})];
inherit_tags([Step = #step{phase = scenario_outline, action = examples} | Steps], {FeatureTags, ScenarioTags}) ->
ExamplesTags = proplists:get_value(tags, Step#step.meta, []),
NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags ++ ExamplesTags} | Step#step.meta],
[Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})];
inherit_tags([Step | Steps], Tags) ->
[Step | inherit_tags(Steps, Tags)].
build_table([], Table) ->
{[], lists:reverse(Table)};
build_table([{_Row, Line, [<<"|">> | _Tokens]} | Lines], Table) ->
Cells = [list_to_binary(string:strip(Field)) || Field <- string:tokens(string:strip(binary_to_list(Line)), "|")],
build_table(Lines, [Cells | Table]);
build_table(Lines, Table) ->
{Lines, lists:reverse(Table)}.
build_docstring([], Docstring) ->
{[], Docstring};
build_docstring([{_Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines], Docstring) ->
{Lines, list_to_binary(Docstring)};
build_docstring([{_Row, Line, _Tokens} | Lines], []) ->
build_docstring(Lines, string:strip(binary_to_list(Line)));
build_docstring([{_Row, Line, _Tokens} | Lines], Docstring) ->
build_docstring(Lines, Docstring ++ "\n" ++ string:strip(binary_to_list(Line))).
normalize_token(Token) ->
hd(string:tokens(string:to_lower(binary_to_list(Token)), ":")).
tokens(Line) ->
[strip_quotes(Token) || Token <- tokens(Line, [], <<>>)].
tokens(<<>>, Tokens, <<>>) ->
Tokens;
tokens(<<>>, [<<$", $">>], <<$">>) ->
[<<$", $", $">>];
tokens(<<>>, Tokens, Buffer) ->
Tokens ++ [Buffer];
tokens(<<$\s, Line/binary>>, Tokens, <<>>) ->
tokens(Line, Tokens, <<>>);
tokens(<<$\s, Line/binary>>, Tokens, <<$", Buffer/binary>>) ->
tokens(Line, Tokens, <<$", Buffer/binary, $\s>>);
tokens(<<$\s, Line/binary>>, Tokens, Buffer) ->
tokens(<<$\s, Line/binary>>, Tokens ++ [Buffer], <<>>);
tokens(<<$\", Line/binary>>, Tokens, <<>>) ->
tokens(Line, Tokens, <<$">>);
tokens(<<$\", Line/binary>>, Tokens, <<$", Buffer/binary>>) ->
tokens(Line, Tokens ++ [<<$", Buffer/binary, $">>], <<>>);
tokens(<<$\", Line/binary>>, Tokens, Buffer) ->
tokens(Line, Tokens ++ [Buffer], <<$">>);
tokens(<<Character, Line/binary>>, Tokens, Buffer) ->
tokens(Line, Tokens, <<Buffer/binary, Character>>).
strip_quotes(<<>>) ->
<<>>;
strip_quotes(<<$", $", $">>) ->
<<$", $", $">>;
strip_quotes(<<$", Token/binary>>) ->
case binary:last(Token) of
$" ->
binary:part(Token, {0, byte_size(Token) - 1});
_ ->
Token
end;
strip_quotes(Token) ->
Token.