Bind ERB Template from more than one class - ruby

I am trying to interpolate ERB template from multiple objects in Ruby. It works fine if the source of variables is one class. Whats the best way of doing the interpolation when the ERB contains variables present in different classes.
Here is a strip down version of what I am trying to achieve:
#!/usr/bin/ruby
require 'erb'
require 'pp'
class Person
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
#first_name = first_name
#last_name = last_name
end
end
class Animal
attr_reader :animal_type
def initialize(type)
#animal_type = type
end
end
person = Person.new("John", "Doe")
animal = Animal.new("doggie")
template = "<%=first_name%> <%=last_name%> has a <%=type%>"
puts ERB.new(template).result(person.instance_eval { binding })
The above fail with undefined local variable or method 'type' which is correct, since that attribute belongs to object of Animal class.
One work around I have found is to create hashes and use merge to collapse them in to one, but that would be mean a lot of changes to the existing code. Is there a better way to achieve this?

You can use openstruct to make merging attributes a little more friendly so do don't have to rewrite your templates as much.
# in config/application.rb
require 'ostruct'
# in the file where you compile ERB
person = OpenStruct.new Person.new("John", "Doe").attributes
person.type = animal.type
With OpenStruct your object will be a hash but methods like person.first_name will still work. And you can add arbitrary key-vals, so you can make person.type return any value you want.

Related

Using a mixin inside a class

I would like to make use of a mixin (HTTParty) and access those mixed in methods from inside a class instance. With my current implementation, Ruby is telling me it doesn't know about the method "get":
class Recipe
include HTTParty
base_uri 'http://www.food2fork.com/api'
default_params :key => #key
format :json
def initialize(key)
#key = key
end
def search(term)
get('/search', query: { q: term})['recipes']
end
def self.for(term)
get('/search', query: { q: term})['recipes']
end
end
Scoping is still a big point of confusion for me in Ruby. I am sure this is trivial to the right person. Most examples of a mixin make use of this with a static reference like
Recipe.for('term')
That's not very OO to me so I want to do it like:
recipes = Recipe.new('key')
recipes.search('chocolate')
get is a class method of HTTParty. Rewrite your instance method search like this:
def search(term)
self.class.get('/search', query: { q: term})['recipes']
end

Ruby class initialize override module initialize

I am using super to pass arguments to the parent initialize method, which is not called by default. This is what it looks like. (Notice the use of super on the last two arguments)
module Pet
def initialize name, is_pet
#is_pet = is_pet
if is_pet
#name = name
else
#name = "Unnamed"
end
end
def pet?
return #is_pet
end
def get_name
return #name
end
end
class Dog
include Pet
def initialize tricks, name, is_pet
#tricks = tricks
super name, is_pet
end
def get_tricks
return #tricks
end
end
Here's what I can do with it:
d = Dog.new ["roll", "speak", "play dead"], "Spots", true
d.pet? #=> true
d.get_tricks #=> ["roll", "speak", "play dead"]
d.get_name #=> "Spots"
It works fine, but I'm just wondering if there's a better way to do this.
It is not a good programming practice to hard code a fixed string like "Unnamed" as the value for #name. In such case, you should assign nil, and do whatever modification to it when you print it. Suppose you do this.
Then is_pet can be deduced from whether name is nil or not, so it is redundant to have that as an instance variable. You can simply apply !! to name in order to get is_pet. Therefore, you should get rid of such instance variable.
You have get_ prefixes as getter methods, but in Ruby, it is a better practice to have the same name as the instance variables (without the atmark) as the getter name.
This will give you:
module Pet
attr_reader :name
def initialize name; #name = name end
end
class Dog
include Pet
attr_reader :tricks
def initialize tricks, name
#tricks = tricks
super(name)
end
end
d = Dog.new ["roll", "speak", "play dead"], "Spots"
d.tricks #=> ["roll", "speak", "play dead"]
d.name #=> "Spots"
!!d.name #=> true (= `is_pet`)
Do not write code that calls super to get into an included module. Don't write modules that will expect children to call super. That's not the point of modules.
It's good object oriented style to not ask about what things are. Look up "tell, don't ask" and duck typing in general.
If you want to provide a default initialize method, you probably want inheritance. But there are occasionally valid use cases for overriding initialize in a module. The idiomatic thing to do here is a hook method:
module Pet
def initialize(options = {})
#name = options[:name]
post_initialize(options)
end
def post_initialize(options = {})
# can be overridden in including modules
end
end
class Dog
include Pet
def post_initialize(options = {})
#tricks = options[:tricks]
end
end
dog = Dog.new(name: "Fido", tricks: ["play dead", "roll over"])
A module is for including some shared behavior among many different things. It's good to consider it like an adjective describing what you might do with a class that includes it. Words that end in "-able" (like Enumerable or Comparable), describing a receiving class, or "-or" (Iterator, Interactor), describing a doing class, are good candidates for being modules.

