Skip to content

Commit

Permalink
Merge pull request #14 from inaka/elbrujohalcon.13.fix.spec.generation
Browse files Browse the repository at this point in the history
[#13] Initial attempt at fixing spec generation
  • Loading branch information
elbrujohalcon authored Oct 8, 2018
2 parents 48691de + 0d52137 commit 9eee332
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 42 deletions.
44 changes: 37 additions & 7 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Mixer

Mix in functions from other modules.

## Concept
The original motivation for this parse transform was to permit reuse of functions implementing common logic for tasks such as signature verification and authorization across multiple webmachine resources.
It allows you to provide shared implementations of behavior callbacks without copy&pasting.

## Examples

`foo.erl`:

```erlang
-module(foo).

-export([doit/0, doit/1, doit/2]).
Expand All @@ -12,49 +23,68 @@

doit(A, B) ->
[doit, A, B].
```

Module `bar.erl` which 'mixes in' `foo`:
Module `bar.erl` which 'mixes in' all functions from `foo`:

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([foo]).
```

or all except specific functions from `foo`:

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([{foo, except, [doit/0, doit/2]}]).
```

or only specific functions from `foo`:

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([{foo, [doit/0, doit/2]}]).
```

Another version of `bar.erl` which mixes in all functions from `foo` and select functions from `baz`:
Another version of `bar.erl` which mixes in all functions from `foo` and some functions from `baz`:

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([foo, {baz, [doit/0, doit/1]}]).
```

One more version of `bar.erl` which mixes in `foo:doit/0` and renames it to `do_it_now/0`:

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([{foo, [{doit/0, do_it_now}]}]).

```

Yet another version of `bar.erl` which mixes in all of `foo`'s public functions not implemented by `bar`.
In this case the functions `foo:doit/0` and `foo:doit/1` will be injected into `bar`.

```
```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin([{foo, except, module}]).
-export([doit/2]).

doit(A, B) ->
[bar_did_it, A, B].
```

The original motivation for this parse transform was to permit reuse of functions implementing common
logic for tasks such as signature verification and authorization across multiple webmachine resources.
Last version of `bar.erl`, this time including specs for functions.

```erlang
-module(bar).
-include_lib("mixer/include/mixer.hrl").
-mixin_specs(all).
-mixin([foo]).
```

(At this time the only valid value for `mixin_specs` is `all`).
48 changes: 28 additions & 20 deletions src/mixer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ parse_transform(Forms, _Options) ->
[set_mod_info(Form) || Form <- Forms],
set_mod_info(Forms),
{EOF, Forms1} = strip_eof(Forms),
case parse_and_expand_mixins(Forms1, {[], []}) of
{[], _} ->
case parse_and_expand_mixins(Forms1, {[], [], none}) of
{[], _, _} ->
Forms;
{Mixins, Exports} ->
{Mixins, Exports, Specs} ->
Mixins1 = inject_overrides(Mixins, lists:sort(Exports), []),
no_dupes(Mixins1),
{EOF1, Forms2} = insert_stubs(Mixins1, EOF, Forms1),
{EOF1, Forms2} = insert_stubs(Mixins1, Specs, EOF, Forms1),
finalize(Mixins1, EOF1, Forms2)
end.

Expand Down Expand Up @@ -104,16 +104,18 @@ strip_eof([{eof, EOF}|T], Accum) ->
strip_eof([H|T], Accum) ->
strip_eof(T, [H|Accum]).

parse_and_expand_mixins([], {[], _}) ->
{[], []};
parse_and_expand_mixins([], {Mixins, Exports}) ->
{group_mixins({none, 0}, lists:keysort(2, Mixins), []), Exports};
parse_and_expand_mixins([{attribute, Line, mixin, Mixins0}|T], {Mixins, Exports})
parse_and_expand_mixins([], {[], _, Specs}) ->
{[], [], Specs};
parse_and_expand_mixins([], {Mixins, Exports, Specs}) ->
{group_mixins({none, 0}, lists:keysort(2, Mixins), []), Exports, Specs};
parse_and_expand_mixins([{attribute, Line, mixin, Mixins0}|T], {Mixins, Exports, Specs})
when is_list(Mixins0) ->
Mixins1 = [expand_mixin(Line, Mixin) || Mixin <- Mixins0],
parse_and_expand_mixins(T, {lists:flatten([Mixins, Mixins1]), Exports});
parse_and_expand_mixins([{attribute, _Line, export, Exports1}|T], {Mixins, Exports}) ->
parse_and_expand_mixins(T, {Mixins, lists:flatten(Exports, Exports1)});
parse_and_expand_mixins(T, {lists:flatten([Mixins, Mixins1]), Exports, Specs});
parse_and_expand_mixins([{attribute, _Line, mixin_specs, Specs}|T], {Mixins, Exports, _Specs}) ->
parse_and_expand_mixins(T, {Mixins, Exports, Specs});
parse_and_expand_mixins([{attribute, _Line, export, Exports1}|T], {Mixins, Exports, Specs}) ->
parse_and_expand_mixins(T, {Mixins, lists:flatten(Exports, Exports1), Specs});
parse_and_expand_mixins([_|T], Accum) ->
parse_and_expand_mixins(T, Accum).

Expand Down Expand Up @@ -193,7 +195,7 @@ find_dupe(Fun, Arity, [#mixin{mod=Name, fname=Fun, arity=Arity}|_]) ->
find_dupe(Fun, Arity, [_|T]) ->
find_dupe(Fun, Arity, T).

insert_stubs(Mixins, EOF, Forms) ->
insert_stubs(Mixins, Specs, EOF, Forms) ->
F =
fun(#mixin{} = Mixin, {CurrEOF, Acc}) ->
#mixin{mod=Mod, fname=Fun, arity=Arity, alias=Alias} = Mixin,
Expand All @@ -202,19 +204,25 @@ insert_stubs(Mixins, EOF, Forms) ->
binary_to_list(iolist_to_binary(io_lib:format("~p", [Mod]))),
binary_to_list(iolist_to_binary(io_lib:format("~p", [Alias]))),
binary_to_list(iolist_to_binary(io_lib:format("~p", [Fun]))),
Arity, CurrEOF) |Acc]
Arity, Specs, CurrEOF) |Acc]
};
(#override_mixin{}, {CurrEOF, Acc}) -> {CurrEOF, Acc}
end,
{EOF1, Stubs} = lists:foldr(F, {EOF, []}, Mixins),
{EOF1, Forms ++ lists:reverse(lists:flatten(Stubs))}.

generate_stub(Mixin, Alias, Name, Arity, CurrEOF) when Arity =< ?ARITY_LIMIT ->
AnyList = lists:duplicate(Arity, "any()"),
ArgTypeList = string:join(AnyList, ", "),
SpecCode = "-spec " ++ Alias ++ "(" ++ ArgTypeList ++ ") -> any().",
{ok, SpecTokens, _} = erl_scan:string(SpecCode),
{ok, SpecForm} = erl_parse:parse_form(SpecTokens),
generate_stub(Mixin, Alias, Name, Arity, Specs, CurrEOF) when Arity =< ?ARITY_LIMIT ->
{ok, SpecForm} =
case Specs of
all ->
AnyList = lists:duplicate(Arity, "any()"),
ArgTypeList = string:join(AnyList, ", "),
SpecCode = "-spec " ++ Alias ++ "(" ++ ArgTypeList ++ ") -> any().",
{ok, SpecTokens, _} = erl_scan:string(SpecCode),
erl_parse:parse_form(SpecTokens);
none ->
{ok, []}
end,

ArgList = "(" ++ make_param_list(Arity) ++ ")",
Code = Alias ++ ArgList ++ "-> " ++ Mixin ++ ":" ++ Name ++ ArgList ++ ".",
Expand Down
53 changes: 38 additions & 15 deletions test/import_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@

-include_lib("eunit/include/eunit.hrl").

-define(EXPORTS(Mod), Mod:module_info(exports)).

single_test_() ->
[{<<"All functions on 'single' stubbed properly">>,
[?_assert(lists:member({doit, 0}, ?EXPORTS(single))),
?_assert(lists:member({doit, 1}, ?EXPORTS(single))),
?_assert(lists:member({doit, 2}, ?EXPORTS(single)))]},
[?_assert(lists:member({doit, 0}, exports(single))),
?_assert(lists:member({doit, 1}, exports(single))),
?_assert(lists:member({doit, 2}, exports(single)))]},
{<<"All functions on 'single' work correctly">>,
[?_assertMatch(doit, single:doit()),
?_assertMatch([doit, 1], single:doit(1)),
?_assertMatch([doit, 1, 2], single:doit(1, 2))]}].

