NO LEAS ESTE ARCHIVO EN GITHUB, LAS GUÍAS ESTÁN PUBLICADAS EN https://guides.rubyonrails.org.

Creación y Personalización de Generadores y Plantillas de Rails

Los generadores de Rails son una herramienta esencial para mejorar tu flujo de trabajo. Con esta guía, aprenderás cómo crear generadores y personalizar los existentes.

Después de leer esta guía, sabrás:


1 Primer Contacto

Cuando creas una aplicación usando el comando rails, de hecho estás usando un generador de Rails. Después de eso, puedes obtener una lista de todos los generadores disponibles invocando bin/rails generate:

$ rails new myapp
$ cd myapp
$ bin/rails generate

NOTA: Para crear una aplicación de Rails usamos el comando global rails que utiliza la versión de Rails instalada a través de gem install rails. Cuando estamos dentro del directorio de tu aplicación, usamos el comando bin/rails que utiliza la versión de Rails incluida con la aplicación.

Obtendrás una lista de todos los generadores que vienen con Rails. Para ver una descripción detallada de un generador en particular, invoca el generador con la opción --help. Por ejemplo:

$ bin/rails generate scaffold --help

2 Creando Tu Primer Generador

Los generadores están construidos sobre Thor, que proporciona opciones poderosas para el análisis y una gran API para manipular archivos.

Vamos a construir un generador que crea un archivo inicializador llamado initializer.rb dentro de config/initializers. El primer paso es crear un archivo en lib/generators/initializer_generator.rb con el siguiente contenido:

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # Añade contenido de inicialización aquí
    RUBY
  end
end

Nuestro nuevo generador es bastante simple: hereda de Rails::Generators::Base y tiene una definición de método. Cuando se invoca un generador, cada método público en el generador se ejecuta secuencialmente en el orden en que está definido. Nuestro método invoca create_file, que creará un archivo en el destino dado con el contenido dado.

Para invocar nuestro nuevo generador, ejecutamos:

$ bin/rails generate initializer

Antes de continuar, veamos la descripción de nuestro nuevo generador:

$ bin/rails generate initializer --help

Rails generalmente puede derivar una buena descripción si un generador está en un espacio de nombres, como ActiveRecord::Generators::ModelGenerator, pero no en este caso. Podemos resolver este problema de dos maneras. La primera forma de agregar una descripción es llamando a desc dentro de nuestro generador:

class InitializerGenerator < Rails::Generators::Base
  desc "Este generador crea un archivo inicializador en config/initializers"
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # Añade contenido de inicialización aquí
    RUBY
  end
end

Ahora podemos ver la nueva descripción invocando --help en el nuevo generador.

La segunda forma de agregar una descripción es creando un archivo llamado USAGE en el mismo directorio que nuestro generador. Vamos a hacer eso en el siguiente paso.

3 Creando Generadores con Generadores

Los generadores en sí mismos tienen un generador. Vamos a eliminar nuestro InitializerGenerator y usar bin/rails generate generator para generar uno nuevo:

$ rm lib/generators/initializer_generator.rb

$ bin/rails generate generator initializer
      create  lib/generators/initializer
      create  lib/generators/initializer/initializer_generator.rb
      create  lib/generators/initializer/USAGE
      create  lib/generators/initializer/templates
      invoke  test_unit
      create    test/lib/generators/initializer_generator_test.rb

Este es el generador recién creado:

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

Primero, nota que el generador hereda de Rails::Generators::NamedBase en lugar de Rails::Generators::Base. Esto significa que nuestro generador espera al menos un argumento, que será el nombre del inicializador y estará disponible para nuestro código a través de name.

Podemos ver eso verificando la descripción del nuevo generador:

$ bin/rails generate initializer --help
Usage:
  bin/rails generate initializer NAME [options]

Además, nota que el generador tiene un método de clase llamado source_root. Este método apunta a la ubicación de nuestras plantillas, si las hay. Por defecto, apunta al directorio lib/generators/initializer/templates que acaba de ser creado.

Para entender cómo funcionan las plantillas de generador, vamos a crear el archivo lib/generators/initializer/templates/initializer.rb con el siguiente contenido:

# Añade contenido de inicialización aquí

Y vamos a cambiar el generador para copiar esta plantilla cuando se invoque:

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

  def copy_initializer_file
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

Ahora ejecutemos nuestro generador:

$ bin/rails generate initializer core_extensions
      create  config/initializers/core_extensions.rb

$ cat config/initializers/core_extensions.rb
# Añade contenido de inicialización aquí

