How can I run a method at the end of a file from a gem? - ruby

I have a gem (e.g. mygem) and as is normal, I add mygem to a file by putting require "mygem" at the top. What if I have a method in mygem called finish_jobs and I want it to run in the following location:
require "mygem"
# code, code code
finish_jobs
How would I do that without forcing the user to add the method every time they use the gem?
Specifically, what I am trying to do is write a server app (with rack) and I need the methods in the body of the file to be processed before the server is started.

This is certainly possible.
Why not just add the code directly into the Gem (since it sounds like it is under your control and is not an external dependency)?
module MyGem
def printSomething
p 2 + 2
end
module_function :printSomething
printSomething()
# => 4
end
If this isn't what you had in mind, let me know and I can update the solution.
Also, see Kernel#at_exit
A more explanatory guide on Kernel#at_exit

I don't know of a way to do what you're describing.
One workaround would be to provide an API which accepts a block. This approach allows you to run code after the user's setup without exposing implementation details to them.
A user could call your library method, providing a block to set up their server:
require "mygem"
MyGem.code_code_code {
# user's code goes here
}
Then, your library code would:
Accept the block
Call some library code
Here's an example implementation:
module MyGem
# Run some user-provided code by `yield`-ing the block
# Then run the gem's finalizer
def self.code_code_code
# Execute the block:
yield
# Finalize:
finish_jobs
end
end
This way, you can accept code from the user but still control setup and finalization.
I hope it helps!

Related

What are the ways to override a frozen variable in ruby?

This is a selenium-webdriver commands.rb file where I want to edit the upload_file key of COMMANDS variable from [:post, 'session/:session_id/se/file'] to [:post, 'session/:session_id/file']. I want to extend this class to one of mine's and make this change permanent so that even if i bundle install it, this change shouldn't be gone.
module Selenium
module WebDriver
module Remote
module W3C
class Bridge
COMMANDS = {
upload_file: [:post, 'session/:session_id/se/file']
}.freeze
end
end
end
end
end
You can get around the issue of unfreezing by just assigning the constant to a new value:
Selenium::WebDriver::Remote::W3C::Bridge.const_set(:COMMANDS, {
upload_file: [:post, 'session/:session_id/file']
}.freeze)
You will get a warning, but it will work.
If you really want to unfreeze, I have to point you to another question on the topic: How to unfreeze an object in Ruby?
in response to comment
The easiest way is to use ActiveSupport Hash#deep_dup from ActiveSupport. If this is a non-rails project, you can add the activesupport gem and require 'active_support/all':
my_commands = Selenium::WebDriver::Remote::W3C::Bridge::COMMANDS.deep_dup
# Here we can change one key only, or do any other manipulation:
my_commands[:upload_file] = [:post, 'session/:session_id/file']
Selenium::WebDriver::Remote::W3C::Bridge.const_set(:COMMANDS, my_commands)
You can also do it without ActiveSupport, but you will need to be a little more careful about how you clone the object because deep_dup is not available, something like this would work instead:
my_commands = Selenium::WebDriver::Remote::W3C::Bridge::COMMANDS.clone.transform_values(&:clone)
And then run the same stuff as in the previous example.
To understand this, read up on the difference between a "shallow" vs "deep" copy of an Object in Ruby, or the difference between "clone" and "deep_dup". Also see Hash#transform_values which I used in that snippet, if you're not familiar with it.

Rails : jump to function chosen at realtime (variable name)

