ActiveModel dynamic attribute type - ruby-on-rails

Is it possible to decide which type casts an attribute in ActiveModel::Attributes on runtime? I have the following code
class DynamicType < ActiveModel::Type::Value
def cast(value)
value # here I don't have access to the underlying instance of MyModel
end
end
class MyModel
include ActiveModel::Model
include ActiveModel::Attributes
attribute :value, DynamicType.new
attribute :type, :string
end
MyModel.new(type: "Integer", value: "12.23").value
I'd like to decide based on the assigned value of type how to cast the value attribute. I tried it with a custom type, but it turns out, inside the #cast method you don't have access to the underlying instance of MyModel (which also could be a good thing, thinking of separation of concerns)
I also tried to use a lambda block, assuming that maybe ActiveModel see's an object that responds to #call and calls this block on runtime (it doesn't):
class MyModel
include ActiveModel::Model
include ActiveModel::Attributes
attribute :value, ->(my_model) {
if my_model.type == "Integer"
# some casting logic
end
}
attribute :type, :string
end
# => throws error
# /usr/local/bundle/gems/activemodel-5.2.5/lib/active_model/attribute.rb:71:in `with_value_from_user': undefined method `assert_valid_value' for #<Proc:0x000056202c03cff8 foo.rb:15 (lambda)> (NoMethodError)
Background: type comes from a DB field and can have various classes in it that do far more than just casting an Integer.
I could just do a def value and build the custom logic there, but I need this multiple times and I also use other ActiveModel features like validations, nested attributes... so I would have to take care about the integration myself.
So maybe there is a way to do this with ActiveModel itself.

Your are storing the value as a string in the DB, so ActiveRecord will retreive it as a string. You will need to convert it manually. Rails provides these methods, that I am sure you are familiar with:
"1".to_i => to integer => 1
"foo.to_sym => to symbol => :foo
"1".to_f => to float => "1.0"
123.to_s => to string => "123"
(1..10).to_a => to array => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Then in your model you could do:
def value
case type
when "Integer"
value.to_i
when "Float"
value.to_f
...
end
This could work but I see a problem with this approach. You decalre an attribute with the name type, you could get some erros of Rails complaining that type is a reserve word for STI, or get weird bugs, I suggest you to rename it.

Related

Datatype validation in rails ActiveModel::Validations

I wanted to validate class attributes with respect to their data type.
I am writing an API in ruby and i need to check if a particular parameter data is of type String, hash, Array etc.
eg.
we have
class ApiValidation
include ActiveModel::Validations
attr_accessor :fieldName
validates :fieldName, :presence => true
end
so similar to :presence => true, i want to add one more validation like :datatype => :string
Please suggest me some custom validation or any other approach to achieve my requirement.
You can check out dry-rb stack.
There is https://github.com/dry-rb/dry-types (https://dry-rb.org/gems/dry-types/1.2/) gem which do exactly what you want.
From docs:
Strict types will raise an error if passed an attribute of the wrong type:
class User < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Strict::Integer
end
User.new(name: 'Bob', age: '18')
# => Dry::Struct::Error: [User.new] "18" (String) has invalid type for :age
In Ruby, "datatypes" are actually just classes. So, all you really have to do is check which class fieldName belongs to (fieldName.class) in your custom validation.
Parameters are always received as text. Ruby doesn't have static types, but it does have classes, so your code needs to parse each attribute and convert them into the class you expect.
This could be as simple as riding on Ruby's inbuilt conversion methods, eg. to_i, to_sym, to_d, to_h, or you might need to add more logic, eg. JSON.parse.
Do note the difference between strict and non-strict parsing which will require different control flow handling.
>> "1one".to_i
=> 1
>> Integer("1one")
ArgumentError: invalid value for Integer(): "1one"
According to the Rails Guides and Rails API docs, we could use validates_each method which is available in ActiveModel::Validations.
For example:
class Person
include ActiveModel::Validations
attr_accessor :first_name, :last_name
validates_each :first_name, :last_name do |record, attr, value|
record.errors.add attr, " must be a string." unless value.is_a? String
end
end

What does include EnumeratedType and DomainModel means in ruby

I came across few ruby codes and found that some class structures are like
module A
module B
class C
include EnumeratedType
declare :an, :value => 1, :description => "AN",
declare :bn, :value => 1, :description => "BN"
end
end
end
Similarly I have noticed like include DomainModel.
Also I have seen that there is no name field but it seems we can use "declare" key as "name" key when retreiving above as map. Is it so ??
Thanks in advance!
The include keyword means that, it lets you mix-in instance methods from the module with the same name as the parameter. In your case, the include EnumeratedType means that somewhere in your application or Gem, has a module named EnumeratedType and you want to use its instance methods in class C
Same with include DomainModel.

