Using classes from submodules in module tree - ruby-on-rails

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>

Related

Keep namespacing of a controller when including a model in 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.

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

Possible to DRY up the same method in two different controllers?

I'm have a Rails project which has an api part and the regular one. They have some common methods, for example, I have two controllers like so:
class PlacementController < ApplicationSafeController
def zip
file = ZipService::ZipGenerator.create_zip
send_data(file.read, type: 'application/zip')
File.delete(file.path)
end
end
and
class Api::ZipsController < ApiController
def single_zip
file = ZipService::ZipGenerator.create_zip
send_data(file.read, type: 'application/zip')
File.delete(file.path)
end
end
And both my ApiController and ApplicationSafeController inherit from ApplicationController. My question is, what is the best way to clean this up without making the root ApplicationController dirty? (by adding a new method there). Thanks!
You a module/concern to share code. Enclose shareable code in a module, then include that module in the required controllers. It's the Ruby way of doing this thing.
module Zippable
def zip
file = ZipService::ZipGenerator.create_zip
send_data(file.read, type: 'application/zip')
File.delete(file.path)
end
end
class PlacementController < ApplicationSafeController
include Zippable
#Example Usage
def show
zip
end
end
class Api::ZipsController < ApiController
include Zippable
end

Reusing code ruby on rails

I've got a module in my project in lib/. it's content is like this :
module Search
module Score
def get_score
return 'something'
end
end
end
This Search has many different modules I need to use Score. I realize I need to add require in my model (I'm trying to use this from model). So here is my code (model) :
require 'search'
class User < ActiveRecord::Base
def get_user_score
#tried this :
p Search::Score.get_score #error
#this as well
score_instance = Score.new #error
score = Search::Score.get_score # error undefined method `get_score'
end
end
So how do I reuse the code I have in other class (module)?
To get it working you can either mix the module into your class:
require 'search'
class User < ActiveRecord::Base
include Search::Score
def get_user_score
p get_score # => "something"
end
end
Or you can define the method inside your module similar to class methods:
module Search
module Score
def self.get_score
return 'something'
end
end
end
If you do that, you can call get_score like expected:
require 'search'
class User < ActiveRecord::Base
def get_user_score
p Search::Score.get_score # => "something"
end
end
See this tutorial for a more in depth explanation about modules in Ruby.
First, see "Best Practices for reusing code between controllers in Ruby on Rails".
About reuse code as a module, take a look at "Rethinking code reuse with Modularity for Ruby".
"Modules are crippled classes"
Modules are like crippled classes in Ruby. If you look into the inheritance chain you see that a Class actually inherits from Module.
Module cannot be instanciated. So the call to .new is not working.
What you CAN do however is to specify your method as a 'class' method (I know I said it is not a class...)
So you would add a self in front like this:
module Search
module Score
def self.get_score
return 'something'
end
end
end
Then you can call this method as a class method like you tried in your code example
Search::Score is a module and not a class, so Score.new will not work.
You can try to change the signature of the get_score function to self.get_score.
In addition to def self.get_score in the above answers, there is also extend self, like so:
module Search
module Score
extend self
def get_score
return 'something'
end
end
end
and module_function:
module Search
module Score
module_function
def get_score
return 'something'
end
end
end
The latter is actually the preferred method in RuboCop (source), though in practice I personally have not seen it so often.

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 :)

Resources