How do I include types from a gem with sigs? - sorbet

Let's say I have a shared gem called thing that gets used in multiple other projects, like example-project. I want to use sorbet in my thing gem to take advantage of the type checking. How do I get my consuming projects to check against the types in my gem?
For example, the code might look like this
thing gem
# typed: strict
require 'sorbet-runtime'
class Thing
extend T::Sig
sig { params(phrase: String).returns(String) }
def say(phrase)
"Thing said: '#{phrase}'"
end
end
example-project
# typed: strict
require 'thing'
Thing.new.say(1) # Should cause a type error
What happens
After running srb rbi update, I get an sorbet/rbi/gems/thing.rbi
class Thing
def say(*args, &blk); end
extend T::Private::Methods::MethodHooks
extend T::Private::Methods::SingletonMethodHooks
extend T::Sig
end
This doesn't have the sigs from the gem that I'm expecting, and srb tc doesn't raise the error I'm expecting.
Other things I've tried
Including rbi/thing.rbi in the gem
This works, but I'd rather write my sigs inline in the gem code than maintain separate rbi files. Is there any tooling that could take the .rb files with sigs and output the .rbi files (and maybe strip the sigs from the .rb files so them gem can be distributed without sorbet). I'm thinking of a workflow similar to typescript's tsc --declaration, which goes from .ts => .d.ts + .js.

https://github.com/AaronC81/brain_freeze
is a new project that aims to make it possible to generate an RBI file from RB files with inline declarations. It's still early, but might be worth trying out

Related

Resque and Resque_mailer with Devise