Vemos que copy_file creó config/initializers/core_extensions.rb con el contenido de nuestra plantilla. (El método file_name usado en la ruta de destino se hereda de Rails::Generators::NamedBase).

4 Opciones de Línea de Comando del Generador

Los generadores pueden soportar opciones de línea de comando usando class_option. Por ejemplo:

class InitializerGenerator < Rails::Generators::NamedBase
  class_option :scope, type: :string, default: "app"
end

Ahora nuestro generador puede ser invocado con una opción --scope:

$ bin/rails generate initializer theme --scope dashboard

Los valores de opción son accesibles en los métodos del generador a través de options:

def copy_initializer_file
  @scope = options["scope"]
end

5 Resolución del Generador

Al resolver el nombre de un generador, Rails busca el generador usando múltiples nombres de archivo. Por ejemplo, cuando ejecutas bin/rails generate initializer core_extensions, Rails intenta cargar cada uno de los siguientes archivos, en orden, hasta que se encuentra uno:

  • rails/generators/initializer/initializer_generator.rb
  • generators/initializer/initializer_generator.rb
  • rails/generators/initializer_generator.rb
  • generators/initializer_generator.rb

Si ninguno de estos se encuentra, se generará un error.

Colocamos nuestro generador en el directorio lib/ de la aplicación porque ese directorio está en $LOAD_PATH, permitiendo así que Rails encuentre y cargue el archivo.

6 Sobrescribiendo Plantillas de Generadores de Rails

Rails también buscará en múltiples lugares al resolver archivos de plantilla de generador. Uno de esos lugares es el directorio lib/templates/ de la aplicación. Este comportamiento nos permite sobrescribir las plantillas utilizadas por los generadores integrados de Rails. Por ejemplo, podríamos sobrescribir la plantilla del controlador scaffold o las plantillas de vistas scaffold.

Para ver esto en acción, vamos a crear un archivo lib/templates/erb/scaffold/index.html.erb.tt con el siguiente contenido:

<%% @<%= plural_table_name %>.count %> <%= human_name.pluralize %>

Nota que la plantilla es una plantilla ERB que renderiza otra plantilla ERB. Así que cualquier <% que deba aparecer en la plantilla resultante debe escaparse como <%% en la plantilla del generador.

Ahora ejecutemos el generador scaffold integrado de Rails:

$ bin/rails generate scaffold Post title:string
      ...
      create      app/views/posts/index.html.erb
      ...

El contenido de app/views/posts/index.html.erb es:

<% @posts.count %> Posts

7 Sobrescribiendo Generadores de Rails

Los generadores integrados de Rails se pueden configurar a través de config.generators, incluyendo sobrescribir algunos generadores por completo.

Primero, echemos un vistazo más de cerca a cómo funciona el generador scaffold.

$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20230518000000_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      create      app/views/users/_user.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder

Del resultado, podemos ver que el generador scaffold invoca otros generadores, como el generador scaffold_controller. Y algunos de esos generadores invocan otros generadores también. En particular, el generador scaffold_controller invoca varios otros generadores, incluyendo el generador helper.

Vamos a sobrescribir el generador helper integrado con un nuevo generador. Nombraremos el generador my_helper:

$ bin/rails generate generator rails/my_helper
      create  lib/generators/rails/my_helper
      create  lib/generators/rails/my_helper/my_helper_generator.rb
      create  lib/generators/rails/my_helper/USAGE
      create  lib/generators/rails/my_helper/templates
      invoke  test_unit
      create    test/lib/generators/rails/my_helper_generator_test.rb

Y en lib/generators/rails/my_helper/my_helper_generator.rb definiremos el generador como:

class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<~RUBY
      module #{class_name}Helper
        # ¡Estoy ayudando!
      end
    RUBY
  end
end

Finalmente, necesitamos decirle a Rails que use el generador my_helper en lugar del generador helper integrado. Para eso usamos config.generators. En config/application.rb, agreguemos:

config.generators do |g|
  g.helper :my_helper
end

Ahora, si ejecutamos el generador scaffold nuevamente, vemos el generador my_helper en acción:

$ bin/rails generate scaffold Article body:text
      ...
      invoke  scaffold_controller
      ...
      invoke    my_helper
      create      app/helpers/articles_helper.rb
      ...

NOTA: Puedes notar que la salida para el generador helper integrado incluye "invoke test_unit", mientras que la salida para my_helper no. Aunque el generador helper no genera pruebas por defecto, sí proporciona un gancho para hacerlo usando hook_for. Podemos hacer lo mismo incluyendo hook_for :test_framework, as: :helper en la clase MyHelperGenerator. Consulta la documentación de hook_for para más información.

