My challenge is that in the database, JSON code was untidily stored.
{'isr_comment':'Test Comment',
'isr_depression_1': '1',
'isr_depression_2': '1'
'isr_depression_3': '1'
'isr_tested': 'true'
}
You see, all values are defined as string but some should be integers. It would be the best to have clean data already in the database but I cannot control how the data is entered. However my model looks like this.
class SessionPart < ApplicationRecord
...
serialize :answers, JSON
...
end
As expected after deserialization is done I get strings as well.
#data=
{"isr_Comment"=>"Test Comment",
"isr_depression_1"=>"1",
"isr_depression_2"=>"1",
"isr_depression_3"=>"1",
"isr_tested" => "true"}
But I need to do some calculation with this data so I need all possible values with a meaningful type.
#data=
{"isr_Comment"=>"Test Comment",
"isr_depression_1"=>1,
"isr_depression_2"=>1,
"isr_depression_3"=>1,
"isr_tested" => true}
Is there any way to cast such data automatically?
You can pass your custom serializer to serialize function. That custom serializer would use JSON as source serializer and update the values as per your requirements.
class SessionPart < ApplicationRecord
...
serialize :answers, CustomSerializer #CustomSerializer must write 2 class level function named dump & load for serializing and de-serializing respectively
...
end
class CustomSerializer
def self.load(value)
normalize_hash(JSON.load(value))
end
def self.dump(value)
JSON.dump(value)
end
private
def self.normalize_hash hash
return hash unless hash.is_a? Hash
hash.transform_values {|v| normalize(v)}
end
#change this function as per your requirement, Currently it's handling boolean,integer,float & null rule set
def self.normalize(value)
case (value)
when 'true'
true
when 'false'
false
when 'null','nil'
nil
when /\A-?\d+\z/
value.to_i
when /\A-?\d+\.\d+\z/
value.to_f
else
value.is_a?(Hash) ? normalize_hash(value) : value
end
end
end
The suggested CustomSerializer seems to do its job very well, thanks. I did some small adjustments to be able to nest hashes.
class CustomSerializer
def self.load(value)
normalize_hash(JSON.load(value))
end
def self.dump(value)
JSON.dump(value)
end
private
def self.normalize_hash hash
return hash unless hash.is_a? Hash
hash.transform_values {|v| normalize(v) }
end
#change this function as per your requirement, Currently it's handling boolean,integer,float & null rule set
def self.normalize(value)
case (value)
when 'true'
true
when 'false'
false
when 'null','nil'
nil
when /\A-?\d+\z/
value.to_i
when /\A-?\d+\.\d+\z/
value.to_f
else
value.is_a?(Hash) ? normalize_hash(value) : value
end
end
end
Related
I've become confused while writing custom to_json and from_json methods for a class. I have actually found a solution, but don't understand why it works, nor why my initial attempt does not work.
I have a People class that initializes by taking an instance of a Person class as a paramater.
The to_json/from_json methods in Person have been copied in from a generic
external module, and therefore a bit wordy...but worked when I tested it on an individual instance of a Person object.
The problem comes when re-creating the People object from JSON. For the #person instance variable, I'm expecting:
#<Person:0x00000001a0b440 #name="Jon", #age=22, #gender="male">
Instead, I'm getting #person as an array (and thus, only the keys):
#<People:0x00000001b5c038 #person=["#name", "#age", "#gender"]>
Full code is as follows:
require "json"
class Person
attr_accessor :name, :age, :gender
def initialize(name, age, gender)
#name = name
#age = age
#gender = gender
end
def to_json
obj = {}
instance_variables.map do |var|
obj[var] = instance_variable_get(var)
end
JSON.dump(obj)
end
def from_json(string)
obj = JSON.parse(string)
obj.keys.each do |key|
instance_variable_set(key, obj[key])
end
end
end
class People
attr_accessor :person
def initialize(person)
#person = person
end
def to_json
obj = {}
obj[:person] = #person.to_json
JSON.dump(obj)
end
def from_json(string)
obj = JSON.parse(string, {:symbolize_names => true})
person = Person.new("", 0, "")
#person = person.from_json(obj[:person])
end
def <<(person)
#persons << person
end
end
After re-writing the to_json and from_json methods as below, the problem seems to have been solved...and now correctly re-assembles #person as expected.
def to_json
obj = {}
obj[:persons] = [#person.to_json]
JSON.dump(obj)
end
def from_json(string)
obj = JSON.parse(string, {:symbolize_names => true})
persons = []
obj[:persons].each do |person_string|
person = Person.new("", 0, "")
person.from_json(person_string)
persons << person
end
#person = persons[0]
end
I'm happy to have found a solution, but I can't understand why encasing the single Person object in an array would solve the situation.
I'm sure there are many other ways to solve this particular situation (and other methods completely... eg: other gems, or using Rails Active Support...), but I'm simply trying to get a more solid understanding why my initial idea doesn't work...to help later on when things get more complicated.
Thank you for any help you can offer...
Your first solution works. You simply forgot to return from your method. See below:
def from_json(string)
obj = JSON.parse(string, {:symbolize_names => true})
person = Person.new("", 0, "")
#person = person.from_json(obj[:person])
obj #RETURN THE OBJECT HERE
end
In the second solution you are still missing the return value however Ruby implicitly returns the last evaluated expression from the method which by chance happens to be correct. This is the same reason why your first solution didn't work.
So I'm trying to write a method in the model that will allow me to return posts who have a specific field value that is greater than 0.
So I have posts that have fields that are essentially tags. Basically I post has four fields, hiphop, electro, house and pop. Each field has a value between 0 and 10.
I'm trying to make it so if someone clicks on a button the the view that says "Hip Hop" it will return all posts that have a hiphop field value that is greater than 0.
I know this is wrong but I'm thinking something like this
def self.tagSearch(query)
where("#{query} > 0")
end
and in my controller I would have something like this
def index
if params[:search]
#songs = Song.search(params[:search]).order("created_at DESC")
elsif params[:tag]
#songs = Songs.tagSearch(params[:tag]).order("created_at DESC")
else
#songs = Song.all
end
end
And I'm not sure about the view but maybe a button that passes the tag value parameter. The thing is I just want it to be a button, I don't need them to input anything.
I hope this isn't too confusing.
Thank you!
Matt
To expand on RaVen post:
1) Use ruby naming conventions tagSearch should be tag_search; methods are snake case (lower case with underscores).
2) where("#{query} > 0") is exposing you to SQL injection attacks - recommended to install the brakeman gem which can expose security issues like this:
http://brakemanscanner.org/docs/warning_types/sql_injection/
http://brakemanscanner.org/docs/
3) You can simplify your code by chaining scopes, scopes that return nil will not effect the query
class Song
scope :search, -> (query) do
where("name LIKE ?", "#{query}%") if query.present?
end
scope :tag_search, -> (tag) do
where(tag > 0) if tag.present?
end
scope :ordered, -> do
order(created_at: :desc)
end
end
class SongsController
def index
#songs = Song.search(params[:search])
.tag_search(params[:tag])
.ordered
end
end
4) Making queries based on a user specified column and avoiding sql injection:
This is one way to do it, there are probably other better ones available, like using the models arel_table, anyhow this one is pretty straight forward
scope :tag_search, -> (tag) do
where("#{self.white_list(tag)} > 0") if tag.present?
end
def self.white_list(column_name)
# if the specified column_name matches a model attribute then return that attribute
# otherwise return nil which will cause a sql error
# but it won't let arbitrary sql execution
self.attribute_names.detect { |attribute| attribute == column_name }
end
Rails support "scopes" which return an ActiveRecord::Relation which means you can chain them together.
class Song
scope :tag_search, -> (something) { where(something > 0) }
scope :ordered, -> { order(created_at: :desc) }
end
class SongsController
def index
if params[:search]
#songs = Song.search(params[:search]).ordered
elsif params[:tag]
#songs = Songs.tag_search(params[:tag]).ordered
else
#songs = Song.all
end
end
end
I would overthink the design of this.
Plus your tagSearch function is really dangerous. SQL INJECTION!
I have a function that is inserting a record into my DB (MySQL). It has many columns, many of which have default values in the DB. Passing in values for these variables is therefore optional.
def assign_X_to_Y( options = {} )
. . .
#bar.var1 = options[:foo]
. . .
end
I would like to do the following:
-If a variable exists (ex: options[:foo]), add it to the record I'm making.
#bar.var1 = options[:foo]
-If it doesn't, I don't want to add it--I want to use the DB default.
I know I can simply do an if:
if options[:foo]
#bar.var1 = options[:foo]
end
But I have a lot of these variables and so I think there must be a nicer way that having loads of if-statements. Something like the "if doesn't exist set to null" expression:
#bar.var1 = options[:foo] || nil
Is there anything like what I am saying? I can't use the above expression because I don't want to set it to null (which I think it would do), I want to use the default value…
Thanks in advance!
If #bar is an model you can simply pass a hash:
Bar.create(hash) # creates a Bar with the defaults from your schema
#bar.assign_attributes(hash)
#bar.update(hash) # same as object but commits the changes to the db
If bar is a Plain Old Ruby class you can give it the same functionality by:
class Bar
attr_accessor :foo
attr_accessor :baz
attr_accessor :woggle
def initialize(hash)
assign_values(hash)
end
def assign_attributes(hash)
assign_values(hash)
end
private
def assign_values(hash)
hash.each do |k,v|
send "#{k}=", v
end
end
end
Then I can simply create an object with:
Bar.new(foo: 1, baz: 3)
Note that this will respect object encapsulation - if I try to do:
Bar.new(haxxored: true)
It will raise a NoMethodError. Just like #bar.haxxored = true.
If I'm understanding your question correctly, the best way to handle this would be to use the public_send method in Ruby:
def set_new_property(obj, prop_name, prop_value)
obj.class.__send__(attr_accessor: "#{prop_name}")
obj.public_send("#{prop_name}=", prop_value)
end
Bear in mind that you'll have to set explicit attribute accessors for each potential property being assigned.
Using DataMapper Enum type for the first time, and I noticed the first value in the enum translates to a 1. I need this to be zero based to be backward compatiable with another ORM layer in a different application also reading this database.
You should be able to monkey-patch enum.rb in dm-types to support this. You will need to replace the initialize method with a slightly modified copy where #flag_map[i+1] is replaced with #flag_map[i]:
module DataMapper
class Property
class Enum < Object
def initialize(model, name, options = {})
#flag_map = {}
flags = options.fetch(:flags, self.class.flags)
flags.each_with_index do |flag, i|
#flag_map[i] = flag
end
if self.class.accepted_options.include?(:set) && !options.include?(:set)
options[:set] = #flag_map.values_at(*#flag_map.keys.sort)
end
super
end # end initialize
end # end class Enum
end # end class Property
end # end module DataMapper
Is there an easy way (i.e. a configuration) to force ActiveRecord to save empty strings as NULL in the DB (if the column allows)?
The reason for this is that if you have a NULLable string column in the DB without a default value, new records that do not set this value will contain NULL, whereas new records that set this value to the empty string will not be NULL, leading to inconsistencies in the database that I'd like to avoid.
Right now I'm doing stuff like this in my models:
before_save :set_nil
def set_nil
[:foo, :bar].each do |att|
self[att] = nil if self[att].blank?
end
end
which works but isn't very efficient or DRY. I could factor this out into a method and mix it into ActiveRecord, but before I go down that route, I'd like to know if there's a way to do this already.
Yes, the only option at the moment is to use a callback.
before_save :normalize_blank_values
def normalize_blank_values
attributes.each do |column, value|
self[column].present? || self[column] = nil
end
end
You can convert the code into a mixin to easily include it in several models.
module NormalizeBlankValues
extend ActiveSupport::Concern
included do
before_save :normalize_blank_values
end
def normalize_blank_values
attributes.each do |column, value|
self[column].present? || self[column] = nil
end
end
end
class User
include NormalizeBlankValues
end
Or you can define it in ActiveRecord::Base to have it in all your models.
Finally, you can also include it in ActiveRecord::Base but enable it when required.
module NormalizeBlankValues
extend ActiveSupport::Concern
def normalize_blank_values
attributes.each do |column, value|
self[column].present? || self[column] = nil
end
end
module ClassMethods
def normalize_blank_values
before_save :normalize_blank_values
end
end
end
ActiveRecord::Base.send(:include, NormalizeBlankValues)
class User
end
class Post
normalize_blank_values
# ...
end
Try if this gem works:
https://github.com/rubiety/nilify_blanks
Provides a framework for saving incoming blank values as nil in the database in instances where you'd rather use DB NULL than simply a blank string...
In Rails when saving a model from a form and values are not provided by the user, an empty string is recorded to the database instead of a NULL as many would prefer (mixing blanks and NULLs can become confusing). This plugin allows you to specify a list of attributes (or exceptions from all the attributes) that will be converted to nil if they are blank before a model is saved.
Only attributes responding to blank? with a value of true will be converted to nil. Therefore, this does not work with integer fields with the value of 0, for example...
Another option is to provide custom setters, instead of handling this in a hook. E.g.:
def foo=(val)
super(val == "" ? nil : val)
end
My suggestion:
# app/models/contact_message.rb
class ContactMessage < ActiveRecord::Base
include CommonValidations
include Shared::Normalizer
end
# app/models/concerns/shared/normalizer.rb
module Shared::Normalizer
extend ActiveSupport::Concern
included do
before_save :nilify_blanks
end
def nilify_blanks
attributes.each do |column, value|
# ugly but work
# self[column] = nil if !self[column].present? && self[column] != false
# best way
#
self[column] = nil if self[column].kind_of? String and self[column].empty?
end
end
end
Sorry for necroposting, but I didn't find exact thing here in answers, if you need solution to specify fields which should be nilified:
module EnforceNil
extend ActiveSupport::Concern
module ClassMethods
def enforce_nil(*args)
self.class_eval do
define_method(:enforce_nil) do
args.each do |argument|
field=self.send(argument)
self.send("#{argument}=", nil) if field.blank?
end
end
before_save :enforce_nil
end
end
end
end
ActiveRecord::Base.send(:include, EnforceNil)
This way:
class User
enforce_nil :phone #,:is_hobbit, etc
end
Enforcing certain field is handy when let's say you have field1 and field2. Field1 has unique index in SQL, but can be blank, so you need enforcement(NULL considered unique, "" - not by SQL), but for field2 you don't actually care and you have already dozens of callbacks or methods, which work when field2 is "", but will dig your app under the layer of errors if field2 is nil. Situation I faced with.
May be useful for someone.
Strip Attributes Gem
There's a handy gem that does this automatically when saving a record, whether that's in a user form or in the console or in a rake task, etc.
It's called strip_attributes and is extremely easy to use, with sane defaults right out of the box.
It does two main things by default that should almost always be done:
Strip leading and trailing white space:
" My Value " #=> "My Value"
Turn empty Strings into NULL:
"" #=> NULL
" " #=> NULL
Install
You can add it to your gem file with:
gem strip_attributes
Usage
Add it to any (or all) models that you want to strip leading/trailing whitespace from and turn empty strings into NULL:
class DrunkPokerPlayer < ActiveRecord::Base
strip_attributes
end
Advanced Usage
There are additional options that you can pass on a per-Model basis to handle exceptions, like if you want to retain leading/trailing white space or not, etc.
You can view all of the options on the GitHub repository here:
https://github.com/rmm5t/strip_attributes#examples
I use the attribute normalizer gem to normalize attributes before they into the db.