Store accessor for nested JSON - ruby-on-rails

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

Related

Rails ActiveAttr Gem, manipulation of attributes within Class?

I have a Rails 5 class which includes ActiveAttr::Model, ActiveAttr:MassAssignment and ActiveAttr::AttributeDefaults.
It defines a couple of attributes using the method attribute and has some instance methods. I have some trouble manipulating the defined attributes. My problem is how to set an attribute value within the initializer. Some code:
class CompanyPresenter
include ActiveAttr::Model
include ActiveAttr::MassAssignment
include ActiveAttr::AttributeDefaults
attribute :identifier
# ...
attribute :street_address
attribute :postal_code
attribute :city
attribute :country
# ...
attribute :logo
attribute :schema_org_identifier
attribute :productontology
attribute :website
def initialize(attributes = nil, options = {})
super
fetch_po_field
end
def fetch_po_field
productontology = g_i_f_n('ontology') if identifier
end
def uri
#uri ||= URI.parse(website)
end
# ...
end
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable (g_i_f_n(...) is defined farther down, it works and its return value is correct). The only way I have found to set this variable is to write self.productontology instead. Moreover, the instance variable #uri is not defined as an attribute, instead it is written down only in this place and visible from outside.
Probably I have simply forgotten the basics of Ruby and Rails, I've done this for so long with ActiveRecord and ActiveModel. Can anybody explain why I need to write self.productontology, using #productontology doesn't work, and why my predecessor who wrote the original code mixed the # notation in #uri with the attribute-declaration style? I suppose he must have had some reason to do it like this.
I am also happy with any pointers to documentation. I haven't been able to find docs for ActiveAttr showing manipulation of instance variables in methods of an ActiveAttr class.
Thank you :-)
To start you most likely don't need the ActiveAttr gem as it really just replicates APIs that are already available in Rails 5.
See https://api.rubyonrails.org/classes/ActiveModel.html.
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable.
This is really just a Ruby thing and has nothing to do with the Rails Attributes API or the ActiveAttr gem.
When using assignment you must explicitly set the recipient unless you want to set a local variable. This line:
self.productontology = g_i_f_n('ontology') if identifier
Is actually calling the setter method productontology= on self using the rval as the argument.
Can anybody explain why I need to write self.productontology, using
#productontology doesn't work
Consider this plain old ruby example:
class Thing
def initialize(**attrs)
#storage = attrs
end
def foo
#storage[:foo]
end
def foo=(value)
#storage[:foo] = value
end
end
irb(main):020:0> Thing.new(foo: "bar").foo
=> "bar"
irb(main):021:0> Thing.new(foo: "bar").instance_variable_get("#foo")
=> nil
This looks quite a bit different then the standard accessors you create with attr_accessor. Instead of storing the "attributes" in one instance variable per attribute we use a hash as the internal storage and create accessors to expose the stored values.
The Rails attributes API does the exact same thing except its not just a simple hash and the accessors are defined with metaprogramming. Why? Because Ruby does not let you track changes to simple instance variables. If you set #foo = "bar" there is no way the model can track the changes to the attribute or do stuff like type casting.
When you use attribute :identifier you're writing both the setter and getter instance methods as well as some metadata about the attribute like its "type", defaults etc. which are stored in the class.

How self[:name] works rails

I created a reader method in my users model
def name
self[:name]
end
I'm having a hard time understanding self[:name]
it looks like I'm accessing a value with a key in a Hash but from what i can tell its not a Hash.
I have also tried to create classes in ruby to emulate this but cant get them to work so i"m not sure whether this is ruby or rails thing that I'm not understanding.
ActiveRecord supplies a [] method:
[](attr_name)
Returns the value of the attribute identified by attr_name after it has been typecast...
So saying self[:name] is just a round-about way to access the name attribute of your model.
[] is a method like any other in Ruby, you can define your own in any class you want:
class C
def [](k)
# do whatever you want
end
end
c = C.new
c[:pancakes]
ActiveRecord is used with data that is, more or less, a Hash backed by a relational database so saying model[:attribute_name] is fairly natural. Hence the existence of the [] method.

Rails: serializing a model's validators into json

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

Rails return JSON serialized attribute with_indifferent_access

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.

custom attr_reader in rails

Mostly in rails if you write my_obj.attr it looks up attr in the database and reports it back. How do you create a custom def attr method that internally queries the database for attr, modifies it and returns? In other words, what's the missing piece here:
# Within a model. Basic attr_reader method, will later modify the body.
def attr
get_from_database :attr # <-- how do I get the value of attr from the db?
end
Something like that:
def attr
value = read_attribute :attr
value = modify(value)
write_attribute :attr, value
save
value
end
Neutrino's method is good if you want to save a modified value back to the database each time you get the attribute. This not recommended since it will execute an extra database query every time you try to read the attribute even if it has not changed.
If you simply want to modify the attribute (such as capitalizing it for example) you can just do the following:
def attr
return read_attribute(:attr).capitalize #(or whatever method you wish to apply to the value)
end

Resources