Skip to content
Permalink
Browse files

Allow annotation of GitHub readme's

  • Loading branch information...
Khaja Minhajuddin authored and minhajuddin committed Mar 18, 2019
1 parent 164a274 commit 39f8edd387ac8c84ad969816bf959acb622ad55b
Showing with 300 additions and 30 deletions.
  1. +62 −0 lib/awesome_toolbox.ex
  2. +24 −29 lib/awesome_toolbox/github.ex
  3. +60 −0 lib/http/http.ex
  4. +47 −0 lib/http/response.ex
  5. +2 −1 mix.exs
  6. +1 −0 mix.lock
  7. +104 −0 test/http_response_test.exs
@@ -1,2 +1,64 @@
defmodule AwesomeToolbox do
alias AwesomeToolbox.Github
require Logger

def annotate_with_time(repo_name) do
{us, {:ok, readme}} = :timer.tc(fn -> annotate_readme(repo_name) end)

IO.puts("""
# ------------------------------
# Time taken: #{us / 1000} milliseconds
# ------------------------------
# README
# ------------------------------
#{readme}
""")
end

# annotates a readme with star count
def annotate_readme(repo_name) do
# if we don't find a readme, just return the error
with {:ok, readme} <- Github.readme(repo_name) do
annotated_readme =
readme
# split the readme on newlines
|> String.split("\n")
# annotate each line
|> Enum.map(&annotate_line/1)
# join them back using newlines
|> Enum.join("\n")

{:ok, annotated_readme}
end
end

