Keep namespacing of a controller when including a model in Rails - ruby-on-rails

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.

Related

Having trouble with specs for a Ruby mixin module

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

Using classes from submodules in module tree

I have the following structure:
api/
api/
module1/
controller/
controller1.rb
model/
model1.rb
serializer/
serializer1.rb
controller1.rb looks like this:
class Api::Module1::Controller::Controller1 < ApplicationController
def index
# how do I prevent having to use the prefixed full module path ?
render json: Api::Module1::Model::Model1.all, each_serializer: Api::Module1::Serializer::Serializer1
end
end
model1.rb looks like this:
class Api::Module1::Model::Model1 < ActiveRecord::Base
end
My question:
How do I circumvent using Api::Module1::Model as a prefix to use my model classes in my controller ? I would like to use just Model1.all instead of Api::Module1::Model::Model1.all.
I tried using include Api::Module1::Model at the top of my controller1.rb but that doesn't work either because if I then use Model1.all it obviously tries to use the module path from the controller, i.e. Api::Module1::Controller::MenuController::Model, which is not what I intend to use obviously.
You should be able to do Model::Model1.all (omitting the common root). Is this good enough for you? Imagine that you also have Serializer::Model1 and FormObject::Model1, so you can't just use Model1. Otherwise, how will you tell these apart?
module Api
module MyApp
module Controller
class Controller1
attr_reader :model
def initialize
#model = Model::User.new
end
end
end
end
end
module Api
module MyApp
module Model
class User
end
end
end
end
c = Api::MyApp::Controller::Controller1.new
c.model # => #<Api::MyApp::Model::User:0x007fa36411e888>

Returning Module Class instead of Model Class with self.class Ruby/Rails

I am trying to DRY my code by implementing modules. However, I have constants stored in models (not the module) that I am trying to access with self.class.
Here are (I hope) the relevant snippets:
module Conversion
def constant(name_str)
self.class.const_get(name_str.upcase)
end
end
module DarkElixir
def dark_elixir(th_level)
structure.map { |name_str| structure_dark_elixir(name_str, th_level) if constant(name_str)[0][:dark_elixir_cost] }.compact.reduce(:+)
end
end
class Army < ActiveRecord::Base
include Conversion, DarkElixir
TH_LEVEL = [...]
end
def structure_dark_elixir(name_str, th_level)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * TH_LEVEL[th_level][sym_qty(name)]
end
When I place the structure_dark_elixir method inside the DarkElixir module, I get an error, "uninitialized constant DarkElixir::TH_LEVEL"
While if I place it inside the Army class, it finds the appropriate constant.
I believe it is because I am not scoping the self.constant_get correctly. I would like to keep the method in question in the module as other models need to run the method referencing their own TH_LEVEL constants.
How might I accomplish this?
Why not just use class methods?
module DarkElixir
def dark_elixir(th_level)
# simplified example
th_level * self.class.my_th_level
end
end
class Army < ActiveRecord::Base
include DarkElixir
def self.my_th_level
5
end
end
Ugh. Method in question uses two constants. It was the second constant that was tripping up, not the first. Added "self.class::" prior to the second constant--back in business.
def structure_dark_elixir(name_str, th_lvl)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * self.class::TH_LEVEL[th_lvl][sym_qty(name_str)]
end

Ruby on Rails: shared method between models

If a few of my models have a privacy column, is there a way I can write one method shared by all the models, lets call it is_public?
so, I'd like to be able to do object_var.is_public?
One possible way is to put shared methods in a module like this (RAILS_ROOT/lib/shared_methods.rb)
module SharedMethods
def is_public?
# your code
end
end
Then you need to include this module in every model that should have these methods (i.e. app/models/your_model.rb)
class YourModel < ActiveRecord::Base
include SharedMethods
end
UPDATE:
In Rails 4 there is a new way to do this. You should place shared Code like this in app/models/concerns instead of lib
Also you can add class methods and execute code on inclusion like this
module SharedMethods
extend ActiveSupport::Concern
included do
scope :public, -> { where(…) }
end
def is_public?
# your code
end
module ClassMethods
def find_all_public
where #some condition
end
end
end
You can also do this by inheriting the models from a common ancestor which includes the shared methods.
class BaseModel < ActiveRecord::Base
def is_public?
# blah blah
end
end
class ChildModel < BaseModel
end
In practice, jigfox's approach often works out better, so don't feel obligated to use inheritance merely out of love for OOP theory :)

Re-using unit tests for models using STI

I have a number of models that use STI and I would like to use the same unit test to test each model. For example, I have:
class RegularList < List
class OtherList < List
class ListTest < ActiveSupport::TestCase
fixtures :lists
def test_word_count
list = lists(:regular_list)
assert_equal(0, list.count)
end
end
How would I go about using the test_word_count test for the OtherList model. The test is much longer so I would rather not have to retype it for each model. Thanks.
EDIT: I am trying to use a mixin as per Randy's suggestion. This is what I have but am getting the error: "Object is not missing constant ListTestMethods! (ArgumentError)":
in lib/list_test_methods.rb:
module ListTestMethods
fixtures :lists
def test_word_count
...
end
end
in regular_list_test.rb:
require File.dirname(__FILE__) + '/../test_helper'
class RegularListTest < ActiveSupport::TestCase
include ListTestMethods
protected
def list_type
return :regular_list
end
end
EDIT: Everything seems to work if I put the fixtures call in the RegularListTest and remove it from the module.
I actually had a similar problem and used a mixin to solve it.
module ListTestMethods
def test_word_count
# the list type method is implemented by the including class
list = lists(list_type)
assert_equal(0, list.count)
end
end
class RegularListTest < ActiveSupport::TestCase
fixtures :lists
include ::ListTestMethods
# Put any regular list specific tests here
protected
def list_type
return :regular_list
end
end
class OtherListTest < ActiveSupport::TestCase
fixtures :lists
include ::ListTestMethods
# Put any other list specific tests here
protected
def list_type
return :other_list
end
end
What works well here is that OtherListTest and RegularListTest are able to grow independently of each other.
Potentially, you could also do this with a base class but since Ruby does not support abstract base classes it isn't as clean of a solution.

Resources