Change argument before passing to hook in rails generator - ruby-on-rails

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

Related

Using partials when building custom generators

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?

Defaulting area_code to true for number_to_phone in rails

I'm using NumberHelper's number_to_phone method many times in my app. It looks like this...
number_to_phone(phone_number, area_code: true)
But there's never a place where I want the area_code to be false. How should I have it default to true?
One way to do this would be to write your own method that only takes a phone number argument and some options, merges the options with a default value for :area_code, and calls #number_to_phone. You could do this in ApplicationHelper like so:
# application_helper.rb
def num_to_phone(phone_number, opts={})
opts = {area_code: true}.merge(opts)
number_to_phone(phone_number, opts)
end
This way, you can just use your wrapper method without having to worry about trying to monkey patch the original one.
I guess this is what I was looking for.
# application_helper.rb
def formatted_phone(number, options={area_code: true})
number_to_phone(number, options)
end
1) You can set area_code: true through callback method like:-
Class Model
before_create :set_area_code_to_true
private
def set_area_code_to_true
self.area_code = true
end
end
2) You can set default value of area code, when added a new attribute in table through migration like:-
rails g migration add_default_value_to_table
def change
change_column :table_name, :area_code, :boolean, default: true
end

How can I dynamically add custom validators in rails?

I have a case where I need to add and remove custom validators for different instances of the same object.
For example...
class MyCustomValidator < ActiveModel::Validator
...
end
class Foo
include ActiveModel::Validations
validates_with MyCustomValidator
end
Context A:
Foo.new.valid? #=> This should use the custom validator
Context B:
f = Foo.new
f.class.clear_validators!
f.valid? #=> This should no longer call the custom validator
Context C:
f = Foo.new
f.class.clear_validators!
f.valid? #=> This should no longer call the custom validator
# This is where I need to do something to bring the validator back so I can run
f.valid? #=> This should use the custom validator
Is there a way to accomplish this?
You could do something like:
class Foo
include ActiveModel::Validations
validates_with MyCustomValidator, if: :use_custom_validators?
def use_custom_validators?
!#custom_validator_disabled
end
def without_custom_validators(&block)
prev_disabled, #custom_validator_disabled = #custom_validator_disabled, true
#custom_validator_disabled = true
instance_eval(&block)
#custom_validator_disabled = prev_disabled
end
end
f = Foo.new
f.valid? # Custom validator used
f.without_custom_validators do
valid? # Custom validator not used
end
f.valid? # Custom validator used
If the necessity to use a validator is defined by certain state of the object, you can use a state_machine gem and explicitly define a state flag. After that, you can define state-specific validations.
For that to work, you need to define two states with an extra validation only defined on one of them. Then you need to make an event that transitions from the "clean" to "validated" state (or whatever you call them).
Example:
class LockableSwitch < ActiveRecord::Base
state_machine initial: :off
state :off # It's probably redundant. I dunno.
# if you're not storing state as strings, you'd specify
# state :off, value: 0 # if `state` column was an integer
state :on do
# Can only be switched on if a key is inserted
validates :key, presence: true
end
event :switch do # An event that causes the apple to become ripe
transition :off => :on # `=>` justifies the use of that old syntax
transition :on => :off # The first matched transition is used
end
end
end
#l = LockableSwitch.new # :off by default
#l.switch! # fails, bang-method throws an error on failed transition
#l.key = "something"
#l.switch! # works
It's yet another library and extra memory/complexity (even though not too much), but it makes your code much clearer in "what it does".

Defining a Rails helper (or non-helper) function for use everywhere, including models

