Ruby: Mildly 'exotic' inheritance doesn't work? - ruby-on-rails

I would like to factor a bunch of common code from subclasses into a superclass method. The superclass method must refer to a nonexistent (in the superclass) method that will be defined in the subclasses. But I can't get this to work.
This is one try out of many multiple variations I have tried:
class Superclass
def chunk_of_code
# <code...>
nonexistant_superclass_method_defined_in_subclass params
# <more code...>
end
end
class Subclass < Superclass
def nonexistant_superclass_method_defined_in_subclass params
# whatever...
end
end
Subclass.new.chunk_of_code params
This doesn't work. Other variations don't work either. Is this kind of coding possible in Ruby (I thought it was)? I did this kind of thing all the time working in Smalltalk.
Any way to achieve what I want? Please avoid advising me to use "mix-ins" or "modules," as I'd just like to just learn and use Ruby's inheritance for right now.
*Running latest version of Ruby.
Thanks.
EDIT: This is in a Rails app. The superclass is ApplicationController.
EDIT: Here is actual code from one of many iterations I've tried to do this. This particular example craps out with "undefined method `each' for nil:NilClass" in the view, apparently because the whole thing is running in the context of the super (where it isn't defined) instead of the sub, or at least that's my interpretation:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :authenticate_registration!
# models and x defined in subclass
def index
models = x.where registration_id: current_registration.id
respond_to do |format|
format.html # index.html.erb
format.json { render json: models }
end
end
# more code here...
# ...
end
class PositionsController < ApplicationController
def x
Position
end
def models= blah
#positions = blah
end
# more code here...
# ...
end

The only error you have here is the definition of the chunk_of_code. This method has to accept some formal parameter, like:
def chunk_of_code params
And then you're free to call it:
params = 'something'
Subclass.new.chunk_of_code params

Your error is actually nothing to do with inheritance and is on this line
models = x.where registration_id: current_registration.id
This is potentially ambiguous: does this mean call the method models= or does it mean assign to a local variable called models? In this (and similar) situation ruby assumes you're trying to deal with the local variable. If you want to call the method instead you need to do
self.models = x.where registration_id: current_registration.id
Since you models= method doesn't get called, #positions is nil and I assume your view tries to use it.
You might also be interested in gems such as make_resourceful that handle this common controller stuff.

