ActiveModelSerializer and VOID types responses - sorbet

Solved!
Kids, don't define read_attribute_for_serialization to return void
So I'm trying to integrate sorbet in one of my rails apps. I've added sorbet in the Gemfile as so
gem "sorbet-runtime", "~> 0.5.5657"
gem "sorbet-rails", "~> 0.6.5.1"
gem "sorbet", "~> 0.5.5657", group: [:development, :test]
All looks good an I was able to add sigs to Price class and resolve issues with type checking inside the app.
The problem I'm experiencing is the return values from serializers
So the serializer looks roughly like this
# typed: false
# frozen_string_literal: true
class MySuperAwesomeSerializer < ActiveModel::Serializer
attributes :start_date
has_one :price, serializer: PriceSerializer
end
In specs, when I want to validate the return value from the associated (has_one) attribute I get this
{
start_date: 2020-05-22,
price: {
total_amount: T::Private::Types::Void::VOID,
taxless_amount: T::Private::Types::Void::VOID,
vat_amount: T::Private::Types::Void::VOID
}
}
Price serializer looks roughly like this
# typed: false
# frozen_string_literal: true
class PriceSerializer < ActiveModel::Serializer
attributes :taxless_amount, :total_amount, :vat_amount
end
I fail to understand what and why is exactly happening here. Thanks in advance
EDIT: added the "solution" at the top of the post

I haven’t encountered this problem before, but it looks like a potential sorbet bug.
T::Private::Types::Void::Void is the return type that sorbet swap out for methods that have void as the return type. In this case, you don’t define any sig for the attributes so it shouldn’t have stub out the returning value.
To work around, you can define explicit sig for those attributes like
sig { returns(Integer) }
def taxless_amount
...
end
I haven’t tried but this may also work
sig { returns(Integer) }
attributes :taxless_amount
Edit: can you show the actual PriceSerializer class btw? It looks like it works without problem for the attributes in the other Serializer.

If you define read_attribute_for_serialization don't do it this way:
sig { params(attr: Symbol).void }
def read_attribute_for_serialization(attr)
public_send(attr)
end

Related

Delegate throws error for mongoid document

I have a problem that I don't understand and can't find a solution to:
works:
class Document
CONSTANT_ARRAY = [0,1,2,3]
delegate :sum, to: :CONSTANT_ARRAY
end
does not work:
class Document
include Mongoid::Document
CONSTANT_ARRAY = [0,1,2,3]
delegate :sum, to: :CONSTANT_ARRAY
end
The latter throws error ArgumentError: wrong number of arguments (given 2, expected 1)
To be added, the code was working before the mongoid upgrade, in version ~> 5.0, rails 4, now I have mongoid 7.1.0, rails 5.2.4.1
I'm not sure if it is relevant to add, the code gets called from another class
class Items
include Mongoid::Document
embeds_many :document_fields, class_name: 'Document', cascade_callbacks: true
end
class Another
include Mongoid::Document
embeds_many :items, class_name: 'Item', cascade_callbacks: true
def document_fields
items.flat_map(&:document_fields)
end
end
I have reduced the amount of code in the classes, because I don't see the relevance.
UPDATE: So I've figured out that this works. But is it the right way?
CONSTANT_ARRAY = [0,1,2,3]
delegate :sum => :CONSTANT_ARRAY
logger.debug Document.new.sum # prints 6 as it is supposed to
This is an issue in Mongoid 7.1.0: https://jira.mongodb.org/browse/MONGOID-4849

Validating Child Object with ActiveModel Validations

I have two plain Ruby classes, Account and Contact. I am using Simple Form's simple_form_for and simple_fields_for to create nested attributes. I am looking to fulfill the following validation requirements:
An associated Contact must exist for the new Account
The associated Contact must be valid (i.e., account.contact.valid?)
It looks like ActiveModel no longer includes the validates_associated method, as using that method results in an undefined method error. I considered requiring ActiveRecord::Validations, but this led down a stretch of various errors (e.g., undefined method `marked_for_destruction?')
I also considered defining validate on the Account class and calling valid? on the associated object, but that only prevented the form from submitting if there was also an error on the parent object.
validate do |account|
account.contact.valid?
# required for form to fail
errors.add(:base, "some error")
end
Is there something I'm not aware of to solve this? Thanks.
I recently (7 years after this question has been asked!) faced the same issue and solved it by implementing the AssociatedValidator based on the ActiveRecord one.
I simply included it in config/initializers folder:
module ActiveModel
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if Array(value).reject { |r| valid_object?(r) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
end
end
private
def valid_object?(record)
record.valid?
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
now you can use validates_associated in ActiveModel too.
class Person
include Virtus
include ActiveModel::Model
attribute :address, Address, :default => Address.new
validate :address_valid
private
def address_valid
errors.add(:base, 'address is not valid') unless address.valid?
end
end
class Address
include Virtus::ValueObject
include ActiveModel::Validations
attribute :line_1, String
attribute :line_2, String
validates :line_1, :presence => true
validates :line_2, :presence => true
end
The errors show up in the form if you pass an object to simple_fields_for:
= form.simple_fields_for person.address do |af|
= af.input :line_1
Another option is overriding valid?:
def valid?
super & address.valid?
end
Note its & not && so the conditions are not short circuited if the first returns false.

How to render rails partial from presenter layer?

Well, I'm into this situation as well now using rails 3.2.1
Following is the presenter in app/presenters/form_presenter.rb
class FormPresenter
def render_form
ActionView::Base.new.render partial: "passions/add_form"
end
end
From the view I'm calling,
...
= AddFormPresenter.new.render_form
...
But it blows with the following error:
13:14:12 ActionView::MissingTemplate - Missing partial passions/passion_concept_add_form with {:locale=>[:en], :formats=>[:html, :text, :js, :css, :ics, :csv, :png, :jpeg, :gif, :bmp, :tiff, :mpeg, :xml, :rss, :atom, :yaml, :multipart_form, :url_encoded_form, :json, :pdf, :zip], :handlers=>[:erb, :builder, :slim, :coffee, :rabl]}. Searched in:
...
There is this similar question at RAILS-3.1 render method for ActionView::Base but its not helpful.
How to render this partial from the presenter layer?
Well, I just did it by grabbing the view context using a before filter. My reference was this: https://github.com/amatsuda/active_decorator/blob/master/lib/active_decorator/view_context.rb
So something like:
class FormPresenter
def render_form
FromPresenter.view_context.render partial: "passions/add_form"
end
class << self
def view_context
Thread.current[:view_context]
end
def view_context=(view_context)
Thread.current[:view_context] = view_context
end
end
module Controller
extend ActiveSupport::Concern
included do
before_filter do |controller|
FormPresenter.view_context = controller.view_context
end
end
end
end
and in application_controller.rb
class ApplicationController < ActionController::Base
...
include FormPresenter::Controller
...
end
This isn't typical of the presenter pattern. Presenters are for centralizing complicated data and logic needed to simpify the view's rendering task. Here you are rendering inside the presenter. Is this really what you intend?
Say the answer is yes. Then just creating a new ActionView::Base is asking for trouble because initializing it is non-trivial as shown here. Something strange is going on with class or some other kind of nesting. Where did the passion_concept_ prefix come from in the error message? It looks like you're not telling us all we need about your app.
You may find joy by telling the presenter explicitly where it's rendering:
class FormPresenter
def self.render_form(view)
view.render partial: "passions/add_form"
end
end
Then in the view:
= FormPresenter.render_form(self)
(Here again the explanation is not clear. What is AddFormPresenter?) I don't have a machine where I can try this at the moment, but it ought to be more debuggable than what you've got.

Serialize and deserialize

I have an active record class with an embedded sample:
class LabResults < ActiveRecord::Base
serialize :sample
end
class Sample
attr_accessor :values # GSL::Vector of responses
def to_yaml
YAML.quick_emit( self, opts ) { |out|
out.map( "!testfile,2012-02-27" ) { |map|
#values.map{|v| v.to_a }
}
}
end
def analyze; end; # do stuff with values
end
I want to serialize and store sample in the database, but GSL::Vector (from gsl gem), does not have a to_yaml method. Defining to_yaml and YAML.quick_emit for Sample is apparently deprecated when using Rails 3.2's default YAML engine Psych.
Any ideas how to serialize and de-serialize this object?
You can write a custom (de)serializer for the column, and pass it as the second argument to "serialize", e.g.:
serialize :sample, SampleSerializer.new
Where SampleSerializer is a class that defines "load" and "dump" methods.
More detail in this answer: ActiveRecord serialize using JSON instead of YAML

instance method in scope

I don't know even if its possible? I need to use instance method with/within scope. Something like this:
scope :public, lambda{ where ({:public => true}) }
and call instance method(complete?) on each record to see if it is completed. public scope here should return all records that are public and are completed and completion of a record is determined by a instance method 'complete?'
Any possibility?
Thanks
Scopes are about generating query logic using ARel. If you can't represent the logic of the complete? method in SQL then you're kind of stuck
Scopes - in rails 3 at least - are meant for chaining together query logic without returning a result set. If you need a result set to do your testing for complete you'll need to do something like
class MyModel < ActiveRecord::Base
scope :public, lambda{ where ({:public => true}) }
def self.completed_public_records
MyModel.public.all.select { |r| r.completed? }
end
end
# elsewhere
MyModel.completed_public_records
Or if you need more flexibility
class MyModel < ActiveRecord::Base
scope :public, lambda{ where ({:public => true}) }
# some other scopes etc
def self.completed_filter(finder_obj)
unless finder_obj.is_a?(ActiveRecord::Relation)
raise ArgumentError, "An ActiveRecord::Relation object is required"
end
finder_obj.all.select { |r| r.completed? }
end
end
# elsewhere
MyModel.completed_filter(MyModel.public.another_scope.some_other_scope)
I created a rubygem for this exact problem a few months back when I had the same problem.
It allows you to add methods that work on the result set of the query, but abstracts the methods into another class so its not muddled in with your model.
Check it out: https://github.com/coryodaniel/collectively