-
-
Notifications
You must be signed in to change notification settings - Fork 402
Bootstrap practice exercises #774
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
Changes from all commits
ea5b6b4
f5ec820
93c9750
ac95c3c
682f3fc
35ecec0
ddbec2b
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,277 @@ | ||
# Generate all files required for a practice exercise. | ||
# File content is filled as much as possible, but some, including tests, need manual input. | ||
# | ||
# Run the following command from the root of the repo: | ||
# $ elixir bin/boostrap_practice_exercise.exs complex-numbers | ||
# Pass the name of the exercise (e. g., "complex-numbers") as an argument | ||
|
||
Mix.install([ | ||
{:jason, "~> 1.2"}, | ||
{:toml, "~> 0.6"} | ||
]) | ||
|
||
defmodule Generate do | ||
def explore_properties(%{"cases" => cases}) when is_list(cases) do | ||
Enum.map(cases, &Generate.explore_properties/1) | ||
|> Enum.reduce( | ||
%{}, | ||
&Map.merge(&1, &2, fn _prop, p1, p2 -> %{p1 | error: p1.error or p2.error} end) | ||
) | ||
end | ||
|
||
def explore_properties(%{"property" => property, "input" => input, "expected" => expected}), | ||
do: %{ | ||
property => %{ | ||
name: Macro.underscore(property), | ||
variables: | ||
if match?(%{}, input) do | ||
Enum.map(input, fn | ||
{var, %{} = val} -> {var, Enum.map(val, fn {v, _} -> v end)} | ||
{var, _} -> {var, nil} | ||
end) | ||
else | ||
[{"input", nil}] | ||
end, | ||
error: match?(%{"error" => _err}, expected) | ||
} | ||
} | ||
|
||
def explore_properties(_data), do: %{} | ||
|
||
def print_property(%{name: name, variables: variables, error: error}) do | ||
return_type = if error, do: "{:ok, TODO.t()} | {:error, String.t()}", else: "TODO.t()" | ||
|
||
variable_types = | ||
Enum.map_join(variables, ", ", fn | ||
{var, nil} -> | ||
"#{var} :: TODO.t()" | ||
|
||
{var, sub_vars} -> | ||
"#{var} :: %{#{Enum.map_join(sub_vars, ", ", &(&1 <> " : TODO.t()"))}}" | ||
end) | ||
|
||
variable_list = Enum.map_join(variables, ", ", fn {var, _} -> var end) | ||
|
||
""" | ||
@doc \"\"\" | ||
TODO: add function description and replace types in @spec | ||
\"\"\" | ||
@spec #{name}(#{variable_types}) :: #{return_type} | ||
def #{name}(#{variable_list}) do | ||
end | ||
""" | ||
end | ||
|
||
def print_comments(comments, do_not_print) do | ||
comments | ||
|> Enum.reject(fn {field, _value} -> field in do_not_print end) | ||
|> Enum.map_join("\n", fn | ||
{"comments", values} when is_list(values) -> | ||
"# #{Enum.map_join(values, "\n# ", &String.trim/1)}" | ||
|
||
{field, values} when is_list(values) -> | ||
"#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect/1)}" | ||
|
||
{field, value} -> | ||
"#\n# -- #{field} --\n# #{inspect(value)}" | ||
end) | ||
end | ||
|
||
def print_input(%{} = input), | ||
do: Enum.map_join(input, "\n", fn {variable, value} -> "#{variable} = #{inspect(value)}" end) | ||
|
||
def print_input(input), do: "input = #{inspect(input)}" | ||
|
||
def print_expected(%{"error" => err}, _error), do: "{:error, #{inspect(err)}}" | ||
def print_expected(expected, true), do: "{:ok, #{inspect(expected)}}" | ||
def print_expected(expected, false), do: inspect(expected) | ||
|
||
def print_test_case( | ||
%{"description" => description, "cases" => sub_cases} = category, | ||
properties, | ||
module | ||
) do | ||
""" | ||
describe \"#{description}\" do | ||
#{Generate.print_comments(category, ["description", "cases"])} | ||
#{Enum.map_join(sub_cases, "\n\n", &Generate.print_test_case(&1, properties, module))} | ||
end | ||
""" | ||
end | ||
|
||
def print_test_case( | ||
%{ | ||
"description" => description, | ||
"property" => property, | ||
"input" => input, | ||
"expected" => expected | ||
} = test, | ||
properties, | ||
module | ||
) do | ||
%{name: name, variables: variables, error: error} = properties[property] | ||
variable_list = Enum.map_join(variables, ", ", fn {var, _} -> var end) | ||
|
||
""" | ||
@tag :pending | ||
test \"#{description}\" do | ||
#{Generate.print_comments(test, ["description", "property", "input", "expected", "uuid"])} | ||
#{print_input(input)} | ||
output = #{module}.#{name}(#{variable_list}) | ||
expected = #{print_expected(expected, error)} | ||
|
||
assert output == expected | ||
end | ||
""" | ||
end | ||
end | ||
|
||
[exercise] = System.argv() | ||
|
||
exercise_snake_case = String.replace(exercise, "-", "_") | ||
|
||
module = | ||
exercise | ||
|> String.split("-") | ||
|> Enum.map_join("", &String.capitalize/1) | ||
|
||
## Step 1: create folder structure | ||
|
||
Mix.Generator.create_directory("exercises/practice/#{exercise}") | ||
Mix.Generator.create_directory("exercises/practice/#{exercise}/.docs") | ||
Mix.Generator.create_directory("exercises/practice/#{exercise}/.meta") | ||
Mix.Generator.create_directory("exercises/practice/#{exercise}/lib") | ||
Mix.Generator.create_directory("exercises/practice/#{exercise}/test") | ||
|
||
## Step 2: add common files | ||
|
||
# .formatter.exs | ||
format = """ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] | ||
""" | ||
|
||
Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format) | ||
|
||
# mix.exs | ||
mix = """ | ||
defmodule #{module}.MixProject do | ||
use Mix.Project | ||
|
||
def project do | ||
[ | ||
app: :#{exercise_snake_case}, | ||
version: "0.1.0", | ||
# elixir: "~> 1.8", | ||
start_permanent: Mix.env() == :prod, | ||
deps: deps() | ||
] | ||
end | ||
|
||
# Run "mix help compile.app" to learn about applications. | ||
def application do | ||
[ | ||
extra_applications: [:logger] | ||
] | ||
end | ||
|
||
# Run "mix help deps" to learn about dependencies. | ||
defp deps do | ||
[ | ||
# {:dep_from_hexpm, "~> 0.3.0"}, | ||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} | ||
] | ||
end | ||
end | ||
""" | ||
Comment on lines
+160
to
+188
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. Minor: if we can't run mix format on this file, can we indent here 2 spaces for this binding? 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.
|
||
|
||
Mix.Generator.create_file("exercises/practice/#{exercise}/mix.exs", mix) | ||
|
||
# test/test_helper.exs | ||
test_helper = """ | ||
ExUnit.start() | ||
ExUnit.configure(exclude: :pending, trace: true) | ||
""" | ||
Comment on lines
+194
to
+196
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. Minor, same indent as mentioned above 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. Here the indentation looks good already 🤔 meaning: level 0, there's no indentation. |
||
|
||
Mix.Generator.create_file("exercises/practice/#{exercise}/test/test_helper.exs", test_helper) | ||
|
||
## Step 3: write files that depend on problem specifications | ||
|
||
url = | ||
'https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise}' | ||
|
||
:inets.start() | ||
:ssl.start() | ||
|
||
# .docs/instructions.md | ||
{:ok, {_status, _header, description}} = | ||
:httpc.request(:get, {url ++ '/description.md', []}, [], []) | ||
|
||
Mix.Generator.create_file("exercises/practice/#{exercise}/.docs/instructions.md", description) | ||
|
||
# .meta/config.json | ||
{:ok, {_status, _header, metadata}} = :httpc.request(:get, {url ++ '/metadata.toml', []}, [], []) | ||
|
||
metadata = | ||
metadata | ||
|> to_string | ||
|> Toml.decode!() | ||
|
||
config = %{ | ||
authors: [], | ||
contributors: [], | ||
files: %{ | ||
solution: ["lib/#{exercise_snake_case}.ex"], | ||
test: ["test/#{exercise_snake_case}_test.exs"], | ||
example: [".meta/example.ex"] | ||
} | ||
} | ||
|
||
config = | ||
Map.merge(metadata, config) | ||
|> Jason.encode!(pretty: true) | ||
|
||
Mix.Generator.create_file("exercises/practice/#{exercise}/.meta/config.json", config) | ||
IO.puts("Don't forget to add your name and the names of contributors") | ||
|
||
# tests and lib files | ||
{:ok, {_status, _header, data}} = | ||
:httpc.request(:get, {url ++ '/canonical-data.json', []}, [], []) | ||
|
||
data = Jason.decode!(data) | ||
|
||
properties = Generate.explore_properties(data) | ||
|
||
# Generating lib file | ||
lib_file = """ | ||
defmodule #{module} do | ||
|
||
#{properties |> Map.values() |> Enum.map_join("\n\n", &Generate.print_property/1)} | ||
|
||
end | ||
""" | ||
|
||
path = "exercises/practice/#{exercise}/lib/#{exercise_snake_case}.ex" | ||
Mix.Generator.create_file(path, lib_file) | ||
|
||
Mix.Generator.copy_file(path, "exercises/practice/#{exercise}/.meta/example.ex") | ||
|
||
# Generating test file | ||
test_file = | ||
""" | ||
defmodule #{module}Test do | ||
use ExUnit.Case | ||
|
||
#{Generate.print_comments(data, ["cases", "exercise"])} | ||
#{Enum.map_join(data["cases"], "\n\n", &Generate.print_test_case(&1, properties, module))} | ||
end | ||
""" | ||
|> String.replace("@tag", "# @tag", global: false) | ||
|
||
path = "exercises/practice/#{exercise}/test/#{exercise_snake_case}_test.exs" | ||
Mix.Generator.create_file(path, test_file) | ||
|
||
# mix format all files | ||
Mix.Tasks.Format.run(["exercises/practice/#{exercise}/**/*.{ex,exs}"]) |
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.
I had no idea about this module 😍 awesome
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.
It is quite nice. And actually it wouldn't be much more work to make this script into a
Mix.Task
. It would be kind of cool:mix ex.practice.gen darts
.