Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added first very basic widget downloader using gist github api #38

Closed
Closed
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
100 changes: 100 additions & 0 deletions lib/mix/tasks/kitto.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
defmodule Mix.Tasks.Kitto.Install do
use Mix.Task
@shortdoc "Install community Widget/Job from a Github Gist"
Copy link
Member

Choose a reason for hiding this comment

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

Please add newline above.

@supported_languages ["JavaScript", "SCSS", "Markdown", "Elixir"]
@github_url "https://api.github.com/gists/"

@moduledoc """
Copy link
Member

Choose a reason for hiding this comment

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

Can you place the moduledoc at the top inside the module?

Installs community Widget/Job from a Github Gist

mix kitto.install --widget test_widget --gist JanStevens/0209a4a80cee782e5cdbe11a1e9bc393
mix kitto.install --gist 0209a4a80cee782e5cdbe11a1e9bc393
Copy link
Member

Choose a reason for hiding this comment

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

Why is the second evocation missing the owner of the gist. I expected it to also have JanStevens/0209a4a80cee782e5cdbe11a1e9bc393

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we only need the last part of the gist url, you could even copy past the full url and it would still work

Copy link
Member

Choose a reason for hiding this comment

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

I didn't know that. I tried it and it works! 🌈


## Options

* `--widget` - specifies the widget name that will be used as directory name
in the widgets directory. By default we use the js filename as directory

* `--gist` - The gist to download from, specified as `Username/Gist` or `Gist`

"""
def run(args) do
Copy link
Member

Choose a reason for hiding this comment

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

Add a separate @doc for this function.

{:ok, _started} = Application.ensure_all_started(:httpoison)
{opts, _parsed, _} = OptionParser.parse(args, strict: [widget: :string, gist: :string])
opts_map = Enum.into(opts, %{})
process(opts_map)
end

defp process(%{gist: gist, widget: widget}) do
files = gist |> String.split("/")
|> build_gist_url
|> download_gist
|> Map.get(:files)
|> Enum.map(&extract_file_properties/1)
|> Enum.filter(&supported_file_type?/1)

widget_dir = widget || find_widget_filename(files)
files
|> Enum.map(&(determine_file_location(&1, widget_dir)))
|> Enum.each(&write_file/1)
end

defp process(%{gist: gist}), do: process(%{gist: gist, widget: nil})

defp process(_) do
Mix.shell.error "Unsupported arguments"
end

defp write_file(file) do
Mix.Generator.create_directory(file.path)
filename = Path.join([file.path, file.filename])
Mix.Generator.create_file(filename, file.content)
end

defp determine_file_location(_file, widget_name) when widget_name == nil do
Mix.shell.error "Please specify a widget directory using the --widget flag"
Mix.raise "Installation failed"
end

# Elixir files we place in the jobs dir
defp determine_file_location(%{language: "Elixir"} = file, _) do
Copy link
Member

Choose a reason for hiding this comment

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

I think that we can make the assumption that .exs files are for jobs and .ex belong to lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oke I'll look into it, do we also want to namespace the ex files in lib?

Copy link
Member

Choose a reason for hiding this comment

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

No need to namespace them.

Map.put(file, :path, "jobs")
end

# Other files all go into the widget dir
defp determine_file_location(file, widget_name) do
Map.put(file, :path, Path.join(["widgets", widget_name]))
end

defp find_widget_filename(files) do
files
|> Enum.filter(fn(file) -> file.language == "JavaScript" end)
|> List.first
|> extract_widget_dir
end

defp extract_widget_dir(%{filename: filename}) do
filename |> String.replace(~r/\.js$/, "")
end

defp extract_widget_dir(nil), do: nil

defp supported_file_type?(file) do
Enum.member?(@supported_languages, file.language)
end

def extract_file_properties({_filename, file}), do: file

defp download_gist(url), do: url |> HTTPoison.get! |> process_response

