Welcome to my second issue of 8bit-sized1! Even if you’re relatively new to the Elixir ecosystem, you have likely come across the concept of generators. These generators are very common and you may recognize them from running commands like mix new hello_world, mix phx.new hello_world, or other Phoenix related commands.

The cool thing is that these generators are not unique to Phoenix, or other big projects you have been following or using in the past. You can develop custom generators for your projects quite easily if they benefit from having something similar in place.

A code generator can be helpful when you need to write some boilerplate code several times, or just a repeating file “structure” that you can automate. It’s essentially a Mix.Task, and there are several ways we can make it generate code. Here we’ll get to see two of them.

Where to start

Well, the first thing we need is to have an Elixir Project (I’m advancing that part), then create a new file under lib/my_app/tasks/new_task.ex. The name of this file (new_task) will then be used to call and execute the task from the terminal, running mix new_task. Having this information, we can move the actual part of getting to generate code inside the task.

First Approach using File.write

I got to this approach by first searching Google on how to do these types of tasks, and then, asking ChatGPT to give me a simple generator Mix.Task. Funnily (or not), I got pretty much the same result. Both solutions were using File.write to create a new file, at a given path with a custom payload.

Searching Google

By searching Google, I arrived at this StackOverflow question that’s pretty explanatory on how to create files and define the content we want to have there. You can check it and obtain your own conclusions.

Asking ChatGPT

The following code snippet was fully generated by ChatGPT. I’m not showing the prompt I used because it “misunderstood” my first question, and I had to correct it on what I was indeed querying for. Also be aware that this piece of code has a couple of bugs. For example aliases/0 and deps/0 functions are not being used, while @args is being used but doesn’t exist in the snippet. You can ask ChatGPT to solve the bugs, but that’s a post for another day…

defmodule MyApp.Tasks.GenerateModule do
  @moduledoc """
  Generates a new module and corresponding test file.
  """

  use Mix.Task

  @shortdoc "The name of the new module."
  defp module_name_arg do
    {arg, _, _} = OptionParser.parse(@args)
    arg
  end

  def run(_) do
    module_name = module_name_arg()

    # Generate the module file
    module_file = "lib/#{module_name}.ex"
    File.write(module_file, "defmodule #{module_name} do\nend\n")

    # Generate the test file
    test_file = "test/#{module_name}_test.exs"
    File.write(test_file, "defmodule #{module_name}Test do\n  use ExUnit.Case\n  doctest #{module_name}\nend\n")

    # Display success message
    IO.puts "Generated module '#{module_name}' in '#{module_file}' and test file '#{test_file}'."
  end

  defp aliases do
    ["gen_module": "generate_module"]
  end

  defp deps do
    []
  end
end

Even though StackOverflow and ChatGPT had given me a similar answer, I was truly suspicious if there wasn’t any better solution than manually writing to the file the contents we wanted. This way I continued exploring…

Second Approach using Mix.Generator

I found a second approach when investigating how other projects, like Phoenix, and others, did their code generators. That’s where I found about Mix.Generator. This Module should already be available in your app through Mix, and you don’t need to add any extra dependencies.

It exports several functions, but for an easy start, copy_template/4 offers the exact thing we want. This function evaluates and copies templates at specific a source to the respective defined target. The template in the source is evaluated with the given assigns (just like the way we build views when using Phoenix). Also, if the target file already exists and the contents are not the same, it asks for user confirmation, which is also a cool thing out of the box. Example:

defmodule MyApp.Tasks.GenerateModule do
  use Mix.Task
  
  @template_path "priv/templates/myapp.generate_module/module.ex"
  @base_output_path "lib/myapp"
  
  def run() do
    output_file = "#{@base_output_path}/my_module.ex"
    template_data = %{module_name: "MyModule"}

    Mix.Generator.copy_template(@template_path, output_file, template_data)
  end
end

And the template file would look like this:

defmodule MyApp.<%= @module_name %> do
  def my_function do
    IO.inspect("Hello, I was generated!")  
  end
end

I believe that with the code snippets above, you already have information enough to build more complex templates, that take advantage of if, for and other useful things, to have different execution paths.

Wrapping up

There are more things you can do with these types of code generators. For example, instead of just creating new files, you can also “inject” or update code in existing files. However, there are a few differences, that I’ll probably talk about in a different blog post.

For now, I can just leave the “taste” and invite you to check the generators I did for a past project at finiam, or for more complex and robust generators check the ones built by the Phoenix team and collaborators (link for the repo).

As always, thank you for your attention, and see you at the next one 👋