I am trying to not jsonify a string within a hash. It already has been escaped.
Reading around the way of handling this for PORO is to overwrite as_json. So I wrapped the string in another object. But as I'm dealing with just a string that leads to a stack level too deep when I return the object. When I return the already encoded string, it obviously tries to escape it. activesupport-6.0.3.2/lib/active_support/json/encoding.rb
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
value.as_json
when Hash
Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
when Array
value.map { |v| jsonify(v) }
else
jsonify value.as_json
end
end
What I could do is monkey patch the above method to accept the class I have wrapped the string in and do nothing.
I guess the other option is to parse the JSON string back into a hash and let it follow the normal procedure. That would impact our performance too much though.
So my question is, Is there are more elegant way of telling Rails to do nothing when encountering a specified object when it tries to jsonify it.?
Edit:
Im using this already jsonified string like:
{foo: MyStringJsonWrapper.new('{"bar":"foobar"}')}.to_json
I was looking around in the file activesupport-6.0.3.2/lib/active_support/json/encoding.rb and noticed you can configure which class is used to perform the json encoding. It is currently JSONGemEncoder which is contained within that class.
So I made my own class:
class MyJSONEncoder < ActiveSupport::JSON::Encoding::JSONGemEncoder
class AlreadyEscapedString < String
def to_json(*)
self
end
end
private
def jsonify(value)
if value.is_a?(AlreadyEncodedStringWrapper)
AlreadyEscapedString.new(value.already_jsonified)
else
super
end
end
end
and
class AlreadyEncodedStringWrapper
attr_accessor :already_jsonified
def initialize already_jsonified
#already_jsonified = already_jsonified
end
def as_json(options = {})
self
end
end
The extra class of AlreadyEscapedString exists to overwrite the behaviour of EscapedString(found in the same encoding class) but do nothing.
Then just set:
ActiveSupport.json_encoder = MyJSONEncoder
Example:
jsonified_hash = {foobar: "barbaz"}.to_json
already_encoded_string = AlreadyEncodedStringWrapper.new(jsonified_hash)
{foo: already_encoded_string}.to_json
=> "{\"foo\": {\"foobar\": \"barbaz\"}}"
Related
I'm trying to call a class and method which are both stored as strings in the DB.
I would be looking to create an object from these 2 strings:
class_name = 'Some::Class'
method_name = 'a_method'
params = {foo: 'bar'}
class_name.new(params).method_name
I have used constantize before for a similar approach, but I'm not sure exactly what the best solution is here?
You can use const_get:
Class.const_get(class_name).new(params).public_send(method_name)
class_name.constantize.new(params).send(method_name) should work?
If you want to send args with the method, you can use, for example, .send(method_name, params)
If you're getting the data from external sources, I'm not sure how secure constantize is, but it works smoothly and is nicely readable. You can also use classify if the format of the class name could vary.
You tagged your question with ruby-on-rails, therefore I suggest using safe_constantize (available in Rails only) and public_send (plain Ruby):
class_name = 'Some::Class'
method_name = 'a_method'
params = {foo: 'bar'}
class_name.safe_constantize.new(params).public_send(method_name)
If the use of Kernel#eval is safe you could do the following.
module Some
class Klass
def initialize(params)
#params = params
end
def a_method
puts "keys = #{#params.keys}"
end
end
end
class_name = 'Some::Klass'
params = { foo: 'bar', oof: 'rab' }
method_name = 'a_method'
str = "%s.new(%s).%s" % [class_name, params, method_name]
#=> "Some::Klass.new({:foo=>\"bar\", :oof=>\"rab\"}).a_method"
eval str
keys = [:foo, :oof]
This approach obviously has wide application.
I'm having problems with weird behaviour in RoR. I'm having a Hash that i'm converting to json using to_json() like so:
data = Hash.new
# ...
data = data.to_json()
This code appears inside a model class. Basically, I'm converting the hash to JSON when saving to database. The problem is, the string gets saved to database with its surrounding quotes. For example, saving an empty hash results in: "{}". This quoted string fails to parse when loading from the database.
How do I get rid of the quotes?
The code is:
def do_before_save
#_data = self.data
self.data = self.data.to_json()
end
EDIT:
Due to confusions, I'm showing my entire model class
require 'json'
class User::User < ActiveRecord::Base
after_find { |user|
user.data = JSON.parse(user.data)
}
after_initialize { |user|
self.data = Hash.new unless self.data
}
before_save :do_before_save
after_save :do_after_save
private
def do_before_save
#_data = self.data
self.data = self.data.to_json()
end
def do_after_save
self.data = #_data
end
end
The data field is TEXT in mysql.
I'm willing to bet money that this is the result of you calling .to_json on the same data twice (without parsing it in between). I've had a fair share of these problems before I devised a rule: "don't mutate data in a lossy way like this".
If your original data was {}, then first .to_json would produce "{}". But if you were to jsonify it again, you'd get "\"{}\"" because a string is a valid json data type.
I suggest that you put a breakpoint in your before_save filter and see who's calling it the second time and why.
Update
"call .to_json twice" is a general description and can also mean that there are two subsequent saves on the same object, and since self.data is reassigned, this leads to data corruption. (thanks, #mudasobwa)
It depends on your model's database field type.
If the field is string type (like VARCHAR or TEXT) it should be stored as string (no need to get rid of the quotes - they are fine). Make sure calling to_json once.
If the field is Postgres JSON type, then you can just assign a hash to the model's field, no need to call to_json at all.
If you are saving hash as a JSON string in a varchar column you can use serialize to handle marshalling/unmarshaling the data:
class Thing < ActiveRecord::Base
serialize :foo, JSON
end
Knowing exactly when to convert the data in the lifecycle of a record is actually quite a bit harder than your naive implementation. So don't reinvent the wheel.
However a huge drawback is that the data cannot be queried in the DB*. If you are using Postgres or MySQL you can instead use a JSON or JSONB (postgres only) column type which allows querying. This example is from the Rails guide docs:
# db/migrate/20131220144913_create_events.rb
create_table :events do |t|
t.json 'payload'
end
# app/models/event.rb
class Event < ApplicationRecord
end
# Usage
Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
event = Event.first
event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
## Query based on JSON document
# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
Event.where("payload->>'kind' = ?", "user_renamed")
use {}.as_json instead of {}.to_json
ex:
a = {}
a.as_json # => {}
a.to_json # => "{}"
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json
Assume we have a rails params hash full of nested hashes and arrays. Is there a way to alter every string value (whether in nested hashes or arrays) which matches a certain criteria (e.g. regex) and still keep the output as a params hash (still containing nested hashes arrays?
I want to do some sort of string manipulation on some attributes before even assigning them to a model. Is there any better way to achieve this?
[UPDATE]
Let's say we want to select the strings that have an h in the beginning and replace it with a 'b'. so we have:
before:
{ a: "h343", b: { c: ["h2", "s21"] } }
after:
{ a: "b343", b: { c: ["b2", "s21"] } }
For some reasons I can't do this with model callbacks and stuff, so it should have be done before assigning to the respective attributes.
still keep the output as a params hash (still containing nested hashes arrays
Sure.
You'll have to manipulate the params hash, which is done in the controller.
Whilst I don't have lots of experience with this I just spent a bunch of time testing -- you can use a blend of the ActionController::Parameters class and then using gsub! -- like this:
#app/controllers/your_controller.rb
class YourController < ApplicationController
before_action :set_params, only: :create
def create
# Params are passed from the browser request
#model = Model.new params_hash
end
private
def params_hash
params.require(:x).permit(:y).each do |k,v|
v.gsub!(/[regex]/, 'string')
end
end
end
I tested this on one of our test apps, and it worked perfectly:
--
There are several important points.
Firstly, when you call a strong_params hash, params.permit creates a new hash out of the passed params. This means you can't just modify the passed params with params[:description] = etc. You have to do it to the permitted params.
Secondly, I could only get the .each block working with a bang-operator (gsub!), as this changes the value directly. I'd have to spend more time to work out how to do more elaborate changes.
--
Update
If you wanted to include nested hashes, you'd have to call another loop:
def params_hash
params.require(:x).permit(:y).each do |k,v|
if /_attributes/ ~= k
k.each do |deep_k, deep_v|
deep_v.gsub!(/[regex]/, 'string'
end
else
v.gsub!(/[regex]/, 'string')
end
end
end
In general you should not alter the original params hash. When you use strong parameters to whitelist the params you are actually creating a copy of the params - which can be modified if you really need to.
def whitelist_params
params.require(:foo).permit(:bar, :baz)
end
But if mapping the input to a model is too complex or you don't want to do it on the model layer you should consider using a service object.
Assuming you have a hash like this:
hash = { "hello" => { "hello" => "hello", "world" => { "hello" => "world", "world" => { "hello" => "world" } } }, "world" => "hello" }
Then add a function that transforms the "ello" part of all keys and values into "i" (meaning that "hello" and "yellow" will become "hi" and "yiw")
def transform_hash(hash, &block)
hash.inject({}){ |result, (key,value)|
value = value.is_a?(Hash) ? transform_hash(value, &block) : value.gsub(/ello/, 'i')
block.call(result, key.gsub(/ello/, 'i'), value)
result
}
end
Use the function like:
new_hash = transform_hash(hash) {|hash, key, value| hash[key] = value }
This will transform your hash and it's values regardless of the nesting level. However, the values should be strings (or another Hash) otherwise you'll get an error. to solve this problem just change the value.is_a?(Hash) conditional a bit.
NOTE that I strongly recommend you NOT to change the keys of the hash!
I collect tags in a nice javascript UI widget. It then takes all the tags and passes them to the server as tag1,tag2,tag3,etc in one text_field input. The server receives them:
params[:event][:tags] = params[:event][:tags].split(',') # convert to array
#event = Event.find(params[:id])
Is there a better way to convert the string to the array? It seems like a code smell. I have to put this both in update and in the new actions of the controller.
you could do this in the model:
I have seldom experience on mongoid. The following would work in active record (the only difference is the write_attribute part)
class Event
def tags=(value_from_form)
value_from_form = "" unless value_from_form.respond_to(:split)
write_attribute(:tags, value_from_form.split(','))
end
end
On the other hand, for consistency, you may want to do the following:
class Event
def tags_for_form=(value_from_form)
value_from_form = "" unless value_from_form.respond_to(:split)
self.tags = value_from_form.split(',')
end
def tags_for_form
self.tags
end
# no need to change tags and tags= methods. and tags and tags= methods would return an array and accept an array respectively
end
In the first case (directly overwriting the tags= method), tags= accepts a string but tags returns an array.
In the second case, tags_for_form= and tags_for_form accepts and returns string, while tags= and tags accepts and returns array.
I just create another model attribute that wraps the tags attribute like so:
class Event
def tags_list=(tags_string)
self.tags = tags_string.split(',').map(&:strip)
end
def tags_list
self.tags.join(',')
end
end
In your form, just read/write the tags_list attribute which will always accept, or return a preformated string. (The .map(:strip) part simply removes spaces on the ends in case the tags get entered with spaces: tag1, tag2, tag3.
PeterWong's answer misses the '?' from the respond_to() method;
class Event
def tags=(value_from_form)
value_from_form = "" unless value_from_form.respond_to?(:split)
write_attribute(:tags, value_from_form.split(','))
end
end
I'm starting to work with Ruby and I have the following module:
module RadioreportHelper
#totalsum = 0
#radio
#daysAndTotals
def load(service, transactionsPerDay)
puts "#{service} : #{transactionsPerDay}"
#radio = service
#daysAndTotals = transactionsPerDay
transactionsPerDay.each{|day, total|
#totalsum += total
}
end
attr_reader :radio, :daysAndTotals, :totalsum
end
I'm running the following unit test case:
class RadioreportHelperTest < ActionView::TestCase
fixtures :services
def test_should_return_populated_radio_module
service = Service.find_by_name("Mix")
transactionsPerDay = {1=>20, 2=>30, 4=>40, 5=>50}
radioReport = RadioreportHelper.load(service, transactionsPerDay)
assert_not_nil(radioReport)
assert_equal("Mix", radioReport.radio.name)
end
end
But I always get the following error:
TypeError: can't convert Service into String
I want the service object to be in the module RadioreportHelper and stored it in the #radio variable not the string.
Thanks, for the all the help guys!
Try adding a to_s method to your Service model.
def to_s
service # or some other method in the model that returns a string
end
It is not necessary to call to_s from inside an interpolated expression, i.e. "#{service}" will return the result of service.to_s.
EDIT
Never mind all of this. Your RadioreportHelper.load method is not being reached -- instead you are getting load from ActiveSupport::Dependencies::Loadable. Try renaming the load method to something else.
(I hate name collisions.)
You should definitely try this:
puts "#{service.to_s} : #{transactionsPerDay}"
Although I am not sure how interpolation for hashes is handled in strings either, so you may need to use
puts "#{service.to_s} : #{transactionsPerDay.to_s}"