I am implementing background email processing with Resque using the resque_mailer gem (https://github.com/zapnap/resque_mailer). I was able to get it to work for all my emails except the ones sent by Devise.
I went through a bunch of SO questions, and blog posts (for instance http://teeparham.posterous.com/send-devise-emails-with-resque) but could not find a way to get it to work.
What are the precise steps to follow to get resque_mailer to work with Devise?
I went through tee's answer and several resources online, but couldn't find a working solution.
After a few days of reading through resque-mailer and devise code, a solution that worked for me. Thanks to tee for gist which put me in right direction.
Assuming your app/mailers/application_mailer.rb looks similar to
class ApplicationMailer < ActionMailer::Base
include Resque::Mailer # This will add a `self.perform` class method, which we will overwrite in DeviseResqueMailer
end
In config/initializers/devise.rb
Devise.parent_mailer = "ApplicationMailer"
Devise.setup do |config|
config.mailer = 'DeviseResqueMailer'
end
In the resource class which uses devise, overwrite the send_devise_notification method to send resource class and id instead of object to prevent marshalling
# app/models/user.rb
protected
def send_devise_notification(notification, *args)
# Based on https://github.com/zapnap/resque_mailer/blob/64d2be9687e320de4295c1bd1b645f42bd547743/lib/resque_mailer.rb#L81
# Mailer may completely skip Resque::Mailer in certain cases - and will fail as we write custom handle in DeviseResqueMailer assuming mails are handled via resque
# So in those cases, don't retain original devise_mailer so things work properly
if ActionMailer::Base.perform_deliveries && Resque::Mailer.excluded_environments.exclude?(Rails.env.to_sym)
# Originally devise_mailer.send(notification, self, *args).deliver
# Modified to ensure devise mails are safely sent via resque
resource_id, resource_class = self.id, self.class.name
devise_mailer.send(notification, {resource_id: resource_id, resource_class: resource_class}, *args).deliver
else
super
end
end
Finally, in app/mailers/devise_resque_mailer.rb, fetch the record again from the database and continue
class DeviseResqueMailer < Devise::Mailer
def self.perform(action, *args)
# Hack to prevent RuntimeError - Could not find a valid mapping for admin.attributes
record_hash = args.shift
record = record_hash["resource_class"].constantize.find(record_hash["resource_id"])
args.unshift(record)
super # From resque-mailer
end
end
I feel this approach is a better than using devise-async as all the mails go through same code path. Its easier to control and overwrite if needed.
I'd take a look at devise-async. Looks like it fits your use case. Devise Async

How to prevent initializers from running when running `rails generate`

I want to prepopulate my cache with an initializer, but I don't need this code to run every time I run rake or rails g, etc. Rake and Bundler are easy to deal with, but a similar solution does not work for the generators:
# config/initializers/prepop_cache.rb
if !defined?(::Bundler) and !defined?(::Rake) and !defined(Rails::Generators)
# do stuff
end
This must be because rails/generators (or something similar) is requireed at runtime. How can I check to see if the command being run is rails g xyz?
Update:
Two solutions can be found here: Rails 3 initializers that run only on `rails server` and not `rails generate`, etc
Still would like to know if it's possible in the manner I've tried above.
In Rails 3, what you're looking to do is conceivably possible, but in a hacky way. Here's how:
When you make a rails generate call, the callpath looks like this:
bin/rails is called, which eventually routes you to execute script/rails
script/rails is executed which requires rails/commands
rails/commands is loaded, which is the main point of focus.
Within rails/commands the code that runs for generate:
ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"c" => "console",
"s" => "server",
"db" => "dbconsole"
}
command = ARGV.shift # <= #1
command = aliases[command] || command
case command
when 'generate', 'destroy', 'plugin', 'benchmarker', 'profiler'
require APP_PATH
Rails.application.require_environment! # <= #2
require "rails/commands/#{command}" # <= #3
The points of interest are numbered above. Namely, that at point #1 the command that you're running is shifting off of ARGV. Which in your case means generate is going to be removed from the command line args.
At point #2 your environment gets loaded, at which point your initializers are going to be executed. And herein is the tough part - because nothing indicating a specific command has been loaded at this point (this occurs at #3) there is no information to determine a generator is being run!
Let's insert a script into config/initializer/debug.rb to see what is available if we ran rails generate model meep:
puts $0 #=> "script/rails"
puts ARGV #=> ["model", "meep"]
As you can see, there is no direct information that a generator is being run. That said, there is indirect information. Namely ARGV[0] #=> "model". Conceivably you could create a list of possible generators and check to see if that generator has been called on ARGV[0]. The responsible developer in me says this is a hack and may break in ways you'd not expect so I'd use this cautiously.
The only other option is to modify script/rails like you suggested -- which isn't too bad a solution, but would likely break when you upgrade to Rails 4.
In Rails 4, you've got more hope! By the time the application environment is being loaded, the generators namespace has already been loaded. This means that in an initializer you could do the following:
if defined? Rails::Generators #=> "constant"
# code to run if generators loaded
else
# code to run if generators not loaded
end

How to autoload files in folder under rails app's root

I am trying to have files under myapplication/somefolder. Google and Stackoverflow say I should add this:
config.autoload_paths += %W(#{config.root}/somefolder)
in my config/application.rb, so I did.
But the files don't get loaded.
I tried namig somefolder/myclass.rb both class Myclass and class Somefolder::Myclass but still no luck.
I can see that the dir was found in Rails.application.config.autoload_paths in console does indeed include my /path/to/myapplication/somefolder directory, so that should be okay.
All the other questions around this topic use theapp/app/somefolder or theapp/lib/somefolder but not theapp/somefolder so maybe thats where it gets rotten.
So I tried referencing the class with ::Somefolder::MyClass but not even that helped.
I am using Rails 3.2.1
Just ran into this myself today and decided to dive deep.
The reason you don't see in ActiveSupport::Dependencies.autoload_paths the paths you're adding to config.autoload_paths in config/application.rb is that they aren't copied over until the application is initialized. See rails/engine.rb in the railties gem:
module Rails
class Engine < Railtie
…
# Set the paths from which Rails will automatically load source files,
# and the load_once paths.
#
# This needs to be an initializer, since it needs to run once
# per engine and get the engine as a block parameter
initializer :set_autoload_paths, :before => :bootstrap_hook do |app|
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
# Freeze so future modifications will fail rather than do nothing mysteriously
config.autoload_paths.freeze
config.eager_load_paths.freeze
config.autoload_once_paths.freeze
end
…
def _all_autoload_paths
#_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
end
…
end
end
Were you by any chance trying to invoke MyClass from within config/application.rb or, even earlier, from a script or module that requires config/application.rb? If so, you'll have to explicitly require the file defining MyClass, e.g.:
require File.expand_path('../../somefolder/my_class', __FILE__)
# now use MyClass
The workaround is to go to ActiveSupport::Dependencies.autoload_paths directly.
ActiveSupport::Dependencies.autoload_paths << "#{config.root}/somefolder"
But I am still looking for the reason why config.autoload_paths didn't work so if you post an aswer to that, I'll accept it!
You should name somefolder/my_class.rb in order to autoload MyClass. You also should keep that config.autoload_paths += %W(#{config.root}/somefolder) line in your config/application.rb.

Extend a module in Rails 3

I want to define a function available_translations which lists the translations I have made for my application into the I18n module.
I tried putting the following into the file lib/i18n.rb, but it doesn't work when I try to use it from the rails console:
module I18n
# Return the translations available for this application.
def self.available_translations
languages = []
Dir.glob(Rails.root.to_s + '/config/locales/*.yml') do |filename|
if md = filename.match #^.+/(\w+).yml$#
languages << md[1]
end
end
languages
end
end
Console:
ruby-1.9.2-p290 :003 > require Rails.root.to_s + '/lib/i18n.rb'
=> false
ruby-1.9.2-p290 :004 > I18n.available_translations
NoMethodError: undefined method `available_translations' for I18n:Module
...
Besides solving my concrete problem, I would be very pleased to learn how this whole module thing in Ruby on Rails works because it still confuses me, so I would appreciate links to the docs or source code very much.
Either of these will solve your problem:
move the code to config/initializers/i18n.rb, or
require your file from config/application.rb, or
name your class otherwise (to trigger autoload)
The code in lib/i18n.rb wil not be loaded by autoload since I18n name will be already loaded, so either you load it yourself or change the class name (and file name) so the new name will trigger autoload behavior.
BTW, the I18n.available_locales() method is presented in rails.

How can I use mixins or modules in my controllers in Rails 3?

I have some behavior in my controller that I pulled out into a module in order to test better and re-use it in a few places. Two questions about this:
Where is a good place to put my modules? They need to run in order to be available to the controllers, so I was thinking the config/initializers/ directory. That seems a little suspect to me though. lib/?
How do I ensure the code gets run so the modules are available to include in my controllers?
Thank you kindly sirs.
lib/ is an excellent place for modules; much better than config/initializers/--at least in my opinion. If it's several modules, or one large one, you can also consider making it a plugin and placing it in vendor/plugins.
If you put it in lib/, you'll need to manually require the file. Rails, by default, does not autoload files in the lib/ directory. You can place the require in one of your config files.
I usually put my additional autoloads in config/application.rb. Something like this should do the trick (assuming that your .rb file is in a directory called lib/my_module):
config.autoload_paths += Dir["#{Rails.root}/lib/my_module"]
You have to make sure that your module is an actual module and not a class. Then, you can simply include it:
# lib/my_module/foobar.rb
module Foobar
def foobar
"Hello world!"
end
end
# app/models/my_model.rb
class MyModel < ActiveRecord::Base
include Foobar
end
# rails console
>> obj = MyModel.first
=> #<MyModel id: 1, ...>
>> obj.id
=> 1
>> obj.foobar
=> "Hello world!"
1) I like to put:
my class extentions under app/extentions
my modules under /app/mixins
my services under /app/services
2) You can configure your application to load all of these in config/application.rb:
class extentions should be required right way
and the mixins and services can be added to your autoload path
class Application < Rails::Application
# require class extentions right now
Dir[Rails.root.join('app', 'extentions', "*.rb")].each {|l| require l }
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += Dir[Rails.root.join('app', 'mixins', '{**}')]
config.autoload_paths += Dir[Rails.root.join('app', 'services', '{**}')]
(I'm using rails 3)
Try putting controller specific modules in app/controllers. No require required.