Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
import Config

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
1 change: 1 addition & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
1 change: 1 addition & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
1 change: 1 addition & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
42 changes: 42 additions & 0 deletions lib/aura/model/common.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Aura.Model.Common do
@moduledoc false

@spec prepare(m :: map()) :: map()
def prepare(m) when is_map(m) do
prepare_keys(m)
|> prepare_values()
|> Enum.reject(fn {_k, v} -> v == nil end)
|> Enum.map(fn {key, val} ->
{key, serialize(key, val)}
end)
end

defp prepare_keys(m) do
m
|> snake_case_keys()
|> atomize_keys()
end

def prepare_values(m) do
Enum.map(m, fn {key, val} ->
if val && String.ends_with?("#{key}", "_at"),
do: {key, DateTimeParser.parse_datetime!(val)},
else: {key, val}
end)
end

defp snake_case_keys(m) do
Enum.map(m, fn {key, val} ->
{ProperCase.snake_case(key), val}
end)
end

defp atomize_keys(m) do
Enum.map(m, fn {key, val} ->
key = String.to_atom(key)
{key, val}
end)
end

defp serialize(_k, v), do: v
end
24 changes: 24 additions & 0 deletions lib/aura/model/hex_repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Aura.Model.HexRepo do
@moduledoc false

import Aura.Model.Common

alias Aura.Model.HexRepo

defstruct [
:name,
:public,
:active,
:billing_active,
:inserted_at,
:updated_at
]

def build(m) when is_map(m) do
fields =
m
|> prepare()

struct(HexRepo, fields)
end
end
22 changes: 22 additions & 0 deletions lib/aura/repos.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Aura.Repos do
@moduledoc false

alias Aura.Model.HexRepo
alias Aura.Requester

def get_all_repos do
with {:ok, %{body: body}} <- Requester.request(:get, "/repos") do
results =
body
|> Enum.map(&HexRepo.build/1)

{:ok, results}
end
end

def get_repo(repo_name) when is_bitstring(repo_name) do
with {:ok, %{body: body}} <- Requester.request(:get, "/repos/#{repo_name}") do
{:ok, HexRepo.build(body)}
end
end
end
3 changes: 3 additions & 0 deletions lib/aura/util/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Aura.Cache do
@moduledoc false
end
128 changes: 128 additions & 0 deletions lib/aura/util/requester.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# SPDX-License-Identifier: Apache-2.0
defmodule Aura.Requester do
@moduledoc false

require Logger

@base_url "https://hex.pm/api"

def request(method, path, opts \\ []) do
qparams = opts[:qparams]
is_retry = opts[:is_retry]

opts =
opts
|> Keyword.delete(:qparams)
|> Keyword.delete(:is_retry)
|> Keyword.put(:headers, [user_agent_header()])

path =
@base_url
|> Path.join(path)
|> handle_qparams(qparams)

method
|> make_request(path, opts)
|> case do
{:ok,
%Req.Response{
status: 200,
body: _body,
headers: headers
}} = resp ->
respect_limits(headers)
resp

{:ok, %Req.Response{status: 204, body: _body, headers: headers}} = resp ->
respect_limits(headers)
resp

{:ok, %Req.Response{status: 429, headers: headers}} ->
if is_retry do
{:error, "Rate limit exceeded"}
else
respect_limits(headers)
new_opts = Keyword.put(opts, :is_retry, true)
request(method, path, new_opts)
end

other ->
{:error, other}
end
end

def get(path, opts \\ []), do: request(:get, path, opts)

def post(path, opts \\ []), do: request(:post, path, opts)

def put(path, opts \\ []), do: request(:put, path, opts)

def patch(path, opts \\ []), do: request(:patch, path, opts)

def delete(path, opts \\ []), do: request(:delete, path, opts)

defp user_agent_header do
config = Mix.Project.config()
app = config[:app] || :unknown
version = config[:version] || "0.0.0"
e_version = System.version()
mix_env = Mix.env()
user_agent = "#{app}/#{version} (Elixir/#{e_version}) (OTP/#{otp_version()})(#{mix_env})"
{"User-Agent", user_agent}
end

defp otp_version do
major = :erlang.system_info(:otp_release) |> List.to_string()
vsn_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"])

try do
{:ok, contents} = File.read(vsn_file)
String.split(contents, "\n", trim: true)
else
[full] -> full
_ -> major
catch
:error, _ -> major
end
end

defp handle_qparams(url, nil), do: url

defp handle_qparams(url, qparams) do
qparams =
qparams
|> Enum.filter(fn {_k, v} -> v end)
|> URI.encode_query()

url
|> URI.parse()
|> Map.put(:query, qparams)
|> URI.to_string()
end

defp make_request(:get, path, opts), do: Req.get(path, opts)
defp make_request(:post, path, opts), do: Req.post(path, opts)
defp make_request(:put, path, opts), do: Req.put(path, opts)
defp make_request(:delete, path, opts), do: Req.delete(path, opts)
defp make_request(:patch, path, opts), do: Req.patch(path, opts)

