Dynamic namespace rakes and parser classes with rails? - ruby-on-rails

I have a collection of parsers that differ significantly in logic, but have the exact same methods and outputs.
I have devised somewhat of a master Rake and I am curious if what I have come up with could lead to some unexpected or weird behavior.
Essentially my parse.rake looks something like:
require 'app/models/provider'
require 'config/environment.rb'
Provider.all.each do |provider|
require "lib/tasks/#{provider.name}.rb"
Kernel.const_get(provider.name).provider = provider
namespace provider.name do
task :provider_task do #Parse method
Kernel.const_get(provider.name).provider_task()
end
end
end
Since classes are constants in ruby, Kernel.const_get is what I used to access class methods from a varname. My classes look like (ABC.rb):
Class ABC
cattr_accessor :provider
def self.provider_task()
#Parse and add stuff to environment
end
end
With this setup, I can easily invoke rake ABC:provider_task to run a specific parser. Also the cattr_accessor allows me to easily refer to the actual provider model. Thoughts?

I have used this with no issues for a few weeks. After reviewing with some colleagues, I found a few who previously used a similar approach with no issues. Only potential danger is naming conflicts with your ruby classes.

Related

Is it possible to use concerns outside of Rails

I like the idea of ActiveSupport::Concerns but wondering if possible to use in a vanilla Ruby app. Or is it only in a Rails app? I'm thinking of the example using ActiveRecord in a Sinatra app.
Edit
Looks like you can by requiring 'active_support', though I am not sure if any nuance about this doesn't work.
Like:
require 'active_support'
module Printable
include ActiveSupport::Concern
def print
puts "I will print here"
end
end
class User
include Printable
def initialize(name)
#name = name
end
def say_my_name
puts "my name: #{#name}"
end
end
Yes, you can (and it seems like you've discovered how). Much of ActiveSupport is written in a way that it can be added by itself to other projects.
You can even require just Concern with require "active_support/concern"
Opinion
I'm generally against using ActiveSupport::Concern though:
violates Composition Over Inheritance (see Nothing is Something by Sandi Metz)
95% of functionality is provided by vanilla Ruby already, in only 0-3 more lines of code (zero meaning you didn't need Concern at all)
Examples:
The code snippet you've pasted works perfectly fine with a bare Ruby module
Rail's own documentation on Concern explains usage with vanilla Ruby examples of the same length and complexity
In that documentation, it says: "Given a Foo module and a Bar module which depends on the former...", as if you'd want to deal with mixin hell

Problem with constant autoloading in a Rails project (works occasionally)

I am working with a Rails project and don't quite understand how Rails autoloading works in my particular case. I read some articles about Rails' autoloading and its pitfalls but those didn't really help me
I am building a processor for tasks (exercises). Each task has its custom processor class in Tasks::<TaskName>::Processor that mixes in module Tasks::Processor that contain shared code for task processors. Processors contain class Get (for processing GET requests) located in Tasks::<TaskName>::Processor::Get that mixes in Tasks::Processor::Get containing generic Get's code.
I've simplified the code a little bit so it's easier to understand and removed all the business logic but it's still enough to reproduce the problem.
So the problem is:
when I run Tasks::TaskOne::Processor.new.get it works fine, but if I run Tasks::TaskTwo::Processor.new.get after that it throws an error: NoMethodError: undefined method `new' for Tasks::Processor::Get:Module. It also works the other way round: if I run TaskTwo's processor's code first then it works fine but the TaskOne's processor will throw the error. It just fails to find the specific implementation of Get and instead finds the generic module and tries to instantiate it which is obviously impossible.
Here is the code together with the structure.
Shared code:
app/models/tasks/processor.rb:
module Tasks
# generic Processor (mixed in by custom processors)
module Processor
# ...
end
end
app/models/tasks/processor/get.rb:
module Tasks
module Processor
# generic Get
module Get
# ...
end
end
end
TaskOne's code:
app/models/tasks/task_one/processor.rb:
module Tasks
module TaskOne
# processor for task_one
class Processor
include Tasks::Processor # mix in generic task processor
def get
Get.new.call
end
end
end
end
app/models/tasks/task_one/processor/get.rb:
module Tasks
module TaskOne
class Processor
# task_one's processor's custom Get
class Get
include Tasks::Processor::Get # mix in generic Get
def call
puts "in task_one's Processor's Get"
end
end
end
end
end
And practically identical code for the TaskTwo:
app/models/tasks/task_two/processor.rb:
module Tasks
module TaskTwo
# processor for task_two
class Processor
include Tasks::Processor # mix in generic task processor
def get
Get.new.call
end
end
end
end
app/models/tasks/task_two/processor/get.rb:
module Tasks
module TaskTwo
class Processor
# task_two's processor's custom Get
class Get
include Tasks::Processor::Get # mix in generic Get
def call
puts "in task_two's Processor's Get"
end
end
end
end
end
It has most likely something to do with Rails' autoloading, because when I use plain ruby and manually require all the files and try to run the code the problem doesn't happen.
Could you, please, explain why it works like this and tell me what the best way to avoid this problem is? Seems like Rails doesn't like the fact that I have a class and a module with same name and it gets confused, but I thought it shouldn't be a problem as they are in different namespaces.
I could have just named the generic class something different, but I'd really like to understand why using the same class name for both specific implementation and generic one only works for the first thing to load but not for the next. Thank you very much for your help!
P.S. my version of Ruby is 2.5.1 and Rails version is 5.2.1
I was literally reading about autoloading yesterday. Your problem is the same as the one outlined here:
https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#when-constants-aren-t-missed
Basically, any time you write Get.new.call, you need to be more specific. It doesn’t know which Get to use in the tree of possible Gets. The first time you call it, it hasn’t had to load up more than one Get class, and so it actually finds the right one. After that call, you’ve now auto loaded MORE classes, and now things start to get dicey. You need to either qualify your Get to be more specific, and/or use require_dependency to force the right classes to be loaded in. However given your case, I think require_dependency will just make it fail every time, since you’ll now have all of the classes loaded up.

Source code for rspec "describe" method (and others)?

I'm sauntering through Michael Hartl's Rails Tutorial right now, and am finding that I'm constantly encouraged to use wonderful methods that inexplicably do amazing things. He does a generally competent job of explaining what they do, but there is no real nitty gritty of why and how they work.
Specifically, I have just been plundering the rspec gem on github searching for the source code to the "describe" method. I cannot find it. Having now read a large amount of the source code (at an apprehension rate of about 25%) searching for it, I know that once found, I will need to look at its parent classes and modules to understand a certain amount of inheritance before I can really grasp (and then never let go of) the flesh and bones of "describe".
I don't mind struggling to grasp the concept, I'm a fan of attempting to read code in new languages before I fully understand it so that I can read it again later and use the comparison of my comprehension as a gauge of my fluency. I'd just like a kicker. Either a description or a file location with maybe a little helper hint to get me started.
For example...
I found this:
# RSpec.describe "something" do # << This describe method is defined in
# # << RSpec::Core::DSL, included in the
# # << global namespace (optional)
and rpsec/core/dsl states:
# DSL defines methods to group examples, most notably `describe`,
# and exposes them as class methods of {RSpec}. They can also be
# exposed globally (on `main` and instances of `Module`) through
# the {Configuration} option `expose_dsl_globally`.
but then there is no "class Describe" or def "describe" or such in that file.
SO: can anyone tell me where the "describe" method is, how it works, exactly, or (if not) why I am naively searching for the wrong thing in the wrong locations?
As you may know, there is no difference between describe and context methods and you can use them interchangably. Rspec developers could not let themselves to repeat the same code for different method names, so they moved the declaration to
module RSpec
module Core
class ExampleGroup
def self.define_example_group_method(name, metadata={})
# here they really define a method with given name using ruby metaprogramming
define_singleton_method(name) do |*args, &example_group_block|
And call that method a bit later for all the same-functionality DSL methods:
define_example_group_method :example_group
define_example_group_method :describe
define_example_group_method :context
So in case you are looking for describe method source, dive into define_example_group_method with assumption that name argument equals to describe and example_group_block is your block body.
The RSpec code base is not a trivial thing to get your head round. However, these links should get you started ...
This line defines the describe keyword:
https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/example_group.rb#L246
The method above that line does the heavy lifting for you. Take your time reading it.
This part then exposes the generated method:
https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/dsl.rb#L54
Good luck!

Rails, Custom Folders and Namespaces

I'm running Rails 3.2.7,
I have a folder '/app/jobs'
and the following in my 'config/application.rb' file
config.autoload_paths += %W(#{Rails.root}/app/jobs)
And everything is okay.
However if I want to namespace my classes eg
class Jobs::UpdateGameStatus
#methods etc
end
Rather than
class UpdateGameStatus
#methods etc
end
Then I get
uninitialized constant Jobs (NameError)
It's not the end of the world but I'd love to know why...
I fixed it in the end, wrapping all my classes with a Jobs module was what I needed to do.
my files were located in 'app/jobs'
and looked like this
module Jobs
class JobName
#methods etc
end
end
and are used like so
Jobs::JobName.method(args)
I know you have already sorted this out, and this is old, but in ruby, it is also possible to declare the namespaced class directly using class Jobs::JobName. It's a little less typing, and achieves the same result.
Edit: As #D-side pointed out, Jobs has to already be defined. My own code that uses this is based around STI, which presumes that the previous class/module I am extending already exists.

How to access a controller constant in an rspec test under Rails 3

Using Rails 3.2 I have a controller in a subdirectory (e.g. /controllers/data_feeds/acme_feed_controller.rb)
This controller has some constants as below
class DataFeeds::AcmeFeedController < ApplicationController
MY_CONSTANT = "hi
def do_something do
...
end
end
In my rspec controller spec (which is in /spec/controllers/data_feeds/acme_feed_controller_spec.rb) I want to access that constant and below are two ways I've tried it (both commented out in the code below)
describe AcmeFeedController do
if "tests something" do
#c = AcmeFeedController.MY_CONSTANT
#c = DataFeeds::AcmeFeedController.MY_CONSTANT
end
end
I'm clearly not understanding something about the scope in which the spec test is run. What do I need to do and equally important why (i.e. what's happening with the scopes).
Thanks for your help.
Constants cannot be referenced with dot syntax, so DataFeeds::AcmeFeedController.MY_CONSTANT would never work in any context. You need to use :: to reference constants: DataFeeds::AcmeFeedController::MY_CONSTANT.
Note that is a ruby issue and has nothing to do with RSpec. When you face an issue like this, I recommend you figure out how to do it with plain ruby (e.g. in IRB) before worrying about how it works in RSpec (usually it will be the same, anyway).
If you want to know how constants work in ruby, I commend you watch this talk that explains them in detail.
Also, you can do this without repeating controller class name spaces.
describe AcmeFeedController do
if "tests something" do
c = controller.class.const_get('MY_CONSTANT')
end
end
This kind of trick may not be approved in application codes, but in tests it may be.

Resources