35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Bouw Custom Rails 8 Generators om Repetitieve Boilerplate te Elimineren

Bouw Custom Rails 8 Generators om Repetitieve Boilerplate te Elimineren

TTB Software
rails

Elk Rails-team ontwikkelt patronen. Service objects met een specifieke interface. Form objects die van een base class erven. API serializers die een naamconventie volgen. Je kopieert de laatste die je schreef, hernoemt het bestand, verwijdert de implementatie en begint opnieuw.

Custom Rails generators lossen dit op. Je voert één commando uit en het bestand verschijnt — correct benoemd, goed gestructureerd, met de juiste tests ernaast. Hier lees je hoe je ze bouwt in Rails 8.

Hoe Rails Generators Onder de Motorkap Werken

Rails generators gebruiken Thor, een toolkit voor het bouwen van command-line interfaces. Elke generator is een Ruby-klasse die erft van Rails::Generators::NamedBase (of Rails::Generators::Base voor generators zonder naam-argument).

Wanneer je bin/rails generate service_object CreateUser uitvoert, doet Rails het volgende:

  1. Zoekt in lib/generators/ van je app, dan in geïnstalleerde gems, dan in Rails zelf
  2. Vindt een klasse genaamd ServiceObjectGenerator
  3. Roept de publieke methoden aan in volgorde van definitie
  4. Gebruikt Thor’s template-systeem om bestanden te maken van ERB-templates

De template-resolutie volgt een conventie: templates staan in een templates/-directory naast het generator-bestand.

Een Service Object Generator Bouwen

Stel dat je team dit service object patroon gebruikt:

# app/services/create_user.rb
class CreateUser < ApplicationService
  def initialize(params:, current_user:)
    @params = params
    @current_user = current_user
  end

  def call
    # implementatie
  end
end

Met een base class:

# app/services/application_service.rb
class ApplicationService
  def self.call(...)
    new(...).call
  end
end

Stap 1: Maak de Generator-klasse

# lib/generators/service_object/service_object_generator.rb
class ServiceObjectGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  argument :attributes, type: :array, default: [], banner: "param param"

  def create_service_file
    template "service.rb.tt", File.join("app/services", class_path, "#{file_name}.rb")
  end

  def create_test_file
    template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}_test.rb")
  end

  private

  def initialize_params
    attributes.map { |attr| "@#{attr} = #{attr}" }.join("\n    ")
  end

  def initialize_signature
    attributes.map { |attr| "#{attr}:" }.join(", ")
  end
end

De source_root-aanroep vertelt Thor waar templates te vinden zijn. NamedBase geeft je file_name, class_name en class_path gratis — inclusief correcte afhandeling van namespaces zoals bin/rails generate service_object admin/create_user dat Admin::CreateUser produceert in de juiste directory.

Stap 2: Schrijf de Templates

<%# lib/generators/service_object/templates/service.rb.tt %>
<% module_namespacing do -%>
class <%= class_name %> < ApplicationService
  def initialize(<%= initialize_signature %>)
    <%= initialize_params %>
  end

  def call
    # TODO: implementeer <%= class_name %>
  end
end
<% end -%>
<%# lib/generators/service_object/templates/service_test.rb.tt %>
require "test_helper"

<% module_namespacing do -%>
class <%= class_name %>Test < ActiveSupport::TestCase
  test "voert succesvol uit" do
    result = <%= class_name %>.call(<%= attributes.map { |a| "#{a}: nil" }.join(", ") %>)
    assert result
  end
end
<% end -%>

De .tt-extensie signaleert Thor-templates. module_namespacing wikkelt de klasse in de juiste module wanneer de naam een namespace bevat (zoals Admin::CreateUser).

Stap 3: Uitvoeren

$ bin/rails generate service_object CreateOrder items: user:
      create  app/services/create_order.rb
      create  test/services/create_order_test.rb

De gegenereerde service:

class CreateOrder < ApplicationService
  def initialize(items:, user:)
    @items = items
    @user = user
  end

  def call
    # TODO: implementeer CreateOrder
  end
end

Een Destroy Generator Toevoegen

Rails verwacht dat generators omkeerbaar zijn. Wanneer iemand bin/rails destroy service_object CreateOrder uitvoert, roept Rails dezelfde generator-methoden aan maar inverteert de bestandsoperaties — create_file wordt remove_file.

Template-gebaseerde generators krijgen dit gratis. De template-methode is al omkeerbaar. Maar als je generator meer doet dan bestanden aanmaken (zoals toevoegen aan een routes-bestand), moet je het omgekeerde geval afhandelen:

def add_route
  route "resources :#{plural_name}, only: [:create]"
end

De route-helper in Rails 8 is standaard omkeerbaar. Voor aangepaste omkeerbare acties kun je inject_into_file gebruiken met een blok dat geïdentificeerd en verwijderd kan worden.

Generator Hooks: Verbinding met Andere Generators

Rails generators kunnen andere generators aanroepen. Als je service objects altijd een bijbehorend API-endpoint nodig hebben, koppel ze dan:

class ServiceObjectGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  hook_for :test_framework, as: :unit

  class_option :api_endpoint, type: :boolean, default: false,
    desc: "Genereer een bijpassende API controller action"

  def create_service_file
    template "service.rb.tt", File.join("app/services", class_path, "#{file_name}.rb")
  end

  def create_api_endpoint
    return unless options[:api_endpoint]
    generate "controller", "api/v1/#{plural_name} create --no-helper --no-assets --skip-routes"
  end
