Skip to content

Living generators

WARNING: Experimental feature

This tutorial shows how to build a generator that uses morph to extend an existing serializer with new attributes - safely and idempotently across multiple runs.

The Problem

The problem with generators is that you usually run them once, and if you want to modify something later, the only option is to overwrite them completely, often losing all manual changes made since the first generation. With the Morph tool, you can extend your generators by applying them multiple times without losing what you've already written.

Running these two commands in sequence:

bash
loci g serializer user name email
loci g serializer user avatar_url created_at

will produce a single file with all four attributes.

The Generator

ruby
module Serializer
  class SerializerGenerator < Loci::BaseGenerator
    arg :model
    args :attributes

    step "create serializer" do
      skip "already exists" if File.exist?(file_path)

      # thanks to `up` we can run --reverse to remove entries from 
      # serializer instead of deleting serializer itself
      up do
        create_file file_path, <<~RUBY
          class #{class_name}
          end
        RUBY
      end
    end

    step "first" do
      attrs = params.attributes.map { "  attribute :#{it}" }

      morph file_path, <<~RUBY
        class #{class_name}
          #{attrs.join("\n")}
        end
      RUBY
    end

    private

    def file_path
      params.model.singularize.suffix(:_serializer).ext(:rb).to_path
    end

    def class_name
      params.model.singularize.suffix(:_serializer).to_class
    end
  end
end

The generator has two steps. The first creates the file skeleton if it does not already exist. The second uses morph to merge attributes into the class. This separation matters: the Morph tool works better when it has something to diff against.

First Run

bash
loci g serializer user name email

The file does not exist. The first step creates the skeleton, then the second step morphs the attributes in:

ruby
class UserSerializer
  attribute :name
  attribute :email
end

Second Run

bash
loci g serializer user avatar_url created_at

The file exists, so the first step is skipped. Morph compares the patch against the current file, skips attribute :name and attribute :email (already present), and inserts the new ones:

ruby
class UserSerializer
  attribute :name
  attribute :email
  attribute :avatar_url
  attribute :created_at
end

Running the same command again produces no changes - the generator is fully idempotent.

Reverse Mode

Running with --reverse removes the specified attributes:

bash
loci g serializer user email avatar_url --reverse
ruby
class UserSerializer
  attribute :name
  attribute :created_at
end