diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0fbf4..fa9a664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.6.3 - 2024-01-28 + +- Added `mix gleam.toml [--replace]` task. It generates gleam.toml in Mix project. This might be useful for Gleam tooling support in Mix projects, such as Gleam LSP. + ## v0.6.2 - 2023-11-16 - Updated for Elixir v1.15 and Gleam v0.32 compatibility. Make sure to set diff --git a/lib/mix/tasks/gleam/deps/get.ex b/lib/mix/tasks/gleam/deps/get.ex index f3a3f27..f00e43a 100644 --- a/lib/mix/tasks/gleam/deps/get.ex +++ b/lib/mix/tasks/gleam/deps/get.ex @@ -13,8 +13,7 @@ defmodule Mix.Tasks.Gleam.Deps.Get do Include this task in your project's `mix.exs` with, e.g.: - def project do - [ + def project do [ aliases: ["deps.get": ["deps.get", "gleam.deps.get"]], ] end @@ -49,7 +48,7 @@ defmodule Mix.Tasks.Gleam.Deps.Get do |> Map.to_list() |> Enum.map(&Tuple.append(&1, only: [:dev, :test], runtime: false)) - deps = deps ++ dev_deps + deps = unmap(deps ++ dev_deps) # TODO use eex template File.write!("mix.exs", MixGleam.Config.render_mix(app, version, deps)) :cont @@ -70,4 +69,13 @@ defmodule Mix.Tasks.Gleam.Deps.Get do MixGleam.IO.debug_info("Deps.Get End") end + + defp unmap(x) do + cond do + is_map(x) -> x |> Map.to_list() |> unmap + is_list(x) -> x |> Enum.map(&unmap/1) + is_tuple(x) -> x |> Tuple.to_list() |> unmap |> List.to_tuple() + true -> x + end + end end diff --git a/lib/mix/tasks/gleam/toml.ex b/lib/mix/tasks/gleam/toml.ex new file mode 100644 index 0000000..9ca0261 --- /dev/null +++ b/lib/mix/tasks/gleam/toml.ex @@ -0,0 +1,93 @@ +defmodule Mix.Tasks.Gleam.Toml do + use Mix.Task + @shortdoc "Generates gleam.toml in Mix project." + @moduledoc """ + #{@shortdoc} + + This might be useful for Gleam tooling support in Mix projects, such as Gleam LSP. + + Print gleam.toml into stdout: + + mix gleam.toml + + Replace gleam.toml file: + + mix gleam.toml --replace + + Automate gleam.toml sync using mix.exs project aliases: + + aliases: [ + "deps.get": ["deps.get", "gleam.deps.get", "gleam.toml --replace"] + ] + """ + @impl true + @shell Mix.shell() + def run(argv) do + replace = + argv + |> OptionParser.parse!(switches: [replace: :boolean]) + |> elem(0) + |> Keyword.get(:replace, false) + + Mix.Project.get!() + cfg = Mix.Project.config() + cwd = Mix.Project.project_file() |> Path.dirname() + + deps = + Mix.Dep.load_and_cache() + |> Enum.flat_map(fn %Mix.Dep{app: dep, opts: opts} -> + dst = Keyword.fetch!(opts, :dest) + + if dst |> Path.join("gleam.toml") |> File.regular?() do + [{dep, path: Path.relative_to(dst, cwd)}] + else + [] + end + end) + |> Enum.sort() + + toml = + [ + name: Keyword.fetch!(cfg, :app), + version: Keyword.fetch!(cfg, :version), + dependencies: deps + ] + |> mk_toml + + if replace do + cwd + |> Path.join("gleam.toml") + |> File.write!(toml) + else + @shell.info(toml) + end + end + + defp mk_toml(x) do + x + |> Enum.map(&mk_toml_row/1) + |> Enum.join("\n") + end + + defp mk_toml_row({k, v}), do: "#{mk_toml_key(k)} = #{mk_toml_val(v)}" + + defp mk_toml_key(x) when is_atom(x) or is_binary(x) or is_number(x) do + x |> to_string |> inspect + end + + defp mk_toml_val(x) do + cond do + is_map(x) or Keyword.keyword?(x) -> + "{#{x |> Enum.map(&mk_toml_row/1) |> Enum.join(", ")}}" + + is_list(x) -> + "[#{x |> Enum.map(&mk_toml_val/1) |> Enum.join(", ")}]" + + is_number(x) -> + to_string(x) + + true -> + mk_toml_key(x) + end + end +end diff --git a/lib/mix_gleam/config.ex b/lib/mix_gleam/config.ex index f73970a..6039937 100644 --- a/lib/mix_gleam/config.ex +++ b/lib/mix_gleam/config.ex @@ -70,8 +70,7 @@ end) File.read!(path) |> String.split(definition, trim: true) - |> Stream.map(&tokenize/1) - |> Stream.reject(&({[], nil} == &1)) + |> Stream.flat_map(&tokenize/1) |> Stream.map(&atomize/1) |> Stream.map(&parse/1) |> structure @@ -167,14 +166,35 @@ end) cond do match = Regex.run(table, line) -> [_, key] = match - {[key], nil} + [{[key], nil}] match = Regex.run(assignment, line) -> [_, key, value] = match - {[key], value} + + tokenize_inline_table([key], value) true -> - {[], nil} + [] + end + end + + defp tokenize_inline_table(prev_keys, prev_value) do + re = ~r/\{(.+?)\}/ + + case Regex.run(re, prev_value, capture: :all_but_first) do + [pairs] -> + pairs + |> String.split(",") + |> Stream.map(&String.split(&1, "=")) + |> Enum.flat_map(fn [next_key, next_value] -> + tokenize_inline_table( + prev_keys ++ [String.trim(next_key)], + String.trim(next_value) + ) + end) + + _ -> + [{prev_keys, prev_value}] end end end diff --git a/mix.exs b/mix.exs index dc37ecf..b008dbd 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule MixGleam.MixProject do def project do [ app: :mix_gleam, - version: "0.6.2", + version: "0.6.3", elixir: "~> 1.9", start_permanent: Mix.env() == :prod, name: "mix_gleam", diff --git a/test_projects/basic_project/mix.exs b/test_projects/basic_project/mix.exs index 3da0eea..d1d3453 100644 --- a/test_projects/basic_project/mix.exs +++ b/test_projects/basic_project/mix.exs @@ -35,9 +35,9 @@ defmodule BasicProject.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - # {:mix_gleam, path: "../../"} {:gleam_stdlib, "~> 0.32"}, - {:gleeunit, "~> 1.0", only: [:dev, :test], runtime: false} + {:gleeunit, "~> 1.0", only: [:dev, :test], runtime: false}, + {:mix_gleam, only: [:dev, :test], runtime: false, path: "../../"} ] end end diff --git a/test_projects/basic_project/test/basic_project_test.exs b/test_projects/basic_project/test/basic_project_test.exs index 16881df..f53f60f 100644 --- a/test_projects/basic_project/test/basic_project_test.exs +++ b/test_projects/basic_project/test/basic_project_test.exs @@ -1,5 +1,6 @@ defmodule BasicProjectTest do use ExUnit.Case + import ExUnit.CaptureIO doctest BasicProject test "can call Elixir code" do @@ -13,4 +14,16 @@ defmodule BasicProjectTest do test "can call Gleam library" do assert :gleam@list.reverse([1, 2, 3]) == [3, 2, 1] end + + test "gleam.toml mix task" do + lhs = """ + "name" = "basic_project" + "version" = "0.1.0" + "dependencies" = {"gleam_stdlib" = {"path" = "deps/gleam_stdlib"}, "gleeunit" = {"path" = "deps/gleeunit"}} + """ + + rhs = capture_io(fn -> Mix.Tasks.Gleam.Toml.run([]) end) + + assert lhs == rhs + end end