Sorbet: check for a class that includes a target module - sorbet

Sorbet's T.class_of can be used to match descendant classes, but not classes that include a module:
module X; end
class A; end
class B < A; include X; end
B.ancestors # => [B, X, A, Object, PP::ObjectMixin, Kernel, BasicObject]
sig { params(x: T.class_of(A)).void }
def is_a(x); end
is_a(A) # => ok
is_a(B) # => ok
sig { params(x: T.class_of(X)).void }
def is_b(x); end
is_b(X) # => ok
is_b(B) # => error
According to https://sorbet.org/docs/class-of, this is because B is not an
instance of X's singleton_class:
B.is_a?(A.singleton_class) # => true
B.is_a?(B.singleton_class) # => true
B.is_a?(X.singleton_class) # => false
But no alternative suggestion is provided. Is there a way to type check based on the contents of a class' ancestors? i.e. something like class_of that can be used to check for a class mixing in a module?

Related

Sorbet: how do you add a signature for a dynamically generated method?

I'm using https://github.com/kenn/active_flag and https://github.com/chanzuckerberg/sorbet-rails
This is what its rbi looks like:
module ActiveFlag
extend ActiveSupport::Concern
end
class ActiveFlag::Definition
def column; end
def human(key, options = nil); end
def humans; end
def initialize(column, keys, klass); end
def keys; end
def maps; end
def pairs; end
def set_all!(key); end
def to_array(integer); end
def to_i(arg); end
def to_value(instance, integer); end
def unset_all!(key); end
end
class ActiveFlag::Railtie < Rails::Railtie
end
class ActiveFlag::Value < Set
def method_missing(symbol, *args, &block); end
def raw; end
def set!(key, options = nil); end
def set(key); end
def set?(key); end
def to_human; end
def to_s; end
def unset!(key, options = nil); end
def unset(key); end
def unset?(key); end
def with(instance, definition); end
end
module ActiveFlag::ClassMethods
def flag(column, keys); end
end
class ActiveRecord::Base
extend ActiveFlag::ClassMethods
end
The last bit (extending AR::Base) I added, the rest srb rbi gems generated automatically.
To actually use Active Flag, I then do this in my model:
flag :visible_to, [:employee, :manager, :admin]
visible_to is an integer column. Sorbet Rails has already typed it as such:
sig { returns(Integer) }
def visible_to; end
sig { params(value: Integer).void }
def visible_to=(value); end
sig { returns(T::Boolean) }
def visible_to?; end
I could change this definition, but it's an autogenerated file and I assume the changes will get lost. So the next thing I tried was adding a sig directly above where the method gets used:
sig { returns(ActiveFlag::Value) }
def visible_to; end
flag :visible_to, [:employee, :manager, :admin]
The problem here is that Sorbet fails because the method I've "defined" doesn't return anything. Which I know is fine, because it'll get overriden when flag is called (it uses define_method internally), but I don't know how to communicate this to Sorbet.
Returning value that does not conform to method result type https://srb.help/7005
54 | def visible_to; end
^^^^^^^^^^^^^^^^^^^
Expected ActiveFlag::Value
Method visible_to has return type ActiveFlag::Value
54 | def visible_to; end
^^^^^^^^^^^^^^
Got NilClass originating from:
54 | def visible_to; e
So my question is. What's the best way to tell Sorbet that this method will return an ActiveFlag::Value once it gets defined, ideally without making a manual change to an autogenerated file?
btw. I tried to see how types for enum in Rails are handled... it doesn't look like that's been done on sorbet-typed yet. Possibly for the same reason.
When you want to override a generated rbi file you can create a second rbi file for the same class in your workspace and move the methods in there. Sorbet will handle merging multiple definitions files for you. We keep these files in a separate sorbet/custom directory next to the generated files; others keep the rbi files next to the rb files in their application directory.
As to enums, Sorbet does not natively support enum literal types so that's likely why.
You could implement a custom plugin for sorbet-rails to generate methods added by active_flag and remove the sigs generated wrongly. Here is the documentation for it:
https://github.com/chanzuckerberg/sorbet-rails/blob/master/README.md#extending-model-generation-task-with-custom-plugins