I have a function that does this:
def blank_to_negative(value)
value.is_number? ? value : -1
end
If the value passed is not a number, it converts the value to -1.
I mainly created this function for a certain model, but it doesn't seem appropriate to define this function in any certain model because the scope of applications of this function could obviously extend beyond any one particular model. I'll almost certainly need this function in other models, and probably in views.
What's the most "Rails Way" way to define this function and then use it everywhere, especially in models?
I tried to define it in ApplicationHelper, but it didn't work:
class UserSkill < ActiveRecord::Base
include ApplicationHelper
belongs_to :user
belongs_to :skill
def self.splice_levels(current_proficiency_levels, interest_levels)
Skill.all.reject { |skill| !current_proficiency_levels[skill.id.to_s].is_number? and !interest_levels[skill.id.to_s].is_number? }.collect { |skill| {
:skill_id => skill.id,
:current_proficiency_level => blank_to_negative(current_proficiency_levels[skill.id.to_s]),
:interest_level => blank_to_negative(interest_levels[skill.id.to_s]) }}
end
end
That told me
undefined method `blank_to_negative' for #
I've read that you're "never" supposed to do that kind of thing, anyway, so I'm kind of confused.
if you want to have such a helper method in every class in your project, than you are free to add this as a method to Object or whatever you see fits:
module MyApp
module CoreExtensions
module Object
def blank_to_negative
self.is_number? ? self : -1
end
end
end
end
Object.send :include, MyApp::CoreExtensions::Object
There are a few options:
Monkey-patch the method into ActiveRecord and it will be available across all of your models:
class ActiveRecord::Base
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
Add a "concern" module which you then mix into selected models:
# app/concerns/blank_to_negate.rb
module BlankToNegate
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
# app/models/user_skill.rb
class UserSkill < ActiveRecord::Base
include BlankToNegate
# ...
end
Ruby Datatypes functionality can be extended. They are not sealed. Since you wan to use it in all places why not extend FIXNUM functionality and add a method blank_to_negative to it.
Here's what I ended up doing. I put this code in config/initializers/string_extensions.rb.
class String
def is_number?
true if Float(self) rescue false
end
def negative_if_not_numeric
self.is_number? ? self : -1
end
end
Also, I renamed blank_to_negative to negative_if_not_numeric, since some_string.negative_if_not_numeric makes more sense than some_string.blank_to_negative.

How do I access options passed to an acts_as plugin (ActiveRecord decorator) from instance methods?

At the moment I store each option in its own class attribute but this leads to hard to read code when I need to access the passed options from instance methods.
For example if I pass a column name as an option I have to use self.send(self.class.path_finder_column) to get the column value from an instance method.
Notice I have prefixed the class attribute with the name of my plugin to prevent name clashes.
Here is a simple code example of a plugin which is passed an option, column, which is then accessed from the instance method set_path. Can the getters/setters be simplified to be more readable?
# usage: path_find :column => 'path'
module PathFinder
def path_finder(options = {})
send :include, InstanceMethods
# Create class attributes for options
self.cattr_accessor :path_finder_column
self.path_finder_column = options[:column]
module InstanceMethods
def set_path
# setter
self.send(self.class.path_finder_column + '=', 'some value')
# getter
self.send(self.class.path_finder_column)
end
end
end
end
ActiveRecord::Base.send :extend, PathFinder
You can generate all those methods at runtime.
module PathFinder
def path_finder(options = {})
# Create class attributes for options
self.cattr_accessor :path_finder_options
self.path_finder_options = options
class_eval <<-RUBY
def path_finder(value)
self.#{options[:column]} = value
end
def path_finder
self.#{options[:column]}
end
RUBY
end
end
ActiveRecord::Base.send :extend, PathFinder
Unless you need to store the options, you can also delete the lines
self.cattr_accessor :path_finder_options
self.path_finder_options = options
Note that my solution doesn't need a setter and a getter as long as you always use path_finder and path_finder=.
So, the shortest solution is (assuming only the :column option and no other requirements)
module PathFinder
def path_finder(options = {})
# here more logic
# ...
class_eval <<-RUBY
def path_finder(value)
self.#{options[:column]} = value
end
def path_finder
self.#{options[:column]}
end
RUBY
end
end
ActiveRecord::Base.send :extend, PathFinder
This approach is similar to the one adopted by acts_as_list and acts_as_tree.
To start with cattr_accessor creates a class variable for each symbol it's given. In ruby, class variables have their names prefixed with ##.
So you can use ##path_finder_column in place of self.class.path_finder_column.
However that's a moot point considering what I'm going to suggest next.
In the specific case presented by the code in the question. The combination getter and setter you've defined doesn't fit ruby conventions. Seeing as how you're essentially rebranding the accessors generated for the path_finder_column with a generic name, you can reduce it all to just a pair of aliases.
Assuming there's an error in the combo accessor (how is the code supposed to know whether to get or set), The finalized module will look like this:
module PathFinder
def path_finder(options = {})
send :include, InstanceMethods
# Create class attributes for options
self.cattr_accessor :path_finder_column
self.path_finder_column = options[:column]
alias :set_path, path_finder_column
alias :set_path=, "#{path_finder_column}="
end
module InstanceMethods
# other instance methods here.
end
end
You can use cattr_accessor to store the configuration value at a class level and use in all your instance methods. You can see an example at http://github.com/smsohan/acts_as_permalinkable/blob/master/lib/active_record/acts/permalinkable.rb
The code to look at is this:
def acts_as_permalinkable(options = {})
send :cattr_accessor, :permalink_options
self.permalink_options = { :permalink_method => :name, :permalink_field_name => :permalink, :length => 200 }
self.permalink_options.update(options) if options.is_a?(Hash)
send :include, InstanceMethods
send :after_create, :generate_permalink
end
Hope it helps!

Resources