@github_repo_rx ~r/https:\/\/github.com\/(?<repo_name>[0-9a-zA-Z._-]+\/[0-9a-zA-Z._-]+)/
@star <<0x2B_50::utf8>>
defp annotate_line(line) do
# find the github repo link
with %{"repo_name" => repo_name} <- Regex.named_captures(@github_repo_rx, line),
# get the star count
{:repo_info, {:ok, %{"stargazers_count" => stargazers_count}}} <-
{:repo_info, Github.repo_info(repo_name)} do
# append it to the link
Regex.replace(
~r/(\(?https:\/\/github.com\/#{repo_name}\)?)/,
line,
"\\1 (#{stargazers_count} #{@star})"
)
else
# in case of an error log and return the unchanged line
{:error, error} ->
Logger.error("ANNOTATE_LINE_ERROR: #{inspect(error)}")
line

{:repo_info, err_resp} ->
Logger.error("ANNOTATE_LINE_ERROR: #{inspect(err_resp)}")

# if we don't find a github link, return the unchanged line
_ ->
line
end
end
end
@@ -1,36 +1,31 @@
defmodule AwesomeToolbox.Github do
def zen do
# open a new http connection to api.github.com and get a handle to the connection struct
{:ok, conn} = Mint.HTTP.connect(_scheme = :https, _host = "api.github.com", _port = 443)

# make a GET request to the `/zen` path using the above connection without any special headers
{:ok, conn, request_ref} =
Mint.HTTP.request(conn, _method = "GET", _path = "/zen", _headers = [])

# receive and parse the response
receive do
message ->
# send received message to `Mint` to be parsed
{:ok, conn, responses} = Mint.HTTP.stream(conn, message)

for response <- responses do
case response do
{:status, ^request_ref, status_code} ->
IO.puts("> Response status code #{status_code}")

{:headers, ^request_ref, headers} ->
IO.puts("> Response headers: #{inspect(headers)}")
require Logger

{:data, ^request_ref, data} ->
IO.puts("> Response body")
IO.puts(data)
def zen do
{:ok, resp} = HTTP.get("https://api.github.com/zen")
{:ok, resp.body}
end

{:done, ^request_ref} ->
IO.puts("> Response fully received")
end
end
def readme(repo_name) do
# make the request
with {:ok, %HTTP.Response{status_code: 200} = resp} <-
HTTP.get("https://api.github.com/repos/#{repo_name}/readme"),
# decode json
{:ok, json} <- Jason.decode(resp.body),
{:ok, readme} <- Base.decode64(json["content"], ignore: :whitespace) do
{:ok, readme}
else
err -> {:error, err}
end
end

Mint.HTTP.close(conn)
def repo_info(repo_name) do
with {:ok, %HTTP.Response{status_code: 200} = resp} <-
HTTP.get("https://api.github.com/repos/#{repo_name}"),
{:ok, repo_info} <- Jason.decode(resp.body) do
{:ok, repo_info}
else
err -> {:error, err}
end
end
end
@@ -0,0 +1,60 @@
defmodule HTTP do
require Logger
def get(uri, headers \\ []), do: request("GET", uri, headers)

def request(method, uri, headers \\ [], body \\ [])

def request(method, uri, headers, body)
when is_list(headers) and method in ~w[GET POST PUT PATCH DELETE OPTIONS HEAD] and
is_binary(uri) do
request(method, URI.parse(uri), headers, body)
end

def request(method, uri = %URI{}, headers, body)
when is_list(headers) and method in ~w[GET POST PUT PATCH DELETE OPTIONS HEAD] do
# TODO: spawn a new process per request so that the messages don't get intermingled

Logger.info("#{method} #{uri}")
{:ok, conn} = Mint.HTTP.connect(scheme_atom(uri.scheme), uri.host, uri.port)

{:ok, conn, request_ref} = Mint.HTTP.request(conn, method, path(uri), headers, body)

{:ok, http_response = %HTTP.Response{}} = recv_response(conn, request_ref)

Mint.HTTP.close(conn)
{:ok, http_response}
end

defp recv_response(conn, request_ref, http_response \\ %HTTP.Response{}) do
receive do
message ->
# send received message to `Mint` to be parsed
# TODO: handle :error
{:ok, conn, mint_messages} = Mint.HTTP.stream(conn, message)

case HTTP.Response.parse(mint_messages, request_ref, http_response) do
{:ok, http_response = %HTTP.Response{complete?: true}} ->
{:ok, http_response}

{:ok, http_response} ->
recv_response(conn, request_ref, http_response)

error ->
error
end
end
end

# copied over from Mint
defp path(uri) do
IO.iodata_to_binary([
if(uri.path, do: uri.path, else: ["/"]),
if(uri.query, do: ["?" | uri.query], else: []),
if(uri.fragment, do: ["#" | uri.fragment], else: [])
])
end

defp scheme_atom("https"), do: :https
defp scheme_atom("http"), do: :http
defp scheme_atom(_), do: throw(:invalid_scheme)
end
@@ -0,0 +1,47 @@
defmodule HTTP.Response do
defstruct status_code: nil, headers: nil, body: [], complete?: false

def parse(mint_messages, request_ref, http_response \\ %__MODULE__{})

def parse(
[{:status, request_ref, status_code} | mint_messages],
request_ref,
http_response = %HTTP.Response{}
) do
parse(mint_messages, request_ref, %{http_response | status_code: status_code})
end

def parse(
[{:headers, request_ref, headers} | mint_messages],
request_ref,
http_response = %HTTP.Response{}
) do
parse(mint_messages, request_ref, %{http_response | headers: headers})
end

def parse(
[{:data, request_ref, data} | mint_messages],
request_ref,
http_response = %HTTP.Response{}
) do
parse(mint_messages, request_ref, %{http_response | body: [data | http_response.body]})
end

def parse(
[{:done, request_ref}],
request_ref,
http_response = %HTTP.Response{}
) do
{:ok, %{http_response | body: Enum.reverse(http_response.body), complete?: true}}
end

def parse([{_, mint_request_ref, _} | _], request_ref, _)
when mint_request_ref != request_ref,
do: {:error, :invalid_ref}

def parse([{_, mint_request_ref} | _], request_ref, _)
when mint_request_ref != request_ref,
do: {:error, :invalid_ref}

def parse([], _request_ref, http_response), do: {:ok, http_response}
end
@@ -23,7 +23,8 @@ defmodule AwesomeToolbox.MixProject do
defp deps do
[
{:mint, ">= 0.0.0"},
{:castore, ">= 0.0.0"}
{:castore, ">= 0.0.0"},
{:jason, ">= 0.0.0"}
]
end
end
@@ -1,4 +1,5 @@
%{
"castore": {:hex, :castore, "0.1.1", "a8905530209152ddb74989fa2a5bd4fa3a2d3ff5d15ad12578caa7460d807c8b", [:mix], [], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"mint": {:hex, :mint, "0.1.0", "f5a82a909bb95a03222e0cfa5384c287f04c271fd2363e81323020cd33c70712", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm"},
}
@@ -0,0 +1,104 @@
defmodule AwesomeToolbox.HTTPResponseTest do
use ExUnit.Case

alias HTTP.Response
import Response

test "parses a status message" do
ref = make_ref()

assert {:ok, %Response{status_code: 200, complete?: false}} =
parse(
[
{:status, ref, 200}
],
ref
)
end

test "parses a headers message" do
ref = make_ref()

assert {:ok,
%Response{
status_code: 300,
complete?: false,
headers: [{"content-type", "text/html"}]
}} =
parse(
[
{:headers, ref, [{"content-type", "text/html"}]}
],
ref,
%Response{status_code: 300}
)
end

test "parses a data message" do
ref = make_ref()

assert {:ok, %Response{status_code: 200, complete?: false, body: ["iolist"]}} =
parse(
[
{:data, ref, "iolist"}
],
ref,
%Response{status_code: 200}
)
end

test "parses multiple data messages" do
ref = make_ref()

assert {:ok, %Response{body: ["iolist1", "iolist2"], complete?: true}} =
parse(
[
{:data, ref, "iolist1"},
{:data, ref, "iolist2"},
{:done, ref}
],
ref
)
end

test "parses multiple messages for a full response" do
ref = make_ref()

assert {:ok,
%Response{
status_code: 200,
headers: [{"content-type", "text/html"}],
body: ["iolist1", "iolist2", "iolist3"],
complete?: true
}} =
parse(
[
{:status, ref, 200},
{:headers, ref, [{"content-type", "text/html"}]},
{:data, ref, "iolist1"},
{:data, ref, "iolist2"},
{:data, ref, "iolist3"},
{:done, ref}
],
ref
)
end

test "returns an error for invalid ref" do
assert {:error, :invalid_ref} =
parse(
[
{:status, make_ref(), 200}
],
make_ref()
)

assert {:error, :invalid_ref} =
parse(
[
{:done, make_ref()}
],
make_ref()
)
end
end

0 comments on commit 39f8edd

Please sign in to comment.
You can’t perform that action at this time.