Rails active model serializer, how to return empty string instead of null

I have to work with an legacy app and have to rewrite the old (PHP base)rest api. In the old api, when an attribute was null, it became an empty string.
Rails however just returns a null, which breaks the app. Rewriting the app is not the solution (even it this would be the cleanest way). Also in the old api, the other values are strings to (integers, booleans, numbers). So I was wondering, how can I do a to_s on every attribute witihout overriding every attribute ofcourse. I'm using active model serializer.
A little metaprogramming should help:
class MySerializer < ActiveModel::Serializer
[:id, :attr1, :attr2, :attr2].each do |attr|
# Tell serializer its an attribute
attribute attr
# Define a method with the same name as the attribute that calls the
# underlying object and to_s on the result
define_method attr do
object.send(attr).to_s
end
end
end

How to restrict values for a column

I want to restrict available values for a field. So the value of the column must be from specified set of values. Is it possible using migration/models? Or I have to do it manually in my DB?
You'll use validations for this. There's a whole Rails guide on the topic. The specific helper you're looking for in this case is :inclusion, e.g.:
class Person < ActiveRecord::Base
validates :relationship_status,
:inclusion => { :in => [ 'Single', 'Married', 'Divorced', 'Other' ],
:message => "%{value} is not a valid relationship status" }
end
Edit Aug. 2015: As of Rails 4.1, you can use the enum class method for this. It requires that your column be an integer type:
class Person < ActiveRecord::Base
enum relationship_status: [ :single, :married, :divorced, :other ]
end
It automatically defines some handy methods for you, too:
p = Person.new(relationship_status: :married)
p.married? # => true
p.single? # => false
p.single!
p.single? # => true
You can read the documentation for enum here: http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html
It depends on the amount of confidence you need. You could just add a validator to your model to restrict it to those values but then you wont be sure that existing data will match (and will cause subsequent saves to fail because of validation) and also that other changes could be made by other apps/raw sql that would get around it.
If you want absolute confidence, use the database.
Here's what you might want to use if you do it in the database (which is quite limited compared to what a rails validator could do: http://www.w3schools.com/sql/sql_check.asp

How do I define synthetic attributes for an ActiveRecord model?

I have an ActiveRecord model whose fields mostly come from the database. There are additional attributes, which come from a nested serialised blob of stuff. This has been done so that I can use these attributes from forms without having to jump through hoops (or so I thought in the beginning, anyway) while allowing forwards and backwards compatibility without having to write complicated migrations.
Basically I am doing this:
class Licence < ActiveRecord::Base
attr_accessor :load_worker_count
strip_attributes!
validates_numericality_of :load_worker_count,
:greater_than => 2, :allow_nil => true, :allow_blank => true
before_save :serialise_fields_into_properties
def serialise_fields_into_properties
...
end
def after_initialize
...
end
...
end
The problem I noticed was that I can't get empty values in :load_worker_count to be accepted by the validator, because:
If I omit :allow_blank, it fails validation complaining about it being blank
If I put in :allow_blank, it converts the blank to 0, which when fails on the :greater_than => 2
In tracking down why these blank values are getting to the validation stage in the first place, I discovered the root of the problem: strip_attributes! only affects actual attributes, as returned by the attributes method. So the values which should be nil at time of validation are not. So it feels like the root cause is that the synthetic attributes I added in aren't seen when setting which attributes to strip, so therefore I ask:
Is there a proper way of creating synthetic attributes which are recognised as proper attributes by other code which integrates with ActiveRecord?
I assume you are talking of the strip_attributes plugin; looking at the code, it uses the method attributes, defined in active_record/base.rb, which uses #attributes, which is initialized (in initialize) as #attributes = attributes_from_column_definition.
Maybe it's possible to hack ActiveRecord::Base somehow, but it would be a hard work: #attributes is also used when getting/putting stuff from/to db, so you would have to do a lot of hacking.
There's a much simpler solution:
before_validate :serialise_fields_into_properties
...
def serialise_fields_into_properties
if load_worker_count.respond_to? :strip
load_worker_count = load_worker_count.blank? ? nil : load_worker_count.strip
end
...
end
After all, this is what strip_attributes! does.
Wouldn't it be easier to just use Rails' serialize macro here?
class License < ActiveRecord::Base
serialize :special_attributes
end
Now you can assign a hash or array or whatever you need to special_attributes and Rails will serialize it a text field in the database.
license = License.new
license.special_attributes = { :beer => true, :water => false }
This will keep your code clean and you don't have to worry about serializing/deserializing attributes yourself.

Resources