Current section

Files

Jump to
frontier lib frontier.ex
Raw

lib/frontier.ex

defmodule Frontier do
@moduledoc """
Smart boundary enforcement for Elixir projects.
Frontier enforces architectural boundaries using convention-based defaults.
Context roots are public, schemas are auto-exported, and internal modules are private.
You only configure exceptions.
## Usage
# Project root — global config
defmodule MyApp do
use Frontier,
globals: [Result, Mailer],
ignore: [SomeLegacyThing]
end
# Context root — zero config
defmodule MyApp.Accounts do
use Frontier
end
# Context root — with restrictions
defmodule MyApp.Billing do
use Frontier,
reaches: [MyApp.Accounts],
externals: [:stripe],
exports: [InvoiceGenerator]
end
"""
defmacro __using__(opts) do
# Expand aliases at compile time so we store fully qualified module names.
# We do this in a clean env to avoid creating compile-time dependencies.
expanded_opts =
opts
|> Macro.prewalk(fn term ->
with {:__aliases__, _, _} <- term do
Macro.expand(term, %{__CALLER__ | function: {:frontier, 1}, lexical_tracker: nil})
end
end)
quote bind_quoted: [opts: expanded_opts] do
@after_compile {Frontier, :__after_compile__}
Module.register_attribute(__MODULE__, :frontier_opts, persist: true)
Module.put_attribute(__MODULE__, :frontier_opts, opts)
end
end
def __after_compile__(%{module: module}, _bytecode) do
Frontier.Store.init()
opts = Module.get_attribute(module, :frontier_opts) || []
case Frontier.Definition.parse_opts(module, opts) do
{:root, config} ->
Frontier.Store.register_root(module, config)
for ignored_module <- config.ignore do
Frontier.Store.register_ignored(ignored_module)
end
{:context, config} ->
Frontier.Store.register_context(module, config)
{:ignored, mod} ->
Frontier.Store.register_ignored(mod)
{:reclassified, target} ->
Frontier.Store.register_reclassified(module, target)
end
end
end