I would like to jump to an "import function" which can be one off the file I already wrote in lib/import/...
The user chooses an option in a select box and from this choice, I want to execute a specific portion of code on one uploaded file (import excel with different possible layouts)
I wrote this :
# Loading and executing the template chosen by user at step 1
template_path = 'import/'+params[:template]+'/import.rb'
require template_path
Import.action(filename, session, params, current_project)
I have several import.rb files located each in a separate directory. One of these is :
module Import
require 'spreadsheet'
def Import.action(filepath, session, params, project)
# My import code
end
end
The problem is that Rails is always calling the action method from the first directory in lib/firstdirectory/import.rb
I never reach another import.rb file located in lib/otherdirectory/import.rb
Is there a better way to execute a "jump to" functionnality in realtime ?
Why does Rails jump to always the same function ?
Edit :
My application.rb configuration file contains
config.autoload_paths += Dir["#{config.root}/lib/import/**/"]
Edit 2 :
# lib/importer/importer.rb
module Importer
class Base
# Whatever common logic the import.rb files have.
end
end
#lib/importer/Import_test/import_test.rb Note the Big letter for the directory (constant)
module Importer
class Import_test < Base
def self.import
logger.debug(">>>>>>>>>>>>> special function Test <<<<<<<<<<<<<<<<<<<<")
end
end
end
# Call from controller
logger.debug "------------------>> "+params[:template]
raise "Invalid input" unless params[:template].constantize.superclass == Importer::Base
params[:template].constantize.import()
The params[:template] returns the string Importer::Import_test (with capital letters)
I get the error :
NoMethodError (undefined method 'superclass' for Importer::Import_test:Module):
app/controllers/import_controller.rb:57:in `step2'
Your code using the first directory entry makes sense. When you reference a constant whose definition has not yet been loaded, Rails checks the entries in autoload_paths for a corresponding file. Since you already have that import.rb in your first directory, your application loads that file.
A better design for this IMHO would be something along:
config.autoload_paths += ["#{config.root}/lib"]
# lib/importer.rb
module Importer
class Base
# Whatever common logic the import.rb files have.
end
end
# lib/importer/foo.rb
module Importer
class Foo < Base
def self.import
# ...
end
end
end
# lib/importer/bar.rb
module Importer
class Bar < Base
def self.import
# ...
end
end
end
# In your view, a way to identify these:
select_tag :layout, options_for_select({
"Foo" => "Importer::Foo",
"Bar" => "Importer::Bar"
})
# In your controller:
raise "Invalid input" unless params[:layout].constantize.superclass == Importer::Base
params[:layout].constantize.import( ... )
Update:
Rails looks for files this way: Say you want to use FooBar::Baz. If it doesn't have FooBar yet, it will load lib/foo_bar.rb and there is supposed to be something there. Next, it will try to access FooBar::Baz. Only if it doesn't have that yet (already after loading lib/foo_bar.rb), it will load lib/foo_bar/baz.rb and there is supposed to be something there.
If you want to use autoload_paths and not require ruby files yourself, please use the convention of using proper camelcase that Rails can easily change to underscore. e.g. Use camelcase ImporterTest without the underscore and have lib/importer/importer_test.rb so the framework will be able to find the correct file and your definition.
:-) Good luck.
There are much better ways. I suggest having a hash of template names to objects that execute the action. Import all your importers, and construct the hash. Than use the hash to get the function, and execute it.

Most appropriate place to require library in Padrino/Sinatra

I'm using "therubyracer" in a model, and I'm requiring at the top of the model as so:
require 'v8'
class Thing
def self.ctx; ##ctx ||= V8::Context.new; end;
def self.eval(script); ctx.eval(script); end;
end
However, I intermittently get:
NameError - uninitialized constant Thing::V8:
/app/thing.rb:3:in `ctx'
When testing requests through a local Padrino server, apparently after I modify code in Thing. This is corrected by restarting the padrino server. I'm assuming requiring v8 somewhere else would fix this problem, wheres the correct place?
This looks like it might be caused by the Padrino reloader getting confused when it reloads your thing.rb file, causing Ruby to look for V8 in the Thing namespace.
Try explicitly specifying V8 is in the top level using the :: prefix:
def self.ctx; ##ctx ||= ::V8::Context.new; end;
You can put it wherever you want if you add it on the Gemfile. Did you added it?
Thanks!

How can I refactor my Sinatra app?

