How to create a Rails-specific Rubygem? - ruby-on-rails

We're building a Chat service, which people are able to use from within their code.
Amongst the tools we're building, we've made a Ruby gem to allow people to quickly add a Chat window to their Ruby web-application.
We'd like to create a Rails-specific wrapper, though, because currently the user has to manually call .html_safe.
How is it possible to use these Rails-specific features from within a Ruby gem? I've heard this might be called 'Railsties' but I have not been able to find any comprehensive documentation about these, and how to use them.
Specifically, we'd like to:
Call html_safe on some string output so the user does not have to do this manually.
Put some configuration settings in a file in config/initializers/some_name.rb rather than having to specify these inline.
Potentially create a generator that the user can run to fill this initializer automatically.
How can we use these features? Is there some other gem-dependency we can include in our gem to access these features?

Engines can be considered miniature applications that provide
functionality to their host applications. A Rails application is
actually just a "supercharged" engine, with the Rails::Application
class inheriting a lot of its behavior from Rails::Engine.
https://guides.rubyonrails.org/engines.html
An engine can contain models, controllers, routes, generators, middleware and any arbitrary code that you can mount in the host application. Engines are usually packaged as gems.
Devise for example is a rails engine that provides authorization.
Rails has a generator command for creating engines:
rails plugin new chatty --mountable
For this example lets call it chatty.
Since an engine is mounted in a Rails application you have full access to the Rails stack (such as .html_safe). This also means that you test engines by mounting them in a dummy application.
If you have packaged the application as a gem than you simply mount it in the host application by adding it to the Gemfile.
To make your engine configurable you can follow the "MyGem.configure pattern":
# lib/chatty.rb
module Chatty
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end
class Configuration
attr_accessor :foo
def initialize
#foo = 'some_value'
end
end
end
To create a user configuration file you use a generator:
# lib/generators/chatty/install/install_generator.rb
module Chatty
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('templates', __dir__)
desc "Creates a Chatty initializer."
def copy_initializer
template 'chatty.rb', 'config/initializers/chatty.rb'
end
end
end
And a code template:
# /lib/generators/chatty/install/templates/chatty.rb
Chatty.configure do |config|
config.foo = "bar"
end
You can now run rails g chatty:install and it will create the file in the host application.

Related

Versioning API Helpers in Ruby on Rails

We have a versioned API that follows all the conventions of Rails API versioning. i.e
module API::V4
class Test < ApiController
end
end
But is there a proper way to version helpers? The current helpers directory looks like something like this:
app
|__helpers
|__helper_a.rb
|__helper_b.rb
and all helpers are defined modules.
Is there a way to do this?
module Helpers::V2
class HelperA
end
end
I tried creating a directory app/helpers/v2/helper_a.rb, adding app/helpers/helpers.rb and defining module Helpers But for some reason rails always fails to see Helpers::V2::HelperA
Rails searches each subdirectory of app starting from the subdirectory. app/models/foo/bar.rb contains Foo::Bar not Models::Foo::Bar. app/controllers/api/v4/test.rb contains Api::V4::Test, not Controllers::Api::V4::Test.
So app/helpers/v2/helper_a.rb contains V2::HelperA.
module V2
module HelperA
end
end
If you want to relate the helper version with your API version, it makes sense to mirror the directory structure.
# app/helpers/api/v2/helper_a.rb
# A helper for API v2.
module Api
module V2
module HelperA
end
end
end
Note that it's Api to follow Rails conventions. The autoloader will map app/helpers/api/v2/helper_a.rb to Api::V2::HelperA. It might work with the Rails 5 autoloader, but not the Rails 6 autoloader.
If we use API::V2::HelperA...
[1] pry(main)> API::V2::HelperA
NameError: uninitialized constant API
Did you mean? Api

Sharing a mongoid model between 2 applications - using engine vs. plugin

