Suppose there's gem that implements authentication strategy for Warden or a storage engine for Paperclip or an ActiveRecord database adapter. Should it include the gems it supposed to be used with as regular dependencies? If so, what should be the version constraints? It certainly has to include "parent" gems as development dependencies but what about regular ones?
It’s all up to you. Unless you rely in your code on gem presence (read: surround all your code with if const_defined?('Paperclip') or like,) your gem might gracefully reject to operate unless the necessary dependencies are already included in the target project. The version should correspond the version you have your code tested against.
As a good example of the case when direct dependence is not a good choice, would be the “umbrella” gem, simplifying work with different, say, authorization engines. One won’t require all these devises, pundits, and family. Instead one should do “clever” initialization:
case
when Kernel.const_defined?('Devise')
Logger.debug 'Will initialize Devise bridge.'
# initialization of bindings etc.
when Kernel.const_defined?('Pundit')
Logger.debug 'Will use Pundit bridge.'
# initialization of bindings etc.
...
else
Logger.warn 'No authorization backend found.'
raise
end
Or, imagine the logger, that might send airbrakes. For the logger to operate, the airbrake presense is not crucial. Hence one might check the presence of airbrake gem in the target project and switch the additional functionality on on success.
Hope it helps.
Related
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.
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.
I have changed this question to better reflect what it is I do not understand.
For example if I try to access the methods in the railties class AppBuilder.
require 'rails/generators/rails/app/app_generator'
g = Rails::AppBuilder.new
puts g.rakefile.inspect
I get an error message activesupport-3.1.3/lib/active_support/secure_random.rb:5:in `': uninitialized constant SecureRandom (NameError)
I do not understand this. Should not each class be "independent" from other classes? Is that not the whole point of object oriented programing?
And now If it is not so more importantly how can I figure out what dependences I need to add? Is it some kind of workflow to solve this? Can I somehow figure out what dependencies to add looking at the documentation? Do this problem have something to do with load path? Can I load all dependences from a gem or rails or whatever? I just don't get it!
doc: http://api.rubyonrails.org/classes/Rails/AppBuilder.html github: https://gist.github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/app/app_generator.rb
there is no easy way to find out which dependencies are used within AppBuilder, cause most of the dependencies are setup somewhere else. so most of the times you need to do some trial and error to get the dependencies right.
the code that you posted has bad style. please get familiar with how to write ruby code properly. buy yourself a book i.e. eloquent ruby or just start reading ruby blogs.
dependencies in ruby is quite simple. every file that you require will be loaded and the load will recurse through the files and then load other requires. the loading will only work if all the required files are on the load path. this load path is similar to your system path and you can add directories to it to tell ruby where to look for files.
in general, there are dedicated entry-points for libraries and their dependencies. those are normally documented, so that if you use them, you get all dependencies right from the beginning. an example for this would be to require 'rails' in order to use rails or require 'active_support/all' if you just want to use active-support. if you wan't to chery-pick files/classes than you are on your own finding out which other classes you need. that part has nothing to do with oop, it's more an dependency-issue (other languages have explicit decleration of dependencies).
in your case, the next step would be to add require "securerandom" to the beginning of your file and then check wich error comes up next.
I've written a command-line tool for manipulating with genome scaffolds called "Scaffolder". At the moment all the tools I want to use are hard-coded into the library. For instance these tools "validate" or "build" the scaffold. I'd like to split these tools out into their own gems, make it more modular, and to allow third parties to write their own commands.
The ideal case would be that I run "gem install scaffolder-validate" and this gem-bundled command would then be available as part of scaffolder. I know a couple of libraries make it easy to build a command-line interface: thor, commander, gli, .... However I don't think any of them cater for this type of functionality.
My question is how can I use a gem structure to create a module structure for installing these commands? Specifically how can the installed commands be auto-detected and loaded? With some prefix in the gem name scaffolder-* then searching rubygems? How could I test this with cucumber?
So, one thing you can do is to decide on a canonical name for your plugins, and then use that convention to load things dynamically.
It looks like your code is all under a module Scaffolder, so you can create plugins following the following rules:
Scaffolder gems must be named scaffold-tools-plugin-pluginname
All plugins expose one class, named Scaffolder::Plugin::Pluginname
That class must conform to some interface you document, and possibly provide a base class for
Given that, you can then accept a command-line argument of the plugins to load (assuming OptionParser):
plugin_names = []
opts.on('--plugins PLUGINS','List of plugins') do |plug|
plugin_names << plug
end
Then:
plugin_classes = []
plugin_names.each do |plugin_name|
require "scaffold-tools-plugin-#{plugin_name}"
plugin_classes << Kernel.const_get("Scaffold::Plugin::#{plugin_name}")
end
Now plugin_classes is an Array of the class objects for the plugins configured. Supposing they all conform to some common constructor and some common methods:
plugin_classes.each do |plugin_class|
plugin = plugin_class.new(args)
plugin.do_its_thing(other,args)
end
Obviously, when doing a lot of dynamic class loading like this, you need to be careful and trust the code that you are running. I'm assuming for such a small domain, it won't be a concern, but just be wary of requireing random code.
Hm, tricky one. One simple idea I have is that the main gem just tries to require all the others and catches the load error when they are not there and disables the respective features. I do this in one of my gems. If HighLine is present, the user gets prompted for a password, if it isn't there has to be a config file.
begin
require 'highline'
rescue LoadError
highline = false
end
If you have a lot of gems this could become ugly though...
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.