Current section
Files
Jump to
Current section
Files
lib/cfdi/complementos/registry.ex
defmodule Cfdi.Complementos.Registry do
@moduledoc """
Índice inverso de los complementos: dado lo que aparece en un XML,
resuelve el módulo que lo maneja.
Es la pieza que permite decodificar (`CFDI.from_xml/2`): al serializar,
cada complemento sabe su propia `key`; al deserializar hace falta el
camino contrario, `"http://www.sat.gob.mx/Pagos20"` →
`Cfdi.Complementos.Pago20`.
## Descubrimiento
Los módulos NO se listan a mano: se descubren en runtime recorriendo los
módulos de la aplicación `:cfdi_complementos` y quedándose con los que
exponen `__complemento__/0` (marca que agrega
`use Cfdi.Complementos.Complemento`). Agregar un complemento nuevo lo
registra solo — no hay lista que se desincronice.
El índice se construye una vez y se cachea en `:persistent_term`.
## Resolver por namespace, no por prefijo
`by_xmlns/1` es la vía correcta al decodificar XML: la URI del namespace
es el identificador autoritativo. El prefijo es convención y un emisor
puede usar el que quiera. Ver `Cfdi.Complementos.Complemento`.
"""
@app :cfdi_complementos
@doc """
Todos los módulos de complemento conocidos.
"""
@spec all() :: [module()]
def all(), do: table().modules
@doc """
Resuelve el módulo por la URI de su namespace. Vía preferida al decodificar.
iex> Cfdi.Complementos.Registry.by_xmlns("http://www.sat.gob.mx/Pagos20")
Cfdi.Complementos.Pago20
"""
@spec by_xmlns(String.t()) :: module() | nil
def by_xmlns(uri) when is_binary(uri), do: Map.get(table().by_xmlns, uri)
@doc """
Resuelve el módulo por su key canónica (con prefijo del SAT).
Sólo sirve cuando la key ya viene en la convención oficial; para XML de
terceros usar `by_xmlns/1`.
"""
@spec by_key(String.t()) :: module() | nil
def by_key(key) when is_binary(key), do: Map.get(table().by_key, key)
@doc """
Descarta el índice cacheado. Útil tras recompilar en desarrollo.
"""
@spec refresh() :: :ok
def refresh() do
:persistent_term.erase(__MODULE__)
:ok
end
defp table() do
case :persistent_term.get(__MODULE__, nil) do
nil ->
built = build()
:persistent_term.put(__MODULE__, built)
built
cached ->
cached
end
end
defp build() do
modules = discover()
%{
modules: modules,
by_xmlns: index_by(modules, :xmlns),
by_key: index_by(modules, :key)
}
end
defp discover() do
_ = Application.load(@app)
case :application.get_key(@app, :modules) do
{:ok, modules} -> Enum.filter(modules, &complemento?/1)
_ -> []
end
end
defp complemento?(module) do
Code.ensure_loaded?(module) and function_exported?(module, :__complemento__, 0)
end
# Un choque de xmlns o key entre dos complementos es un bug de programación
# (dos módulos peleando por el mismo elemento del SAT): reventamos fuerte en
# vez de dejar que uno pise al otro silenciosamente.
defp index_by(modules, fun) do
modules
|> Enum.group_by(&apply(&1, fun, []))
|> Map.new(fn
{value, [module]} ->
{value, module}
{value, colliding} ->
raise ArgumentError,
"complementos con #{fun} duplicado #{inspect(value)}: #{inspect(colliding)}"
end)
end
end