Current section
Files
Jump to
Current section
Files
lib/ccxt/generator/dispatch.ex
defmodule CCXT.Generator.Dispatch do
@moduledoc """
Shared runtime dispatcher for generated endpoint functions.
Instead of inlining ~30 lines of glue code per endpoint (87-99 times per
exchange), generated functions become thin wrappers that call `call/5`.
This module contains the shared pipeline logic: param merging, emulation
dispatch, path interpolation, prefix handling, and HTTP execution.
## Architecture
- **`build_config/3` / `build_configs/2`** — Called at compile time to
pre-compute per-endpoint configuration (path style, prefix strategy,
reversed mappings, etc.)
- **`call/5`** — Called at runtime by thin wrapper functions. Receives
the exchange module, endpoint name, pre-filtered param map, user opts,
and optional parser mapping.
## Pipeline (mirrors endpoints.ex:206-271)
1. Look up endpoint config and spec from exchange module
2. Merge params: defaults → function args → opts[:params]
3. Emulation dispatch (return early if emulated)
4. On `:passthrough`:
- Resolve generated placeholders
- Denormalize symbol param
- Convert OHLCV timestamps (if applicable)
- Interpolate path placeholders
- Apply endpoint param mappings
- Apply path prefix strategy
- Build client opts
- Execute HTTP request
"""
alias CCXT.Generator.Helpers
# OHLCV endpoints that need timestamp conversion and timeframe translation
@ohlcv_endpoints [
:fetch_ohlcv,
:fetch_index_ohlcv,
:fetch_mark_ohlcv,
:fetch_premium_index_ohlcv
]
# Pre-compiled regexes for path interpolation
@colon_regex ~r/:(\w+)/
@curly_regex ~r/\{([\w-]+)\}/
# ============================================================================
# Compile-time config builders
# ============================================================================
@doc """
Builds endpoint configs for all endpoints in a spec.
Called at compile time by `CCXT.Generator` to produce a map of
`%{endpoint_name => config}` stored as a module attribute.
## Parameters
- `spec` - The exchange specification
- `pipeline` - Optional pipeline keyword list with `:coercer` and `:parsers`
"""
@spec build_configs(CCXT.Spec.t(), keyword()) :: %{atom() => map()}
def build_configs(spec, pipeline \\ []) do
spec.endpoints
|> Enum.filter(fn endpoint -> Map.get(spec.has, endpoint[:name], true) end)
|> Map.new(fn endpoint ->
{endpoint[:name], build_config(endpoint, spec, pipeline)}
end)
end
@doc """
Builds a single endpoint config map.
Pre-computes values that would otherwise be recalculated on every call:
path style, reversed mappings, prefix strategy, and response type.
## Parameters
- `endpoint` - Endpoint map from spec (`:name`, `:method`, `:path`, etc.)
- `spec` - The exchange specification
- `pipeline` - Optional pipeline keyword list with `:coercer`
"""
@spec build_config(map(), CCXT.Spec.t(), keyword()) :: map()
def build_config(endpoint, spec, pipeline \\ []) do
path = endpoint[:path]
param_mappings = Map.get(endpoint, :param_mappings, %{})
coercer = pipeline[:coercer]
name = endpoint[:name]
path_style = detect_path_style(path)
%{
method: endpoint[:method],
path: path,
auth: endpoint[:auth],
param_mappings: param_mappings,
default_params: Map.get(endpoint, :default_params, %{}),
base_url: endpoint[:base_url],
market_type: endpoint[:market_type],
response_transformer: endpoint[:response_transformer],
response_type: if(coercer, do: coercer.infer_response_type(name)),
coercer: coercer,
ohlcv?: name in @ohlcv_endpoints,
path_style: path_style,
reversed_mappings: if(path_style == :curly, do: reverse_mappings(param_mappings), else: %{}),
prefix_strategy: compute_prefix_strategy(spec.path_prefix, path)
}
end
# ============================================================================
# Runtime dispatch
# ============================================================================
@doc """
Runtime dispatch entry point for generated endpoint functions.
Called by thin wrappers generated in 227c. Executes the full pipeline:
param merging, emulation check, path interpolation, and HTTP request.
## Parameters
- `exchange_module` - The generated exchange module (e.g., `MyApp.Bybit`)
- `endpoint_name` - Endpoint atom (e.g., `:fetch_ticker`)
- `param_map` - Pre-filtered parameter map from function args (nil values excluded)
- `opts` - User-provided keyword options
- `parser_mapping` - Optional parser mapping for response coercion
"""
@spec call(module(), atom(), map(), keyword(), term()) ::
{:ok, term()} | {:error, CCXT.Error.t()}
def call(exchange_module, endpoint_name, param_map, opts, parser_mapping \\ nil) do
config = exchange_module.__endpoint_config__(endpoint_name)
spec = exchange_module.__ccxt_spec__()
# Build params: defaults → function args → extra opts params
# Strip defaults that collide with non-nil function args (via param_mappings)
params =
config.default_params
|> Helpers.strip_overridden_defaults(param_map, config.param_mappings)
|> Map.merge(param_map)
|> Map.merge(Map.new(Keyword.get(opts, :params, [])))
# Capture credentials (nil for public endpoints)
credentials = if config.auth, do: Keyword.get(opts, :credentials)
# Emulated method dispatch (REST scope)
case CCXT.Emulation.dispatch(spec, endpoint_name, :rest, %{
exchange_module: exchange_module,
params: params,
opts: opts,
credentials: credentials
}) do
:passthrough ->
execute_passthrough(spec, config, params, opts, credentials, parser_mapping)
{:ok, result} ->
{:ok, result}
{:error, _} = error ->
error
end
end
# ============================================================================
# Passthrough pipeline (HTTP execution path)
# ============================================================================
@doc false
@spec execute_passthrough(CCXT.Spec.t(), map(), map(), keyword(), term(), term()) ::
{:ok, term()} | {:error, CCXT.Error.t()}
defp execute_passthrough(spec, config, params, opts, credentials, parser_mapping) do
params =
params
|> Helpers.resolve_generated_placeholders()
|> Helpers.denormalize_symbol_param(spec, config.market_type)
# Convert OHLCV timestamps if needed
params =
if config.ohlcv? do
Helpers.convert_ohlcv_timestamps(params, spec.ohlcv_timestamp_resolution)
else
params
end
# Translate unified timeframe values to exchange-native format (e.g., "1h" → "60" for Bybit)
params = Helpers.translate_timeframe(params, spec.timeframes)
# Interpolate path, apply mappings, prepend prefix
base_path = interpolate_path(config.path, config.path_style, params, config.reversed_mappings)
params = Helpers.apply_endpoint_mappings(params, config.param_mappings)
path = build_prefixed_path(config.prefix_strategy, base_path)
# Execute HTTP request with response transformation and optional coercion
client_opts = build_client_opts(opts, params, credentials, config.auth, config.base_url)
Helpers.execute_request(
spec,
config.method,
path,
client_opts,
config.response_transformer,
config.response_type,
parser_mapping,
opts,
config.coercer
)
end
# ============================================================================
# Path interpolation (runtime, data-driven)
# ============================================================================
@doc false
# Interpolates path placeholders using pre-detected path style.
# sobelow_skip ["DOS.StringToAtom"]
@spec interpolate_path(String.t(), atom(), map(), map()) :: String.t()
defp interpolate_path(path, :plain, _params, _reversed_mappings), do: path
defp interpolate_path(path, :colon, params, _reversed_mappings) do
Regex.replace(@colon_regex, path, fn _match, key ->
# Safe: path placeholders come from trusted spec files loaded at compile time
# sobelow_skip ["DOS.StringToAtom"]
key_atom = String.to_atom(key)
to_string(Map.get(params, key_atom, ""))
end)
end
defp interpolate_path(path, :curly, params, reversed_mappings) do
Regex.replace(@curly_regex, path, fn _match, key ->
# First check reversed param_mappings, then try direct atom conversion
# Safe: path placeholders come from trusted spec files loaded at compile time
# sobelow_skip ["DOS.StringToAtom"]
param_key = Map.get(reversed_mappings, key, String.to_atom(key))
Helpers.get_path_param(params, param_key, key)
end)
end
# ============================================================================
# Path prefix application (runtime, pre-computed strategy)
# ============================================================================
@doc false
# Applies pre-computed prefix strategy to the interpolated base path.
@spec build_prefixed_path(atom() | tuple(), String.t()) :: String.t()
defp build_prefixed_path(:passthrough, base_path), do: base_path
defp build_prefixed_path({:concat, prefix}, base_path), do: prefix <> base_path
defp build_prefixed_path({:strip_slash, prefix}, base_path) do
prefix <> String.slice(base_path, 1..-1//1)
end
defp build_prefixed_path({:version_override, new_prefix, version_prefix}, base_path) do
stripped = String.replace_prefix(base_path, version_prefix, "")
new_prefix <> String.trim_leading(stripped, "/")
end
# ============================================================================
# Client opts builder
# ============================================================================
@doc false
# Builds keyword list for HTTP client from user opts, processed params,
# credentials, and endpoint-specific base_url.
@spec build_client_opts(keyword(), map(), term(), boolean(), String.t() | nil) :: keyword()
defp build_client_opts(opts, params, credentials, auth, base_url) do
client_opts =
opts
|> Keyword.delete(:params)
|> Keyword.delete(:credentials)
|> Keyword.put(:params, params)
client_opts = if auth, do: Keyword.put(client_opts, :credentials, credentials), else: client_opts
if base_url, do: Keyword.put(client_opts, :base_url, base_url), else: client_opts
end
# ============================================================================
# Config computation helpers (compile-time)
# ============================================================================
@doc false
# Detects path placeholder style from path string.
@spec detect_path_style(String.t()) :: :plain | :colon | :curly
defp detect_path_style(path) do
cond do
String.contains?(path, ":") -> :colon
String.contains?(path, "{") -> :curly
true -> :plain
end
end
@doc false
# Reverses param_mappings for curly-brace path interpolation.
# %{"id" => "order-id"} → %{"order-id" => :id}
# sobelow_skip ["DOS.StringToAtom"]
@spec reverse_mappings(map()) :: map()
defp reverse_mappings(param_mappings) do
for {unified, request} <- param_mappings, into: %{} do
{request, String.to_atom(unified)}
end
end
@doc false
# Pre-computes prefix handling strategy from spec.path_prefix and endpoint path.
@spec compute_prefix_strategy(String.t(), String.t()) :: atom() | tuple()
defp compute_prefix_strategy("", _path), do: :passthrough
defp compute_prefix_strategy(prefix, path) do
cond do
String.starts_with?(path, prefix) ->
:passthrough
Regex.match?(~r{^/v\d+/}, path) and Regex.match?(~r{v\d+}, prefix) ->
[_, path_ver] = Regex.run(~r{^/(v\d+)}, path)
new_prefix = Regex.replace(~r{v\d+}, prefix, path_ver, global: false)
version_prefix = "/" <> path_ver
{:version_override, new_prefix, version_prefix}
String.ends_with?(prefix, "/") and String.starts_with?(path, "/") ->
{:strip_slash, prefix}
true ->
{:concat, prefix}
end
end
end