defmodule Rak do @moduledoc """ Rak Game Server Maker The top level module is used to define how your server will be composed. In here, we define which services to use, and how they will connect to each other. ```elixir defmodule MyApp do use Rak, metrics: Rak.Metrics.Prometheus, service: MyApp.Service, module: :auto, discovery: Rak.Discovery.Consul, connector: [ module: Rak.Connector.Web, message: Rak.Message.Raw, session: Rak.Session.Basic ] end ``` You may specify multiple entries for each components, or remove them if you do not use them. `module: :auto` is a wildcard configuration entry that lets Rak know to load all top-level modules automatically. Finally, you may want to configure which modules to start depending on the environment you are running. For instance, you may not want to start the discovery service when developing locally. In such cases, you may move the configuration to your `config/config.exs` and override it depending on the environment. > lib/my_app.ex ```elixir defmodule MyApp do use Rak end ``` > config/config.exs ```elixir use Mix.Config config :my_app, rak: [ metrics: Rak.Metrics.Prometheus, service: MyApp.Service, module: :auto, discovery: Rak.Discovery.Consul, connector: [ module: Rak.Connector.Web, message: Rak.Message.Raw, session: Rak.Session.Basic ] ] """ @components [ metrics: "Metrics system", service: "Top-level application service", module: "Application module", session: "Session module", discovery: "Discovery and clustering (optional)", connector: "Network connector for clients to connect to the server", ] defmacro __using__(config) do children = config_children(config) quote do use Application use Supervisor @doc """ Start """ def start({:takeover, node}, _) do raise Rak.Exception, message: "Takeover is not supported", details: "Ref: https://hexdocs.pm/elixir/Application.html#callbacks" end def start({:failover, node}, _) do raise Rak.Exception, message: "Failover is not supported", details: "Ref: https://hexdocs.pm/elixir/Application.html#callbacks" end def start(:normal, args) do args = parse_args(args) Supervisor.start_link(__MODULE__, args, name: __MODULE__) end def init(args) do # Flattening the list to allow for runtime generators # See: get_module_autoload_child_spec/0 children = List.flatten(unquote(children)) Supervisor.init(children, strategy: :one_for_one) end end end @doc false def autoload_modules(app_module) do app_module_name = Atom.to_string(app_module) app_module_depth = app_module_name |> String.split(".") |> length :code.all_loaded |> Stream.map(fn({ module, _ }) -> module_name = Atom.to_string(module) { module, module_name } end) |> Stream.filter(fn({ _, module_name }) -> String.starts_with?(module_name, app_module_name) end) |> Stream.filter(fn({ _, module_name }) -> module_depth = module_name |> String.split(".") |> length module_depth == app_module_depth + 1 end) |> Enum.filter(fn({ module, _ }) -> Keyword.has_key?(module.__info__(:attributes), :rak_module) end) end defp config_children(config) do session_modules = get_session_modules(config) app_modules = get_app_modules(config) config = Keyword.drop(:module) config = app_modules ++ session_modules ++ config @components |> Keyword.keys |> Enum.reduce([], fn(component, children) -> children ++ config |> Keyword.get_values(component) |> Enum.map(fn (config) -> get_child_spec(component, config) end) end) end defp get_app_modules(config) do modules = config |> Keyword.get_values(:module) auto_index = Enum.find_index(modules, fn (val) -> val == :auto end) if auto_index do {pre, [_|post]} = Enum.split(modules, auto_index) pre ++ get_module_autoload_child_spec() ++ post else modules end end defp get_session_modules(config) do config |> Keyword.get_values(:connector) |> Enum.map(&get_session_module/1) end defp get_session_module(%{ session: session }), do: {:session, session} defp get_session_module(%{ module: module }) do raise Rak.Exception, message: "Missing session module for connector #{module}", details: "Without a session module, connections cannot be handled" end defp get_child_spec(:connector, config) do module = config[:module] config = Keyword.drop(config, [:module]) quote do unquote(module).child_spec(unquote(config)) end end defp get_child_spec(_, module) do quote do unquote(module).child_spec([]) end end defp get_module_autoload_child_spec() do quote do Rak.autoload_modules(__MODULE__) end end end