I've just started writing a reasonably straightforward site using sinatra. My problem is that I wanted to refactor the main app.rb file but am getting errors trying to access the url params.
In my get '/' action, Sinatra's looking at which params are set and then needs to do a few different things depending on what's in the url. Something like this.
class App < Sinatra::Application
...
get '/' do
if params['code1']
#network = 'code1'
mode code here
elsif params['called'] && params['mac']
#network = 'code2'
mode code here
elsif params['code3']
#network = 'code3'
mode code here
end
end
The problem is that I need to require a file that also uses the params.
I've put the following in the above code:
require File.dirname(__FILE__) + '/lib/networks/code1.rb'
Where code1.rb includes:
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
elsif
...
But that gives me the following error:
undefined local variable or method `params' for main:Object
How can I refactor this without causing an error
As far as i know you can't use two (or more) Sinatra applications in, well one application. Since both files define a Sinatra::Application descendant this isn't possible.
Also if you want to use values from the params-hash you should define helper methods Helper Documentation, which you call when processing the route, or you just create Class which has class or instance methods which take params-values as parameters. Actually calling params from another file/class doesn't seem like good practice.
To put this in context: Sinatra applications are organised as handlers. The Sinatra::Application descendant is something like the main handler which uses support methods(helpers and instance methods of the Sinatra::Application descendant) or support Classes, which are usually defined in other files, but do not descend from Sinatra::Application.
To make this a little bit more clearly:
Your main Sinatra file:
require_relative 'another_file.rb'
class App < Sinatra::Application
# ...
#a_handler = MyHandler.new
get '/' do
if params['something'] == 'wanted_value'
#a_handler.handle_it(params)
end
end
Another file ('another_file.rb'):
class MyHandler
def initialize
#an_instance_variable = 'foobar'
end
def handle_it(params_hash)
if params_hash['login'] # == 'login'
pass = 'uampass'
elsif
# ...
end
# ...
# do some stuff
# ....
return pass
end
end
Actual code would of course depend on the real problem you're trying to solve, so if you would elaborate i could be more precise...
The error message contains everything you need to know, and it's nothing to do with Sinatra.
You are requiring code1.rb, which contains this (slightly edited so it will run):
require 'sinatra'
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
end
end
Ruby evaluates code as it encounters it. In this case, you've required 'code1.rb', so it evaluates the code in that file. It encounters 'params' and asks "is there a local variable or method with that name?". There isn't, so it fails as you've seen. Open an irb session and check it out.
Class definitions in ruby are just an expression with a scope.
In relation to Sinatra: params is available in the block that a route declaration takes.
I'd recommend reading Sinatra: Up and Running, which explains some of the 'magic' that is going on (a good companion to the Sinatra Book).

Writing a Sinatra Extension using options in routes

Lets say I'm writing a sinatra extension which mounts a second public directory at a given mount point.
require 'sinatra'
require 'sinatra/moar-public'
set :moar_local, './downloads/'
set :moar_remote, 'dls'
I now expect a user going to http://myapp.com/downloads/thing.bin to be given the file at [sinatra_root]/dls/thing.bin.
Writing this extension (obviously, it's a simplified example) I have something like this:
require 'sinatra/base'
module Sinatra
module MoarPublic
def self.registered(app)
app.set :moar_local, './downloads/'
app.set :moar_remote, 'downloads'
app.get "/#{app.options.moar_remote}/:filename" do
# Logic
end
end
end
register MoarPublic
end
But app.get has already been called with the default value for moar_remote so the download files are available at /downloads/thing.bin, not at /dls/thing.bin as I'd like. Any ideas?
You're asking for dynamic routes, but Sinatra compiles the route information so it won't work the way you're looking for.
As a work around, you might consider defining a catch-all route, and checking the route information inside the catch-all, e.g.
get %r{^/(*)/bar$} do |capture|
if settings.url_prefix == capture # or perhaps check against request.path_info
# get file
else
status 404
end
end
Obviously, there are still many things to be done there, but you get the drift.
I had no problem registering an extension explicitily in a modular configuration. Illustration below.
class Service < Sinatra::Base
set :url_prefix, 'foo'
register Common
end
module Common
def self.registered(app)
app.get "/#{app.options.url_prefix}/bar" do
"hello world"
end
end
end

Resources