end

De hook_for :test_framework regel is hoe Rails generators automatisch schakelen tussen Minitest en RSpec — het delegeert test-bestandsgeneratie aan het geconfigureerde framework.

Je Generator Testen

Rails biedt Rails::Generators::TestCase voor het testen van generators in isolatie:

# test/lib/generators/service_object_generator_test.rb
require "test_helper"
require "generators/service_object/service_object_generator"

class ServiceObjectGeneratorTest < Rails::Generators::TestCase
  tests ServiceObjectGenerator
  destination Rails.root.join("tmp/generators")
  setup :prepare_destination

  test "genereert service-bestand" do
    run_generator ["create_order", "items", "user"]

    assert_file "app/services/create_order.rb" do |content|
      assert_match(/class CreateOrder < ApplicationService/, content)
      assert_match(/def initialize\(items:, user:\)/, content)
    end
  end

  test "genereert service met namespace" do
    run_generator ["admin/create_order", "items"]

    assert_file "app/services/admin/create_order.rb" do |content|
      assert_match(/module Admin/, content)
      assert_match(/class CreateOrder < ApplicationService/, content)
    end
  end

  test "genereert testbestand" do
    run_generator ["create_order", "items"]
    assert_file "test/services/create_order_test.rb"
  end
end

prepare_destination maakt een schone tijdelijke directory voor elke test. assert_file controleert zowel bestaan als inhoud via het optionele blok.

Praktijkpatronen voor Generators

Na het bouwen van generators voor meerdere Rails 8 productie-apps blijken een paar patronen consistent nuttig:

Form objects met validatie. Als je form objects gebruikt (vooral met ActiveModel::Model), genereer ze dan met standaard attribuutdeclaraties en test-scaffolding die de validatieregels dekt.

Event-klassen voor pub/sub. Bij gebruik van ActiveSupport::Notifications of een library als Wisper bespaart het genereren van event-klassen met een consistente interface tijd en voorkomt het afdrijving.

Policy objects voor autorisatie. Bij gebruik van Pundit of een aangepaste autorisatielaag kun je policy-bestanden genereren naast hun tests met de standaard action-methoden voorgedefinieerd.

Het belangrijkste inzicht: generators werken het best voor patronen waar je team al consensus over heeft. Genereer niet wat je nog aan het uitproberen bent — dat automatiseert alleen inconsistentie.

Generators Verpakken in een Gem

Als je patronen deelt over meerdere Rails-apps, extraheer je generators dan in een gem. De conventie:

my_gem/
├── lib/
│   └── generators/
│       └── my_gem/
│           └── service_object/
│               ├── service_object_generator.rb
│               └── templates/
│                   └── service.rb.tt

Wanneer de gem geladen is, ontdekt Rails automatisch generators onder lib/generators/. Gebruikers voeren ze uit als bin/rails generate my_gem:service_object CreateUser.

Praktische Tips

Gebruik --pretend tijdens ontwikkeling. Het uitvoeren van bin/rails generate service_object CreateUser --pretend toont welke bestanden aangemaakt zouden worden zonder iets te schrijven. Onmisbaar bij het debuggen van template-problemen.

Bekijk bestaande generators als referentie. Voer bin/rails generate --help uit om alle beschikbare generators te zien. Lees Rails’ eigen generator-broncode op railties/lib/rails/generators — de scaffold-generator is verrassend leesbaar en demonstreert de meeste features.

Houd templates minimaal. Een generator die 200 regels code produceert, genereert waarschijnlijk te veel. De sweet spot is genoeg structuur zodat developers het patroon niet hoeven te onthouden, maar niet zoveel dat de gegenereerde code zwaar bewerkt moet worden.

FAQ

Hoe maak ik een Rails generator die geen naam-argument vereist?

Erf van Rails::Generators::Base in plaats van NamedBase. Je krijgt dan niet de automatische file_name en class_name helpers, maar je kunt eigen argumenten en opties definiëren met argument en class_option. Dit is handig voor generators die configuratiebestanden of initializers maken waarbij de uitvoernaam vaststaat.

Kan ik ingebouwde Rails generators overschrijven met aangepaste versies?

Ja. Plaats je generator in lib/generators/ met dezelfde naam als de ingebouwde generator, en Rails gebruikt de jouwe. Bijvoorbeeld: het aanmaken van lib/generators/model/model_generator.rb vervangt de standaard model-generator. Je kunt ook generator-defaults configureren in config/application.rb via config.generators om templates te wijzigen zonder de hele generator te vervangen.

Wat is het verschil tussen template en copy_file in generators?

template verwerkt het bestand door ERB voordat het geschreven wordt — alle <%= %>-tags worden geëvalueerd. copy_file schrijft het bestand exact zoals het is. Gebruik template voor bestanden met dynamische inhoud (klassenamen, argumenten) en copy_file voor statische bestanden zoals configuratie-YAML of initializer-boilerplate die nooit verandert.

Werken generators ook met RSpec in plaats van Minitest?

Ja. Gebruik de hook_for :test_framework methode in je generator-klasse. Wanneer een project geconfigureerd is voor RSpec (via config.generators { |g| g.test_framework :rspec }), delegeert de hook testgeneratie aan de rspec-rails generator die overeenkomt met de naam van jouw generator. Je hebt een bijbehorende RSpec-generator nodig, of je kunt test-templates voor beide frameworks in je generator maken en options[:test_framework] controleren.

#rails 8 #generators #automatisering #productiviteit #ruby
T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation