How can I write code that needs to run after some gem was loaded, but without requiring that gem? - ruby

I am writing a gem that "hooks" onto other gems. It's designed to work with multiple different gems that do similar things, this adds functionality onto any of them that may be present.
What my gem does at startup is something like this:
if defined?(GemAModule)
# Hook onto Gem A's observer methods
end
if defined?(GemBModule)
# Hook onto Gem B's observer methods
end
Because of this, I need this code to run after A and / or B have been loaded. But I can't require either A or B. I want this to work for people that use only A, or only B, so A and B are not in my gem's dependencies, and they can't be in require statements.
I could do this by having a: MyGem.hook() method that users call in their initializers, after they have initialized A or B or both. But I'm trying to avoid that. I want to have it so that adding my gem just "magically" work.
Is this possible? What is the best way to do this?
Can I somehow check if "a gem is in the gemfile", and in that case require the module?

You could make this work in the case where your gem is loaded first by overriding require. Combined with your code that works in the case when your gem is loaded after the other gems, this should solve the problem. Something like this:
module Kernel
alias original_require require
def require(name)
if original_require(name)
case name
when 'gem_A'
# stuff goes here
when 'gem_B'
# more stuff goes here
end
end
end
end
What I personally would do in this situation is have your users require your gem in different ways depending on which other gem they are using, for example:
# if using gem A
require 'gem_A'
require 'your_gem/gem_A_compat'
# if using gem B
require 'gem_B'
require 'your_gem/gem_B_compat'

Don't auto-detect code setup, or do anything overly complicated or "clever", you will regret it when it comes to testing and debugging (you will need both compatible gems in your development dependencies and have tests that load each in turn). Instead, document your own gem's requires so that the end user loads your gem depending on the link they wish to make.
So, assuming your gem is called my_gem, you would have files:
lib/my_gem/gem_a.rb
require 'my_gem'
require 'gem_a'
# Require Hook onto Gem A's observer methods
lib/my_gem/gem_b.rb
require 'my_gem'
require 'gem_b'
# Require Hook onto Gem B's observer methods
And you instruct the end user to require your gem as follows:
require 'my_gem/gem_a'
or . . .
require 'my_gem/gem_b'
as needed.
This is no more work for your gem user, and a lot easier to test, develop and debug for you and developers that use your gem.

Related

How to access Padrino model and database in a "standalone" (bin/) script? [duplicate]