Over on the model side of Rails, I routinely use:
class GenericModel < ActiveRecord::Base
self.abstract_class = true
# define all the generic behavior methods/ model stubs you want.
# model-specific classes can override to their hearts content
# or use the inherited implementation
end
class Feature < GenericModel
# model specific methods, and/or overrides
end
and I use a
class GenericController
# basic show implementation for example
def show
#object = params[:controller].singularize.camelcase.constantize.find(params[:id])
respond_to do |format|
format.pdf { render :layout => false }
format.html # show
format.xml { render :xml => #object.to_xml }
end
end
end
If a specific model's show behavior isn't any different than generic, then that method doesn't appear in that 'model'_controller.rb.

A way to do this is to define it in the parent and raise NotImplementedError as the behavior of the method. By the way what you are trying to do is create an abstract class, which is facilitated more in certain other languages like Java.

Related

Rails 5 API: custom hidden responder that would process value returned by the action

I have rails 5 based api app, using fast_jsonapi
and after a while I observe that all most all my actions are having one common pattern
def action_name
#some_object.perform_action_name # this returns #some_object
render json: ControllerNameSerializer.new(#some_object).to_h
end
I do not wish to write the last render line here and it should work, for that I want that the returned value by the action should be processed by any hidden responder like thing, Serializer klass can be made out looking at controller name.
Perhaps this could be achieved by adding a small middleware. However at first, I find it not a good idea/practise to go for a middleware. In middleware, we do get rendered response, we need a hook prior to that.
I would imagine like
class SomeController ...
respond_with_returned_value
def action_name
#some_object.perform_action_name # this returns #some_object
end
Any suggestions?
Note, do not worry about error/failure cases, #some_object.errors could hold them and I have a mechanism to handle that separately.
Sketched out...
class ApplicationController < ...
def respond_with_returned_value
include MyWrapperModule
end
...
end
module MyWrapperModule
def self.included(base)
base.public_instance_methods.each do |method_name|
original_method_name = "original_#{method_name}".to_sym
rename method_name -> original_method_name
define_method(method_name) { render json: send(original_method_name) }
end
end
end
Seems like there really should be some blessed way to do this - or like someone must have already done it.

How can I pass in a variable defined in a class into a Rails form?

If I have a controller
class MyController < ApplicationController
vals = [...]
def new
...
end
def create
if save
...
else
render 'new'
end
end
how can I make the "vals" variable accessible to both methods? In my "new" view I want to use the "vals" variable for a drop-down menu, but rails is giving me errors. Of course, I could just copy the variable twice, but this solution is inelegant.
As Sebastion mentions a before_ hook / callback is one way to go about it, however as you mentioned it is for a dropdown menu, I am guessing it is a non-changing list, if so I would suggest perhaps using a Constant to define the values, perhaps in the model they are specific to, or if it is to be used in many places a PORO would do nicely to keep things DRY. This will then also allow you to easily access it anywhere, for example in models for a validation check, or to set the options of the dropdown menu in the view, or in the controller if you so wish:
class ExampleModel
DROPDOWN_VALUES = [...].freeze
validates :some_attr, inclusion: { in: DROPDOWN_VALUES }
end
class SomeController < ApplicationController
def new
# can call ExampleModel::DROPDOWN_VALUES here
end
def create
# also here, anywhere actually
end
end
You could use a before_* callback, e.g a before_action, this way you sets your vals variable as an instance one and make it to be available for your both new and create methods, something like:
class SomeController < ApplicationController
before_action :set_vals, only: [:new, :create]
def new
...
# #vals is available here
end
def create
if save
...
# and here
else
render 'new'
end
end
private
def set_vals
#vals = [...]
end
end
A different way from the ones before (although probably just having the instance method is preferred as in Sebastian's solution) is, take advantage of the fact that functions and local variables are called in the same way in ruby and just write:
def vals
#vals ||= [...]
end
and you should be able to access it on the controllers (not the views). If you want it on your views as well you can call at the beginning of the controller
helper_method :vals
If you want to be able to modify vals using vals="some value"
def vals= vals_value
#vals = vals_value
end
Take into account that probably using the intance variable as in Sebastian's solution is preferred, but if you, for whatever reason, are settled on being able to call "vals" instead of "#vals" on the view (for example if you are using send or try), then this should be able to do it for you.
Define in corresponding model
Eg :
class User < ActiveRecord::Base
TYPES = %w{ type1 type2 type3 }
end
and use in ur form like
User::TYPES
=> ["type1", "type2", "type3"]
You can reuse this anywhere in the application.

Rails N+1 query : monkeypatching ActiveRecord::Relation#as_json

Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.

Ruby on Rails - Creating and Using a Custom Method

I am rather new to Rails, and would greatly appreciate any bit of help. I have created the following method:
def name_fix
name = self.split
mod_name = []
name.each do |n|
n.split("")
if n[0]
n.upcase
else
n.downcase
end
mod_name.push(n)
end
mod_name.join
end
I would like to use this method in my Controller as such:
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name].name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
How can I go about accomplishing this? Will this method reside within my Model or Controller? Previously, I've run into an undefined method error.
Note: I'm sure that there is a way to better write my code. I am grateful for help with that as well.
#app/models/patient.rb
class Patient < ActiveRecord::Base
protected
def name=(value)
mod_name = []
value.split.each do |n|
n.split("")
type = n[0] ? "up" : "down"
n.send("#{type}case")
mod_name.push(n)
end
#name = mod_name.join
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient = Patient.new patient_params
#patient.save ? redirect_to(patients_path) : render(:new)
end
private
def patient_params
params.require(:patient).permit(:name)
end
end
What you're doing is trying to override the setter method, which can be done using the above code. Much more efficient and out of the way.
I have created the following method
Since you're new, let me explain something else.
It is important to note where you're using this method.
You've currently put it in the model, which means you'll have to call it to manipulate some attribute / functionality of any object created with said model.
--
Models - in Rails - build the objects which populate your app. Ruby is an object orientated language, which means that every element of your program should revolve around data objects in some degree.
As you can see above, the method of building objects in your system is really about invoking classes. These classes contain methods which can be called, either at class level (IE invoking the class through the method), or at instance level (IE calling a method on an already invoked object).
This is where you get "class" methods (Model.method) and "instance" methods (#model.method) from:
#app/models/patient.rb
class Patient < ActiveRecord::Base
def explode
#this is an instance method
puts "Instance Explode"
end
def self.explode
#this is a class method
puts "Exploded"
end
end
Thus you can call the following:
#patient = Patient.find params[:id]
#patient.explode #-> "Instance explode"
Patient.explode #-> "Exploded"
--
This is important because it gives you a strict framework of where you should, and shouldn't use methods in your models.
It explains why you have controllers & helpers, and allows you to formulate the best way to structure your application as to get the most out of the least code.
For example...
Your use of #patient.name = params[:params][:name].name_fix is incorrect.
It's wrong because you're calling the instance method .name_fix on a piece of data totally unrelated to your model. If you wanted to use .name_fix in a general sense like this, you'd probably use a helper:
#app/helpers/patients_helper.rb
class PatientsHelper
def name_fix value
# stuff here
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient.name = name_fix params[:patient][:name]
end
end
Since you're using the method to populate the .name attribute of your model, it makes sense to override the name= setter. This will not only provide added functionality, but is much smoother and efficient than any other way.
Methods that are called directly are best put in the Controller (or in ApplicationController if you think more than one controller might want to use it).
These are methods like
# app/controllers/my_controller.rb
def foo(bar)
# do something here
end
def create
id = params[:id]
value = foo(id)
end
If you want a chained method that acts as a property method of whatever you're calling it on. Those are characteristic of how Models work - you have your main model and you call attributes or methods on the instance of that model.
# app/models/my_model.rb
def full_name
first_name + " " + last_name
end
# app/controller/my_controller.rb
def create
id = params[:id]
model = MyModel.find(id)
full_name = model.full_name
end
In your case, you want to call name_fix ON whatever is returned by params[:params][:name], which is (I'm guessing) a String.
You have two options
Modify the String class to define a method named name_fix. I highly recommend against this. It's call "monkeypatching" and shouldn't be done without good reason. Just letting you know you can do it in some cases.
Use a direct method in your controller or ApplicationController like the first example above.
#patient.name = name_fix(params[:params][:name])
Edit: As for your request about a better way to write your code... that's difficult to teach or convey in one answer. I'd say read some open source projects out there to see how people write Ruby and some common idioms used to clean up the code. To get you started, here's how I'd re-write your code
def create
#patient = Patient.new(params[:patient])
# 1. Be descriptive with your method names. `name_fix` is vague
# 2. Why is `:name` nested under another `[:params]` hash?
#patient.name = capitalize_name(params[:name])
if #patient.save
# 1. I think `patient_path` has to be singular
# 2. It needs a `Patient` object to know how to construct the URL
# e.g. `/patients/:id`
redirect_to patient_path(#patient)
else
render :new
end
end
def capitalize_name(full_name)
# Example: julio jones
#
# 1. `split` produces an array => ["julio", "jones"]
# 2. `map` applies a function (`capitalize`) to each element
# => ["Julio", "Jones"]
# 3. `join(" ")` rejoins it => "Julio Jones"
full_name.split.map(&:capitalize).join(" ")
end
Assuming your goal with the name_fix method is just to capitalize the first letter of each name, you could just pass name as an argument and store it as a private method on the Controller:
# app/controllers/patient_controller.rb
private
def name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do
#patient.name = name_fix(params[:params][:name])
in the create method.
OR, you could store this method in the model:
# app/models/patient.rb
def self.name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do this instead, in the controller:
#patient.name = Patient.name_fix(params[:params][:name])
I would also suggest renaming your name_fix method to something like capitalize_name.
update your create method as below
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name]
#patient = #patient.name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
It should work.

Why is the strong_parameters method private?

The rails scaffolds give you the resource_params method as private by default:
private
def person_params
params.require(:person).permit(:name, :age)
end
I understand why strong_parameters is a good thing. I also understand that this prevents the method from being accessed outside the controller, but are there any real dangers to making this method public, or what is the reasoning behind having this as a private method? It would be nice to be able to send that method to the controller from a gem that extends ActionController.
In other words, why not access the method outside of the controller? For example, if I have a separate controller that handles authorization and I want to pass an instance variable back to the original controller that contains the initialized object.
Because this method is not called from any external objects.
Mass assignment protection is not connected with 'person params' method visibility, it is just best practice for application design
Controllers using to handless only one request by app design. You should not call methods from one controller in another. If you want to share methods for several controllers, you can use inheritance, mixins, or service objects
inheritance
class BaseController < ApplicationController
private
def shared_method
end
end
class UsersController < BaseController
def index
shared_method
end
end
mixin
module SomeMixin
extend ActiveSupport::Concern
included do
def shared_method
end
end
end
class UsersController < ApplicationController
include SomeMixin
def index
shared_method
end
end
service object
class SomeService
def shared_method(params)
# process params
end
end
class UsersController < ApplicationController
include SomeMixin
def index
SomeService.new.shared_method(params)
end
end
It wasn't always this way and it's there to protect you. Check out this great blog post on the subject.
Here are some relevant snippets:
Problem with Mass-Assignment: Security vulnerability
Mass-assignment saves us the need to assign values to each attribute
of the model, but it can create problems. Since we aren’t restricting
which attributes can be set nor are we checking the valus of these
attributes, a malicious hacker could assign any value to any
attribute. In our example, he could set the value of admin true,
making himself a super user.
Here is what the url might look like
http://www.example.com/user/signup?user[name]=ow3ned&user[admin]=1
Users are able to exploit this if they know even a little bit about your models and cause issues.
In most languages there are classifications in a class for methods, attributes or whatever else it may contain.
Depending on language or inheritence, default behaviour might be public, private...
In ruby, classes by default contain public methods and attributes. You need to specify if a method is private.
The public method or attribute:
This is accessible from out of the class or the instance of the class. So, if you have class:
class Foo
def my_id
10
end
private
def my_class
"Bar"
end
public
def my_friend
"Zonk"
end
end
Then:
2.0.0p247 :001 > #foo = Foo.new
2.0.0p247 :002 > #foo.my_id
=> 10
2.0.0p247 :003 > #foo.my_class
NoMethodError: undefined method `my_class' for #<Foo:0x000000045a53f8>
2.0.0p247 :004 > #foo.my_friend
=> "Zonk"
You see than you can change from private to public as you wish, though maybe not a very good idea.

Resources