7.1 Alternativas de Generadores

Otra forma de sobrescribir generadores específicos es usando alternativas. Una alternativa permite que un espacio de nombres de generador delegue a otro espacio de nombres de generador.

Por ejemplo, digamos que queremos sobrescribir el generador test_unit:model con nuestro propio generador my_test_unit:model, pero no queremos reemplazar todos los demás generadores test_unit:* como test_unit:controller.

Primero, creamos el generador my_test_unit:model en lib/generators/my_test_unit/model/model_generator.rb:

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

    def do_different_stuff
      say "Haciendo cosas diferentes..."
    end
  end
end

A continuación, usamos config.generators para configurar el generador test_framework como my_test_unit, pero también configuramos una alternativa para que cualquier generador my_test_unit:* faltante se resuelva como test_unit:*:

config.generators do |g|
  g.test_framework :my_test_unit, fixture: false
  g.fallbacks[:my_test_unit] = :test_unit
end

Ahora, cuando ejecutamos el generador scaffold, vemos que my_test_unit ha reemplazado a test_unit, pero solo se han afectado las pruebas del modelo:

$ bin/rails generate scaffold Comment body:text
      invoke  active_record
      create    db/migrate/20230518000000_create_comments.rb
      create    app/models/comment.rb
      invoke    my_test_unit
    Haciendo cosas diferentes...
      invoke  resource_route
       route    resources :comments
      invoke  scaffold_controller
      create    app/controllers/comments_controller.rb
      invoke    erb
      create      app/views/comments
      create      app/views/comments/index.html.erb
      create      app/views/comments/edit.html.erb
      create      app/views/comments/show.html.erb
      create      app/views/comments/new.html.erb
      create      app/views/comments/_form.html.erb
      create      app/views/comments/_comment.html.erb
      invoke    resource_route
      invoke    my_test_unit
      create      test/controllers/comments_controller_test.rb
      create      test/system/comments_test.rb
      invoke    helper
      create      app/helpers/comments_helper.rb
      invoke      my_test_unit
      invoke    jbuilder
      create      app/views/comments/index.json.jbuilder
      create      app/views/comments/show.json.jbuilder

8 Plantillas de Aplicación

Las plantillas de aplicación son un tipo especial de generador. Pueden usar todos los métodos auxiliares del generador, pero están escritas como un script Ruby en lugar de una clase Ruby. Aquí hay un ejemplo:

# template.rb

if yes?("¿Te gustaría instalar Devise?")
  gem "devise"
  devise_model = ask("¿Cómo te gustaría que se llamara el modelo de usuario?", default: "User")
end

after_bundle do
  if devise_model
    generate "devise:install"
    generate "devise", devise_model
    rails_command "db:migrate"
  end

  git add: ".", commit: %(-m 'Commit inicial')
end

Primero, la plantilla pregunta al usuario si le gustaría instalar Devise. Si el usuario responde "sí" (o "y"), la plantilla añade Devise al Gemfile, y pregunta al usuario por el nombre del modelo de usuario de Devise (por defecto User). Más tarde, después de que se haya ejecutado bundle install, la plantilla ejecutará los generadores de Devise y rails db:migrate si se especificó un modelo de Devise. Finalmente, la plantilla hará git add y git commit de todo el directorio de la aplicación.

Podemos ejecutar nuestra plantilla al generar una nueva aplicación de Rails pasando la opción -m al comando rails new:

$ rails new my_cool_app -m path/to/template.rb

Alternativamente, podemos ejecutar nuestra plantilla dentro de una aplicación existente con bin/rails app:template:

$ bin/rails app:template LOCATION=path/to/template.rb

Las plantillas tampoco necesitan almacenarse localmente; puedes especificar una URL en lugar de una ruta:

$ rails new my_cool_app -m http://example.com/template.rb
$ bin/rails app:template LOCATION=http://example.com/template.rb

9 Métodos Auxiliares del Generador

Thor proporciona muchos métodos auxiliares para generadores a través de Thor::Actions, tales como:

Además de esos, Rails también proporciona muchos métodos auxiliares a través de Rails::Generators::Actions, tales como:

10 Pruebas de Generadores

Rails proporciona métodos auxiliares para pruebas a través de Rails::Generators::Testing::Behaviour, tales como:

Si ejecutas pruebas contra generadores, necesitarás establecer RAILS_LOG_TO_STDOUT=true para que las herramientas de depuración funcionen.

RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb

Además de esos, Rails también proporciona afirmaciones adicionales a través de Rails::Generators::Testing::Assertions.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.