multiple_test_() ->
[{<<"All functions stubbed">>,
[?_assert(lists:member({doit, 0}, ?EXPORTS(multiple))),
?_assert(lists:member({doit, 1}, ?EXPORTS(multiple))),
?_assert(lists:member({doit, 2}, ?EXPORTS(multiple))),
?_assert(lists:member({canhas, 0}, ?EXPORTS(multiple))),
?_assert(lists:member({canhas, 1}, ?EXPORTS(multiple)))]},
[?_assert(lists:member({doit, 0}, exports(multiple))),
?_assert(lists:member({doit, 1}, exports(multiple))),
?_assert(lists:member({doit, 2}, exports(multiple))),
?_assert(lists:member({canhas, 0}, exports(multiple))),
?_assert(lists:member({canhas, 1}, exports(multiple)))]},
{<<"All stubbed functions work">>,
[?_assertMatch(doit, multiple:doit()),
?_assertMatch({doit, one}, multiple:doit(one)),
Expand All @@ -30,17 +28,17 @@ multiple_test_() ->

alias_test_() ->
[{<<"Function stubbed with alias">>,
[?_assert(lists:member({blah, 0}, ?EXPORTS(alias))),
?_assert(lists:member({can_has, 0}, ?EXPORTS(alias)))]},
[?_assert(lists:member({blah, 0}, exports(alias))),
?_assert(lists:member({can_has, 0}, exports(alias)))]},
{<<"All stubbed functions work">>,
[?_assertMatch(doit, alias:blah()),
?_assertMatch(true, alias:can_has())]}].

override_test_() ->
[{<<"All non-overridden functions are stubbed">>,
[?_assert(lists:member({doit, 0}, ?EXPORTS(override))),
?_assert(lists:member({doit, 1}, ?EXPORTS(override))),
?_assert(lists:member({doit, 2}, ?EXPORTS(override)))]},
[?_assert(lists:member({doit, 0}, exports(override))),
?_assert(lists:member({doit, 1}, exports(override))),
?_assert(lists:member({doit, 2}, exports(override)))]},
{<<"All functions work as expected">>,
[?_assertMatch(doit, override:doit()),
?_assertMatch([5,5], override:doit(5)),
Expand Down Expand Up @@ -72,3 +70,28 @@ strange_atom_format_test_() ->
?_assertEqual({'not', wat}, single:'🎱'(wat)),
?_assertEqual({overridden, wat}, override:'🎱'(wat))
]}].

