Force ActiveResource to not convert complex object from JSON - ruby-on-rails

The ActiveResource docs states that:
Any complex element (one that contains other elements) becomes its own object:
# With this response:
# {"id":1,"first":"Tyler","address":{"street":"Paper St.","state":"CA"}}
#
# for GET http://api.people.com:3000/people/1.json
#
tyler = Person.find(1)
tyler.address # => <Person::Address::xxxxx>
Since an attribute on the object I'm retrieving is a RGeo object that is supposed be be JSON, how do I request that this attribute is not converted. So the above would become:
tyler = Person.find(1)
tyler.address # => {"street":"Paper St.","state":"CA"}

TRy defining it as serialize Hash
class Person < ActiveRecord::Base
serialize :address, Hash
end
It will then treat address field as Hash
#object.address should return you, key value pair

Related

Grape Entity for a Hash with string keys not working

I am using 'grape-entity', '~> 0.7.1'
I have a hash in the format:
temp_data = [{sheet_index: 0, other_names: []},{'sheet_index' => 1, 'other_names': ['a']}]
And I have the following entities
class Sheet < Grape::Entity
expose :sheet_index, documentation: {type: Integer, desc: "Sheet index"}
expose :other_names, documentation: {type: Array, desc: "Other names"}
end
class Sheets < Grape::Entity
present_collection true
expose :items, as: 'sheet_history', using Entities::Sheet
end
# response from the entities
present temp_data, with: Entities::Sheets
Now I need to make sure that no matter the type of keys in my Hash it should still give me the correct output for the above case
expected_response = {"sheet_history" => [{"sheet_index"=>0, "other_names"=>[]}, {"sheet_index"=>1, "other_names"=>["a"]}]}
but the response I am getting is in the format below
actual_response = {"sheet_history" => [{"sheet_index"=>0, "other_names"=>[]}, {"sheet_index"=>nil, "other_names"=>nil}]}
so in the actual response sheet_index and other_names of the second element are nil because their keys were Strings, not Symbols. (Refer to temp_data.)
I have referred to https://github.com/ruby-grape/grape-entity/pull/85 to get the above implementation but still am not able to make it work without using HashWithIndifferentAccess or OpenStructs
You are missing a colon after using, but I wouldn't set up multiple entities like that as it's likely to result in wonky behavior. Try this:
# Dummy definition of your class
class Item
include ActiveModel::Serialization
attr_accessor :sheet_index
attr_accessor :other_names
def initialize(index, names)
#sheet_index = index
#other_names = names
end
end
items = []
items << Item.new(0, [])
items << Item.new(1, ['a'])
=> [
#<Item:0x00007f860f740e40 #other_names=[], #sheet_index=0>,
#<Item:0x00007f860f513618 #other_names=["a"], #sheet_index=1>
]
# Entity Definition
class Sheet < Grape::Entity
# The first arg here is the key to use for a collection,
# the second is the key to use for a single object
root 'sheet_history', 'sheet_history'
expose :sheet_index, documentation: {
type: Integer,
desc: "Sheet index" # Plz use locales
}
expose :other_names, documentation: {
type: Array,
desc: "Other names" # Plz use locales
}
end
# Test it
representation = Sheet.represent(items)
=> {
"sheet_history"=>[
#<Sheet:70106854276160 sheet_index=0 other_names=[]>,
#<Sheet:70106854275680 sheet_index=1 other_names=["a"]>
]
}
# This is just more a more readable, but as you can see it's
# both mapping all the attributes correctly and
# setting the root key that you wanted:
representation['sheet_history'].map do |r| r.serializable_hash end
=> [
{
:sheet_index=>0,
:other_names=>[]
},
{
:sheet_index=>1,
:other_names=>["a"]
}
]
# Endpoint
get do
items = current_user.items # or whatever
present items, with: Entities::Sheet
end
You can send your array of hashes to the represent method, but it doesn't like the stringified key. Ideally you should be passing DB objects to your entity instead of hashes but, if you for some reason cannot, I would pass temp_data.map(&:symbolize_keys) as your argument to the entity to ensure the top-level keys in the hash it's parsing are symbols.

