The options each facade function reads, in one place, and the check that refuses the rest.
A misspelt limt: that is silently ignored returns a plausible answer to a different
question than the caller asked, which is the "nearby substitute where there should be an
error" this family exists to avoid. Every DpExchange.Webull function that takes a keyword
opts, and its DpExchange.Webull.Fake twin, calls this module at the boundary: before
anything is sent, before the facade adds its own :limiter, and after the positional
arguments (symbol, order id, watchlist id, request) have been checked.
What a function may be given
A shared transport key (DpExchange.Core.Options.transport_options/0) is accepted only
where the call reads it, and every one accepted is forwarded to Core.HttpClient.
- A call that sends a request allows every shared key but
:base_url(no call reads it; the hosts come from:environment), plus the keys inown_keys/1.oauth_token/3also refuses:credentials: the client id and secret are arguments. - A write that is always sent exactly once (
place_order/3,place_orders/3,cancel_order/3,replace_order/4, the watchlist writes,create_token/1andoauth_token/3) reads neither:retry_attemptsnor:retry_delay: both are{:error, {:unknown_option, key}}, whatever the value, before anything is sent. get_fees/2,coverage/1,wanted/1,coverage_by_kind/1andsubscribe_notices/1send no request and open no socket: they allow:credentials(exempt, a consumer hands one set to every call) and their own keys, sotimeout: 5_000is refused.streaming?/1andlive?/1allow:credentialsand:environment, which they resolve, and nothing else.
A refused key is {:error, {:unknown_option, key}}. The table below is the second half,
and the only place it is written down:
check/3 reads it, DpExchange.Webull and DpExchange.Webull.Fake both call check/3
(or one of the positional helpers), so the two cannot disagree.
A key that the code reads only to refuse it is still in the table, because the more
specific answer is the useful one: :limit on get_order_book/2 is
{:error, {:unsupported_option, :limit}} (levels per side are :depth), :since on
get_trades/2 likewise, :real_time on the bars (renamed :include_partial), :symbols
on update_watchlist/2 is {:error, :membership_not_updatable_here}.
Two calls take part of their keys from the argument that names the endpoint, so the table
cannot hold all of them: get_fundamental/3 (the fundamentals kind) and get_screener/2
(the screener). Rest.fundamental_option_keys/1 and Rest.screener_option_keys/1 answer
those and are passed as extra to check/3; check_fundamental/3 and check_screener/2
do both steps. get_financials/3 is fixed: all four statement kinds take the same keys.
What it answers
:ok{:error, {:unknown_option, key}}- the first key (in the caller's order) not allowed{:error, {:invalid_options, opts}}-optsis not a keyword list
check!/2 is the same check for a plain-value read with no error channel (coverage/1,
wanted/1, coverage_by_kind/1, streaming?/1, live?/1): it raises ArgumentError.
A function that answers Venue.not_supported() is not in the table and keeps doing so
whatever it is given.
Summary
Functions
Checks opts against the keys function may be given.
check/3 for a call with no error channel: raises ArgumentError instead of answering.
The keys a get_fundamental/3 of kind may be given beyond the transport ones, or the
unknown-kind refusal. Checked first, as the kind names the endpoint.
client_id and then client_secret are checked first ({:error, {:invalid_field, field}},
never echoing the value), then the options.
The order id is checked first ({:error, {:invalid_order_id, id}}), then the options.
The keys a get_screener/2 of name may be given beyond the transport ones, or the
unknown-screener refusal. Checked first, as the name names the endpoint.
The symbol is checked first, then the options: {:error, {:invalid_symbol, symbol}} wins
over an unknown option, as positional arguments do in every venue of this family.
The token is checked first ({:error, {:invalid_field, :token}}; the value is a credential,
so the field is named and the value is not echoed), then the options.
The watchlist id is checked first ({:error, {:invalid_watchlist_id, id}}), then the
options.
The keys function reads beyond DpExchange.Core.Options.transport_options/0.
Functions
@spec check(atom(), term(), [atom()]) :: :ok | {:error, {:unknown_option, atom()} | {:invalid_options, term()}}
Checks opts against the keys function may be given.
extra adds keys that depend on an argument (get_fundamental/3's kind, get_screener/2's
name). Answers :ok, {:error, {:unknown_option, key}} or
{:error, {:invalid_options, opts}}.
check/3 for a call with no error channel: raises ArgumentError instead of answering.
For coverage/1, wanted/1, coverage_by_kind/1, streaming?/1 and live?/1, whose
specs are a map, a list and a boolean. Returning an empty map for a misspelt feed: would
read as "nothing is arriving" and send the caller to debug the stream.
The keys a get_fundamental/3 of kind may be given beyond the transport ones, or the
unknown-kind refusal. Checked first, as the kind names the endpoint.
client_id and then client_secret are checked first ({:error, {:invalid_field, field}},
never echoing the value), then the options.
The order id is checked first ({:error, {:invalid_order_id, id}}), then the options.
The keys a get_screener/2 of name may be given beyond the transport ones, or the
unknown-screener refusal. Checked first, as the name names the endpoint.
The symbol is checked first, then the options: {:error, {:invalid_symbol, symbol}} wins
over an unknown option, as positional arguments do in every venue of this family.
The token is checked first ({:error, {:invalid_field, :token}}; the value is a credential,
so the field is named and the value is not echoed), then the options.
The watchlist id is checked first ({:error, {:invalid_watchlist_id, id}}), then the
options.
The keys function reads beyond DpExchange.Core.Options.transport_options/0.
Raises KeyError for a function that has no entry, which is a bug in the caller.