diff --git a/lib/channel_spec/cache.ex b/lib/channel_spec/cache.ex new file mode 100644 index 0000000..c89511c --- /dev/null +++ b/lib/channel_spec/cache.ex @@ -0,0 +1,37 @@ +defmodule ChannelSpec.Cache do + @moduledoc """ + Cache for ChannelSpec specs. + + Settings: + + ```elixir + config :channel_spec, :cache_adapter, Module + ``` + + ChannelSpec ships with two cache adapters: + + * `ChannelSpec.Cache.PersistentTermCache` - default + * `ChannelSpec.Cache.NoneCache` - none cache + + If you are constantly modifying specs during development, you can configure the cache adapter + in `dev.exs` as follows to disable caching: + + ```elixir + config :channel_spec, :cache_adapter, ChannelSpec.Cache.NoneCache + ``` + """ + + @callback get(module) :: nil | map() + @callback put(module, map()) :: :ok + @callback erase(module) :: :ok + + @default_adapter ChannelSpec.Cache.PersistentTermCache + + @doc """ + Get cache adapter + """ + @spec adapter() :: module() + def adapter() do + Application.get_env(:channel_spec, :cache_adapter, @default_adapter) + end +end diff --git a/lib/channel_spec/cache/none_cache.ex b/lib/channel_spec/cache/none_cache.ex new file mode 100644 index 0000000..7868a48 --- /dev/null +++ b/lib/channel_spec/cache/none_cache.ex @@ -0,0 +1,23 @@ +defmodule ChannelSpec.Cache.NoneCache do + @moduledoc """ + A cache adapter to disable caching. Intended to be used in development. + + Configure it with: + + ```elixir + # config/runtime.exs + config :channel_handler, :cache_adapter, ChannelSpec.Cache.NoneCache + ``` + """ + + @behaviour ChannelSpec.Cache + + @impl true + def get(_spec_module), do: nil + + @impl true + def put(_spec_module, _spec), do: :ok + + @impl true + def erase(_spec_module), do: :ok +end diff --git a/lib/channel_spec/cache/persistent_term_cache.ex b/lib/channel_spec/cache/persistent_term_cache.ex new file mode 100644 index 0000000..4d0d9fa --- /dev/null +++ b/lib/channel_spec/cache/persistent_term_cache.ex @@ -0,0 +1,24 @@ +defmodule ChannelSpec.Cache.PersistentTermCache do + @moduledoc """ + A cache adapter that stores the specs in memory. + + This is the default cache adapter. + """ + + @behaviour ChannelSpec.Cache + + @impl true + def get(spec_module) do + :persistent_term.get(spec_module, nil) + end + + @impl true + def put(spec_module, specs) do + :persistent_term.put(spec_module, specs) + end + + @impl true + def erase(spec_module) do + :persistent_term.erase(spec_module) + end +end diff --git a/lib/channel_spec/operations.ex b/lib/channel_spec/operations.ex index 6750348..341a34d 100644 --- a/lib/channel_spec/operations.ex +++ b/lib/channel_spec/operations.ex @@ -86,16 +86,28 @@ defmodule ChannelSpec.Operations do _other, lines -> lines end) + |> Macro.escape() + + quote location: :keep, + bind_quoted: [ + name: name, + schema: params, + file: file, + line: line, + module: module, + line_metadata: line_metadata + ] do + operation = %{ + schema: schema, + file: file, + line: line, + module: module, + line_metadata: line_metadata + } - quote location: :keep do - @operations {unquote(name), - %{ - schema: unquote(params), - file: unquote(file), - line: unquote(line), - module: unquote(module), - line_metadata: unquote(Macro.escape(line_metadata)) - }} + @operations {name, operation} + + def __channel_spec_operation__(unquote(name)), do: unquote(Macro.escape(operation)) end end @@ -106,7 +118,7 @@ defmodule ChannelSpec.Operations do @doc """ Defines a subscription for the channel. - Subscriptions are messages that are sent from the server to the client, withuot + Subscriptions are messages that are sent from the server to the client, without the client requesting them. They are defined with a name and a schema that describes the payload. @@ -128,17 +140,28 @@ defmodule ChannelSpec.Operations do file = __CALLER__.file line = __CALLER__.line module = __CALLER__.module - line_metadata = %{"event" => %{line: line}} + line_metadata = Macro.escape(%{"event" => %{line: line}}) + + quote location: :keep, + bind_quoted: [ + event: event, + file: file, + line: line, + module: module, + schema: schema, + line_metadata: line_metadata + ] do + subscription = %{ + schema: schema, + file: file, + line: line, + module: module, + line_metadata: line_metadata + } - quote do - @subscriptions {unquote(event), - %{ - schema: unquote(schema), - file: unquote(file), - line: unquote(line), - module: unquote(module), - line_metadata: unquote(Macro.escape(line_metadata)) - }} + @subscriptions {event, subscription} + + def __channel_spec_subscription__(unquote(event)), do: unquote(Macro.escape(subscription)) end end diff --git a/lib/channel_spec/socket.ex b/lib/channel_spec/socket.ex index 2375d3d..208a65d 100644 --- a/lib/channel_spec/socket.ex +++ b/lib/channel_spec/socket.ex @@ -31,7 +31,7 @@ defmodule ChannelSpec.Socket do pattern = String.replace_suffix(topic_pattern, "*", "{string}") topic_pattern = String.replace(topic_pattern, ~r/\{.*\}.*/, "*") - quote do + quote location: :keep do opts = Keyword.update( unquote(opts), @@ -60,11 +60,27 @@ defmodule ChannelSpec.Socket do end def __socket_tree__() do - unquote(__MODULE__).build_ops_tree(@__channels) + case ChannelSpec.Cache.adapter().get({__MODULE__, :socket_tree}) do + nil -> + tree = unquote(__MODULE__).build_ops_tree(@__channels) + ChannelSpec.Cache.adapter().put({__MODULE__, :socket_tree}, tree) + tree + + tree -> + tree + end end def __socket_schemas__() do - unquote(__MODULE__).build_schemas(__socket_tree__()) + case ChannelSpec.Cache.adapter().get({__MODULE__, :socket_schemas}) do + nil -> + tree = unquote(__MODULE__).build_schemas(__socket_tree__()) + ChannelSpec.Cache.adapter().put({__MODULE__, :socket_schemas}, tree) + tree + + tree -> + tree + end end end end @@ -79,6 +95,7 @@ defmodule ChannelSpec.Socket do def build_ops_tree(channels) do channels = for {topic, module, _opts} <- channels, + Code.ensure_compiled(module), function_exported?(module, :spark_dsl_config, 0), into: %{} do router = module.spark_dsl_config()[[:router]] @@ -235,6 +252,8 @@ defmodule ChannelSpec.Socket do end defp get_operations(%ChannelHandler.Dsl.Event{} = event, _router, _prefix) do + Code.ensure_compiled(event.module) + if function_exported?(event.module, :__channel_operations__, 0) do operations = event.module.__channel_operations__() @@ -260,17 +279,23 @@ defmodule ChannelSpec.Socket do end defp get_operations(%ChannelHandler.Dsl.Delegate{} = delegate, _router, _prefix) do - operations = delegate.module.__channel_operations__() - - for {event, operation} <- operations, is_binary(event) do - %{ - event: delegate.prefix <> event, - schema: operation.schema, - module: delegate.module, - function: :handle_in, - file: operation.file, - line: operation.line - } + Code.ensure_compiled(delegate.module) + + if function_exported?(delegate.module, :__channel_operations__, 0) do + operations = delegate.module.__channel_operations__() + + for {event, operation} <- operations, is_binary(event) do + %{ + event: delegate.prefix <> event, + schema: operation.schema, + module: delegate.module, + function: :handle_in, + file: operation.file, + line: operation.line + } + end + else + [] end end diff --git a/lib/mix/channel_spec_routes.ex b/lib/mix/channel_spec_routes.ex index 9d9cd48..dea9005 100644 --- a/lib/mix/channel_spec_routes.ex +++ b/lib/mix/channel_spec_routes.ex @@ -16,6 +16,7 @@ defmodule Mix.Tasks.ChannelSpec.Routes do for {path, socket_module, _} <- endpoint_module.__sockets__() do try do + ChannelSpec.Cache.adapter().erase({socket_module, :socket_tree}) socket_tree = socket_module.__socket_tree__() IO.puts(IO.ANSI.faint() <> "#{path}" <> IO.ANSI.reset()) print_socket_routes(socket_module, socket_tree, opts) diff --git a/mix.exs b/mix.exs index 60f08a8..a828e98 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,7 @@ defmodule ChannelSpec.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:channel_handler, "~> 0.1"}, + {:channel_handler, "~> 0.6"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:mneme, "~> 0.5", only: [:dev, :test]}, {:json_xema, "~> 0.6"}, diff --git a/test/channel_spec/cache/none_cache_test.exs b/test/channel_spec/cache/none_cache_test.exs new file mode 100644 index 0000000..3c2856a --- /dev/null +++ b/test/channel_spec/cache/none_cache_test.exs @@ -0,0 +1,23 @@ +defmodule ChannelSpec.Cache.NoneCacheTest do + use ExUnit.Case, async: true + + alias ChannelSpec.Cache.NoneCache + + describe "get/1" do + test "returns nil" do + assert is_nil(NoneCache.get(SomeModule)) + end + end + + describe "put/2" do + test "returns :ok" do + assert :ok = NoneCache.put(SomeModule, %{}) + end + end + + describe "erase/1" do + test "returns :ok" do + assert :ok = NoneCache.erase(SomeModule) + end + end +end diff --git a/test/channel_spec/cache/persistent_term_cache_test.exs b/test/channel_spec/cache/persistent_term_cache_test.exs new file mode 100644 index 0000000..e69de29