-
Notifications
You must be signed in to change notification settings - Fork 58
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
Changes from all commits
85a72d6
ee0ce5f
75ffd05
c0998d8
0072659
f54570d
b3c38d9
ca7814d
a6136cc
73c34b1
a574bbf
cb54d33
b0ff47d
786bcce
5d7b401
f20510f
059a738
8201c50
e4f5556
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
@supported_languages ["JavaScript", "SCSS", "Markdown", "Elixir"] | ||
@github_url "https://api.github.com/gists/" | ||
|
||
@moduledoc """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a separate |
||
{: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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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"}} |
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 |
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"]} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add newline above.