I am building custom generators to generate rails models. The generator can use one of many templates to build one model. Some logic is the same in all templates.
Is it possible to extract some parts of the templates into partials?
Here's an example of what I want to do:
lib/generators/custom_model/custom_model_generator.rb
class CustomModelGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
argument :model_name, type: :string
argument :model_type, type: :string
...
include GeneratorsHelper
def generate_model
template_path =
case model_type
when 'car' then 'car_model.rb.erb'
when 'plane' then 'plane_model.rb.erb'
...
end
template template_path, "app/models/#{model_name}.rb"
end
end
Here is one template:
lib/generators/custom_model/templates/car_model.rb.erb
class <%= model_name.camelcase %> < ApplicationRecord
def start
puts "Vroum!"
end
end
The #start method will also be used in the other model generators. I would like to extract it it something like a partial. Is that possible?
Related
i wanna this custom generator named form , generate a model and create its migration file in db/migrate
this is output :
rails g form test name:string
create app/models/name:string.rb
create app/controllers/name:strings_controller.rb
create app/javascript/api/name:string.js
create app/javascript/component/name:strings.vue
create app/javascript/pages/name:string/index.vue
create app/javascript/pages/name:string/layout.vue
create app/javascript/store/actions/name:string.js
create app/javascript/store/getters/name:string.js
create app/javascript/store/modules/name:string.js
create app/javascript/store/mutations/name:string.js
create db/migrate/20200816162324_create_name:strings.rb
and this is my custom generator class :
require 'rails/generators/active_record'
class FormGenerator < ActiveRecord::Generators::Base
source_root File.expand_path('templates', __FILE__)
source_root File.expand_path('templates', __dir__)
argument :model, type: :string
def create_template
template "models/form.template", "app/models/#{model}.rb"
template "controllers/forms_controller.template", "app/controllers/#{model}s_controller.rb"
template "javascript/api/form.template", "app/javascript/api/#{model}.js"
template "javascript/pages/component/forms.template", "app/javascript/component/#{model}s.vue"
template "javascript/pages/form/index.template", "app/javascript/pages/#{model}/index.vue"
template "javascript/pages/form/layout.template", "app/javascript/pages/#{model}/layout.vue"
template "javascript/store/actions/form.template", "app/javascript/store/actions/#{model}.js"
template "javascript/store/getters/form.template", "app/javascript/store/getters/#{model}.js"
template "javascript/store/modules/form.template", "app/javascript/store/modules/#{model}.js"
template "javascript/store/mutations/form.template", "app/javascript/store/mutations/#{model}.js"
migration_template "create_forms.template", "db/migrate/create_#{model}s.rb"
end
as you can see generator uses the column that i gave it to model instead of model itself
template "models/form.template", "app/models/#{model}.rb" //code
rails g form test name:string
create app/models/name:string.rb //output
what should i do to fix that?
From Ruby on Rails Guide, if you want to add custom command line arguments to a custom generator you have to declare it as a class_option. In this case your custom arguments are an array of columns. For example:
class FormGenerator < ActiveRecord::Generators::Base
...
class_option :columns, type: :array, default: [] # Options :type and :default are not required
def create_template
#columns = options[:columns] # You can use this variable inside template
...
end
end
For more information about class_option method click here.
I am trying to keep the namespace of a class when including a module.
Lets say I have these Models:
class Shop < ApplicationRecord
self.abstract_class = true
end
class A::Shop < ::Shop
end
class B::Shop < ::Shop
end
And this controller:
module A
class ShopController < AuthenticatedController
include Basic::Features
def test
p Shop.new #YES! its a A::Shop
end
end
end
And this Module:
module Basic
module Features
def test
p Shop.new #Shop (abstract)
end
end
end
In the above example, the namespace is overwritten when including the module.
As I want to use the Basic::Features module at multiple places in my codebase, I would like to automatically switch between A::Shop and B::Shop when including it in the controller.
Anybody any idea if this is possible, and how.
Here is one option:
module Basic
module Features
def test
p Object.const_get('::' + self.class.to_s.split('::').first + '::Shop')
end
end
end
It will not work if you have deeper namespaces, e.g. A::B::Shop, but it could be made to work. Also in rails you could use deconstantize instead of split.
I think the reason you code does not work is because it is looking in A::ShopController namespace and since not found it then tries the root namespace, ::, and finds Shop.
I don't have a great experience with mixin modules. Then, please forgive me if my question seems to be a bit naïve.
I am creating a few modules to integrate a project with music services like Spotify, who have REST APIs. All these modules include another mixin module I created named APIClientBuilder, which provides a small DSL for creating API endpoints.
lib/integrations/api_client_builder.rb
require 'rest-client'
module APIClientBuilder
attr_accessor :api_client, :endpoint, :url, :param
def api_client(api_name)
end
def fetch_client(api_name)
end
def api_endpoint(endpoint_name)
end
def fetch_endpoint(api_name,endpoint_name)
end
def method=(meth)
end
def url=(endpoint_url)
end
def param(param_name,param_value)
end
def call(api_name,api_endpoint,token,*extra_params)
end
end
lib/integrations/spotify.rb
require_relative 'api_client_builder'
module SpotifyIntegration
include APIClientBuilder
def base_url
'https://api.spotify.com/v1'
end
def random_state_string
(0..10).map { (65 + rand(26)).chr }.join
end
api_client('spotify') do |apic|
apic.api_endpoint('request_authorization') do |ep|
ep.method = :get
ep.url = "https://accounts.spotify.com/authorize"
ep.param("client_id",SPOTIFY_KEY)
ep.param("response_type","code")
ep.param("redirect_uri","http://localhost:3000")
end
apic.api_endpoint('my_playlists') do |ep|
ep.method = :get
ep.url = "#{base_url}/me/playlists"
end
end
end
My idea was having in my controllers something like this:
app/controllers/api/v1/users_controller.rb
require 'integrations/spotify.rb'
class UsersController < ApplicationController
include SpotifyIntegration
end
And then have access to the methods in SpotifyIntegration and, through this, to the methods in APIClientBuilder.
It happens that I wrote the following spec file with a very simple test:
spec/lib/integrations/spotify_integration_spec.rb
require 'rails_helper'
require 'integrations/spotify'
class SpotifyClientTester
include SpotifyIntegration
end
RSpec.describe SpotifyIntegration do
context 'Auxiliary methods' do
it 'Two calls to random_state_string shall generate two different strings' do
obj = SpotifyClientTester.new
s1 = obj.random_state_string
s2 = obj.random_state_string
expect(s1).not_to eq(s2)
end
end
end
But when I run it I get
undefined local variable or method base_url for SpotifyIntegration:Module (NameError)
I am not sure about what I am missing. Maybe I should use extend instead of include. I always make some confusion about this.
Can someone put me in the right path? I've been fighting this error for a whole afternoon.
You're misusing mixins. Use mixins for cases where classical inheritance is not suited to add a set of features to objects.
For example:
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
# ...
end
class Video < ApplicationRecord
include Commentable
end
class Hotel < ApplicationRecord
include Commentable
end
As you can see by this example you extend a module with other modules and include modules in classes. Using classical inheritance to add the shared behaviour would be awkward at best since the two classes are apples and pears.
In your specific case you should instead use classical inheritance and not mix the API client into the controller. Rather you controller should invoke it as a distinct object.
class APIClient
# Implement shared behavior for a REST api client
end
class SpotifyClient < APIClient
# ...
end
class FoosController < ApplicationController
def index
client = SpotifyClient.new
#foos = client.get_something
end
end
Why shouldn't you mix a API client into a controller or model? Because of the Single Responsibility Principle and the fact that using smaller parts that do a limited amount of things is preferable to creating god classes.
You need to extend APIClientBuilder if you want to use the methods defined here at class level in module SpotifyIntegration.
module SpotifyIntegration
extend APIClientBuilder
Also, base_url must be a class method too, def self.base_url
I'm trying to make simple view generator and using DRY principle, I don't want to have my own html (erb/haml/slim) templates. I'd like my generator to hook to existing template engine and pass it some arguments.
My view_generator.rb file looks like this:
class ViewGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
def some_custom_method
(...)
end
hook_for :template_engine, :as => :scaffold
end
Everything works fine like this. What I'd like to do in my some_custom_method is to add couple of attributes:
def some_custom_method
new_attribute = Rails::Generators::GeneratedAttribute.new("description")
new_attribute.type = :integer
attributes << new_attribute
end
What happens is that I insert new_attribute in attributes array, but when the hook_for is executed, the attribute variable reverts back to original one passed from command line.
How can I bypass this?
At the point some_custom_method is called, attributes are already set (via ARGV) and by checking the code I don't see a clear way to alter them from there. You can use another approach by overriding start class method in your generator and manipulate the args directly, like this:
class ViewGenerator < Rails::Generators::NamedBase
# your code ...
def self.start(args, config)
args.insert(1, 'description:integer') # 0 being the view name
super
end
end
I'm developing a ruby on rails app and I want to be able to excecute a method on every AR object before each save.
I thought I'd create a layer-super-type like this:
MyObject << DomainObject << ActiveRecord::Base
and put in DomainObject a callback (before_save) with my special method (which basically strips all tags like "H1" from the string attributes of the object).
The catch is that rails is asking for the domain_object table, which I obviously don't have.
My second attempt was to monkeypatch active record, like this:
module ActiveRecord
class Base
def my_method .... end
end
end
And put that under the lib folder.
This doesnt work, it tells me that my_method is undefined.
Any ideas?
Try using an abstract class for your domain object.
class DomainObject < ActiveRecord::Base
self.abstract_class = true
# your stuff goes here
end
With an abstract class, you are creating a model which cannot have objects (cannot be instantiated) and don't have an associated table.
From reading Rails: Where to put the 'other' files from Strictly Untyped,
Files in lib are not loaded when Rails starts. Rails has overridden both Class.const_missing and Module.const_missing to dynamically load the file based on the class name. In fact, this is exactly how Rails loads your models and controllers.
so placing the file in the lib folder, it will not be run when Rails starts and won't monkey patch ActiveRecord::Base. You could place the file in config/initializers, but I think there are better alternatives.
Another method that I used at a previous job for stripping HTML tags from models is to create a plugin. We stripped a lot more than just HTML tags, but here is the HTML stripping portion:
The initializer (vendor/plugins/stripper/init.rb):
require 'active_record/stripper'
ActiveRecord::Base.class_eval do
include ActiveRecord::Stripper
end
The stripping code (vendor/plugins/stripper/lib/active_record/stripper.rb):
module ActiveRecord
module Stripper
module ClassMethods
def strip_html(*args)
opts = args.extract_options!
self.strip_html_fields = args
before_validation :strip_html
end
end
module InstanceMethods
def strip_html
self.class.strip_html_fields.each{ |field| strip_html_field(field) }
end
private
def strip_html_field(field)
clean_attribute(field, /<\/?[^>]*>/, "")
end
def clean_attribute(field, regex, replacement)
self[field].gsub!(regex, replacement) rescue nil
end
end
def self.included(receiver)
receiver.class_inheritable_accessor :strip_html_fields
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
end
Then in your MyObject class, you can selectively strip html from fields by calling:
class MyObject < ActiveRecord::Base
strip_html :first_attr, :second_attr, :etc
end
The HTML stripping plugin code already given would handle the specific use mentioned in the question. In general, to add the same code to a number of classes, including a module will do this easily without requiring everything to inherit from some common base, or adding any methods to ActiveRecord itself.
module MyBeforeSave
def self.included(base)
base.before_save :before_save_tasks
end
def before_save_tasks
puts "in module before_save tasks"
end
end
class MyModel < ActiveRecord::Base
include MyBeforeSave
end
>> m = MyModel.new
=> #<MyModel id: nil>
>> m.save
in module before_save tasks
=> true
I'd monkeypatch ActiveRecord::Base and put the file in config/initializers:
class ActiveRecord::Base
before_create :some_method
def some_method
end
end