Skip to content
Open
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
Binary file modified elixir/github_repo_cloner
Binary file not shown.
8 changes: 5 additions & 3 deletions elixir/lib/cli.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
defmodule GithubRepoCloner.CLI do
alias GithubRepoCloner.Cloner
alias GithubRepoCloner.PageIterator

def main(args \\ []) do
args
|> List.first
|> GithubRepoCloner.Cloner.clone
username = List.first(args)
PageIterator.repeat(&Cloner.clone_page/1, %{username: username, page: 1})
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to rethink the cloner logic here. I could have simply tossed the existing logic in a loop that runs until we reach an empty page. That increases the cyclomatic complexity, or in other words the number of scenarios the function handles. I decided it was better to isolate the logic for cloning one page of repos (Cloner) and extract the logic for handling pagination elsewhere (PageIterator).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name should change to clone_pages ?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might wrap this in a new Cloner.clone_pages function. CLI will use Cloner.clone_pages.

end
end
19 changes: 11 additions & 8 deletions elixir/lib/cloner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ defmodule GithubRepoCloner.Cloner do
@http_client Application.compile_env(:github_repo_cloner, :http_client)
@system Application.compile_env(:github_repo_cloner, :system)

def clone(nil), do: true
def clone_page(%{username: nil}), do: {:error, "No repositories found"}
def clone_page(%{page: nil}), do: {:error, "No repositories found"}

def clone(username) do
def clone_page(%{username: username, page: page}) do
username
|> request_repo_info
|> request_repo_info(page)
|> parse_response
|> clone_repos(username)
end

def clone, do: true

defp request_repo_info(username) do
@http_client.get("https://api.github.com/users/#{username}/repos")
defp request_repo_info(username, page) do
@http_client.get("https://api.github.com/users/#{username}/repos?page=#{page}")
end

defp parse_response({:ok, %Tesla.Env{status: 200, body: body}}) do
Expand All @@ -30,7 +29,11 @@ defmodule GithubRepoCloner.Cloner do
|> run_command
end

defp run_command("") do
{:error, "No repositories found"}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return an error tuple if the command is not run. This includes scenarios where no repos are found for the username or page and if the username is invalid.

end

defp run_command(command) do
@system.cmd("sh", ["-c", command])
{:ok, @system.cmd("sh", ["-c", command])}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return an ok tuple if the clone command is run.

end
end
12 changes: 12 additions & 0 deletions elixir/lib/page_iterator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule GithubRepoCloner.PageIterator do
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules of the page iterator:

  1. Accept an anonymous function and an arguments map that will be passed to the function.
  2. The arguments map must include a page.
  3. Recursively call the function while it returns an ok tuple, increasing the page number on each call.
  4. Stop recursion whenever an error tuple is returned.
  5. Page iterator returns the last successful page reached in an ok tuple at the end.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write these in as a comment in this code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you felt the need to add it here just add it as a comment in the code.

Copy link
Copy Markdown
Collaborator

@MichaelDimmitt MichaelDimmitt Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least leave a comment describing the base condition that terminates the repeat iterator.

def repeat(function, arguments = %{page: page}) do
{status, _} = function.(%{arguments | page: page})
repeat(function, arguments, status)
end

def repeat(function, arguments = %{page: page}, :ok) do
repeat(function, %{arguments | page: page + 1})
end

def repeat(_function, %{page: page}, :error), do: {:ok, page}
end
34 changes: 18 additions & 16 deletions elixir/test/cloner_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ defmodule GithubRepoCloner.ClonerTest do
import Mox
alias GithubRepoCloner.Cloner

test "clone\1: valid username with repos" do
test "clone_page\1: valid username with repos" do
username = "murjax"
page = "2"
clone_url1 = "https://github.com/murjax/spring_engine.git"
clone_url2 = "https://github.com/murjax/burger_bot.git"
name1 = "spring_engine"
Expand All @@ -24,26 +25,26 @@ defmodule GithubRepoCloner.ClonerTest do
{:ok, %Tesla.Env{status: 200, body: serialized_repo_info}}
end)

result = Cloner.clone(username)
{:ok, result} = Cloner.clone_page(%{username: username, page: page})
assert ^result = expected_result
end

test "clone\1: valid username without repos" do
test "clone_page\1: valid username without repos" do
username = "murjax"
expected_result = {:ok, "Command executed: "}
page = "2"

expect(
GithubRepoCloner.Http.Mock, :get, fn _url ->
{:ok, %Tesla.Env{status: 200, body: "[]"}}
end)

result = Cloner.clone(username)
assert ^result = expected_result
{:error, result} = Cloner.clone_page(%{username: username, page: page})
assert ^result = "No repositories found"
end

test "clone\1: username not found" do
test "clone_page\1: username not found" do
username = "foo"
expected_result = {:ok, "Command executed: "}
page = "2"
response_body = %{"message" => "Not Found"}
{_, serialized_response_body} = Poison.encode(response_body)

Expand All @@ -52,17 +53,18 @@ defmodule GithubRepoCloner.ClonerTest do
{:ok, %Tesla.Env{status: 404, body: serialized_response_body}}
end)

result = Cloner.clone(username)
assert ^result = expected_result
{:error, result} = Cloner.clone_page(%{username: username, page: page})
assert ^result = "No repositories found"
end

test "clone\1: nil" do
result = Cloner.clone(nil)
assert ^result = true
test "clone_page/1: nil username" do
{:error, result} = Cloner.clone_page(%{username: nil, page: nil})
assert ^result = "No repositories found"
end

test "clone\0: valid noop" do
result = Cloner.clone
assert ^result = true
test "clone_page/1: nil page" do
username = "murjax"
{:error, result} = Cloner.clone_page(%{username: username, page: nil})
assert ^result = "No repositories found"
end
end
23 changes: 23 additions & 0 deletions elixir/test/page_iterator_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule GithubRepoCloner.PageIteratorTest do
use ExUnit.Case
alias GithubRepoCloner.PageIterator

test "repeat/3: Repeat function with next page until it returns an error" do
{:ok, page_reached} = PageIterator.repeat(&TestIteratable.runner/1, %{username: "murjax", page: 1})
assert ^page_reached = 3
end
end

defmodule TestIteratable do
def runner(%{username: username, page: 1}) do
{:ok, username}
end

def runner(%{username: username, page: 2}) do
{:ok, username}
end

def runner(%{username: _, page: 3}) do
{:error, "Finished"}
end
end