Packages

Type-safe Gleam client library for the Upbit cryptocurrency exchange API

Current section

Files

Jump to
glupbit src glupbit@client.erl
Raw

src/glupbit@client.erl

-module(glupbit@client).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glupbit/client.gleam").
-export([to_public/1, build_array_query/2, encode_query_string/1, parse_rate_limit_value/1, public_get/4, public_get_auth/4, auth_get/4, auth_post/5, auth_delete/4, new_public/0, new_auth/1, new_auth_from_env/0]).
-export_type([public_client/0, auth_client/0, client_config/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" HTTP client infrastructure with two-tier authentication.\n"
"\n"
" `PublicClient` — quotation (market data) endpoints, no auth needed.\n"
" `AuthClient` — all endpoints including exchange (JWT required).\n"
"\n"
" The Gleam type system statically prevents calling exchange endpoints\n"
" with a `PublicClient`.\n"
).
-opaque public_client() :: {public_client, client_config()}.
-opaque auth_client() :: {auth_client,
client_config(),
glupbit@auth:credentials()}.
-type client_config() :: {client_config, binary(), gleam@httpc:configuration()}.
-file("src/glupbit/client.gleam", 64).
?DOC(" Downcast an AuthClient to a PublicClient for quotation calls.\n").
-spec to_public(auth_client()) -> public_client().
to_public(Client) ->
{public_client, erlang:element(2, Client)}.
-file("src/glupbit/client.gleam", 146).
?DOC(" Build `key[]=v1&key[]=v2` style query parameters.\n").
-spec build_array_query(binary(), list(binary())) -> list({binary(), binary()}).
build_array_query(Key, Values) ->
Bracket_key = <<Key/binary, "[]"/utf8>>,
gleam@list:map(Values, fun(V) -> {Bracket_key, V} end).
-file("src/glupbit/client.gleam", 155).
?DOC(" Encode query parameters to a raw `key=value&...` string for hashing.\n").
-spec encode_query_string(list({binary(), binary()})) -> binary().
encode_query_string(Query) ->
_pipe = Query,
_pipe@1 = gleam@list:map(
_pipe,
fun(Pair) ->
<<<<(erlang:element(1, Pair))/binary, "="/utf8>>/binary,
(erlang:element(2, Pair))/binary>>
end
),
gleam@string:join(_pipe@1, <<"&"/utf8>>).
-file("src/glupbit/client.gleam", 162).
?DOC(" Parse `\"group=market; min=573; sec=9\"` into a `RateLimit`.\n").
-spec parse_rate_limit_value(binary()) -> gleam@option:option(glupbit@types:rate_limit()).
parse_rate_limit_value(Value) ->
Parts = begin
_pipe = Value,
_pipe@1 = gleam@string:split(_pipe, <<"; "/utf8>>),
gleam@list:filter_map(
_pipe@1,
fun(Part) -> case gleam@string:split(Part, <<"="/utf8>>) of
[Key, Val] ->
{ok, {gleam@string:trim(Key), gleam@string:trim(Val)}};
_ ->
{error, nil}
end end
)
end,
Find = fun(Key@1) ->
_pipe@2 = gleam@list:find(
Parts,
fun(P) -> erlang:element(1, P) =:= Key@1 end
),
gleam@result:map(_pipe@2, fun(P@1) -> erlang:element(2, P@1) end)
end,
case Find(<<"group"/utf8>>) of
{ok, Group} ->
{some,
{rate_limit,
Group,
begin
_pipe@3 = Find(<<"min"/utf8>>),
_pipe@4 = gleam@result:'try'(
_pipe@3,
fun gleam_stdlib:parse_int/1
),
gleam@result:unwrap(_pipe@4, 0)
end,
begin
_pipe@5 = Find(<<"sec"/utf8>>),
_pipe@6 = gleam@result:'try'(
_pipe@5,
fun gleam_stdlib:parse_int/1
),
gleam@result:unwrap(_pipe@6, 0)
end}};
{error, _} ->
none
end.
-file("src/glupbit/client.gleam", 194).
-spec auth_token_for_query(
glupbit@auth:credentials(),
list({binary(), binary()})
) -> {ok, binary()} | {error, glupbit@types:api_error()}.
auth_token_for_query(Credentials, Query) ->
case Query of
[] ->
glupbit@auth:generate_token(Credentials);
_ ->
glupbit@auth:generate_token_with_query(
Credentials,
encode_query_string(Query)
)
end.
-file("src/glupbit/client.gleam", 226).
-spec set_accept_json(gleam@http@request:request(binary())) -> gleam@http@request:request(binary()).
set_accept_json(Req) ->
gleam@http@request:set_header(
Req,
<<"accept"/utf8>>,
<<"application/json"/utf8>>
).
-file("src/glupbit/client.gleam", 208).
-spec build_request(
client_config(),
gleam@http:method(),
binary(),
list({binary(), binary()})
) -> {ok, gleam@http@request:request(binary())} |
{error, glupbit@types:api_error()}.
build_request(Config, Method, Path, Query) ->
Url = <<<<(erlang:element(2, Config))/binary, "/v1"/utf8>>/binary,
Path/binary>>,
gleam@result:'try'(
begin
_pipe = gleam@http@request:to(Url),
gleam@result:replace_error(
_pipe,
{auth_error, <<"Invalid URL: "/utf8, Url/binary>>}
)
end,
fun(Req) ->
Req@1 = begin
_pipe@1 = Req,
_pipe@2 = gleam@http@request:set_method(_pipe@1, Method),
set_accept_json(_pipe@2)
end,
case Query of
[] ->
{ok, Req@1};
_ ->
{ok, gleam@http@request:set_query(Req@1, Query)}
end
end
).
-file("src/glupbit/client.gleam", 230).
-spec with_auth(gleam@http@request:request(binary()), binary()) -> gleam@http@request:request(binary()).
with_auth(Req, Token) ->
gleam@http@request:set_header(
Req,
<<"authorization"/utf8>>,
<<"Bearer "/utf8, Token/binary>>
).
-file("src/glupbit/client.gleam", 265).
-spec json_decode_error(gleam@json:decode_error()) -> glupbit@types:api_error().
json_decode_error(Err) ->
case Err of
{unable_to_decode, Errs} ->
{decode_error, Errs};
{unexpected_byte, _} ->
{decode_error,
[{decode_error, <<"valid JSON"/utf8>>, <<"invalid"/utf8>>, []}]};
unexpected_end_of_input ->
{decode_error,
[{decode_error, <<"valid JSON"/utf8>>, <<"invalid"/utf8>>, []}]};
{unexpected_sequence, _} ->
{decode_error,
[{decode_error, <<"valid JSON"/utf8>>, <<"invalid"/utf8>>, []}]}
end.
-file("src/glupbit/client.gleam", 277).
-spec parse_error_body(integer(), binary()) -> {ok, any()} |
{error, glupbit@types:api_error()}.
parse_error_body(Status, Body) ->
Name_decoder = gleam@dynamic@decode:one_of(
{decoder, fun gleam@dynamic@decode:decode_string/1},
[begin
_pipe = {decoder, fun gleam@dynamic@decode:decode_int/1},
gleam@dynamic@decode:map(_pipe, fun erlang:integer_to_binary/1)
end]
),
Decoder = begin
gleam@dynamic@decode:subfield(
[<<"error"/utf8>>, <<"name"/utf8>>],
Name_decoder,
fun(Name) ->
gleam@dynamic@decode:subfield(
[<<"error"/utf8>>, <<"message"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Message) ->
gleam@dynamic@decode:success({Name, Message})
end
)
end
)
end,
case gleam@json:parse(Body, Decoder) of
{ok, {Name@1, Message@1}} ->
{error, {upbit_error, Status, Name@1, Message@1}};
{error, _} ->
{error, {upbit_error, Status, <<"unknown"/utf8>>, Body}}
end.
-file("src/glupbit/client.gleam", 237).
-spec execute(
client_config(),
gleam@http@request:request(binary()),
gleam@dynamic@decode:decoder(AAEQ)
) -> {ok, glupbit@types:api_response(AAEQ)} | {error, glupbit@types:api_error()}.
execute(Config, Req, Decoder) ->
gleam@result:'try'(
begin
_pipe = gleam@httpc:dispatch(erlang:element(3, Config), Req),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {http_error, Field@0} end
)
end,
fun(Resp) ->
Rate_limit = begin
_pipe@1 = gleam@http@response:get_header(
Resp,
<<"remaining-req"/utf8>>
),
_pipe@2 = gleam@result:map(
_pipe@1,
fun parse_rate_limit_value/1
),
gleam@result:unwrap(_pipe@2, none)
end,
case erlang:element(2, Resp) of
200 ->
_pipe@3 = gleam@json:parse(erlang:element(4, Resp), Decoder),
_pipe@4 = gleam@result:map(
_pipe@3,
fun(Data) -> {api_response, Data, Rate_limit} end
),
gleam@result:map_error(_pipe@4, fun json_decode_error/1);
201 ->
_pipe@3 = gleam@json:parse(erlang:element(4, Resp), Decoder),
_pipe@4 = gleam@result:map(
_pipe@3,
fun(Data) -> {api_response, Data, Rate_limit} end
),
gleam@result:map_error(_pipe@4, fun json_decode_error/1);
418 ->
case Rate_limit of
{some, Rl} ->
{error, {rate_limited, Rl}};
none ->
{error,
{upbit_error,
erlang:element(2, Resp),
<<"rate_limited"/utf8>>,
<<""/utf8>>}}
end;
429 ->
case Rate_limit of
{some, Rl} ->
{error, {rate_limited, Rl}};
none ->
{error,
{upbit_error,
erlang:element(2, Resp),
<<"rate_limited"/utf8>>,
<<""/utf8>>}}
end;
Status ->
parse_error_body(Status, erlang:element(4, Resp))
end
end
).
-file("src/glupbit/client.gleam", 78).
?DOC(" Execute a public GET request (no auth).\n").
-spec public_get(
public_client(),
binary(),
list({binary(), binary()}),
gleam@dynamic@decode:decoder(AACX)
) -> {ok, glupbit@types:api_response(AACX)} | {error, glupbit@types:api_error()}.
public_get(Client, Path, Query, Decoder) ->
gleam@result:'try'(
build_request(erlang:element(2, Client), get, Path, Query),
fun(Req) -> execute(erlang:element(2, Client), Req, Decoder) end
).
-file("src/glupbit/client.gleam", 89).
?DOC(" Execute a public GET using an AuthClient.\n").
-spec public_get_auth(
auth_client(),
binary(),
list({binary(), binary()}),
gleam@dynamic@decode:decoder(AADD)
) -> {ok, glupbit@types:api_response(AADD)} | {error, glupbit@types:api_error()}.
public_get_auth(Client, Path, Query, Decoder) ->
public_get(to_public(Client), Path, Query, Decoder).
-file("src/glupbit/client.gleam", 99).
?DOC(" Execute an authenticated GET request.\n").
-spec auth_get(
auth_client(),
binary(),
list({binary(), binary()}),
gleam@dynamic@decode:decoder(AADJ)
) -> {ok, glupbit@types:api_response(AADJ)} | {error, glupbit@types:api_error()}.
auth_get(Client, Path, Query, Decoder) ->
gleam@result:'try'(
build_request(erlang:element(2, Client), get, Path, Query),
fun(Req) ->
gleam@result:'try'(
auth_token_for_query(erlang:element(3, Client), Query),
fun(Token) ->
execute(
erlang:element(2, Client),
begin
_pipe = Req,
with_auth(_pipe, Token)
end,
Decoder
)
end
)
end
).
-file("src/glupbit/client.gleam", 111).
?DOC(" Execute an authenticated POST request with JSON body.\n").
-spec auth_post(
auth_client(),
binary(),
gleam@json:json(),
list({binary(), binary()}),
gleam@dynamic@decode:decoder(AADP)
) -> {ok, glupbit@types:api_response(AADP)} | {error, glupbit@types:api_error()}.
auth_post(Client, Path, Body, Params, Decoder) ->
gleam@result:'try'(
build_request(erlang:element(2, Client), post, Path, []),
fun(Req) -> gleam@result:'try'(case Params of
[] ->
glupbit@auth:generate_token(erlang:element(3, Client));
_ ->
glupbit@auth:generate_token_with_body(
erlang:element(3, Client),
Params
)
end, fun(Token) ->
Req@1 = begin
_pipe = Req,
_pipe@1 = with_auth(_pipe, Token),
_pipe@2 = gleam@http@request:set_header(
_pipe@1,
<<"content-type"/utf8>>,
<<"application/json; charset=utf-8"/utf8>>
),
gleam@http@request:set_body(
_pipe@2,
gleam@json:to_string(Body)
)
end,
execute(erlang:element(2, Client), Req@1, Decoder)
end) end
).
-file("src/glupbit/client.gleam", 132).
?DOC(" Execute an authenticated DELETE request.\n").
-spec auth_delete(
auth_client(),
binary(),
list({binary(), binary()}),
gleam@dynamic@decode:decoder(AADV)
) -> {ok, glupbit@types:api_response(AADV)} | {error, glupbit@types:api_error()}.
auth_delete(Client, Path, Query, Decoder) ->
gleam@result:'try'(
build_request(erlang:element(2, Client), delete, Path, Query),
fun(Req) ->
gleam@result:'try'(
auth_token_for_query(erlang:element(3, Client), Query),
fun(Token) ->
execute(
erlang:element(2, Client),
begin
_pipe = Req,
with_auth(_pipe, Token)
end,
Decoder
)
end
)
end
).
-file("src/glupbit/client.gleam", 68).
-spec default_config() -> client_config().
default_config() ->
{client_config,
<<"https://api.upbit.com"/utf8>>,
begin
_pipe = gleam@httpc:configure(),
gleam@httpc:timeout(_pipe, 30000)
end}.
-file("src/glupbit/client.gleam", 49).
?DOC(" Create a public client with default settings.\n").
-spec new_public() -> public_client().
new_public() ->
{public_client, default_config()}.
-file("src/glupbit/client.gleam", 54).
?DOC(" Create an authenticated client.\n").
-spec new_auth(glupbit@auth:credentials()) -> auth_client().
new_auth(Credentials) ->
{auth_client, default_config(), Credentials}.
-file("src/glupbit/client.gleam", 59).
?DOC(" Create an authenticated client from environment variables.\n").
-spec new_auth_from_env() -> {ok, auth_client()} | {error, binary()}.
new_auth_from_env() ->
_pipe = glupbit@auth:credentials_from_env(),
gleam@result:map(_pipe, fun new_auth/1).