defp build_gist_url(gist_url) when length(gist_url) == 1, do: @github_url <> hd(gist_url)
defp build_gist_url([_ | gist_url]), do: build_gist_url(gist_url)

defp process_response(%HTTPoison.Response{status_code: 200, body: body}), do: body |> Poison.decode!(keys: :atoms)
defp process_response(%HTTPoison.Response{status_code: code, body: body}) do
decoded_body = body |> Poison.decode!(keys: :atoms)
Mix.shell.error "Could not fetch the gist from GitHub: " <>
"#{code}: #{decoded_body.message}"
Mix.raise "Installation failed"
end
end
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ defmodule Kitto.Mixfile do
{:credo, "~> 0.5", only: [:dev, :test]},
{:mock, "~> 0.2", only: :test},
{:excoveralls, "~> 0.5", only: :test},
{:inch_ex, ">= 0.0.0", only: :docs}]

{:inch_ex, ">= 0.0.0", only: :docs},
{:httpoison, "~> 0.10.0", only: [:dev, :test]}
]
end

defp description, do: "Framework for creating interactive dashboards"
Expand Down
23 changes: 12 additions & 11 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []},
"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
"certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []},
"combine": {:hex, :combine, "0.7.0"},
"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
"credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]},
"earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.13.2", "1059a588d2ad3ffab25a0b85c58abf08e437d3e7a9124ac255e1d15cec68ab79", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"excoveralls": {:hex, :excoveralls, "0.5.6", "35a903f6f78619ee7f951448dddfbef094b3a0d8581657afaf66465bc930468e", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]},
"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
"gettext": {:hex, :gettext, "0.11.0"},
"hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]},
"hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
"httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]},
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
"inch_ex": {:hex, :inch_ex, "0.5.5", "b63f57e281467bd3456461525fdbc9e158c8edbe603da6e3e4671befde796a3d", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]},
"jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []},
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
"jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []},
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:make, :rebar], []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"mock": {:hex, :mock, "0.2.0", "5991877be6bb514b647dbd6f4869bc12bd7f2829df16e86c98d6108f966d34d7", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]},
"plug": {:hex, :plug, "1.2.0", "496bef96634a49d7803ab2671482f0c5ce9ce0b7b9bc25bc0ae8e09859dd2004", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
"plug": {:hex, :plug, "1.2.2", "cfbda521b54c92ab8ddffb173fbaabed8d8fc94bec07cd9bb58a84c1c501b0bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
"poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []},
"ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []},
"timex": {:hex, :timex, "2.1.4"},
"tzdata": {:hex, :tzdata, "0.5.7"}}
36 changes: 36 additions & 0 deletions test/file_assertion_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Kitto.FileAssertionHelper do
@moduledoc """
Original code from the Phoenix Framework
https://github.com/phoenixframework/phoenix/blob/master/installer/test/mix_helper.exs
"""
import ExUnit.Assertions

def assert_file(file) do
assert File.regular?(file), "Expected #{file} to exist, but does not"
end

def assert_file(file, match) do
cond do
is_list(match) ->
assert_file file, &(Enum.each(match, fn(m) -> assert &1 =~ m end))
is_binary(match) or Regex.regex?(match) ->
assert_file file, &(assert &1 =~ match)
is_function(match, 1) ->
assert_file(file)
match.(File.read!(file))
end
end

def refute_file(file) do
refute File.regular?(file), "Expected #{file} to not exist, but it does"
end

def tmp_path, do: System.tmp_dir()

def in_tmp(which, function) do
path = Path.join(tmp_path(), which)
File.rm_rf! path
File.mkdir_p! path
File.cd! path, function
end
end
115 changes: 115 additions & 0 deletions test/mix/tasks/kitto.install_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
defmodule Mix.Tasks.Kitto.InstallTest do
use ExUnit.Case, async: true
import Mock
import Kitto.FileAssertionHelper

@job_gist_response %{ files:
%{"job.ex" => %{filename: "job.ex", language: "Elixir", content: "job"}}
}

@css_gist_response %{ files:
%{"number_job.scss" => %{filename: "number_job.scss", language: "SCSS", content: "style"}}
}

@gist_response %{ files:
%{
"README.md" => %{filename: "README.md", language: "Markdown", content: "Title"},
"number_job.ex" => %{filename: "number_job.ex", language: "Elixir", content: "job"},
"number.scss" => %{filename: "number.scss", language: "SCSS", content: "style"},
"number.js" => %{filename: "number.js", language: "JavaScript", content: "js"}
}
}

setup do
Mix.Task.clear
:ok
end

test "fails when `--gist` is not provided" do
Mix.Tasks.Kitto.Install.run(["--widget", "numbers"])

assert_received {:mix_shell, :error, ["Unsupported arguments"]}
Copy link
Member

Choose a reason for hiding this comment

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

Please leave a newline above assertions.

end

test "fails when the gist is not found" do
with_mock HTTPoison, [get!: mock_gist_with(404, %{message: "Not Found"})] do

assert_raise Mix.Error, fn ->
Mix.Tasks.Kitto.Install.run(["--widget", "numbers", "--gist", "0209a4a80cee78"])

assert called HTTPoison.get!("https://api.github.com/gists/0209a4a80cee78")
end

assert_received {:mix_shell, :error, ["Could not fetch the gist from GitHub: 404: Not Found"]}
end
end

test "fails when no widget directory is specified or found" do
with_mock HTTPoison, [get!: mock_gist_with(200, @css_gist_response)] do

assert_raise Mix.Error, fn ->
Mix.Tasks.Kitto.Install.run(["--gist", "0209a4a80cee78"])

assert called HTTPoison.get!("https://api.github.com/gists/0209a4a80cee78")
end
end

assert_received {:mix_shell, :error, ["Please specify a widget directory using the --widget flag"]}
end

test "places all the files in the correct locations" do
in_tmp "installs widgets and jobs", fn ->
with_mock HTTPoison, [get!: mock_gist_with(200, @gist_response)] do
Mix.Tasks.Kitto.Install.run(["--gist", "0209a4a80cee78"])

assert_file "widgets/number/number.js", fn contents ->
assert contents =~ "js"
end

assert_file "widgets/number/number.scss", fn contents ->
assert contents =~ "style"
end

assert_file "widgets/number/README.md", fn contents ->
assert contents =~ "Title"
end
refute_file "widgets/number/number_job.ex"

assert_file "jobs/number_job.ex", fn contents ->
assert contents =~ "job"
end
end
end
end

test "uses the widget overwrite for the widget directory" do
in_tmp "installs widgets and jobs using overwrite", fn ->
with_mock HTTPoison, [get!: mock_gist_with(200, @gist_response)] do
Mix.Tasks.Kitto.Install.run(["--gist", "0209a4a80cee78", "--widget", "overwrite"])

assert_file "widgets/overwrite/number.js", fn contents ->
assert contents =~ "js"
end

assert_file "widgets/overwrite/number.scss", fn contents ->
assert contents =~ "style"
end

assert_file "widgets/overwrite/README.md", fn contents ->
assert contents =~ "Title"
end
refute_file "widgets/overwrite/number_job.ex"

assert_file "jobs/number_job.ex", fn contents ->
assert contents =~ "job"
end
end
end
end

def mock_gist_with(status_code, body) do
fn (_url) ->
%HTTPoison.Response{status_code: status_code, body: Poison.encode!(body)}
end
end
end
2 changes: 2 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ defmodule Kitto.TestHelper do
end
end

Mix.shell(Mix.Shell.Process)
ExUnit.configure(exclude: [pending: true])
ExUnit.start()
Code.require_file("file_assertion_helper.exs", __DIR__)