I've been working with Rails and a number of frontend js frameworks and libs, including Ember, Angular, and React. While all three libraries are powerful in their own regard, one pain point (for me at least) has always been form validation. I've always hated having to keep my model validations (in Rails) in sync with my form validations (in Ember/Angular/React).
Lately, I've been attempting to serialize a model's validators into json. However, while calling as_json on a record will give me back a json hash, it doesn't give me the type of validators for a particular attribute.
For example, let's say I have a model called Assignment. When I create a new Assignment record and call _validators on it, this is what I get.
pry(main)> Assignment.new._validators
=>
{
:title=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e123900 #attributes= [:title], #options={}>],
:full_prompt=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e122e60 #attributes=[:full_prompt], #options={}>],
:submission_window=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e1223c0 #attributes=[:submission_window], #options={}>]
}
Now here's what I get when I add on the as_json call:
pry(main)> Assignment.new._validators.as_json
=>
{
"title"=>[{"attributes"=>["title"], "options"=>{}}],
"full_prompt"=>[{"attributes"=>["full_prompt"], "options"=>{}}],
"submission_window"=>[{"attributes"=>["submission_window"], "options"=>{}}]
}
As you can see, calling as_json removes what types of validators were attached to a model's attribute.
Has anybody run into a similar situation and/or has a workaround? Thanks for any help!
Kurt
this should work better... the attributes and options hash will be a sub-hash which will be the value of a hash where the key is the validator class.. all of course themselves sub-hashes under the attributes
hash = {}
Assignment._validators.each { |k, v| v.each {|val| hash[k] ||= {}; hash[k][val.class.to_s] = val }}
hash.as_json
Rails adds a method to Object called as_json. As you can see here it checks to see if the object in question responds to to_hash (which ActiveModel::Validator does not by default).
The fallback it to invoke instance_values.as_json(options) which is how you're getting the default JSON.
I would implement the to_hash method on ActiveModel::Validator and you can put whatever information in there you'd like.
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
{} # custom implementation here...
end
end
If you want to maintain the default JSON and simply append additional information to it you could do something like
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
instance_values.tap do |hash|
# custom implementation here...
end
end
end
Related
I am currently developing a small Rails 5 application where I need to pass on an ActiveRecord object to an external service based on certain events. In my model I have defined the following:
# /models/user.rb
after_create :notify_external_service_of_user_creation
def notify_external_service_of_user_creation
EventHandler.new(
event_kind: :create_user,
content: self
)
end
The EventHandler is then converting this object to JSON and is sending it through an HTTP request to the external service. By calling .to_json on the object this renders a JSON output which would look something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":12,
"another_association_id":356
}
Now, I need a way to include all first level associations directly into this, instead of just showing the foreign_key. So the construct I am looking for would be something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
"another_association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
}
My first idea was to reflect upon the Model like so: object.class.name.constantize.reflect_on_all_associations.map(&:name), where object is an instance of a user in this case, and use this list to loop over the associations and include them in the output. This seems rather tedious though, so I was wondering if there would be a better way of achieving this using Ruby 2.4 and Rails 5.
If you don't want to use an external serializer, you can override as_json for each model. as_json gets called by to_json.
module JsonWithAssociations
def as_json
json_hash = super
self.class.reflect_on_all_associations.map(&:name).each do |assoc_name|
assoc_hash = if send(assoc_name).respond_to?(:first)
send(assoc_name).try(:map, &:as_json) || []
else
send(assoc_name).as_json
end
json_hash.merge!(assoc_name.to_s => assoc_hash)
end
json_hash
end
end
You'll need to prepend this particular module so that it overrides the default as_json method.
User.prepend(JsonWithAssociations)
or
class User
prepend JsonWithAssociations
end
I have the model "Organization" that stores all information related to an organization. There is a field of type JSONB named "integrations" that stores information pertaining to all external service integrations that an organization has.
How do I use store accessors to access information stored in nested JSON, for example :
{
"mailchimp": {"api_key":"vsvsvef", "list_id":"12345"},
"sendgrid" : {"username":"msdvsv", "password":"123456"}
}
I know I can access mailchimp with store accessors like this :
store_accessor :integrations, :mailchimp
How can I easily access the api_key of mailchimp?
You're right, unfortunately store_accessor does not allow you to access nested keys. The reason is that store_accessor is basically just a shortcut which defines getter and setter methods:
# here is a part of store_accessor method code, you can take a look at
# full implementation at
# http://apidock.com/rails/ActiveRecord/Store/ClassMethods/store_accessor
_store_accessors_module.module_eval do
keys.each do |key|
# here we define a setter for each passed key
define_method("#{key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
# and here goes the getter
define_method(key) do
read_store_attribute(store_attribute, key)
end
end
end
So, your options here are:
To implement your own set of getter and setter methods manually:
# somewhere in your model
def mailchimp_api_key
self.mailchimp["api_key"]
end
def mailchimp_api_key= value
self.mailchimp["api_key"] = value
end
This solves a problem, but you'd have to write lots of this repeatedly for each of nested attributes.
To write your own helper method inside of ActiveRecord::Store::ClassMethods module which would define the same setter and getter methods dynamically for the set of attributes you pass in. You'd have to take the basic implementation of Rails store_accessor and add an additional hash keys iteration to it. Not sure if this is going to be an easy one, but would definitely be interesting to see shared as a gem.
Leave Rails itself and use the power of postgres json type support with some pure SQL code. For example, you can access api_key attribute with something like that:
SELECT integrations->'mailchimp'->>'api_key' as mailchimp_api_key FROM your_table_name;
More on postgres json queries can be found here.
You can do this using the Attribute API
store_accessor :integrations, :mailchimp
store_accessor :mailchimp, :api_key, :list_id
attribute :mailchimp, :json # :jsonb also works if you're using a PG column of that type for `integrations`
I was looking for the same thing. As #twonegatives indicated, store_accessor won't help us. But I did find the #dig method to work pretty well for getting the data. So...
#somewhere in Organization model
def api_key
integrations.dig("mailchimp", "api_key")
end
def username
integrations.dig("sendgrid", "username")
end
I previously had:
serialize :params, JSON
But this would return the JSON and convert hash key symbols to strings. I want to reference the hash using symbols, as is most common when working with hashes. I feed it symbols, Rails returns strings. To avoid this, I created my own getter/setter. The setter is simple enough (JSON encode), the getter is:
def params
read_attribute(:params) || JSON.parse(read_attribute(:params).to_json).with_indifferent_access
end
I couldn't reference params directly because that would cause a loop, so I'm using read_attribute, and now my hash keys can be referenced with symbols or strings. However, this does not update the hash:
model.params.merge!(test: 'test')
puts model.params # => returns default params without merge
Which makes me think the hash is being referenced by copy.
My question is twofold. Can I extend active record JSON serialization to return indifferent access hash (or not convert symbols to strings), and still have hash work as above with merge? If not, what can I do to improve my getter so that model.params.merge! works?
I was hoping for something along the lines of (which works):
def params_merge!(hash)
write_attribute(:params, read_attribute(:params).merge(hash))
end
# usage: model.params_merge!(test: 'test')
Better yet, just get Rails to return a hash with indifferent access or not convert my symbols into strings! Appreciate any help.
use the built-in serialize method :
class Whatever < ActiveRecord::Base
serialize :params, HashWithIndifferentAccess
end
see ActiveRecord::Base docs on serialization for more info.
Posting comment as answer, per #fguillen's request... Caveat: I am not typically a Rubyist… so this may not be idiomatic or efficient. Functionally, it got me what I wanted. Seems to work in Rails 3.2 and 4.0...
In application_helper.rb:
module ApplicationHelper
class JSONWithIndifferentAccess
def self.load(str)
obj = HashWithIndifferentAccess.new(JSON.load(str))
#...or simply: obj = JSON.load(str, nil, symbolize_names:true)
obj.freeze #i also want it set all or nothing, not piecemeal; ymmv
obj
end
def self.dump(obj)
JSON.dump(obj)
end
end
end
In my model, I have a field called rule_spec, serialized into a text field:
serialize :rule_spec, ApplicationHelper::JSONWithIndifferentAccess
Ultimately, I realized I just wanted symbols, not indifferent access, but by tweaking the load method you can get either behavior.
Using HashWithIndifferentAccess is great, but it still acts like a Hash, and it can only serialize as YAML in the database.
My preference, using Postgres 9.3 and higher, is to use the json column type in Postgres. This means that when the table is read, ActiveRecord will get a Hash directly from Postgres.
create_table "gadgets" do |t|
t.json "info"
end
ActiveRecord serialize requires that you provide it a single class that is both responsible for reading/writing the data and serializing/deserializing it.
So you can create an object that does the job by inheriting from HashWithIndifferentAccess, or my preference, Hashie::Mash. Then you implement the serialization as the dump and load class methods.
class HashieMashStoredAsJson < Hashie::Mash
def self.dump(obj)
ActiveSupport::JSON.encode(obj.to_h)
end
def self.load(raw_hash)
new(raw_hash || {})
end
end
In your model, you can specify this class for serialization.
class Gadget < ActiveRecord::Base
serialize :info, HashieMashStoredAsJson
# This allows the field to be set as a Hash or anything compatible with it.
def info=(new_value)
self[:info] = HashieMashStoredAsJson.new new_value
end
end
If you don't use the json column type in Postgres, the implementation changes slightly
Full code and documentation here: using a JSON column type and using a string column type.
I ended up using a variation on bimsapi's solution that you can use not only with simple un-nested JSON but any JSON.
Once this is loaded...
module JsonHelper
class JsonWithIndifferentAccess
def self.load(str)
self.indifferent_access JSON.load(str)
end
def self.dump(obj)
JSON.dump(obj)
end
private
def self.indifferent_access(obj)
if obj.is_a? Array
obj.map!{|o| self.indifferent_access(o)}
elsif obj.is_a? Hash
obj.with_indifferent_access
else
obj
end
end
end
end
then instead of calling
JSON.load(http_response)
you just call
JsonHelper::JsonWithIndifferentAccess.load(http_response)
Does the same thing but all the nested hashes are indifferent access.
Should serve you well but think a little before making it your default approach for all parsing as massive JSON payloads will add significant ruby operations on top of the native JSON parser which is optimised in C and more fully designed for performance.
I am using Ruby on Rails 3.2.2 and I would like to know if it is a correct / not dangerous / common approach to pass an ActiveRecord::Relation object as a method parameter.
At this time I am planning to use this approach in a scope method of a my model this way:
class Article < ActiveRecord::Base
def self.with_active_associations(associations, active = nil)
# associations.class
# => ActiveRecord::Relation
case active
when nil
scoped
when 'active'
with_ids(associations.pluck(:associated_id))
when 'not_active'
...
else
...
end
end
end
Note I: I would like to use this approach for performance reasons since the ActiveRecord::Relation is lazy loaded (in my case, if the active parameter value is not active the database is not hit at all).
Note II: the usage of the pluck method may generate an error if I pass as association parameter value an Array instead of an ActiveRecord::Relation.
1) In my opinion it's a sound tradeoff, you lose the ability to send an array as argument but you gain some perfomance. It's not that strange; for example, every time you define a scope you are doing exactly that, a filter than works only on relations and not on arrays.
2) You can always add Enumerable#pluck so the method works transparently with arrays. Of course it won't work if you use more features of relations.
module Enumerable
def pluck(method, *args)
map { |x| x.send(method, *args) }
end
end
I'm trying to convert an ActiveRecord model to JSON in Rails, and while the to_json method generally works, the model's virtual attributes are not included. Is there a way within Rails to list not just the attributes of a model, but also it's attr_accessor and attr_reader attributes in order that all readable attributes are available when the model is converted to JSON?
I did this to put the information in the model and to keep the method compatible with any way another class might call it (as_json is called by to_json and is supposed to return a Hash):
class MyModel < ActiveRecord::Base
def as_json options=nil
options ||= {}
options[:methods] = ((options[:methods] || []) + [:my, :virtual, :attrs])
super options
end
end
(tested in Rails v3.0)
Before Rails 3, use the :method option:
#model.to_json(:method => %w(some_virtual_attribute another_virtual_attribute))
In Rails 3, use :methods option
#model.to_json(:methods => %w(some_virtual_attribute another_virtual_attribute))
I'm not sure whether it changed in Rails 3 but now you have to use the :methods option instead of :method
Other answers refer to the methods call which returns all methods, including those which aren't attributes. An alternative approach would be to use attributes which returns the model's attributes except that it doesn't include its virtual attributes.
If those are known, then adding something like the below to the model might help:
VIRTUAL_ATTRIBUTES = %i{foo bar baz}
VIRTUAL_ATTRIBUTES.each { |a| attr_accessor a } # create accessors
def attributes
VIRTUAL_ATTRIBUTES.each_with_object(super) { |a,h| h[a] = self.send(a) }
end