defmodule CCXT.WS.Subscription.MethodParams do @moduledoc """ Kraken v2 / Crypto.com-style subscribe frame. Accepts two caller conventions per channel list: 1. **Plain strings** — channels are grouped under `params.` (default key `"channel"`). MethodParams.subscribe(["ticker", "book"], %{}) # => %{"method" => "subscribe", "params" => %{"channel" => ["ticker", "book"]}} 2. **Single pre-shaped map** — the caller supplies the `params` object directly (Kraken v2 expects `{"channel": "ticker", "symbol": [...]}`). MethodParams.subscribe([%{"channel" => "ticker", "symbol" => ["BTC/USD"]}], %{}) # => %{"method" => "subscribe", # "params" => %{"channel" => "ticker", "symbol" => ["BTC/USD"]}} Multiple maps per call return `{:error, :multiple_maps_not_supported}`; lists that mix maps and strings return `{:error, :mixed_channel_types}`. Both errors propagate verbatim through `CCXT.WS.Subscription.build_subscribe/3`. Config keys: - `:op_field` — default `"method"` - `:args_field` — default `"params"` - `:channel_key` — default `"channel"` (string-channel branch only) """ @behaviour CCXT.WS.Subscription.Behaviour @impl true def subscribe(channels, config) when is_list(channels) do build(channels, config, "subscribe") end @impl true def unsubscribe(channels, config) when is_list(channels) do build(channels, config, "unsubscribe") end defp build([%{} = params_map], config, action) do method = Map.get(config, :op_field, "method") params = Map.get(config, :args_field, "params") %{method => action, params => params_map} end defp build(channels, config, action) when is_list(channels) do case classify_channels(channels) do :strings -> build_string_frame(channels, config, action) :all_maps -> {:error, :multiple_maps_not_supported} :mixed -> {:error, :mixed_channel_types} end end defp build_string_frame(channels, config, action) do method = Map.get(config, :op_field, "method") params = Map.get(config, :args_field, "params") channel_key = Map.get(config, :channel_key, "channel") %{method => action, params => %{channel_key => channels}} end # Empty list classifies as `:strings` so the existing default-frame # behavior (empty channel array under params) is preserved. defp classify_channels(channels) do cond do Enum.all?(channels, &is_binary/1) -> :strings Enum.all?(channels, &is_map/1) -> :all_maps true -> :mixed end end end