specs_test_() ->
[{<<"Specs are not generated if not requested">>,
[?_assertNot(lists:member({doit, 0}, specs(override))),
?_assertNot(lists:member({doit, 1}, specs(override))),
?_assertNot(lists:member({doit, 2}, specs(override)))]},
{<<"Specs are generated if requested">>,
[?_assert(lists:member({doit, 0}, specs(specs))),
?_assert(lists:member({doit, 1}, specs(specs))),
?_assert(lists:member({doit, 2}, specs(specs)))]},
{<<"All functions are stubbed properly">>,
[?_assert(lists:member({doit, 0}, exports(specs))),
?_assert(lists:member({doit, 1}, exports(specs))),
?_assert(lists:member({doit, 2}, exports(specs)))]},
{<<"All functions on 'specs' work correctly">>,
[?_assertMatch(doit, specs:doit()),
?_assertMatch([doit, 1], specs:doit(1)),
?_assertMatch([doit, 1, 2], specs:doit(1, 2))]}].

exports(Mod) -> Mod:module_info(exports).

specs(Mod) ->
Path = code:which(Mod),
{ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks(Path, [abstract_code]),
[{Fun, Arity} || {attribute, 1, spec, {{Fun, Arity}, _}} <- AC].
7 changes: 7 additions & 0 deletions test/specs.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-module(specs).

-include("mixer.hrl").

-mixin([foo]).

-mixin_specs(all).

0 comments on commit 9eee332

Please sign in to comment.