Variable scope definitions and inheritance

I'm working on a extended search feature for my webpage.
I looked at ransack, however it's lacking some functionalities I need, makes the url-query string very long and has some bugs (reported).
Thus I started to implement my own hack.
First I want to present my idea, afterwards I want to ask kindly how to fix my issue and in the end if there are other ways to improve this.
The idea:
A model defines something like this (additionally, the model is inside an engine):
module EngineName
class Post < ActiveRecord::Base
search_for :name, :as => :string do |b, q|
b.where{name =~ "%#{q}%"}
end
end
end
:name is to define the query-param to use e.g. this would be ?q[name]=something
I know that this is not fully generic like ransack, but well...
:as is to build up the correct form-tag. :string would be for text_field, :integer for number_field and so on. I want to extend it further to implement auto-generating of collections for associations etc.
Now the block is a simple scope to use.
I run into several shortcomings with ransack when building up complex queries (like with count() etc.). Now I can specify my own optimized query in squeel.
I extended ActiveRecord::Base to set up the logic (the global one, not inside the engine. I want to use it everywhere).
I defined a scope :search so I can use Model.search(param[q]) like in ransack.
Also I tried to keep a list of keys which are "searchable" defined by the search_for calls.
class ActiveRecord::Base
##searchable_attributes = Hash.new({})
def self.search_for(name, *opts, &search_scope)
return unless search_scope
##searchable_attributes[name] = {
:type => opts[:as],
:condition => search_scope
}
unless ##searchable_attributes.has_key? :nil
##searchable_attributes[:nil] = Proc.new { scoped }
end
end
scope :search, lambda {|q|
next unless q.kind_of?(Hash)
base = ##searchable_attributes[:nil].call
q.each do |key, search|
next unless base.class.searchable_attributes.has_key?(key)
base = ##searchable_attributes[key][:condition].call(base, search)
end
base
}
end
Now the issues:
It has mostly to do with inheritance of the classes. But even after reading and trying 3, 4 it does not worked.
Please take a look at the second line in the scope :search.
There I'm calling the simple Proc I definied above which only includes "scoped"
This is to get arround the issue that self returns "ActiveRecord::Base" and not the model itself like "Post" or "Comment".
It's because the scope is called on the Base class on inheritance, however I did not find anything to fix this.
As search_for is called on the model itself (e.g. Post) the scope-model returned there is "the right one".
Does anyone know how to circumvent this?
The next question would be, how to store the list of "searchable" scopes. I used ##variables. But as they are shared within every subclass, this would be a no-go.
However, it needs to be static as the search_for is called without initialize a instance (isn't it?)
Last but not least, it is somekind horrible to always specify the base-model to use on every scope so that I can chain them together.
Is there any other possibilities to improve this?
Ok, it seems I got it finally myself my putting several other answers from other questions together.
Model:
module EngineName
class Post < ActiveRecord::Base
searchable
search_for :name, :as => :string do |b, q|
b.where{name =~ "%#{q}%"}
end
end
end
My "Plugin" currently as an initializer:
class ActiveRecord::Base
def self.searchable
include Searchable
end
end
module Searchable
def self.included(base)
base.class_eval {
##searchable_attributes = Hash.new({})
def self.search_for(name, opts)
return unless block_given?
##searchable_attributes[name] = {
:type => opts[:as],
:condition => Proc.new
}
end
# Named scopes
scope :search, lambda {|q|
next unless q.kind_of?(Hash)
base = self.scoped
q.each do |key, search|
key = key.to_sym
next unless ##searchable_attributes.has_key?(key)
base = ##searchable_attributes[key][:condition].call(base, search)
end
base
}
}
end
end
Hope it'll help some others working on the same problem.
Rails provides a helper for class_attribute. This provides inheritable class attributes, but allows subclassess to "change their own value and it will not impact parent class". However a hash which is mutated using []= for example would effect the parent, so you can ensure that a new copy is made when subclassing using rubys inherited method
Therefore you could declare and initialise on the base class like so:
module Searchable
extend ActiveSupport::Concern
included do
class_attribute :searchable_attributes
end
module ClassMethods
def inherited(subclass)
subclass.searchable_attributes = Hash.new({})
end
def search_for(name,opts)
return unless block_given?
searchable_attributes[name] = {
:type => opts[:as],
:condition => Proc.new
}
end
end
end
Note that I used ActiveSupport::Concern to gain the neater syntax for defining stuff directly on the class and also mixing in class methods. Then you can simply add this to active record base:
ActiveRecord::Base.send(:include, Searchable)
now any classes get their own attributes hash:
class Post < ActiveRecord::Base
search_for :name, :as => :string do |b, q|
b.where{name =~ "%#{q}%"}
end
end