defp respect_limits(%{"x-ratelimit-remaining" => ["0"]} = headers) do
[reset] = headers["x-ratelimit-reset"] || ["0"]

unix_reset =
reset
|> String.to_integer()
|> DateTime.from_unix!()

wait_ms =
unix_reset
|> DateTime.diff(DateTime.now!("Etc/UTC"), :millisecond)
|> max(0)

Logger.warning("Hit a rate limit, waiting #{wait_ms} milliseconds and retrying.")

:timer.sleep(wait_ms)
end

defp respect_limits(_), do: :ok
end
11 changes: 9 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExFTP.MixProject do
defmodule Aura.MixProject do
use Mix.Project

@source_url "https://github.com/camatcode/aura"
Expand All @@ -11,6 +11,7 @@ defmodule ExFTP.MixProject do
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps(),
elixirc_paths: elixirc_paths(Mix.env()),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
Expand Down Expand Up @@ -83,7 +84,13 @@ defmodule ExFTP.MixProject do
{:ex_machina, "~> 2.8.0", only: :test},
{:faker, "~> 0.18.0", only: :test},
{:junit_formatter, "~> 3.1", only: [:test]},
{:req, "~> 0.5.10"}
{:req, "~> 0.5.10"},
{:cachex, "~> 4.0"},
{:proper_case, "~> 1.3"},
{:date_time_parser, "~> 1.2.0"}
]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
end
9 changes: 9 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cachex": {:hex, :cachex, "4.1.0", "f9a9123feac30df2b75260c9a74903c0bd7ec54f345f5f6f60c7830b81ead43a", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "77f9417a5549e424bdbfeea3f8060841384c39c0b5f563a8f62f1f2c2b6d2345"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"date_time_parser": {:hex, :date_time_parser, "1.2.0", "3d5a816b91967f51e0f94dcb16a34b2cb780f22cd48931779e81d72f7d3eadb1", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "0cf09ada9f42c0b3bfba02dc0ea2e4b4d2f543d9d2bf99b831a29e6b4a4160e5"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"},
"ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"},
"ex_license": {:hex, :ex_license, "0.1.1", "bbdaba704f861894da3ae80e4399984379f19de1cca4ecf7d799616c832b8821", [:mix], [], "hexpm", "dd95aaaba0c9c6f0e9be2db3e9fac0bf69784557e04f700f94e90629f5e24e1d"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
Expand All @@ -11,7 +15,9 @@
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
"kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
Expand All @@ -20,7 +26,10 @@
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"proper_case": {:hex, :proper_case, "1.3.1", "5f51cabd2d422a45f374c6061b7379191d585b5154456b371432d0fa7cb1ffda", [:mix], [], "hexpm", "6cc715550cc1895e61608060bbe67aef0d7c9cf55d7ddb013c6d7073036811dd"},
"quokka": {:hex, :quokka, "2.6.0", "a34ff876968fcef20594e349bd7d9a940c0ba85830d0efd8416ee8c0de4c430d", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "52ea62131cef122e2daddbb3b9f61b14d04adcb39316fe0dae03ca65eeec270b"},
"req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
}
16 changes: 16 additions & 0 deletions test/aura/model/aura/model/hex_repo_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Aura.Model.HexRepoTest do
use ExUnit.Case

import Aura.Factory

alias Aura.Model.HexRepo

@moduletag :capture_log

doctest HexRepo

test "build_many" do
list = build_list(100, :hex_repo)
assert Enum.count(list) == 100
end
end
17 changes: 17 additions & 0 deletions test/aura/repos_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Aura.ReposTest do
use ExUnit.Case

alias Aura.Repos

doctest Repos

test "get_all_repos/0" do
assert {:ok, [%{name: "hexpm"}]} = Repos.get_all_repos()
end

test "get_repo/1" do
assert {:ok, [hex]} = Repos.get_all_repos()
assert {:ok, returned} = Repos.get_repo(hex.name)
assert returned.name == hex.name
end
end
22 changes: 22 additions & 0 deletions test/support/factories/hex_repo_factory.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Aura.Factory.HexRepoFactory do
@moduledoc false

defmacro __using__(_opts) do
quote do
def hex_repo_factory(attrs) do
inserted_at = Faker.DateTime.backward(40)

%Aura.Model.HexRepo{
name: Faker.Internet.user_name(),
public: Enum.random([true, false]),
active: Enum.random([true, false]),
billing_active: Enum.random([true, false]),
inserted_at: inserted_at,
updated_at: Faker.DateTime.between(inserted_at, DateTime.now!("Etc/UTC"))
}
|> merge_attributes(attrs)
|> evaluate_lazy_attributes()
end
end
end
end
5 changes: 5 additions & 0 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Aura.Factory do
@moduledoc false
use ExMachina
use Aura.Factory.HexRepoFactory
end
8 changes: 7 additions & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
ExUnit.start()
ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter])
{:ok, _} = Application.ensure_all_started(:ex_machina)
ExUnit.start(timeout: 2 * 60 * 1000)

defmodule Helper do
@moduledoc false
end
Loading