I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end
Related
I need to validate email saving in email_list. For validation I have EmailValidator. But I cannot figure out how to use it in pair of validates_each. Are there any other ways to make validations?
class A < ActiveRecord::Base
serialize :email_list
validates_each :email_list do |r, a, v|
# what should it be here?
# EmailValidator.new(options).validate_each r, a, v
end
end
validates_each is for validating multiple attributes. In your case you have one attribute, and you need to validate it in a custom way.
Do it like this:
class A < ActiveRecord::Base
validate :all_emails_are_valid
...
private
def all_emails_are_valid
unless self.email_list.nil?
self.email_list.each do |email|
if # email is valid -- however you want to do that
errors.add(:email_list, "#{email} is not valid")
end
end
end
end
end
Note that you could also make a custom validator for this or put the validation in a proc on the validate call. See here.
Here's an example with a custom validator.
class A < ActiveRecord::Base
class ArrayOfEmailsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.nil?
value.each do |email|
if # email is valid -- however you want to do that
record.errors.add(attribute, "#{email} is not valid")
end
end
end
end
validates :email_list, :array_of_emails => true
...
end
Of course you can put the ArrayOfEmailsValidator class in, i.e., lib/array_of_emails_validator.rb and load it where you need it. This way you can share the validator across models or even projects.
I ended up with this:
https://gist.github.com/amenzhinsky/c961f889a78f4557ae0b
You can write your own EmailValidator according to rails guide and use the ArrayValidator like:
validates :emails, array: { email: true }
I want to filter out specific fields from ActiveRecord/ActiveModel classes when outputting JSON.
The most straightforward way to do this is just overriding as_json, perhaps like so:
def as_json (options = nil)
options ||= {}
super(options.deep_merge({:except => filter_attributes}))
end
def filter_attributes
[:password_digest, :some_attribute]
end
This works, but it's a little verbose and lends itself to not being DRY pretty fast. I thought it would be nice to just declare the filtered properties with a magical class method. For example:
class User < ActiveRecord::Base
include FilterJson
has_secure_password
filter_json :password_digest
#...
end
module FilterJson
extend ActiveSupport::Concern
module ClassMethods
def filter_json (*attributes)
(#filter_attributes ||= Set.new).merge(attributes.map(&:to_s))
end
def filter_attributes
#filter_attributes
end
end
def as_json (options = nil)
options ||= {}
super(options.deep_merge({:except => self.class.filter_attributes.to_a}))
end
end
The problem with this is getting it to deal with inheritance properly. Let's say I subclass User:
class SecretiveUser < User
filter_json :some_attribute, :another_attribute
#...
end
Logically, it makes sense to filter out :some_attribute, :another_attribute, and also :password_digest.
However, this will only filter the attributes declared on the class. To the desired end, I tried to call super within filter_attributes, but that failed. I came up with this, and it's a hack.
def filter_attributes
if superclass.respond_to?(:filter_attributes)
superclass.filter_attributes + #filter_attributes
else
#filter_attributes
end
end
This is obviously brittle and not idiomatic, but there's the "what" that I'm trying to accomplish.
Can anyone think of a way to do it more correctly (and hopefully more elegantly)? Thanks!
I think it is a safer solution to white-list attributes than to black-list them. This will prevent unwanted future attributes added to User or SomeUser from making it into your JSON response because you forgot to add said attributes to filter_json.
You seem to be looking for a solution to your specific inheritance issue. I'm still going to point out active_model_serializers, as I feel it is a saner way to manage serialization.
class UserSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name
end
class SecretUserSerializer < UserSerializer
attributes :secret_attribute, :another_attribute
end
Given some SecretUser s you can do
SecretUserSerializer.new(s).as_json
and you'll get :id, :first_name, :last_name, :secret_attribute, and :another_attribute. The inheritance works as expected.
I have a model which has one actual column in the database. This column is stored as a JSON string of configuration. I use a bunch of virtual attributes which I want to map inside of this configuration JSON attribute. I basically dont want to create a bunch columns in the db, but rather use this one JSON attribute to contain everything. Is there a cleaner way than the below defs of achieving this?
class Device < ActiveRecord::Base
attr_accessible :configuration
serialize :configuration, JSON
attr_accessor :background_color, :title
# below is ew
def background_color; self.configuration["background_color"]; end
def background_color=(value); self.configuration["background_color"] = value; end
def title; self.configuration["title"]; end
def title=(value); self.configuration["title"] = value; end
end
Ideally i'd be looking for something like attr_maps_to_hash :configuration, [:background_color, :title]. Does something like this exist?
You can use ActiveRecord::Store for this.
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ]
end
u = User.new(color: 'black', homepage: '37signals.com')
u.color # Accessor stored attribute
u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
# Add additional accessors to an existing store through store_accessor
class SuperUser < User
store_accessor :settings, :privileges, :servants
end
If you are using PostgreSQL, check out HStore.
Rails as of 3.2 has key-value stores built into ActiveRecord-see here:
Where can i read more about Rails 3.2's Data Store with key-value in textfield?
in your case you can have a text field named configuration and then do this:
class Device < AR::Base
store :configuration, accessors: [:title, :background_color, ...]
...
This should work fine with forms, etc..
Two methods come to mind.
First, you could have an array of your attributes [:background_color, :title] and then iterate over them while calling define_method. You would defined two methods, define(method_name) and define("#{method_name}=").
Second, a similar idea but using method missing.
def method_missing(method_name, *args, &block)
...see if it's a get or set...
...do your stuff...
...rain dance...
...yay...
end
first of all, i am using rails 3.1.3 and carrierwave from the master
branch of the github repo.
i use a after_init hook to determine fields based on an attribute of
the page model instance and define attribute accessors for these field
which store the values in a serialized hash (hope it's clear what i am
talking about). here is a stripped down version of what i am doing:
class Page < ActiveRecord::Base
serialize :fields, Hash
after_initialize :set_accessors
def set_accessors
case self.template
when 'standard'
class << self
define_method 'image' do
self.fields['image']
end
define_method 'image=' do |value|
self.fields['image'] = value
end
end
mount_uploader :image, PageImageUploader
end
end
end
end
leaving out the mount_uploader command gives me access to the
attribute as i want. but when i mount the uploader a get an error
message saying 'undefined method new for nil class'
i read in the source that there are the methods read_uploader and
write_uploader in the extensions module.
how do i have to override these to make the mount_uploader command
work with my 'virtual' attribute.
i hope somebody has an idea how i can solve this problem. thanks a lot
for your help.
best regard. dominik.
Same problem but solved in your model you should override read_uploader(column) and write_uploader(column, identifier) instance methods. I also have a problem with #{column}_will_change! and #{column}_changed? for a virtual column so I had to define them too:
class A < ActiveRecord::Base
serialize :meta, Hash
mount_uploader :image, ImageUploader
def image_will_change!
meta_will_change!
#image_changed = true
end
def image_changed?
#image_changed
end
def write_uploader(column, identifier)
self.meta[column.to_s] = identifier
end
def read_uploader(column)
self.meta[column.to_s]
end
end
Now there's also an add-on to carrierwave which provides the exact functionality as described by Antiarchitect:
https://github.com/timsly/carrierwave-serializable
I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.