Versioning API Helpers in Ruby on Rails - 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

Related

Rails 6: Zeitwerk::NameError doesn't load class from module

I have a file which looks like this
#app/services/account/authenticate/base.rb
module Account
module Authenticate
AuthenticateError = Class.new(StandardError)
class Base < ::Account::Base
def self.call(*attrs)
raise NotImplementedError
end
end
end
end
Now when I will run the code from rails c I have an error
> ::Account::Authenticate::AuthenticateError
=> NameError (uninitialized constant Account::Authenticate::AuthenticateError)
> ::Account::Authenticate.constants
=> [:Base, :ViaToken]
So rails doesn't see AuthenticateError class. But when I will create a nested class from this folder like
=> Account::Authenticate::ViaToken
> ::Account::Authenticate.constants
=> [:Base, :AuthenticateError, :ViaToken]
AuthenticateError class is now visible
> ::Account::Authenticate::AuthenticateError
=> Account::Authenticate::AuthenticateError
The solution for this problem is to create a separate file authenticate_error.rb which will work from the beginning but this solution is not ideal for me. Is there any solution to preload all classes or smth?
(Ruby 2.6 with Rails 6.0.0.rc2)
I experienced this same issue when deploying a Rails 6.0.2 application to an Ubuntu 18.04 server.
Unable to load application: Zeitwerk::NameError: expected file /home/deploy/myapp/app/models/concerns/designation.rb to define constant Designation, but didn't
I found out that the issue was with zeitwerk. Zeitwerk is the new code loader engine used in Rails 6. It’s meant to be the new default for all Rails 6+ projects replacing the old classic engine. Zeitwerk provides the features of code autoloading, eager loading, and reloading.
Here's how I solved it:
Navigate to the config/application.rb file on your project.
Add this line within your application module to switch to classic mode for autoloading:
config.autoloader = :classic
Here's an example:
module MyApp
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.autoloader = :classic
end
end
You can read up more on zeitwerk in this article: Understanding Zeitwerk in Rails 6
updating a Rails app from 5.2 to 6.0 and also hit the Zeitwerk bump!
If you want to continue to use the autoloading mode you're currently using, avoiding Zeitwerk, then add this line to your application.rb file (#PromisePreston answer and Rails doc)
config.autoloader = :classic
If you want to upgrade to Zeitwerk then a command to use is bin/rails zeitwerk:check (From this guide article).
Scenario we hit closest to this particular question was where we had a file within a subfolder like this:
#presenters/submission_files/base.rb
module Presenters
module SubmissionFiles
class Base < Showtime::Presenter
def method_call
#code_here
end
end
end
end
Removing an extra modules to have:
#presenters/submission_files/base.rb
module Presenters
class SubmissionFiles::Base < Showtime::Presenter
def method_call
#code_here
end
end
end
Then, when calling the method in other ruby files in the app use: Presenters::SubmissionFiles::Base.method_call
zeitwerk follow conventional file structure which is able to load your project's classes and modules on demand (autoloading) as long as you follow it's rule.
# service/account/authenticate/base.rb
module Account
module Authenticate
puts "load service ....."
AuthenticateError = Class.new(StandardError)
class Base
end
end
end
::Account::Authenticate::AuthenticateError # uninitialized constant
::Account::Authenticate::Base # load service ....
::Account::Authenticate::AuthenticateError # OK
as you can see, the first time you attempt to reach the constant AuthenticateError, the log load service ... does not show, that because you don't play zeitwerk rule:
whenever it gets a request to load the const ::Account::Authenticate::AuthenticateError, first it'll check and return if that constant already loaded, otherwise it'll look for the file /account/authenticate/authenticate_error.rb which corresponding to the constant ::Account::Authenticate::AuthenticateError to find that constant definition, but it could not find it.
on step 2, when you call ::Account::Authenticate::Base, it could find the file /account/authenticate/base.rb and load it, during this time, it's also load the constant AuthenticateError which is defined on that file, now we have the constant ::Account::Authenticate::AuthenticateError, and of course, it's OK on step 3.
now let try to play with the zeitwerk rule, i create a file /account/authenticate/authenticate_error.rb as below
# service/account/authenticate/authenticate_error.rb
module Account
module Authenticate
puts "load error ....."
AuthenticateError = Class.new(StandardError)
end
end
and try to attempt that constant at the step 1
$ spring stop
$ rails c
> ::Account::Authenticate::AuthenticateError
load error .....
=> Account::Authenticate::AuthenticateError
it worked since zeitwerk found the file account/authenticate/authenticate_error.rb. (note that the file name /____authenticate_error.rb still work)
my thought: i think you could work safely with the constant AuthenticateError inside the module ::Account::Authenticate, in case of you want to expose those error constants to outside, you could create file /account/authenticate/error.rb
# service/account/authenticate/error.rb
module Account
module Authenticate
module Error
AuthenticateError = Class.new(StandardError)
end
end
end
then you could access ::Account::Authenticate::Error::AuthenticateError, in my opinion, it even clearer than put AuthenticateError inside base.rb.
I was able to fix it by not trying to fight Rails 6. Zeitwerk autoloads certain expected folders, which includes app/models, app/controllers, app/helpers and others.
I created a folder app/helpers and moved my services folder into it.
That's it!