NameError (uninitialized constant Twitter::TextExtractor):

I am getting error while using Twitter-text gem.
I am getting -> NameError (uninitialized constant Twitter::TextExtractor) while creating entry.
here is the right code below
class EntryParser
class << self
include Twitter::Extractor
# Default CSS class for auto-linked lists
DEFAULT_LIST_CLASS = "url link-list-slug".freeze
# Default CSS class for auto-linked usernames
DEFAULT_USERNAME_CLASS = "url link-username".freeze
# Default CSS class for auto-linked hashtags
DEFAULT_HASHTAG_CLASS = "url link-hashtag".freeze
# Default CSS class for auto-linked cashtags
DEFAULT_CASHTAG_CLASS = "url link-cashtag".freeze
# Default URL base for auto-linked usernames
DEFAULT_USERNAME_URL_BASE = "/users/".freeze
# Default URL base for auto-linked lists
DEFAULT_LIST_URL_BASE = "/users".freeze
# Default URL base for auto-linked hashtags
DEFAULT_HASHTAG_URL_BASE = "/tags/".freeze
# Default URL base for auto-linked cashtags
DEFAULT_CASHTAG_URL_BASE = "#!tag?q=%24".freeze
# Default attributes for invisible span tag
DEFAULT_INVISIBLE_TAG_ATTRS = "style='position:absolute;left:-9999px;'".freeze
DEFAULT_OPTIONS = {
:list_class => DEFAULT_LIST_CLASS,
:username_class => DEFAULT_USERNAME_CLASS,
:hashtag_class => DEFAULT_HASHTAG_CLASS,
:cashtag_class => DEFAULT_CASHTAG_CLASS,
:username_url_base => DEFAULT_USERNAME_URL_BASE,
:list_url_base => DEFAULT_LIST_URL_BASE,
:hashtag_url_base => DEFAULT_HASHTAG_URL_BASE,
:cashtag_url_base => DEFAULT_CASHTAG_URL_BASE,
:invisible_tag_attrs => DEFAULT_INVISIBLE_TAG_ATTRS
}.freeze
def parse(entry)
sanitizer = Rails::Html::FullSanitizer.new
sanitized = sanitizer.sanitize(entry.body.gsub(/^(\[[\sx]*)\]/, ''))
sanitized.gsub("\n", "<br/>")
end
# Produces an HTML output string with all of the parsed links we want
def auto_link(entry)
parsed = parse(entry)
entities = extract_entities(parsed)
auto_link_entities_wthout_cmmnt(entry, parsed, entities)
end
def auto_link_comment(reaction) # should probably live in a CommentParser
# This will break if we allow comments on comments, then we need to rethink how
# we do this
if reaction.reaction_type == 'comment' && reaction.reactable.class == Entry
sanitizer = Rails::Html::FullSanitizer.new
sanitized = sanitizer.sanitize(reaction.body)
entities = extract_entities(sanitized)
auto_link_entities(reaction.reactable, reaction, sanitized, entities)
else
reaction.body
end
end
def auto_link_entities_wthout_cmmnt(entry, text, entities)
return text if entities.empty?
# TODO: This is likely inefficient, needs caching or preloading
if entry.team.personal?
org_hash = nil
personal = true
else
org_hash = entry.team.organization.hash_id
personal = false
end
Twitter::Rewriter.rewrite_entities(text.dup, entities) do |entity, _chars|
if entity[:url]
image_or_link(entity[:url])
elsif entity[:hashtag]
if personal
"##{entity[:hashtag]}"
else
"##{entity[:hashtag]}"
end
elsif entity[:screen_name]
if !personal && user = entry.team.active_users.where(go_by_name: entity[:screen_name]).first
"##{entity[:screen_name]}"
else
"##{entity[:screen_name]}"
end
end
end
end
def auto_link_entities(entry,comment , text, entities)
return text if entities.empty?
# TODO: This is likely inefficient, needs caching or preloading
if entry.team.personal?
org_hash = nil
personal = true
else
org_hash = entry.team.organization.hash_id
personal = false
end
Twitter::Rewriter.rewrite_entities(text.dup, entities) do |entity, _chars|
if entity[:url]
image_or_link(entity[:url])
elsif entity[:hashtag]
if personal
"##{entity[:hashtag]}"
else
if comment
"##{entity[:hashtag]}"
else
"##{entity[:hashtag]}"
end
end
elsif entity[:screen_name]
if !personal && user = entry.team.active_users.where(go_by_name: entity[:screen_name]).first
"##{entity[:screen_name]}"
else
"##{entity[:screen_name]}"
end
end
end
end
# This is overridden from Twitter::Extractor because we don't want cashtags
def extract_entities(text, &block)
entities = extract_urls_with_indices(text, {extract_url_without_protocol: true}) +
extract_hashtags_with_indices(text, :check_url_overlap => false) +
extract_mentions_or_lists_with_indices(text)
return [] if entities.empty?
entities = remove_overlapping_entities(entities)
entities.each(&block) if block_given?
entities
end
def image_or_link(entity_link)
entity_url = entity_link.length > 30 ? "#{entity_link[0,25]}..." : entity_link
page_path = Addressable::URI.heuristic_parse(entity_link).to_s
begin
if ['.jpg','.png', '.gif', '.jpeg'].any? { |image_ext| page_path.downcase.include?(image_ext) }
"#{entity_url}
<div class='entry-img'>
<img src='#{page_path}' style='max-width:266px; max-height:200px;'>
</div>"
else
"#{entity_url}"
end
rescue
"#{entity_url}"
end
end
end
end
I don't know what is problem here.

