Packages

A Flask-inspired web framework for Elixir.

Current section

Files

Jump to
mflask lib mix tasks mf.serve.ex
Raw

lib/mix/tasks/mf.serve.ex

defmodule Mix.Tasks.Mf.Serve do
use Mix.Task
@shortdoc "Serve a Plug-compatible module from a directory (e.g. mix mf.serve examples/)"
@moduledoc """
Usage:
mix mf.serve [PATH] [--module MyApp] [--port 4000] [--ip 127.0.0.1]
This task will require all Elixir source files under PATH (defaults to ".")
and attempt to find a module to serve with Bandit. If `--module` is provided,
that module will be used. Otherwise the task will pick a sensible module from
the loaded files (preferring `ExampleApp` if present, otherwise the first
module discovered).
The chosen module must implement the Plug interface (i.e. export `call/2`).
"""
@impl true
def run(argv) do
# Ensure the project application is started so dependencies are available.
# We still run inside Mix, but we want runtime apps started.
Mix.Task.run("app.start")
{opts, rest, _} =
OptionParser.parse(argv,
switches: [module: :string, port: :integer, ip: :string],
aliases: [m: :module, p: :port]
)
path =
case rest do
[p | _] -> p
[] -> "."
end
|> Path.expand()
unless File.dir?(path) do
Mix.raise("Path not found or not a directory: #{path}")
end
# Collect .ex and .exs files under the target path
ex_files = Path.wildcard(Path.join(path, "**/*.{ex,exs}")) |> Enum.sort()
modules =
ex_files
|> Enum.flat_map(fn file ->
try do
# Code.require_file returns [{Module, binary}|...]
require_result = Code.require_file(file)
Enum.map(require_result, fn
{m, _bin} -> m
m when is_atom(m) -> m
_ -> nil
end)
|> Enum.filter(&is_atom/1)
rescue
_ -> []
end
end)
|> Enum.uniq()
target_mod =
case opts[:module] do
nil ->
choose_default_module(modules)
mod_name when is_binary(mod_name) ->
string_to_module(mod_name)
end
if target_mod == nil do
Mix.shell().error("No module to serve was detected in #{path}.")
Mix.shell().info("Scanned files:\n #{Enum.join(ex_files, "\n ")}")
Mix.raise(
"Provide --module MODULE or ensure a Plug-compatible module is defined in the path."
)
end
unless function_exported?(target_mod, :call, 2) do
Mix.raise(
"Module #{inspect(target_mod)} does not implement call/2. Please provide a Plug-compatible module."
)
end
port = opts[:port] || 4000
ip = parse_ip(opts[:ip]) || {0, 0, 0, 0}
# Start Bandit and its runtime dependencies
{:ok, _} = Application.ensure_all_started(:bandit)
children = [
{Bandit, plug: target_mod, port: port, ip: ip}
]
{:ok, _sup} = Supervisor.start_link(children, strategy: :one_for_one)
Mix.shell().info("")
Mix.shell().info("Serving #{inspect(target_mod)} at http://#{ip_to_string(ip)}:#{port}")
Mix.shell().info("Press Ctrl+C twice to stop")
# Keep the task running while the server runs
Process.sleep(:infinity)
end
defp choose_default_module(modules) do
cond do
Enum.member?(modules, ExampleApp) -> ExampleApp
modules != [] -> List.first(modules)
true -> nil
end
end
defp string_to_module(name) do
name
|> String.split(".")
|> Enum.map(&String.to_atom/1)
|> Module.concat()
end
defp parse_ip(nil), do: nil
defp parse_ip(<<_::binary>> = s) do
case :inet.parse_address(String.to_charlist(s)) do
{:ok, ip} -> ip
_ -> nil
end
end
defp ip_to_string({a, b, c, d}) when is_integer(a) do
Enum.join([a, b, c, d], ".")
end
defp ip_to_string(ip), do: to_string(:inet.ntoa(ip))
end