How to create a Rails-specific Rubygem?

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.

Rails App to Angular: HAML + Rails Helpers

I'm trying to move my full-stack rails app over to Angular a page at a time. I'm using ui-router (https://github.com/angular-ui/ui-router) and angular-rails-templates (https://github.com/pitr/angular-rails-templates). I assumed the nghaml extension would allow me to continue to use rails helpers such as link_to, paths, etc. in my haml so just copied and pasted my haml page into the template; in an ideal world I would now be at a point where one page was being served client-side and every other page (including the ones it's linked to) were still being served server-side. Instead, I'm getting errors such as:
undefined local variable or method `dashboard_patients_path' for #<Object:0x007fc87865cff0>
and link_to, etc.
I thought this (angularjs with client side haml) would be a solid solution, specifically sharpper's response since it seemed directly applicable.
module CustomHamlEngine
class HamlTemplate < Tilt::HamlTemplate
def evaluate(scope, locals, &block)
scope.class_eval do
include Rails.application.routes.url_helpers
include Rails.application.routes.mounted_helpers
include ActionView::Helpers
end
super
end
end
end
Rails.application.assets.register_engine '.haml', CustomHamlEngine::HamlTemplate
However, even after restarting the server, no dice.
Thoughts?
Got stuck with the same problem and found a solution after investigating angular-rails-templates, attentively reading their documentation and using the solution proposed by sharpper in angularjs with client side haml.
angular-rails-templates needs to recreate a mimeless version of the haml engine. So they extend the classes that are registered with Tilt instead of using the engines that were added to the asset pipelines. Therefore the new CustomHamlEngine that we create and register with the asset pipeline is never used by angular-rails-template. What we need to do instead is to register the engine with Tilt.
Create a file called angular_rails_templates.rb in the config/initializers folder and put this code in it.
# config/initializers/angular_rails_templates.rb
module CustomHamlEngine
class HamlTemplate < Tilt::HamlTemplate
def evaluate(scope, locals, &block)
scope.class_eval do
include Rails.application.routes.url_helpers
include Rails.application.routes.mounted_helpers
include ActionView::Helpers
end
super
end
end
end
Tilt.register CustomHamlEngine::HamlTemplate, '.haml'
That will override the usual .haml engine with the one we just created. Angular-rails-templates will then process your haml file and it will support the rails helpers as well as the path helpers.
Don't forget to restart the server after including the file.

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)

Rails 3 Generator custom templates with Engine

When using the rails generators with an Rails::Engine, it does not seem to pick up any template files that are put into the lib dir. As instructed here
http://guides.rubyonrails.org/generators.html#customizing-your-workflow-by-changing-generators-templates
Right now I have
lib/templates/rails/scaffold_controller
I have also tried
lib/my_engine/templates/rails/scaffold_controller
Has anyone else tried this.
It seems that this is not supported for Engines
In a Rails app the Finisher takes care of adding this to the path
module Rails
class Application
module Finisher
include Initializable
initializer :add_generator_templates do
config.generators.templates.unshift(*paths["lib/templates"].existent)
end
......
So this must be done in the Engine config in order for this to work.
module MyEngine
class Engine < ::Rails::Engine
config.generators.templates.unshift File.expand_path("lib/templates", root)
end
end
Is this a bug or the desired behaviour?
Above answer (by stellard himself) doesn't fix my case in Rails 3.2, but How to override a rails generator template in a gem? fix it. Just pointing out for the person like me.
If you use rails g generator MyGenerator in the root path of an Rails 3.2 engine you'll get something like this:
class MyGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
end
which doesnt pollute your Engine class and is much more localized to the generator.

Resources