defmodule CCXT.Extract.JsonLoader do @moduledoc """ Generates JSON loading boilerplate with `:persistent_term` caching. Eliminates copy-pasted `@json_path`, `path/0`, `load/0`, `reload!/0`, and `read_json!/0` across 5 loader modules. ## Usage use CCXT.Extract.JsonLoader, file: "ccxt_parse_methods.json" Generates: - `@json_path` — compile-time resolved path to `priv/extractor/` - `path/0` — returns the JSON file path - `load/0` — loads with `:persistent_term` caching - `reload!/0` — bypasses cache, re-reads from disk - `read_json!/0` (private) — reads and decodes JSON from disk """ defmacro __using__(opts) do file = Keyword.fetch!(opts, :file) quote do # Inline priv path resolution to avoid compile-time dependency on CCXT.Priv # (which would cause cascading recompilation of ~200 exchange modules) @json_path (case :code.priv_dir(:ccxt_client) do {:error, :bad_name} -> [__DIR__, "..", "..", "priv", "extractor", unquote(file)] |> Path.join() |> Path.expand() priv when is_list(priv) -> Path.join(List.to_string(priv), "extractor/" <> unquote(file)) end) @doc "Returns the JSON file path." @spec path() :: String.t() def path, do: @json_path @doc """ Loads the JSON data with `:persistent_term` caching. Call `reload!/0` to force re-read from disk. """ @spec load() :: map() def load do case :persistent_term.get({__MODULE__, :data}, :missing) do :missing -> data = read_json!() :persistent_term.put({__MODULE__, :data}, data) data data -> data end end @doc "Reloads JSON from disk, bypassing cache." @spec reload!() :: map() def reload! do data = read_json!() :persistent_term.put({__MODULE__, :data}, data) data end @doc false # sobelow_skip ["Traversal.FileModule"] # Reads and parses the JSON file from disk (path is a compile-time constant) defp read_json! do @json_path |> File.read!() |> Jason.decode!() end end end end