I have a Padrino App called Gusy that specifies (Sequel) Models like
# gusy/models/seminar.rb
class Seminar < Sequel::Model
# hopefully irrelevant stuff defined here
end
I want to access this Model from either a second gem, or a script in bin/.
Now, e.g. I require Gusy from a second gem "gusy_fill". The Gemfile is put up to set the path to a Gusy git repository. I can successfullly see the Gusy namespace (and e.g. print the apps Version Gusy::VERSION) if interactively exploring with bundle console.
How can I access the mapped models and where and how do I configure the database connection?
I see nothing relevant in the Padrino:: or Gusy:: modules.
An irb session might look like this:
require 'gusy'
Gusy::Seminar.create(:name => 'from gusy_fill' # => NameError: uninitialized constant Gusy::Seminar
I want to achieve this without creating a second Padrino App that mounts Gusy (for that, pointers are included in the generated gusy/README.md).
As initially statet, I would have the very same issue, if I would do what I want from within the same app: write a small script in gusy/bin that talks with the database, really in the setting like when calling padrino console.
Sorry to hear you're having trouble with this. It's good that you brought it up though because I've been trying to put my thoughts around the subject for a while now and this pushed me into it :). I've prepared a repo for you explaining how to do it with what we have now in Padrino.
The README (which I'm pasting afterwards), explains the reasoning behind it and puts some questions up for us to think about the way we've implemented them. I'd love to hear your thoughts about it :).
Gemified apps in Padrino
This repo intends to answer
How to access Padrino model and database in a “standalon” (bin/) script? and
How to access a gemified Padrino Apps Model from other gem that requires that App.
The issue
In short, there are two issues of the similar nature, both related to models defined in the gemified app:
they need to be accessed from another gems/projects;
they need to be accessed from the gemified app's bin, doing something else other than starting
the Padrino server.
The example
First there's gemified-app. That's a Padrino app that is gemified. It also contains a model
called SomeModel that has one field called property.
Then there's access-gemified-app-without-padrino; a ruby script that loads the gemified app to
access the model.
Finally, there's another-app which is a regular Padrino app that just loads gemified-app to use
its model (SomeModel).
Problems with the current Padrino setup
Creating an app with padrino g project gemified-app --orm sequel --gem --tiny will give you the
following gemspec:
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/gemified-app/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["Darío Javier Cravero"]
gem.email = ["dario#uxtemple.com"]
gem.description = %q{Padrino gemified app example}
gem.summary = %q{Padrino gemified app example}
gem.homepage = ""
gem.files = `git ls-files`.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "gemified-app"
gem.require_paths = ["lib", "app"]
gem.version = GemifiedApp::VERSION
gem.add_dependency 'padrino-core'
end
The key points are gem.require_paths = ["lib", "app"] and gem.add_dependency 'padrino-core'.
gem.require_paths = ["lib", "app"] explains why models/some_model.rb isn't available when we
load the gem somewhere else. It simple isn't added to $LOAD_PATH :(.
gem.add_dependency 'padrino-core' hints us that something might be missing later on. What happens
with dependencies like the ORM or the renderer? Should we load those? I reckon that it's a matter
of what you want to achieve but I'd say that most times yes.
Our gemified app dependencies are still listed in our Gemfile which will only be added in the
current scope and not in any gems requiring our gemified-app gem.
A first attempt at solving this
For this to work there are two things we should do:
Add 'models' to gem.require_paths = ["lib", "app"] so that it becomes:
gem.require_paths = ["lib", "app", "models"].
That will make sure that anything inside the gemified-app/models directory is included in your
gem.
To make it easier to test this, we'll use bundler and in our access-gemified-app-without-padrino
test script we'll add a Gemfile that looks like this:
source 'https://rubygems.org'
gem 'gemified-app', path: '../gemified-app'
gem 'pry'
Now in your new app, go to the REPL bundle exec pry and try to require 'gemified-app'.
Then try SomeModel.all. It will fail. Why? Because you didn't require 'some_model'.
It will still not work if you do that though. Why? Because none of the model's dependencies,
i.e. sequel and sqlite3 (not a direct dependency but it is through the connection) are loaded.
Here you have two choices: you load them manually on your Gemfile or you define them as
dependencies on gemified-app.gemspec.
I regard the latter one as a better choice since you're already including the model and you're
expecting its dependencies to come with it. It would like this:
# gemified-app/gemified-app.gemspec
# ...
gem.add_dependency 'padrino-core'
gem.add_dependency 'padrino-helpers'
gem.add_dependency 'slim'
gem.add_dependency 'sqlite3'
gem.add_dependency 'sequel'
gem.add_development_dependency 'rake'
# ...
# gemified-app/Gemfile
source 'https://rubygems.org'
# Distribute your app as a gem
gemspec
You would have to explicitly include all the gems you will need. This may seem cumbersome but in
all fairness it gives you a greater understanding of what your app needs. Eventually you will
realise you don't even need bundler and the Gemfile :).
Alright, so, go ahead launch your REPL and type require 'gemified-app' and require 'some_model'.
Then try SomeModel.all. And... It will fail :(. Why? Because Sequel::Base isn't defined. Now you might be wondering:
what happened to the reference to sequel I put in my gemified-app.gemspec? Well, it's just that:
a reference and it won't require the gem for you.
This won't happen with Padrino either because we're using
require 'rubygems' unless defined?(Gem)
require 'bundler/setup'
Bundler.require(:default, RACK_ENV)
in our config/boot.rb and that only loads required gems on our Gemfile.
So the question is... Should we load that manually? And if so, where?
Well, since this is a gem itself, I believe that the best place to do so would be in lib/gemified-app.rb.
Loading all the gems needed will make this file look like:
require 'padrino-core'
require 'padrino-helpers'
require 'slim'
require 'sqlite3'
require 'sequel'
module GemifiedApp
extend Padrino::Module
gem! "gemified-app"
end
Alright, so we're all set... Back to the REPL, do your requires
require 'gemified-app'
require 'some_model'
and try SomeModel.all. And... It will fail :(. Again! :/ Why? Because there's no connection to the
database. Padrino was loading this for us through config/database.rb.
Another question arises... Should we include config/database.rb in the gem too?
The way I see it, we shouldn't. The way I see it, the database connection is something every app
should locally define as it may contain specific credentials to access it or stuff like that.
Our sample, access-gemified-app-without-padrino/do-somethin.rb script will then look like this:
require 'gemified-app'
Sequel::Model.plugin(:schema)
Sequel::Model.raise_on_save_failure = false # Do not throw exceptions on failure
Sequel::Model.db = Sequel.connect("sqlite:///" + File.expand_path('../../gemified-app/db/gemified_app_development.db', __FILE__), :loggers => [logger])
require 'some_model'
SomeModel.all.each do |model|
puts %Q[#{model.id}: #{model.property}]
end
Yes, the connection code is pretty much the same than our Padrino app and we're reusing its database
for this example.
That was some ride :) but we finally made it. See the sample apps in the repo for some working
examples.
require some_model :/
I don't know you but I don't like that at all. Having to do something like that means that I
really have to pick my models' names very carefully not to clash with anything I may want to use
in the future.
I reckon that modules are the answer to it but that's the current state of affairs. See the
conclusion for more on this.
An alternative approach
Separate your model layer into its own gem and require it from your (gemified or not) Padrino app.
This might probably be the cleanest as you can isolate tests for your models and even create
different models for different situations that may or may not use the same database underneath.
It could also encapsulate all of the connection details.
Conclusion
I think we should review Padrino's approach to gemified apps.
Should we use the gemspec instead of the Gemfile for hard dependencies?
Should we namespace the models (I know we had some issues in the past with this)?
Should we teach users to do explicit requires in their gems or to inspect the dependecies and
require them for them?
Should we teach our users how to load their dependencies and be more reponsible about it? At the end
of the day, if they went the gemified app route they are clearly much more proficient in Ruby and
should be aware of this kind of stuff.
Thoughts? :)

How to access a gemified Padrino Apps Model from outside (not in controller, but e.g. a standalone script)

I have a Padrino App called Gusy that specifies (Sequel) Models like
# gusy/models/seminar.rb
class Seminar < Sequel::Model
# hopefully irrelevant stuff defined here
end
I want to access this Model from either a second gem, or a script in bin/.
Now, e.g. I require Gusy from a second gem "gusy_fill". The Gemfile is put up to set the path to a Gusy git repository. I can successfullly see the Gusy namespace (and e.g. print the apps Version Gusy::VERSION) if interactively exploring with bundle console.
How can I access the mapped models and where and how do I configure the database connection?
I see nothing relevant in the Padrino:: or Gusy:: modules.
An irb session might look like this:
require 'gusy'
Gusy::Seminar.create(:name => 'from gusy_fill' # => NameError: uninitialized constant Gusy::Seminar
I want to achieve this without creating a second Padrino App that mounts Gusy (for that, pointers are included in the generated gusy/README.md).
As initially statet, I would have the very same issue, if I would do what I want from within the same app: write a small script in gusy/bin that talks with the database, really in the setting like when calling padrino console.
Sorry to hear you're having trouble with this. It's good that you brought it up though because I've been trying to put my thoughts around the subject for a while now and this pushed me into it :). I've prepared a repo for you explaining how to do it with what we have now in Padrino.
The README (which I'm pasting afterwards), explains the reasoning behind it and puts some questions up for us to think about the way we've implemented them. I'd love to hear your thoughts about it :).
Gemified apps in Padrino
This repo intends to answer
How to access Padrino model and database in a “standalon” (bin/) script? and
How to access a gemified Padrino Apps Model from other gem that requires that App.
The issue
In short, there are two issues of the similar nature, both related to models defined in the gemified app:
they need to be accessed from another gems/projects;
they need to be accessed from the gemified app's bin, doing something else other than starting
the Padrino server.
The example
First there's gemified-app. That's a Padrino app that is gemified. It also contains a model
called SomeModel that has one field called property.
Then there's access-gemified-app-without-padrino; a ruby script that loads the gemified app to
access the model.
Finally, there's another-app which is a regular Padrino app that just loads gemified-app to use
its model (SomeModel).
Problems with the current Padrino setup
Creating an app with padrino g project gemified-app --orm sequel --gem --tiny will give you the
following gemspec:
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/gemified-app/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["Darío Javier Cravero"]
gem.email = ["dario#uxtemple.com"]
gem.description = %q{Padrino gemified app example}
gem.summary = %q{Padrino gemified app example}
gem.homepage = ""
gem.files = `git ls-files`.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "gemified-app"
gem.require_paths = ["lib", "app"]
gem.version = GemifiedApp::VERSION
gem.add_dependency 'padrino-core'
end
The key points are gem.require_paths = ["lib", "app"] and gem.add_dependency 'padrino-core'.
gem.require_paths = ["lib", "app"] explains why models/some_model.rb isn't available when we
load the gem somewhere else. It simple isn't added to $LOAD_PATH :(.
gem.add_dependency 'padrino-core' hints us that something might be missing later on. What happens
with dependencies like the ORM or the renderer? Should we load those? I reckon that it's a matter
of what you want to achieve but I'd say that most times yes.
Our gemified app dependencies are still listed in our Gemfile which will only be added in the
current scope and not in any gems requiring our gemified-app gem.
A first attempt at solving this
For this to work there are two things we should do:
Add 'models' to gem.require_paths = ["lib", "app"] so that it becomes:
gem.require_paths = ["lib", "app", "models"].
That will make sure that anything inside the gemified-app/models directory is included in your
gem.
To make it easier to test this, we'll use bundler and in our access-gemified-app-without-padrino
test script we'll add a Gemfile that looks like this:
source 'https://rubygems.org'
gem 'gemified-app', path: '../gemified-app'
gem 'pry'
Now in your new app, go to the REPL bundle exec pry and try to require 'gemified-app'.
Then try SomeModel.all. It will fail. Why? Because you didn't require 'some_model'.
It will still not work if you do that though. Why? Because none of the model's dependencies,
i.e. sequel and sqlite3 (not a direct dependency but it is through the connection) are loaded.
Here you have two choices: you load them manually on your Gemfile or you define them as
dependencies on gemified-app.gemspec.
I regard the latter one as a better choice since you're already including the model and you're
expecting its dependencies to come with it. It would like this:
# gemified-app/gemified-app.gemspec
# ...
gem.add_dependency 'padrino-core'
gem.add_dependency 'padrino-helpers'
gem.add_dependency 'slim'
gem.add_dependency 'sqlite3'
gem.add_dependency 'sequel'
gem.add_development_dependency 'rake'
# ...
# gemified-app/Gemfile
source 'https://rubygems.org'
# Distribute your app as a gem
gemspec
You would have to explicitly include all the gems you will need. This may seem cumbersome but in
all fairness it gives you a greater understanding of what your app needs. Eventually you will
realise you don't even need bundler and the Gemfile :).
Alright, so, go ahead launch your REPL and type require 'gemified-app' and require 'some_model'.
Then try SomeModel.all. And... It will fail :(. Why? Because Sequel::Base isn't defined. Now you might be wondering:
what happened to the reference to sequel I put in my gemified-app.gemspec? Well, it's just that:
a reference and it won't require the gem for you.
This won't happen with Padrino either because we're using
require 'rubygems' unless defined?(Gem)
require 'bundler/setup'
Bundler.require(:default, RACK_ENV)
in our config/boot.rb and that only loads required gems on our Gemfile.
So the question is... Should we load that manually? And if so, where?
Well, since this is a gem itself, I believe that the best place to do so would be in lib/gemified-app.rb.
Loading all the gems needed will make this file look like:
require 'padrino-core'
require 'padrino-helpers'
require 'slim'
require 'sqlite3'
require 'sequel'
module GemifiedApp
extend Padrino::Module
gem! "gemified-app"
end
Alright, so we're all set... Back to the REPL, do your requires
require 'gemified-app'
require 'some_model'
and try SomeModel.all. And... It will fail :(. Again! :/ Why? Because there's no connection to the
database. Padrino was loading this for us through config/database.rb.
Another question arises... Should we include config/database.rb in the gem too?
The way I see it, we shouldn't. The way I see it, the database connection is something every app
should locally define as it may contain specific credentials to access it or stuff like that.
Our sample, access-gemified-app-without-padrino/do-somethin.rb script will then look like this:
require 'gemified-app'
Sequel::Model.plugin(:schema)
Sequel::Model.raise_on_save_failure = false # Do not throw exceptions on failure
Sequel::Model.db = Sequel.connect("sqlite:///" + File.expand_path('../../gemified-app/db/gemified_app_development.db', __FILE__), :loggers => [logger])
require 'some_model'
SomeModel.all.each do |model|
puts %Q[#{model.id}: #{model.property}]
end
Yes, the connection code is pretty much the same than our Padrino app and we're reusing its database
for this example.
That was some ride :) but we finally made it. See the sample apps in the repo for some working
examples.
require some_model :/
I don't know you but I don't like that at all. Having to do something like that means that I
really have to pick my models' names very carefully not to clash with anything I may want to use
in the future.
I reckon that modules are the answer to it but that's the current state of affairs. See the
conclusion for more on this.
An alternative approach
Separate your model layer into its own gem and require it from your (gemified or not) Padrino app.
This might probably be the cleanest as you can isolate tests for your models and even create
different models for different situations that may or may not use the same database underneath.
It could also encapsulate all of the connection details.
Conclusion
I think we should review Padrino's approach to gemified apps.
Should we use the gemspec instead of the Gemfile for hard dependencies?
Should we namespace the models (I know we had some issues in the past with this)?
Should we teach users to do explicit requires in their gems or to inspect the dependecies and
require them for them?
Should we teach our users how to load their dependencies and be more reponsible about it? At the end
of the day, if they went the gemified app route they are clearly much more proficient in Ruby and
should be aware of this kind of stuff.
Thoughts? :)

Highlight Sinatra structure

I actually build a Sinatra app and I'd like to clear some point in Ruby/bundler etc...
What's about requiring the rubygems on the config.ru? Many folks do this but why?
Same question for requiring bundler/setup into the app.rb? Some guys said it's requiring every gems we have into the Gemfile but is it true? (in this case, we'd don't have to require sinatra, active_record...).
Last things I don't really understand is the requiring. The first entry point is config.ru. So if we require rubygems and then ./app.rb, we'd don't have to require rubygems into ./app.rb, right?
What's about requiring the rubygems on the config.ru? Many folks do this but why?
It doesn't matter as, since Ruby v1.9, it's been required by default anyway. I checked a project I'm working on and I've got it right there at the top, so it's just there for no good reason! Maybe I'll get rid of it now…
See http://www.rubyinside.com/why-using-require-rubygems-is-wrong-1478.html for more on this.
Same question for requiring bundler/setup into the app.rb? Some guys said it's requiring every gems we have into the Gemfile but is it true? (in this case, we'd don't have to require sinatra, active_record...).
Yes, Bundler will handle require for you if you tell it to, but I actually use this style:
require 'rubygems'
require 'bundler'
Bundler.setup(:default, :ci)
require 'nokogiri'
as I like to handle my own requires, it makes testing (generally) quicker and more particular, IMO.
Note also, from the linked docs:
For another kind of application (such as a Sinatra application), you will need to set up bundler before trying to require any gems.
Lastly,
The first entry point is config.ru. So if we require rubygems and then ./app.rb, we'd don't have to require rubygems into ./app.rb, right?
Yes, require will only load a library once.
Loads the given name, returning true if successful and false if the feature is already loaded.

Require Dependency Only On Certain Method Call

I have a library of functions that is packaged up as a gem. One of these functions requires the use of a third-party gem that itself has a long list of dependencies.
Is there a way that I can set up my gem so that users can install it and use those functions in the gem that don't have the third-party dependency without the runtime complaining?
I want an exception to be raised when the method with the dependency is called, but the user should be able to use the other functions without a runtime error.
Is this possible?
You can split your functions to modules (maybe modules to files too) that are depending or not depending gem. including GemDependent module trying to require your gem if not then redefine all gem dependent functions to raise exception.
module YourFunctions
module GemDependent
def self.included(klass)
require "yourgem"
rescue LoadError
instance_methods.each do |m|
define_method(m) { raise "you need yourgem to run #{m}" }
end
end
def gem_dependent_function
end
end
include GemDependent
def no_dependent_function
end
end
include YourFunctions
gem_dependent_function
# "you need yourgem to run gem_dependent_function"
if you want to know how to do this, have a look at the various wrapper libraries for http, json, yaml etc.
a good example is https://github.com/rubiii/httpi where you can configure one of 3 http adapters. the adapters themselves require the individual dependencies within ruby.
the problem is, that the end-user needs to somehow know how to install the 3rd party gems. it's currently not possible to implement optional dependencies within ruby gemspecs.

Can I specify dynamic dependencies for my gem based on command line input?

Intro: I'm working on a gem that, by default, will pull information out of some XML data and do some sort of processing on the document. I'm using nokogiri to parse the XML. However, I wish to allow the user to parse the XML themselves and pass in the necessary information for my data processing methods to run, in case they don't want to install nokogiri or have already parsed the XML.
Question: Is there any way to allow the user to specify, during gem installation, that they don't wish to install the nokogiri dependency? For example (very hand-wavey here),
gem install crazy_gem --no-nokogiri
and in the gemspec perhaps something like
Gem::Specification.new do |s|
...
s.add_dependency 'nokogiri' unless Proc.new { install_flags('no-nokogiri') }
...
end
[edit] I don't want to focus too much on the gemspec code above, as I know it doesn't work--it's just an example of the kind of thing I want to do. [/edit]
gem install crazy_gem --ignore-dependencies works great until there are additional dependencies.
I don't think you can do exactly what you're after, but there's apossible solution if you reframe what your gem does. Rather than a gem that by default parses some XML and processes the data, but optionally allows you to pass in the pre-parsed data, what about a gem that is mainly concerned with the processing, but optionally will parse the XML for you (if you have Nokogiri).
To do this just leave Nokogiri out of your gemspec dependencies (you could add it as a development dependency or a requirement).
Inside your code, make sure you only call require 'nokogiri' in a begin..end block with a rescue LoadError and deal with it as appropriate.
Gemspecs are turned into static files at build time, so that wouldn't work. You could try using -f which bypasses dependency checks.

Resources