I am trying to modify a gem (Devise token auth to be precise) to suit my needs. For that I want to override certain functions inside the concern SetUserByToken.
The problem is how do I override that?
I don't want to change the gem files. Is there an easy/standard way of doing that?
Bear in mind here that a "concern" in Rails is just a module with a few programmer conveniences from ActiveSupport::Concern.
When you include a module in a class the methods defined in the class itself will have priority over the included module.
module Greeter
def hello
"hello world"
end
end
class LeetSpeaker
include Greeter
def hello
super.tr("e", "3").tr("o", "0")
end
end
LeetSpeaker.new.hello # => "h3ll0 w0rld"
So you can quite simply redefine the needed methods in ApplicationController or even compose a module of your own of your own without monkey patching the library:
module Greeter
extend ActiveSupport::Concern
def hello
"hello world"
end
class_methods do
def foo
"bar"
end
end
end
module BetterGreeter
extend ActiveSupport::Concern
def hello
super.titlecase
end
# we can override class methods as well.
class_methods do
def foo
"baz"
end
end
end
class Person
include Greeter # the order of inclusion matters
include BetterGreeter
end
Person.new.hello # => "Hello World"
Person.foo # => "baz"
See Monkey patching: the good, the bad and the ugly for a good explanation why it often is better to overlay your custom code on top of a framework or library rather than modifying a library component at runtime.
You can monkey patch concerns like any other module:
module DeviseTokenAuth::Concerns::SetUserByToken
# Your code here
end
If you want to extend the behavior of an existing method, try using an around alias:
http://rubyquicktips.com/post/18427759343/around-alias
Related
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 am new to ruby and trying to override the omniauth library method. I would like to make it support env.header Please see this pull request on omniauth. So how can I make this changes in my project?
In Ruby classes can simply override any methods provided by a mixin (an included module):
module Strategy
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def hello
"Hello from Strategy"
end
end
end
class MyStrategy
include Strategy
def self.hello
"Hello from MyStrategy"
end
end
puts MyStrategy.hello
Which means that a Strategy author can simply override any of the methods when authoring a strategy class. So you can simply override the request_call method in your strategy instead which is a better alternative since most strategies do not need all the request headers to be stored in the session.
Since OmniAuth::Strategy is a module rather than a class you cannot use super to call the original method. What you can do instead is to alias the mixin method before overriding it in your class:
module OmniAuth
class MyStrategy
include Strategy
class << self
alias :original_request_call :request_call
end
def self.request_call
session['omniauth.headers'] = headers
original_request_call
end
# ...
end
end
The weird alias syntax is because its not a method but rather syntactic sugar.
I have this module that is a part of a gem I'm writing. I currently use it as follows:
gem 'foobar' # Gemfile
class Baz < ActiveRecord::Base
include Foo::Bar
say
end
module Foo
module Bar
module ClassMethods
def say
"hello"
end
end
extend ClassMethods
end
end
To make say work, I have to include Foo::Bar before calling it. Is there anyway to call say without first having to include the module? (Have it do the include for me?) I see other gems just magically add methods to classes without using include--just a matter of adding the gem and running bundle. How does this happen?
If you want the say method to be general and not specific to objects, make it a class method:
module Foo
module Bar
def self.say
"hello"
end
end
end
Then you can call it directly:
class Baz < ActiveRecord::Base
Foo::Bar.say
end
Edit: To answer your new question (regarding the gem), you could re-open the ActiveRecord::Base class and define the methods there, although doing it with a separate module is the best way (cleaner and semantically correct).
I'm a total newbie at ruby (java background) so I'm sorry if this is a really dumb question.
I'm working through some tutorials on modules and they seem somewhat similar to static classes. The bit I'm having trouble wrapping my head around is why you would do something like the following:
module ExampleModule
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def myMethod
end
end
end
Why wouldn't you just put the methods in ClassMethods into ExampleModule and save adding the method hook. I'm sure I'm missing something really basic but I've been at this for a while now so I feel the need to ask.
It's a ruby idiom. It's useful when you want a module that:
adds some instance methods to a class
adds class methods in the same time /like Java static methods/
in the same time
Example:
module ExampleModule
def self.included(base)
puts 'included'
base.extend(ClassMethods)
end
def foo
puts 'foo!'
end
module ClassMethods
def bar
puts 'bar!'
end
end
end
class ExampleClass
include ExampleModule
end
ExampleClass.bar
ExampleClass.new.foo
If you only want to add class methods, you don't need this idiom, you can just add a method to your module and 'extend' it instead of including it.
When on Rails, this idiom is obsolete and you should use ActiveSupport::Concern instead.
The patter you use here is common when both class methods and instance methods are included via a module in ruby. It gives you the advantage of just having to write
include ExampleModule
for including instance methods and extending class methods instead of
# include instance methods
include ExampleModule
# extend class methods
extend ExampleModule::ClassMethods
So, if used just to extend the class with some methods, my personal preference is to use extend directly.
module ExtensionAtClassLevel
def bla
puts 'foo'
end
end
class A
extend ExtensionAtClassLevel
end
A.bla #=> 'foo'
If both instance and class methods are added, I use the include hook you described.
Some rubyists tend to prefer using extend via the include hook to pure extend, which has no reason if you are just adding class methods like in your example.
Can we use include statement to include a module anywhere within the class or does it has to be at the beginning of the class?
If I include the module at the beginning of my class declaration, method overriding works as expected. Why is it not working if i include at the end as described below?
# mym.rb
module Mym
def hello
puts "am in the module"
end
end
# myc.rb
class Myc
require 'mym'
def hello
puts "am in class"
end
include Mym
end
Myc.new.hello
=> am in class
When you include a module, its methods do NOT replace methods defined in this class, but rather they are injected into inheritance chain. So, when you call super, method from included module will get called.
They will behave almost the same way with other modules. When a module gets included, it is placed right above the class in inheritance chain, with existing modules placed above it. See example:
module Mym
def hello
puts "am in the module"
end
end
module Mym2
def hello
puts "am in the module2"
super
end
end
class Myc
include Mym
include Mym2
def hello
puts "im in a class"
super
end
end
puts Myc.new.hello
# im in a class
# am in the module2
# am in the module
For more info see this post.
Also read this: http://rhg.rubyforge.org/chapter04.html