Create Ruby Gem from Rails classes to use in Sinatra - ruby-on-rails

I'm trying to extract several classes from a Rails app into its own Gem so that I can reuse the code from a Sinatra app.
In the Rails app I have the following structure:
app > classes > api > (bunch of folders and subfiles)
I'm trying to move the api folder into a gem, for that I created a new gem using bundler:
bundle gem myapp-core --no-exe --no-coc --no-mit --no-ext
so I ended up with a file structure like:
myapp-core > lib > myapp > core > version.rb
myapp-core > lib > myapp > core.rb
I've copied the api folder to myapp-core > lib > myapp > api and tried to require it from sinatra doing:
require 'myapp/api/somefile.rb'
but that didn't work, I have of course added the gem to the Gemfile of the sinatra app.
I tried all kinds of combinations of where to put the folder and how to require the files in it but I either get cannot load such file or uninitialized constant Api (NameError).
What is the correct way to go about this so that ideally from both Sinatra and Rails I would just add the gem to the Gemfile, require whatever file I need and the code that uses those API files would remain unchanged or change as little as possible?

Without the Rails auto-loader you'll need to establish all the intermediate class and module components yourself.
This usually means you need to either declare things like:
module Api
class Somefile
# ...
end
end
Where that Api module is automatically declared, allowing you to require lib/api/somefile directly, or you need to shift this responsibility up the chain:
# lib/api.rb
module Api
# ...
end
require_relative './api/somefile'
Where that automatically loads in any dependencies and you now do require 'api' instead, presuming your $LOAD_PATH includes this particular lib/ path.

Related

Copy a File to rails project folder using gem

I am in the middle of developing a gem for rails and i stuck with this issue. My logic is I have a gem created, and the gem installed to my local machine. What I need is when I type gem_name --install, there is a file called test.rb should be copied to inside a rails project/ config/initializers/. The file to be copied is currently placed in a folder in my gem. I have tried
Dir.pwd
but it is not give me results as expected. Please find a solution for me and TIA..
For a rails gem you would use a generator together with a "template" file.
class FooGenerator < Rails::Generators::NamedBase
source_root File.expand_path("../templates", __FILE__)
def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
end
end
This will would copy the file when the user of the gem runs rails generate foo.
Make sure to read through the rails guides sections on generators and creating engines as there are quite a few gotchas and conventions.

Create a rails plugin to preprocess assets

