Require Dependency Only On Certain Method Call - ruby

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.

Related

undefined local variable or method for extended module

NOTE: the question was edited to reflect the issue.
i would like to work with knife-api rubygem. enclosed a snippet of my code
require 'chef'
require 'chef/knife'
require 'knife/api'
module X
module Y
module Z
include Chef::Knife::API
def self.foo
resp = knife_capture :search, ['*:*']
puts resp
end
end
end
end
when X::Y::Z.foo is called, it returns
gems/knife-api-0.1.7/lib/knife/api.rb:41:in `ensure in knife_capture': undefined local variable or method `revert_io_channels' for X::Y::Z (NameError)
it appears that he enclosing scope functions (Chef::Knife within knife-api) are inaccessible within X::Y::Z.foo.
how should i make this work?
In a complete non-answer, do not use the knife-api gem. It should not exist. The correct gem to use if you want to access Chef API data from Ruby code is chef-api (I know, we're real creative with names). If you want to do something very small, check out the knife exec command which just exposes the raw Chef object API (not as refined as chef-api, but built in).
This gem is a fork of another project that is no longer maintained. Looking at the gem code, it appears there were several issues introduced to the latest version (0.1.7) through some bad refactoring of the original code. That version was released a year ago and it hasn't had any commits since. The repo also does not accept issue tickets, and it has no tests. If you must use this gem, I would try a pessimistic constraint gem 'knife-api', '< 0.1.7' and see if that works. However it might be a better idea to skip it entirely. I made an attempt to fix the issues and submit a PR. You can also try pulling the gem from my forked repo https://github.com/msimonborg/knife-api

Should I depend on gems I'm extending?

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.

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

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.

Combining yard docs for multiple gems

I'm using YARD to generate docs for a couple projects that I'm working on. In one, I'm co-developing a gem that hosts a lot of redundant and shared resources that will be used by other projects.
What'd I'd like is for references from the first project to classes or methods in the gem to show up in the docs for the main project. For example
Gem
class Widget
# Spins the widget of course.
def spin
end
end
Project
class SpecialWidget
# Jump and {#spin}.
def dance
end
end
I'd like the docs for SpecialWidget to generate an actual link to the Widget#spin method.
Can this be done?

How can I build a modular command-line interface using rubygems?

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...

Resources