Ruby Dynamic Nested Namespacing

Given two model that are namespaced as
SomeModule::V1::Api
SomeModule::V2::Api
I want to make a call in my controller like:
api = SomeModule::V1::Api
but have the "V1" portion be a variable, so that I can swap between versions.
Any ideas on how to make that happen?
v = 'V1'
"SomeModule::#{v}::Api".constantize
=> SomeModule::V1::Api
Example:
module SomeModule
module V1; end
module V2; end
end
class SomeModule::V1::Api
def self.foo; 'V1 foo'; end
end
class SomeModule::V2::Api
def self.foo; 'V2 foo'; end
end
v = 'V1'
puts "SomeModule::#{v}::Api".constantize.foo
=> V1 foo
v = 'V2'
puts "SomeModule::#{v}::Api".constantize.foo
=> V2 foo
If you don't want to use #constantize (which is a part of ActiveSupport), you can do it with Plain Old Ruby:
version = "V1"
SomeModule.const_get(version).const_get("Api")
# => SomeModule::V1::Api

Am I extending this inbuilt ruby class correctly?

In my rails app in lib/matrix.rb I have entered the following code to extend the inbuilt Matrix class:
module Matrix
require 'matrix'
class Matrix
def symmetric?
return false if not square?
(0 ... row_size).each do |i|
(0 .. i).each do |j|
return false if self[i,j] != self[j,i]
end
end
true
end
def cholesky_factor
raise ArgumentError, "must provide symmetric matrix" unless symmetric?
l = Array.new(row_size) {Array.new(row_size, 0)}
(0 ... row_size).each do |k|
(0 ... row_size).each do |i|
if i == k
sum = (0 .. k-1).inject(0.0) {|sum, j| sum + l[k][j] ** 2}
val = Math.sqrt(self[k,k] - sum)
l[k][k] = val
elsif i > k
sum = (0 .. k-1).inject(0.0) {|sum, j| sum + l[i][j] * l[k][j]}
val = (self[k,i] - sum) / l[k][k]
l[i][k] = val
end
end
end
Matrix[*l]
end
end
end
Is this the correct way to add methods to an existing class within the rails app? Should I have the require matrix line there?
EDIT 1: Additional info provided
I have now removed the require 'matrix' line.
If I type the following test code in a view page, it only works if I delete my lib/matrix.rb file:
<% require 'matrix' %>
<%
m = Matrix[
[0,0],
[1,1]
]
%>
<%= m.column(0) %>
Otherwise it gives the error:
undefined method `[]' for Matrix:Module
It appears that I am eliminating the default methods of the built in Matrix class when I try to extend the class. Is there a way around this error?
no you should not have to require 'matrix' here. Whoever uses your code(rails app in your case), should use require 'matrix'
To extend a core class in Rails, you simply open it, add your methods, and close it. For example, to extend the Matrix class:
class Matrix
def my_method
"New method"
end
end
You should not need to require 'matrix' in your code either. As long as the file holding your extension is in one of the autoload paths, you should have direct access to the new methods.
If you need to add a directory to your Rails autoload path, simply update /config/application.rb with the following line:
config.autoload_paths += %W(#{config.root}/app/extras) # Autoload /app/extras/*.rb

Two extended classes - one works and the other doesn't

I have had some difficulties in using extended classes in rails, in particular extending the Matrix class - and I have also asked some questions related to this:
Am I extending this inbuilt ruby class correctly
Using custom functions in rails app
Where do I put this matrix class in my rails app
In general the answers have been around autoloading in rails 3. However, when I extend 'Math' new functions work, when I extend 'Matrix' new functions don't work - even if I treat them in the same way
I've tried many iterations and change module names, requires, autoloads but here are my latest key files:
application.rb:
require File.expand_path('../boot', __FILE__)
require 'rails/all'
require 'matrix'
# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)
module SimpleFixed
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# Custom directories with classes and modules you want to be autoloadable.
# **I have tried all these autoload variants**
# config.autoload_paths += %W(#{config.root}/extras)
# config.autoload_paths += %W(#{config.root}/lib)
# config.autoload_paths += Dir["#{config.root}/lib/**/"]
# config.autoload_paths << "#{Rails.root}/lib"
config.autoload_paths << "#{::Rails.root.to_s}/lib" # <-- set path
require "extend_matrix" # <-- forcibly load your matrix extension
*other standard code here*
lib/extend_math.rb
module Math
class << self
def cube_it(num)
num**3
end
def add_five(num)
num+5
end
end
end
lib/extend_matrix.rb
module Extend_matrix **An error is raised if I call this Matrix**
class Matrix
def symmetric?
return false if not square?
(0 ... row_size).each do |i|
(0 .. i).each do |j|
return false if self[i,j] != self[j,i]
end
end
true
end
def cholesky_factor
raise ArgumentError, "must provide symmetric matrix" unless symmetric?
l = Array.new(row_size) {Array.new(row_size, 0)}
(0 ... row_size).each do |k|
(0 ... row_size).each do |i|
if i == k
sum = (0 .. k-1).inject(0.0) {|sum, j| sum + l[k][j] ** 2}
val = Math.sqrt(self[k,k] - sum)
l[k][k] = val
elsif i > k
sum = (0 .. k-1).inject(0.0) {|sum, j| sum + l[i][j] * l[k][j]}
val = (self[k,i] - sum) / l[k][k]
l[i][k] = val
end
end
end
Matrix[*l]
end
end
end
my view page:
<%= Math.add_five(6) %> **works*
<%= Matrix[[25,15,-5],[15,18,0],[-5,0,11]].cholesky_factor %> **doesn't work**
Could it be because Math is a Module in ruby but Matrix is a class? If so, how do I correct for this?
If you have a look at the implementation of Matrix, you will see the reason. For me, it is located in .../ruby192/lib/ruby/1.9.1/matrix.rb. The definition is (omitted all methods):
class Matrix
include Enumerable
include ExceptionForMatrix
...
end
That means that Matrix is not contained in a module. Your source for your additions should begin:
class Matrix
def symmetric?
...
end
def cholesky_factor
...
end
end
So your addition to a class or module has to match the current definition. Matrix is known as Matrix in the Ruby constants, and not as Extend_matrix::Matrix which is what you have defined.