I created a small CSS preprocessor, which is somewhat similar to SASS, and now I would like to test it in a 'real life' scenario, so I'm trying to create a rails plugin for it, like the way sass-rails works.
I tried this, without being really sure of what I was doing:
require 'toss-ruby'
require 'sprockets'
module Toss
module Rails
class Template < ::Tilt::Template
def prepare
end
def evaluate(scope, locals, &block)
g = ::Toss::Generator.new
g.parse_string data
g.generate_string
end
end
end
end
Sprockets.register_engine '.toss', ::Toss::Rails::Template
According to the documentation, the last line should register my template so sprockets can use it, but it doesn't happen, so I assume it's never called. How do some gems like thin, sass-rails, etc... manages to work only by being put in a Gemfile ? When and how is their code called ?
There is a convention that executes code in gems that are included in your Gemfile.
If a file exists in the lib directory of a gem's codebase with the same name as the gem and with a .rb extension, it is required by default when Bundler loads the gem.
Take a gem called "Mygem" as a example. This is a typical directory structure for a gem:
mygem/ (gem's root directory)
|-- lib/
| |-- mygem.rb <-- automatically required!
| `-- mygem/
| |-- base.rb
| `-- version.rb
|-- test/
|-- bin/
|-- Rakefile
`-- mygem.gemspec
When Bundler loads the 'mygem' gem, it automatically requires lib/mygem.rb. Rails plugins and engines rely on this behavior to load their code. Typically, they use this file to require other files in the lib directory. In this case, the lib/mygem.rb file may require lib/mygem/base.rb file, etc.
While developing a gem, it's common to add it to your Gemfile by using the Bundler :path directive, which tells Bundler to look for the gem at a certain path on your local filesystem, rather than on rubygems. Let's assume you are building your gem under the vendor/engines directory of your test application.
Your test app's Gemfile:
gem 'rails'
gem 'mygem', path: './vendor/engines/mygem'
When the Rails app is started, one of the first steps is to load it's gems. When the 'mygem' gem is loaded, Bundler requires './vendor/engines/mygem/lib/mygem.rb' which executes the code in it. Any code you put in there will be run even before Rails is initialized.
If you have code that needs to be run as part of application initialization, you need to follow the guide for creating Rails plugins/engines. Typically, you'll inherit your plugin from ::Rails::Engine and call initializer in it and pass a block containing your initialization code.
With all this in mind, have a look at source code for the sass-rails gem again, specifically lib/sass-rails.rb which gets required when the gem is loaded and how it recursively requires lib/sass/rails/railtie.rb, which does the necessary setup and initialization for the plugin to integrate with Rails.

Upgrade to Rails 4.0.5 results in uninitialized constant Surveyor::Helpers

I have been using the surveyor gem within Rails 3.2.x without any issues in my project.
The gem defines modules that reside within the lib subdirectory of the gem.
Example
lib/surveyor/helpers/surveyor_helper_methods.rb
Then in my app/helpers directory I include the module and extend like follows.
include Surveyor::Helpers::SurveyorHelperMethods
This works fine in Rails 3, but within Rails 4 it results in the error Uninitialized constant Surveyor::Helpers.
As a test I copied the directory from the gem directly into my projects lib directory structure and this got rid of the error; so it seems the include is no longer looking at the gems' lib tree. Moving all of the files directly up into my project isn't a good solution. Is there another way to work around this?
in your helper , just include this file..so it will be something like
require 'surveyor/helpers/surveyor_helper_methods'
module UserHelper
include Surveyor::Helpers::SurveyorHelperMethods
end

How can I automatically reload gem code on each request in development mode in Rails?

I am developing a Rails app where most of the code not specific to the app has been written inside of various gems, including some Rails engines and some 3rd party gems for which I am enhancing or fixing bugs.
gem 'mygem', path: File.expath_path('../../mygem', __FILE__)
Since a lot of the code in these gems is really part of the app, it's still changing frequently. I'd like to be able to utilize the Rails feature where code is reloaded on each request when in development (i.e. when config.cache_classes is false), but this is only done within the normal application structure by default.
How can I configure Rails to reload gem code on each request, just like with the app code?
I have found through trial and error that several steps are required, with the help of ActiveSupport.
Add activesupport as a dependency in the .gemspec files
spec.add_dependency 'activesupport'
Include ActiveSupport::Dependencies in the top-level module of your gem (this was the most elusive requirement)
require 'bundler'; Bundler.setup
require 'active_support/dependencies'
module MyGem
unloadable
include ActiveSupport::Dependencies
end
require 'my_gem/version.rb'
# etc...
Set up your gem to use autoloading. You an either manually use ruby autoload declarations to map symbols into filenames, or use the Rails-style folder-structure-to-module-hierarchy rules (see ActiveSupport #constantize)
In each module and class in your gem, add unloadable.
module MyModule
unloadable
end
In each file that depends on a module or class from the gem, including in the gem itself, declare them at the top of each file using require_dependency. Look up the path of the gem as necessary to properly resolve the paths.
require_dependency "#{Gem.loaded_specs['my_gem'].full_gem_path}/lib/my_gem/myclass"
If you get exceptions after modifying a file and making a request, check that you haven't missed a dependency.
For some interesting details see this comprehensive post on Rails (and ruby) autoloading.
The solution that I used for Rails 6, with a dedicated Zeitwerk class loader and file checker :
Add the gem to the Rails project using the path: option in Gemfile
gem 'mygem', path: 'TODO' # The root directory of the local gem
In the development.rb, setup the classloader and the file watcher
gem_path = 'TODO' # The root directory of the local gem, the same used in Gemfile
# Create a Zeitwerk class loader for each gem
gem_lib_path = gem_path.join('lib').join(gem_path.basename)
gem_loader = Zeitwerk::Registry.loader_for_gem(gem_lib_path)
gem_loader.enable_reloading
gem_loader.setup
# Create a file watcher that will reload the gem classes when a file changes
file_watcher = ActiveSupport::FileUpdateChecker.new(gem_path.glob('**/*')) do
gem_loader.reload
end
# Plug it to Rails to be executed on each request
Rails.application.reloaders << Class.new do
def initialize(file_watcher)
#file_watcher = file_watcher
end
def updated?
#file_watcher.execute_if_updated
end
end.new(file_watcher)
With this, on each request, the class loader will reload the gem classes if one of them has been modified.
For a detailed walkthrough, see my article Embed a gem in a Rails project and enable autoreload.

How does load differ from require in Ruby?

Is there any major difference between load and require in the Ruby on Rails applications? Or do they both have the same functionality?
require searches for the library in all the defined search paths and also appends
.rb or .so to the file name you enter. It also makes sure that a library is only
included once. So if your application requires library A and B and library B requries library A too A would be loaded only once.
With load you need to add the full name of the library and it gets loaded every time you
call load - even if it already is in memory.
Another difference between Kernel#require and Kernel#load is that Kernel#load takes an optional second argument that allows you to wrap the loaded code into an anonymous empty module.
Unfortunately, it's not very useful. First, it's easy for the loaded code to break out of the module, by just accessing the global namespace, i.e. they still can monkeypatch something like class ::String; def foo; end end. And second, load doesn't return the module it wraps the code into, so you basically have to fish it out of ObjectSpace::each_object(Module) by hand.
I was running a Rails application and in Gemfile, I had a specific custom gem I created with the option "require: false". Now when I loaded up rails server or rails console, I was able to require the gem in the initializer and the gem was loaded. However, when I ran a spec feature test with rspec and capybara, I got a load error. And I was completely bewildered why the Gem was not found in $LOAD_PATH when running a test.
So I reviewed all the different ways that load, require, rubygems and bundler interact. And these are a summary of my findings that helped me discover the solution to my particular problem:
load
1) You can pass it an absolute path to a ruby file and it will execute the code in that file.
load('/Users/myuser/foo.rb')
2) You can pass a relative path to load. If you are in same directory as file, it will find it:
> load('./foo.rb')
foo.rb loaded!
=> true
But if you try to load a file from different directory with load(), it will not find it with a relative path based on current working directory (e.g. ./):
> load('./foo.rb')
LoadError: cannot load such file -- foo.rb
3) As shown above, load always returns true (if the file could not be loaded it raises a LoadError).
4) Global variables, classes, constants and methods are all imported, but not local variables.
5) Calling load twice on the same file will execute the code in that file twice. If the specified file defines a constant, it will define that constant twice, which produces a warning.
6) $LOAD_PATH is an array of absolute paths. If you pass load just a file name, it will loop through $LOAD_PATH and search for the file in each directory.
> $LOAD_PATH.push("/Users/myuser")
> load('foo.rb')
foo.rb loaded!
=> true
require
1) Calling require on the same file twice will only execute it once. It’s also smart enough not to load the same file twice if you refer to it once with a relative path and once with an absolute path.
2) require returns true if the file was executed and false if it wasn’t.
3) require keeps track of which files have been loaded already in the global variable $LOADED_FEATURES.
4) You don’t need to include the file extension:
require 'foo'
5) require will look for foo.rb, but also dynamic library files, like foo.so, foo.o, or foo.dll. This is how you can call C code from ruby.
6) require does not check the current directory, since the current directory is by default not in $LOAD_PATH.
7) require_relative takes a path relative to the current file, not the working directory of the process.
Rubygems
1) Rubygems is a package manager designed to easily manage the installation of Ruby libraries called gems.
2) It packages its content as a zip file containing a bunch of ruby files and/or dynamic library files that can be imported by your code, along with some metadata.
3) Rubygems replaces the default require method with its own version. That version will look through your installed gems in addition to the directories in $LOAD_PATH. If Rubygems finds the file in your gems, it will add that gem to your $LOAD_PATH.
4) The gem install command figures out all of the dependencies of a gem and installs them. In fact, it installs all of a gem’s dependencies before it installs the gem itself.
Bundler
1) Bundler lets you specify all the gems your project needs, and optionally what versions of those gems. Then the bundle command installs all those gems and their dependencies.
2) You specify which gems you need in a file called Gemfile.
3) The bundle command also installs all the gems listed in Gemfile.lock at the specific versions listed.
4) Putting bundle exec before a command, e.g. bundle exec rspec, ensures that require will load the version of a gem specified in your Gemfile.lock.
Rails and Bundler
1) In config/boot.rb, require 'bundler/setup' is run. Bundler makes sure that Ruby can find all of the gems in the Gemfile (and all of their dependencies). require 'bundler/setup' will automatically discover your Gemfile, and make all of the gems in your Gemfile available to Ruby (in technical terms, it puts the gems “on the load path”). You can think of it as an adding some extra powers to require 'rubygems'.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
2) Now that your code is available to Ruby, you can require the gems that you need. For instance, you can require 'sinatra'. If you have a lot of dependencies, you might want to say “require all of the gems in my Gemfile”. To do this, put the following code immediately following require 'bundler/setup':
Bundler.require(:default)
3) By default, calling Bundler.require will require each gem in your Gemfile. If the line in the Gemfile says gem 'foo', :require => false then it will make sure foo is installed, but it won’t call require. You’ll have to call require('foo') if you want to use the gem.
So given this breadth of knowledge, I returned to the issue of my test and realized I had to explicitly require the gem in rails_helper.rb, since Bundler.setup added it to $LOAD_PATH but require: false precluded Bundler.require from requiring it explicitly. And then the issue was resolved.

Resources