Existing data serialized as Array isn't stored when upgrading to RoR 5

I have almost upgraded my RoR application from v4 to v5 and there are some more things to be done in order to finish the process.
In my RoR v4 application I was using attributes that was serialized as Hash and Array:
class ModelOne < ApplicationRecord
serialize :attribute_one_names, Hash
end
class ModelTwo < ApplicationRecord
serialize :attribute_two_names, Array
end
Now I need to update records in the database to match the new RoR v5 requirements.
Based on this answer I can successfully migrate the attribute_one_names (Hash) data by running the following migration:
class MigrationOneFromRor4ToRor5 < ActiveRecord::Migration[5.2]
class ModelOne < ApplicationRecord
self.table_name = 'model_one'
serialize :attribute_one_names
end
def up
ModelOne.all.each do |m|
h = m.attribute_one_names.to_unsafe_h.to_h
m.attribute_one_names = h
m.save!
end
end
end
The problem is with the attribute_two_names (Array) data.
class MigrationTwoFromRor4ToRor5 < ActiveRecord::Migration[5.2]
class ModelTwo < ApplicationRecord
self.table_name = 'model_two'
serialize :attribute_two_names
end
def up
ModelTwo.all.each do |m|
array_of_names = []
m.attribute_two_names.each do |name|
array_of_names << name.to_unsafe_h.to_h
end
# Output 1:
puts array_of_names.inspect
# => [{"name"=>"Website1Name", "url"=>"http://www.website1.com"}, {"name"=>"Website2Name", "url"=>"http://www.website2.com"}]
puts m.attribute_two_names.inspect
# => [<ActionController::Parameters {"name"=>"Website1Name", "url"=>"http://www.website1.com"} permitted: false>, <ActionController::Parameters {"name"=>"Website2Name", "url"=>"http://www.website2.com"} permitted: false>]
m.attribute_two_names = array_of_names
# Output 2:
puts m.attribute_two_names.inspect
# => [{"name"=>"Website1Name", "url"=>"http://www.website1.com"}, {"name"=>"Website2Name", "url"=>"http://www.website2.com"}]
m.save!
# Output 3:
puts m.attribute_two_names.inspect
# => []
end
end
end
In fact, by running this migration, --- [] values are stored into the database regardless of the existing data serialized as Array. That is, a --- [] value is stored for each record regardless of the previously data present in the database.
How can I solve the problem?
Note:
Before running the MigrationTwoFromRor4ToRor5, in the attribute_two_names database column there are values like these:
---
- !ruby/hash:ActionController::Parameters
name: Website1Name
url: http://www.website1.com
- !ruby/hash:ActionController::Parameters
name: Website2Name
url: http://www.website2.com
---
- !map:ActiveSupport::HashWithIndifferentAccess
name: Website1Name
url: http://www.website1.com
- !map:ActiveSupport::HashWithIndifferentAccess
name: Website2Name
url: http://www.website2.com
You could try unpacking the YAML by hand to get plain old hashes and arrays then then manually re-YAML-ing that data. Something like this:
class MigrationTwoFromRor4ToRor5 < ActiveRecord::Migration[5.2]
# Make sure this name is not used by any real models
# as we want our own completely separate interface to
# the `model_two` table.
class ModelTwoForMigrationChicanery < ApplicationRecord
self.table_name = 'model_two'
# We'll be treating `attribute_two_names` as just
# a blob of text in here so no `serialize`.
end
def up
ModelTwoForMigrationChicanery.all.each do |m|
# Unpack the YAML that `serialize` uses.
a = YAML.load(m.attribute_two_names)
# Using `as_json` is a convenient way to unwrap
# all the `ActionController::Parameters` and
# `ActiveSupport::HashWithIndifferentAccess`
# noise that go into the database. `#as_json`
# on both of those give you plain old hashes.
a = a.as_json
# Manually YAMLize the cleaned up data.
m.attribute_two_names = a.to_yaml
# And put it back in the database.
m.save!
end
end
end
Once all this is sorted out, you should get something into your models and controllers to ensure that only plain arrays and hashes get through to serialize. I'd also recommend moving away from serialize to something sane like JSON column types (if your database supports it and ActiveRecord supports your database's support of it).

