Current section
Files
Jump to
Current section
Files
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.