-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 = <>, 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).