Save a hash to a parameter?

I would like to save the params from the form submitted into a hash called: hash_options which corresponds to a field in my table.
How do I store the hash of: hash_options as the value for: hash_fields ?
hash_fields is a text field and I am trying to store hash_options in this row as a plain hash.
def person_params
hash_options = {age: params['person']['age'], location: params['person']['location'], gender: params['person']['gender']}
params.require(:person).permit(:first_name, :last_name, :owner, hash_fields: [hash_options])
end
Side question: How does the model access and store the params?
In order to be able to save the hash as is to the database you have a couple of options:
If you are using MongoDB(mongoid) you can just define it as a hash type field in your model with:
field options_hash, type: Hash
You can use postgresql HStore to save schemaless data into that column which enables you to save the hash into the column and work with it as a Hash.
As for the params part there is no need to include it in the permit call, you can just do:
model = Model.new(person_params) do |m|
m.options_hash = hash_options
end
model.save

Saving a nested hash in Ruby on Rails

I'm trying to save a nested Hash to my database and retrieve it, but nested values are lost upon retrieval.
My model looks like this:
class User
serialize :metadata, MetaData
end
The class MetaData looks like this:
class MetaData < Hash
attr_accessor :availability, :validated
end
The code I'm using to store data looks something like this (the real data is coming from a HTML form, though):
user = User.find(id)
user.metadata.validated = true
user.metadata.availability = {'Sunday' => 'Yes', 'Monday' => 'No', 'Tuesday' => 'Yes'}
user.save
When I look at the data in the database, I see the following:
--- !map:MetaData
availability: !map:ActiveSupport::HashWithIndifferentAccess
Sunday: "Yes"
Monday: "No"
Tuesday: "Yes"
validated: true
The problem occurs when I try to get the object again:
user = User.find(id)
user.metadata.validated # <- this is true
user.metadata.availability # <- this is nil
Any ideas? I'm using Rails 3.1 with Postgresql as my datastore.
If you look in the database you see "map:ActiveSupport::HashWithIndifferentAccess" for availability?
My approach would be to separate out the single instance of availablity from the hash collection structure of days available.
you mean user.metadata.validated # <- this is true ?
What DB columns are metadata and availability stored as? They need to be TEXT

Accessing a Hash in a REST POST

I have a hash in Ruby:
params={"username"=>"test"}
I want to add another associative array like:
params["user"]={"name"=>"test2"}
so params should become
params={"username"=>"test","user"=>{"name"=>"test2"}}
but when I post this params to a url, I get:
params[:user][:name] # => nil
when I dump the user data:
params[:user] # => ['name','test2']
what I want is
params[:user] # => output {'name'=>'test2'}
what am I doing wrong? thanks for help.
You're just using wrong key, you think that :user and "user" are the same, which is not.
params["user"]["name"] #=> "test2"
params["user"] #=> {"name"=>"test2"}
UPDATE from Naveed:
:user is an instance of Symbol class while "user" is instance of String
You have created a hash with keys of type string and trying to access with symbol keys. This works only with class HashWithIndifferentAccess.
If you want to achieve the same convert your hash to HashWithIndifferentAccess by using with_indifferent_access method,
> params = {"username"=>"test", "user"=>{"name"=>"test2"}}
=> {"username"=>"test", "user"=>{"name"=>"test2"}}
> params[:user][:name]
=> nil
>params = params.with_indifferent_access
> params[:user][:name]
=> "test2"
Update: request params is an instance of HashWithIndifferentAccess
The following should work:
params["user"]
params={"username"=>"test"}# params is not array nor associative array its a hash
you can add key value pair in hash by
params["key"]="value"
key and value both can be object of any class,be sure you use same object as key to access value or take a look at
HashWithIndifferentAccess
now
params["user"]={"name"=>"blah"}
params["user"] # => {"name"=>"blah"}
params["user"]["name"] # => "blah"

Resources