/src/hex_api.erl

https://github.com/hexpm/hex_core · Erlang · 163 lines · 98 code · 27 blank · 38 comment · 0 complexity · a539a368729b43fb3bf1716d4486158d MD5 · raw file

  1. %% @doc
  2. %% Hex HTTP API
  3. -module(hex_api).
  4. -export([
  5. delete/2,
  6. get/2,
  7. post/3,
  8. put/3,
  9. encode_query_string/1,
  10. build_repository_path/2,
  11. build_organization_path/2,
  12. join_path_segments/1
  13. ]).
  14. -define(ERL_CONTENT_TYPE, <<"application/vnd.hex+erlang">>).
  15. -export_type([response/0]).
  16. -type response() :: {ok, {hex_http:status(), hex_http:headers(), body() | nil}} | {error, term()}.
  17. -type body() :: [body()] | #{binary() => body() | binary()}.
  18. %% @private
  19. get(Config, Path) ->
  20. request(Config, get, Path, undefined).
  21. %% @private
  22. post(Config, Path, Body) ->
  23. request(Config, post, Path, encode_body(Body)).
  24. %% @private
  25. put(Config, Path, Body) ->
  26. request(Config, put, Path, encode_body(Body)).
  27. %% @private
  28. delete(Config, Path) ->
  29. request(Config, delete, Path, undefined).
  30. %% @private
  31. encode_query_string(List) ->
  32. Pairs = lists:map(fun ({K, V}) -> {to_list(K), to_list(V)} end, List),
  33. list_to_binary(compose_query(Pairs)).
  34. %% OTP 21+
  35. %% @private
  36. -ifdef (OTP_RELEASE).
  37. compose_query(Pairs) ->
  38. uri_string:compose_query(Pairs).
  39. -else.
  40. compose_query(Pairs) ->
  41. String = join("&", lists:map(fun ({K, V}) -> K ++ "=" ++ V end, Pairs)),
  42. http_uri:encode(String).
  43. -endif.
  44. %% @private
  45. build_repository_path(#{api_repository := Repo}, Path) when is_binary(Repo) ->
  46. ["repos", Repo | Path];
  47. build_repository_path(#{api_repository := undefined}, Path) ->
  48. Path.
  49. %% @private
  50. build_organization_path(#{api_organization := Org}, Path) when is_binary(Org) ->
  51. ["orgs", Org | Path];
  52. build_organization_path(#{api_organization := undefined}, Path) ->
  53. Path.
  54. %% @private
  55. join_path_segments(Segments) ->
  56. iolist_to_binary(recompose(Segments)).
  57. %% OTP 21+
  58. %% @private
  59. -ifdef (OTP_RELEASE).
  60. recompose(Segments) ->
  61. Concatenated = join(<<"/">>, Segments),
  62. %% uri_string:recompose/1 accepts path segments as a list,
  63. %% both strings and binaries
  64. uri_string:recompose(#{path => Concatenated}).
  65. -else.
  66. recompose(Segments) ->
  67. join(<<"/">>, lists:map(fun encode_segment/1, Segments)).
  68. %% @private
  69. encode_segment(Binary) when is_binary(Binary) ->
  70. encode_segment(binary_to_list(Binary));
  71. encode_segment(String) when is_list(String) ->
  72. http_uri:encode(String).
  73. -endif.
  74. %%====================================================================
  75. %% Internal functions
  76. %%====================================================================
  77. request(Config, Method, PathSegments, Body) when is_list(PathSegments) ->
  78. Path = join_path_segments(PathSegments),
  79. request(Config, Method, Path, Body);
  80. request(Config, Method, Path, Body) when is_binary(Path) and is_map(Config) ->
  81. DefaultHeaders = make_headers(Config),
  82. ReqHeaders = maps:merge(maps:get(http_headers, Config, #{}), DefaultHeaders),
  83. ReqHeaders2 = put_new(<<"accept">>, ?ERL_CONTENT_TYPE, ReqHeaders),
  84. case hex_http:request(Config, Method, build_url(Path, Config), ReqHeaders2, Body) of
  85. {ok, {Status, RespHeaders, RespBody}} ->
  86. ContentType = maps:get(<<"content-type">>, RespHeaders, <<"">>),
  87. case binary:match(ContentType, ?ERL_CONTENT_TYPE) of
  88. {_, _} ->
  89. {ok, {Status, RespHeaders, binary_to_term(RespBody)}};
  90. nomatch ->
  91. {ok, {Status, RespHeaders, nil}}
  92. end;
  93. Other ->
  94. Other
  95. end.
  96. %% TODO: not needed after exdoc is fixed
  97. %% @private
  98. build_url(Path, #{api_url := URI}) ->
  99. <<URI/binary, "/", Path/binary>>.
  100. %% TODO: not needed after exdoc is fixed
  101. %% @private
  102. encode_body({_ContentType, _Body} = Body) ->
  103. Body;
  104. encode_body(Body) ->
  105. {binary_to_list(?ERL_CONTENT_TYPE), term_to_binary(Body)}.
  106. %% TODO: not needed after exdoc is fixed
  107. %% @private
  108. %% TODO: copy-pasted from hex_repo
  109. make_headers(Config) ->
  110. maps:fold(fun set_header/3, #{}, Config).
  111. %% TODO: not needed after exdoc is fixed
  112. %% @private
  113. set_header(api_key, Token, Headers) when is_binary(Token) -> maps:put(<<"authorization">>, Token, Headers);
  114. set_header(_, _, Headers) -> Headers.
  115. %% TODO: not needed after exdoc is fixed
  116. %% @private
  117. put_new(Key, Value, Map) ->
  118. case maps:find(Key, Map) of
  119. {ok, _} -> Map;
  120. error -> maps:put(Key, Value, Map)
  121. end.
  122. %% TODO: not needed after exdoc is fixed
  123. %% @private
  124. %% https://github.com/erlang/otp/blob/OTP-20.3/lib/stdlib/src/lists.erl#L1449:L1453
  125. join(_Sep, []) -> [];
  126. join(Sep, [H|T]) -> [H|join_prepend(Sep, T)].
  127. %% TODO: not needed after exdoc is fixed
  128. %% @private
  129. join_prepend(_Sep, []) -> [];
  130. join_prepend(Sep, [H|T]) -> [Sep,H|join_prepend(Sep,T)].
  131. %% TODO: not needed after exdoc is fixed
  132. %% @private
  133. to_list(A) when is_atom(A) -> atom_to_list(A);
  134. to_list(B) when is_binary(B) -> unicode:characters_to_list(B);
  135. to_list(I) when is_integer(I) -> integer_to_list(I);
  136. to_list(Str) -> unicode:characters_to_list(Str).