AndyL

AndyL

Igniter early preview (Codemods and code generators)

Interesting discussion in the may office hours!

Questions about igniter:

  • it sounds like igniter works with a tree of code-elements that perform generation. what do you call the bit of code that implements an ‘generator operation’? an agent? a performer? an action? (ansible calls them ‘modules’)
  • is the sequencing and structure of the “igniter tree” defined within the ‘modules’ (functional composition), or by a playbook type of recipe comparable to how ansible does it?
  • how is idempotency handled?
  • could igniter emit data that describes the application structure (someone said something about a diff…)
  • can someone post the URL of the igniter repo, to let outsiders study the pre-release code?

If it’s premature for questions like these, no problemo I’m happy to wait. Just thought I’d post these questions while they are fresh in my mind. :wink:

Most Liked

zachdaniel

zachdaniel

Creator of Ash

In the spirit of building in the open, the source code has been made public: GitHub - ash-project/igniter: A code generation and project patching framework. · GitHub There isn’t much to it yet, but more will come, and a 0.1 release should happen in the next few weeks ideally.

zachdaniel

zachdaniel

Creator of Ash

:wave: great questions!

So there are two levels of granularity that are relevant here. There is a “task”, which is an abstraction over a mix task with one minor change, which is that you only define an igniter/2 function, which takes an igniter (a small wrapper around a Rewrite) and returns an igniter. This enables chaining. For example, here is a very early version of mix igniter.install.ash.resource

  defmodule Mix.Tasks.Igniter.Gen.Ash.Resource do
    use Igniter.Mix.Task

    @impl Igniter.Mix.Task
    def igniter(igniter, argv) do
      with {:ok, domain_underscore} <-
             Igniter.Args.validate_nth_present_and_underscored(
               igniter,
               argv,
               0,
               :domain,
               "Required first argument (snake_case domain) is missing"
             ),
           {:ok, resource_underscore} <-
              Igniter.Args.validate_nth_present_and_underscored(
                igniter,
                argv,
                1,
               :resource,
               "Required second argument (snake_case domain) is missing"
             ) do
        app_name = Igniter.Tasks.app_name()
        domain_module_name = Igniter.Module.module_name(Macro.camelize(domain_underscore))

        resource_module_name =
          Module.concat([domain_module_name, Macro.camelize(resource_underscore)])

        igniter
        |> Igniter.compose_task("igniter.gen.ash.resource_reference", argv)
        |> Igniter.create_new_elixir_file(
          "lib/#{app_name}/#{domain_underscore}/#{resource_underscore}.ex",
          """
          defmodule #{inspect(resource_module_name)} do
            use Ash.Resource,
              domain: #{inspect(domain_module_name)}

            actions do
              defaults [:read, :destroy, create: [], update: []]
            end

            attributes do
              uuid_primary_key :id
            end
          end
          """
        )
      else
        {:error, igniter} ->
          igniter
      end
    end
  end

That level of granularity would be called a task, and tasks can be composed with Igniter.compose_task/2. This means any task can be called via mix or composed, which is a very useful property.

At a lower level of granularity, you have individual functions that will modify a rewrite to make some code modification. We don’t currently have a name for these aside from “functions”, but you could perhaps call them “codemods”. Here is an example snippet from mix igniter.install.ash

      igniter
      |> Igniter.Deps.add_dependency(:picosat_elixir, "~> 0.2")
      |> Igniter.compose_task("igniter.install.spark", argv)
      |> Igniter.Formatter.import_dep(:ash)
      |> Igniter.Config.configure("config.exs", :spark, [:formatter, :"Ash.Resource"], [], fn x ->
        x
      end)
      |> Igniter.Config.configure("config.exs", :spark, [:formatter, :"Ash.Domain"], [], fn x ->
        x
      end)

add_dependency, import_dep and configure are examples of the building blocks Igniter will provide.

As you can see above, it is functional composition. A playbook/DSL version may come some day, but is not necessary to start.

When you run an igniter task, it displays a diff of all changes. Here is example output from running mix igniter.install ash in a new package (ignore the bit about the local dependency. You have to use a local dependency to test/work on your igniter :slight_smile:

Not looking up dependency, because a local dependency is detected
Igniter: Installing ash...

Igniter: Proposed changes:

.formatter.exs

  1 + |# Used by "mix format"
  2 + |[
  3 + |  import_deps: [:ash],
  4 + |  plugins: [Spark.Formatter],
  5 + |  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
  6 + |]
1 7   |
2   - |


config/config.exs

  1 + |import Config
  2 + |config :spark, formatter: ["Ash.Domain": [], "Ash.Resource": [], remove_parens?: true]
  3 + |config :igniter_example, :ash_domains, [IgniterExample.ExampleDomain]
1 4   |
2   - |


lib/igniter_example/example_domain.ex

  1 + |defmodule IgniterExample.ExampleDomain do
  2 + |  use Ash.Domain
1 3   |
  4 + |  resources do
  5 + |    resource(IgniterExample.ExampleDomain.ExampleResource)
  6 + |  end
  7 + |end


lib/igniter_example/example_domain/example_resource.ex

    1 + |defmodule IgniterExample.ExampleDomain.ExampleResource do
    2 + |  use Ash.Resource,
    3 + |    domain: IgniterExample.ExampleDomain
 1  4   |
    5 + |  actions do
    6 + |    defaults([:read, :destroy, create: [], update: []])
    7 + |  end
 2  8   |
    9 + |  attributes do
   10 + |    uuid_primary_key(:id)
   11 + |  end
   12 + |end
   13 + |


Proceed with changes? [Yn]

Since those are all new files, I need to figure out a way to hide the left side of those line numbers representing the original files, just haven’t done that yet :slight_smile:

The codebase is not quite ready, but I will share it before too long.

Where Next?

Popular in Discussions Top

matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New
mikl
I wanted to capitalize a string, and tried using String.capitalize(). That generally works well, until you try to capitalize a word like...
New
cvkmohan
The upcoming Phoenix 1.6 release looks very interesting. Became a habit to watch the commits - and - what they are bringing in. phx.gen...
New
laiboonh
Hi all, I am trying to convince my team to use liveview over the current react. What are some of the points where one should consider us...
New
AstonJ
Are there any Elixir or Erlang libraries that help with this? I’ve been thinking how streaming services like twitch have exploded recentl...
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19204 150
New
AstonJ
I’ve just started the Phoenix part of the utterly brilliant online course by @pragdave. On generating the Phoenix app he uses the --no-ec...
New
shishini
I think this twitter post and youtube video didn’t get as much attention as I hoped I am still new to Elixir, so can’t really judge ...
New
PragTob
Hey everyone, this has been on my mind for some time and I’d love your input on it! TLDR: I feel like maps are superioer for storing and...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement