Current section
Files
Jump to
Current section
Files
lib/sugar.ex
# Copyright (c) 2018 James Laver
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Bricks.Sugar do
@moduledoc """
Macros for handling sockets in active mode - supporting both
explicit receive and GenServer styles
## Explicit Receive Usage
```
def receive_til_close(socket) do
# Generates bindings against the socket to match against
binding socket do
receive do
# Generate matches for the individual types
match_data(data) ->
Logger.info("Socket data: " <> data)
receive_til_close(socket)
match_error(reason) ->
Logger.info("Socket error: " <> reason)
match_closed() -> :ok
Logger.info("Socket closed")
match_passive() -> :ok
:ok = Socket.extend_active(socket)
Logger.info("Extended socket bounded active mode")
receive_til_close(socket)
end
end
end
```
## GenServer Usage
```
defhandle_info data(%{socket: SOCKET}=state, data) do
Logger.info("Socket data: " <> data)
{:noreply, state}
end
defhandle_info error(%{socket: SOCKET}=state, reason) do
Logger.info("Socket error: " <> reason)
{:stop, reason, state}
end
defhandle_info closed(%{socket: SOCKET}=state) do
Logger.info("Socket closed")
{:stop, :closed, state}
end
defhandle_info passive(%{socket: SOCKET=socket}=state) do
Logger.info("Extended socket bounded active mode")
:ok = Socket.extend_active(socket)
{:noreply, state}
end
```
"""
require Logger
### Macros
@doc """
Generates a handle_info clause corresponding to one of the four
socket notification messages. Takes a match against state which must
embed the atom `SOCKET` that identifies where in the state the
socket to match against is. The return value from the body should be
the same as for any `handle_info` callback.
Forms:
- `defhandle_info data(state_match, data_match) do ... end`
- `defhandle_info error(state_match, reason_match) do ... end`
- `defhandle_info closed(state_match) do ... end`
- `defhandle_info passive(state_match) do ... end`
See module docs for examples.
"""
defmacro defhandle_info({:data, _meta, [match, arg]}, do: body) do
handle_info_clause(match, :data_tag, arg, body)
end
defmacro defhandle_info({:error, _meta, [match, arg]}, do: body) do
handle_info_clause(match, :error_tag, arg, body)
end
defmacro defhandle_info({:closed, _meta, [match]}, do: body) do
handle_info_clause(match, :closed_tag, nil, body)
end
defmacro defhandle_info({:passive, _meta, [match]}, do: body) do
handle_info_clause(match, :passive_tag, nil, body)
end
@doc """
Creates a region in which the `match_*` macros will expand to
matches against the provided socket. See module docs for examples.
"""
defmacro binding(socket, do: body) do
binding_socket(socket, body)
end
@doc """
Expands to a match clause matching a data notification message
for the socket within whose `binding/2` region we are in.
Must be run within the region created by the `binding/2` macro.
See module docs for examples.
"""
defmacro match_data(match) do
match_clause(match, :bricks__binding_data_tag)
end
@doc """
Expands to a match clause matching an error notification message for
the socket within whose `binding/2` region we are in. Takes an
argument which is a match for the data (you probably want this to be
a plain atom, i.e. simple assignment).
Must be run within the region created by the `binding/2` macro.
See module docs for examples.
"""
defmacro match_error(match) do
match_clause(match, :bricks__binding_error_tag)
end
@doc """
Expands to a match clause matching a closed notification message for
the socket within whose `binding/2` region we are in. Takes an
argument which is a match for the error (you probably want this to
be a plain atom, i.e. simple assignment).
Must be run within the region created by the `binding/2` macro.
See module docs for examples.
"""
defmacro match_closed() do
match_clause(nil, :bricks__binding_closed_tag)
end
@doc """
Expands to a match clause matching a passive notification message
for the socket within whose `binding/2` region we are in.
Must be run within the region created by the `binding/2` macro.
See module docs for examples.
"""
defmacro match_passive() do
match_clause(nil, :bricks__binding_passive_tag)
end
### Implementation
#### match
# Generates a block in which certain bindings are available for use the `match` macro
defp binding_socket(match, body) do
body = reify_vars(body)
body = Macro.prewalk(body, &Macro.expand(&1, __ENV__))
ret =
quote generated: true do
%Bricks.Socket{
handle: unquote({:bricks__binding_handle, [generated: true], nil}),
data_tag: unquote({:bricks__binding_data_tag, [generated: true], nil}),
error_tag: unquote({:bricks__binding_error_tag, [generated: true], nil}),
closed_tag: unquote({:bricks__binding_closed_tag, [generated: true], nil}),
passive_tag: unquote({:bricks__binding_passive_tag, [generated: true], nil})
} = unquote(match)
unquote(body)
end
ret
end
# generates a match clause for inside a receive
defp match_clause(arg, tag_name) do
tag = Macro.var(tag_name, nil)
handle = Macro.var(:bricks__binding_handle, nil)
case arg do
nil ->
quote do
{^unquote(tag), ^unquote(handle)}
end
_ ->
quote do
{^unquote(tag), ^unquote(handle), unquote(reify_vars(arg))}
end
end
end
#### defhandle_info
defp guards(head, guards), do: {:when, [context: nil], [head, guards]}
defp defun(name, args, body, guards) do
{:def, [context: nil], [guards({name, [context: nil], args}, guards), [do: body]]}
end
defp info_clause_args(nil, match) do
tag = Macro.var(:bricks__binding_tag, nil)
handle = Macro.var(:bricks__binding_handle, nil)
[{:{}, [], [tag, handle]}, match]
end
defp info_clause_args({arg, _, nil}, match) when is_atom(arg) do
tag = Macro.var(:bricks__binding_tag, nil)
handle = Macro.var(:bricks__binding_handle, nil)
arg = Macro.var(arg, nil)
[{:{}, [], [tag, handle, arg]}, match]
end
# generates a handle_info clause
defp handle_info_clause(match, tag_name, arg, body) do
match = expand_info_match(match, tag_name)
body = reify_vars(body)
tag = Macro.var(:bricks__binding_tag, nil)
tag2 = Macro.var(:bricks__binding_tag2, nil)
handle = Macro.var(:bricks__binding_handle, nil)
handle2 = Macro.var(:bricks__binding_handle2, nil)
args = info_clause_args(arg, match)
guards =
quote do
unquote(tag) == unquote(tag2) and unquote(handle) == unquote(handle2)
end
defun(:handle_info, args, body, guards)
end
defp reify_vars(body) do
Macro.prewalk(body, fn ast ->
case ast do
{name, _meta, args}
when is_atom(name) and (args == [] or is_nil(args) or is_atom(args)) ->
Macro.var(name, nil)
_ ->
ast
end
end)
end
defp expand_info_match(match, tag) do
{ret, socket?} = expand_info_match(match, tag, false)
if not socket?, do: throw(:must_bind_socket)
ret
end
defp expand_info_match({:%{}, whatever, map}, tag, seen?) do
{list, seen?} =
Enum.reduce(map, {[], seen?}, fn {k, v}, {acc, seen?} ->
{v, seen?} = expand_info_match(v, tag, seen?)
{[{k, v} | acc], seen?}
end)
{{:%{}, whatever, list}, seen?}
end
defp expand_info_match({:{}, whatever, items}, tag, seen?) do
{val, seen?} = expand_info_match(items, tag, seen?)
{{:{}, whatever, val}, seen?}
end
defp expand_info_match({:%, whatever, [name, map = {:%{}, _, _}]}, tag, seen?) do
{r, seen?} = expand_info_match(map, tag, seen?)
{{:%, whatever, [name, r]}, seen?}
end
defp expand_info_match(list, tag, seen?) when is_list(list) do
{list, seen?} =
Enum.reduce(list, {[], seen?}, fn val, {acc, seen?} ->
{val, seen?} = expand_info_match(val, tag, seen?)
{[val | acc], seen?}
end)
{Enum.reverse(list), seen?}
end
defp expand_info_match({:__aliases__, _, [:SOCKET]}, tag, false) do
tag2 = Macro.var(:bricks__binding_tag2, nil)
handle2 = Macro.var(:bricks__binding_handle2, nil)
r =
quote do
%Bricks.Socket{
:handle => unquote(handle2),
unquote(tag) => unquote(tag2)
}
end
{r, true}
end
defp expand_info_match({:__aliases__, _, [:SOCKET]}, _tag, true) do
throw(:double_bound_socket)
end
defp expand_info_match({:=, meta, args}, tag, seen?) do
{args, seen?} = expand_info_match(args, tag, seen?)
{{:=, meta, args}, seen?}
end
defp expand_info_match({name, _meta, args}, _tag, seen?)
when is_atom(name) and (args == [] or is_nil(args) or is_atom(args)) do
{Macro.var(name, nil), seen?}
end
defp expand_info_match({_, _, _} = orig, _tag, _seen?) do
throw({:unsupported_binding, :funcall, orig})
end
# not 3 items, plain data
defp expand_info_match(match, tag, seen?) when is_tuple(match) do
{ret, seen?} =
Tuple.to_list(match)
|> expand_info_match(tag, seen?)
{List.to_tuple(ret), seen?}
end
defp expand_info_match(x, _tag, seen?)
when is_integer(x) or is_float(x) or is_binary(x) or is_atom(x) or is_boolean(x) do
{x, seen?}
end
end