I want to share a model between 2 (maybe more in the future) of my rails apps. I couldn't find any clear suggestions, but I picked up some of the questions and answers I've read and came to a conclusion that it has to be done with a "gemmed" plugin engine.
I decide to go with an plugin, because I read that engine is simply a kind of a "full" plugin.
So I created a plugin using: rails plugin new my_models --skip-active-record --skip-test-unit --dummy-path=spec/dummy (the options are for skipping activerecord as an ORM and using rspec for testing).
After I created the plugin, I got the following files:
my_models.gemspec Gemfile Gemfile.lock lib MIT-LICENSE Rakefile README.rdoc spec
I tried to include the model using the following methods:
Just creating an app/models directory and put my model inside
As suggested in this tutorial (and I could see in devise's github), I created a generator in an attempt to generate the model.
Both of them failed, and then I decided to go with the engine suggestion (by just adding --mountable to the options list of the "rails new" command), I got the full rails app structure (with app, bin, db and the rest of the directories), put my model in the app/models dir and it worked like a magic!
As I believe I'm a programmer and not I magician, I don't to do such magics, so can you tell me what's wrong with both of my thin plugin solutions (using generator/creating a model)?? Moreover, what are the advantages of using those generators?
I'm attaching my generator's code, maybe I miss something:
require 'rails/generators/named_base'
require 'mongoid'
module Mongoid
module AttackGenerator
def generate_model
invoke "mongoid:model", [name] unless model_exists? && behavior == :invoke
end
def inject_field_types
inject_into_file model_path, migration_data, after: "include Mongoid::Document\n" if model_exists?
end
def migration_data
field :link_url, type: String
field :token, type: String
end
def model_exists?
File.exists?(File.join(destination_root, model_path))
end
def model_path
#model_path ||= File.join("app", "models", "#{file_path}.rb")
end
end
end
An engine (very good guide) is basically a small Rails app: has controllers, can inject into your rails code in various ways (sharing classes/controllers and such) and most important, can use your main Rails application code transparently.
After thinking a bit about it, I believe you need an engine, the reason is, a simple model still require migrations. Notice that an engine is basically a gem plus some few additions provided by rails.
Although miguiding (plugins in rails are not used anymore), the command rails plugin new blorgh --mountable creates a gem which is a rails engine.
How do you understand if a gem is a rails engine? By the file engine.rb (can be named differently, the contents are the important stuff).
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
The other important thing you should be aware of, is that rails (actually Bundler.require) auto requires one file for you when you add your custom gem to your gemfile: the file named lib/yourgemname.rb, in this case, lib/blorgh.rb. That's your entry point.
Aside from that, all the other things (gemspec and all the other files) are things created for rubygems. The important part is that you use .gemspec file as your gemfile, just add gems using add_dependency instead of the standard Gemfile syntax. If you want (and you should) learn more about ruby gems, this article is really good
The app directory is autoloaded like rails does, so adding app/models/yourmodel.rb is a good way to have your model autoloaded and shared with all your apps.
The last important thing, are migrations. You have to remember to run your_engine_name:install:migrations in your Rails app to copy your migrations from your engine to your rails app and run them (otherwise you can see the suggestions on this article)
And you are ready. One note! To install your gem you have two options: use your remote git repository (best option) or for local development you can use :path, here two examples that can be added to your Rails apps gemfiles:
# Use this only for development purposes
gem 'yourgem', '1.0.0', path: 'your/local/path/to/gem'
# Use this for deploy and such, you'll need access to that repository
gem 'yourgem', '1.0.0', git: 'yourgiturl'
This should be enough

Rails mountable engine: how should apps set configuration variables?

I have a mountable engine called Blog that apps can use.
What's the best way to allow apps that use the engine to set a configuration variable like site_name (so that the engine can display it in the design)?
Update:
I've seen some gems create a 'config/initializers/gem_name.rb' file. Is there any specification on how to:
Create a file like that on the engine's side
Copy it into the app's side
How to access those set variables on the engine's side?
I tried creating Blog.site_name = "My Site" in the app's config/initializers/blog.rb file but get an Undefined method error.
Figured out an even better solution that also allows you to set default values (incase the app using the engine doesn't specify a config)...
Create config variables in your app's /config/initializers/blog.rb like this:
Blog.setup do |config|
config.site_name = "My Site Name"
end
In your engine's /lib/blog/engine.rb set default values like this:
module Blog
self.mattr_accessor :site_name
self.site_name = "Site Name"
# add default values of more config vars here
# this function maps the vars from your app into your engine
def self.setup(&block)
yield self
end
end
Now you can simply access the config variables in your engine like this:
Blog.site_name
Much cleaner.
After a lot of testing and looking into existing gems, here is what works in Rails 4:
Considering your engine's name is Blog:
In your engine's /lib/blog/engine.rb put in this:
module Blog
def self.setup(&block)
#config ||= Blog::Engine::Configuration.new
yield #config if block
#config
end
def self.config
Rails.application.config
end
end
In your app, create a file called /config/initalizers/blog.rb and set config vars like this:
Blog.setup do |config|
config.testingvar = "asdfasdf"
end
Then you can access these config variables ANYWHERE in your engine like this:
Blog.config.testingvar
Hope this helps someone. There is very little documentation on this right now so it was all trial and error.
I know this is a fairly old post, but in the event someone in the future finds this, I'd like to recommend the Angostura gem for passing dependencies into a Rails engine. To use it, assuming my engine is called 'Blog' and I want to access a variable called 'site_name', the engine's lib/blog.rb looks something like:
require "blog/engine"
require "angostura"
module Blog
include Angostura::Dependencies
dependency :site_name
end
In my main app, in config/initializers/blog.rb, I added
Blog.setup do |config|
config.site_name = "My site name"
end
Now, I can access site_name in my engine by calling Blog.site_name.
I'd like to point out that defaults are also supported, so you could do something like dependency site_name: 'Default site name' in lib/blog.rb. Furthermore, you can pass in whole classes as dependencies by sending it stringified classnames, like config.my_class = 'MyClass'.
For posterity, in order to use the gem, I added s.add_dependency "angostura", "0.6.0" in my engine's gemspec, and then ran bundle install.

Organizing models/controllers and classes in rails in subfolders

I am working in a Rails project in which i have used the below names for model/controller and class files
/app/models/friends/friend.rb
/app/controllers/friends/friends_controller.rb
/lib/classes/friends/friend.rb
I tried to add all the models, controllers and class files in autoload path in application.rb.
But i am facing issues since the class names are same.
How should i handle this? and organize files in such a way that files are organized with name spaces.
Thanks,
Balan
A much better approach would be to use Rails Engines & divide your app in isolated modules.
rails plugin new friends --full --mountable --dummy-path spec/dummy
the above command will generate a full mountable engine with isolated namespace, meaning that all the controllers and models from this engine will be isolated within the namespace of the engine. For instance, the Post model later will be called Friends::Post, and not simply Post. to mount this app inside your main rails app, you need do two things:
Add entry to Gemfile
gem 'friends', path: "/path/to/friends/engine"
And then add route to config/routes.rb
mount Friends::Engine, at: "/friends"
For more information on this approch, checkout:
Rails Guide to Engines
Taming Rails Apps with Engines
RailsCast #277 Mountable Engines
The class names are same but path's are different, and you don't need to add classes to autoload except /lib/classes/friends/friend.rb
Did you tried the following way:
# app/models/friends/friend.rb
class Friends::Friends
#...
end
# Friends::Friends.new
# app/controllers/friends/friends_controller.rb
class Friends::FriendsController < ApplicationController
#...
end
# lib/classes/friends/friend.rb
module Classes
module Friends
class Friends
#...
end
end
end
# Classes::Friends::Friends.new
To add lib files to autoload add following to your applicaion.rb
config.autoload_paths += %W(#{config.root}/lib)

How do I make functions in a gem available to Sinatra views?

The question here asks how to extract Rails view helper functions into a gem, and the accept answer is pretty good.
I am wondering - how to do the same for Sinatra? I'm making a gem that has a bunch of helper functions defined in a module, and I'd like to make these functions available to Sinatra views. But whatever I try, I cannot seem to access the functions, I just get a undefined local variable or method error.
So far, my gem structure looks like this (other stuff like gemspec omitted):
cool_gem/
lib/
cool_gem/
helper_functions.rb
sinatra.rb
cool_gem.rb
In cool_gem.rb, I have:
if defined?(Sinatra) and Sinatra.respond_to? :register
require 'cool_gem/sinatra'
end
In helper_functions.rb, I have:
module CoolGem
module HelperFunctions
def heading_tag(text)
"<h1>#{text}</h1>"
end
# + many more functions
end
end
In sinatra.rb, I have:
require 'cool_gem/helper_functions'
module CoolGem
module Sinatra
module MyHelpers
include CoolGem::HelperFunctions
end
def self.registered(app)
app.helpers MyHelpers
end
end
end
This doesn't work. Where am I going wrong?
(And in case you're wondering, yes, I need the helper functions in a separate file. I plan to make the gem compatible with Rails as well, so I want to keep the functions isolated/de-coupled if possible).
You’re mainly just missing the call to Sinatra.register (in cool_gem/sinatra.rb):
require 'sinatra/base'
require 'cool_gem/helper_functions'
module CoolGem
# you could just put this directly in the CoolGem module if you wanted,
# rather than have a Sinatra sub-module
module Sinatra
def self.registered(app)
#no need to create another module here
app.helpers CoolGem::HelperFunctions
end
end
end
# this is what you're missing:
Sinatra.register CoolGem::Sinatra
Now any classic style Sinatra app that requires cool_gem will have the helpers available. If you use the modular style you’ll also need to call register CoolGem::Sinatra inside the Sinatra::Base subclass.
In this case, if you are just providing some helper methods, an easier way might be to just use the helpers method (again in cool_gem/sinatra.rb):
require 'sinatra/base'
require 'cool_gem/helper_functions'
Sinatra.helpers CoolGem::HelperFunctions
Now the methods will be available in classic style apps, and modular style apps will need to call helpers CoolGem::HelperFunctions. This is a bit simpler, but if you are adding methods to the DSL context you will need to use registered as above.

Resources