Current section
Files
Jump to
Current section
Files
lib/frontier/classifier.ex
defmodule Frontier.Classifier do
@moduledoc "Classifies modules by priority: ignored, global, reclassified, context root, schema, exported, internal, or unowned."
alias Frontier.Store
@type classification ::
:ignored
| :global
| :reclassified
| :context_root
| :schema
| :exported
| :internal
| :unowned
@doc """
Classifies a module according to Frontier's priority rules:
1. Ignored (per-module or root ignore list)
2. Global (in root globals list, or submodule of a global)
3. Reclassified (has belongs_to:)
4. Context root (has use Frontier)
5. Schema (defines __schema__/1)
6. Exported (listed in context's exports:)
7. Internal (under a context namespace)
8. Unowned (not under any context)
"""
@spec classify(module()) :: classification()
def classify(module) do
cond do
ignored?(module) -> :ignored
global?(module) -> :global
reclassified?(module) -> :reclassified
context_root?(module) -> :context_root
schema?(module) and under_context?(module) and public_schema?(module) -> :schema
exported?(module) -> :exported
under_context?(module) -> :internal
true -> :unowned
end
end
@doc """
Returns the owning context for a module, or nil if the module isn't owned by any context.
"""
@spec owning_context(module()) :: module() | nil
def owning_context(module) do
cond do
reclassified?(module) ->
Store.reclassified_to(module)
context_root?(module) ->
module
true ->
find_nearest_context(module)
end
end
defp ignored?(module) do
Store.ignored?(module) || root_ignored?(module)
end
defp root_ignored?(module) do
case Store.root() do
{_root_mod, %{ignore: ignore_list}} -> module in ignore_list
nil -> false
end
end
defp global?(module) do
case Store.root() do
{_root_mod, %{globals: globals}} ->
Enum.any?(globals, fn global ->
module == global || submodule_of?(module, global)
end)
nil ->
false
end
end
defp reclassified?(module) do
Store.reclassified_to(module) != nil
end
defp context_root?(module) do
Store.context(module) != nil
end
defp schema?(module) do
Code.ensure_loaded?(module) && function_exported?(module, :__schema__, 1)
end
defp exported?(module) do
case find_nearest_context(module) do
nil ->
false
context ->
config = Store.context(context)
case config.exports do
:all -> true
exports when is_list(exports) -> module in exports
end
end
end
defp under_context?(module) do
find_nearest_context(module) != nil
end
defp find_nearest_context(module) do
module
|> Module.split()
|> find_context_by_prefix()
end
defp find_context_by_prefix(parts) when length(parts) <= 1, do: nil
defp find_context_by_prefix(parts) do
parent_parts = Enum.take(parts, length(parts) - 1)
candidate = Module.concat(parent_parts)
if Store.context(candidate) != nil do
candidate
else
find_context_by_prefix(parent_parts)
end
end
defp public_schema?(module) do
context = find_nearest_context(module)
context_config = Store.context(context)
setting =
case context_config do
%{public_schemas: nil} -> root_public_schemas()
%{public_schemas: value} -> value
_ -> root_public_schemas()
end
case setting do
true -> true
false -> false
list when is_list(list) -> module in list
end
end
defp root_public_schemas do
case Store.root() do
{_, %{public_schemas: setting}} -> setting
_ -> true
end
end
defp submodule_of?(module, parent) do
module_str = Atom.to_string(module)
parent_str = Atom.to_string(parent)
String.starts_with?(module_str, parent_str <> ".")
end
end