Rails Model without a table

I want to create a select list for lets say colors, but dont want to create a table for the colors. I have seen it anywhere, but can't find it on google.
My question is: How can I put the colors in a model without a database table?
Or is there a better rails way for doing that?
I have seen someone putting an array or a hash directly in the model, but now I couldn't find it.
class Model
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :whatever
validates :whatever, :presence => true
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
attr_accessor will create your attributes and you will create the object with initialize() and set attributes.
The method persisted will tell there is no link with the database. You can find examples like this one:
http://railscasts.com/episodes/219-active-model?language=en&view=asciicast
Which will explain you the logic.
The answers are fine for 2013 but since Rails 4 all the database independent features of ActiveRecord are extracted into ActiveModel. Also, there's an awesome official guide for it.
You can include as many of the modules as you want, or as little.
As an example, you just need to include ActiveModel::Model and you can forgo such an initialize method:
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
Just use:
attr_accessor :name, :age
The easiest answer is simply to not subclass from ActiveRecord::Base. Then you can just write your object code.
What worked for me in Rails 6:
class MyClass
include ActiveModel::Model
attr_accessor :my_property
end
If the reason you need a model without an associated table is to create an abstract class real models inherit from - ActiveRecord supports that:
class ModelBase < ActiveRecord::Base
self.abstract_class = true
end
If you want to have a select list (which does not evolve) you can define a method in your ApplicationHelper that returns a list, for example:
def my_color_list
[
"red",
"green",
"blue"
]
end

methods created by attr_accessor are not available to sub classes

http://gist.github.com/172341 ( stackoverflow was breaking the formatting )
In the following case method name created by Human is not available to Boy. Is my understanding correct that attr_accessor methods are not
available to subclasses. I need to use superclass to access the method added by attr_accessor.
What you're looking for is cattr_accessor which fixes this specific problem:
http://apidock.com/rails/Class/cattr_accessor
Here's your example, fixed:
class Human
def self.age
#age = 50
end
def self.age=(input)
#age = input
end
cattr_accessor :name
self.name = 'human'
end
class Boy < Human
end
puts Human.age
puts Boy.age
puts Human.name
puts Boy.superclass.name
puts Boy.name # => 'human'
Human and Boy are two different objects. Two objects can never share a single instance variable. They do both have the method, but the method will access the appropriate ivar for the object.
Rails class_attribute method would be better in this case.
Rails Guide

Resources