Rails: Storing static data like ActiveRecords - ruby-on-rails

I've been looking for a while for gems and/or plugins that implement static storage similar ActiveRecords but is not database-based. Let's call this class NonDBRecord. It should have the following property:
class Foo < NonDBRecord
add_item('a', :property1 => 'some value', :property2 => 'some more value')
add_item('b', :property1 => 'some value', :property2 => 'some more value')
end
class Bar < ActiveRecord::Base
belongs_to_nondbrecord :foo, :class_name => 'Foo'
end
# NonDBRecord declare constants automatically
[ Foo::A, Foo::B ]
# NonDBRecord is enumerable
Foo.all # returns [Foo::A,Foo::B]
# NonDBRecord is id-based
Bar.create(:foo_id => Foo::A.id)
# ...so you can search by it
x = Bar.find(:first, :conditions => { :foo_id => Foo::A.id })
# ...and is stored, retrieved, and instantiated by its id
x.foo # returns Foo::A
I've thought about simply using ActiveRecords (and database storage), but I don't feel good about it. Plus I've had to tip-toe around some eager loading problems with the ActiveRecord solution. Any help would be appreciated before I start writing my own solution.
edit
These records are meant to be enumerations. For example, let's say you're making a card game. I want to be able to do something like
class Card < NonDBRecord
attr_reader :suit, :index
end
class Game
belongs_to :wild_card, :class_name => 'Card'
end

I would say ActiveModel is what you are looking for. It comes with Rails 3 and encapsulates all kind of goodies from ActiveRecord, such as Validation, Serialization and sorts. There is a Ryan Bates railscast on that issue. Hope this helps!

As BigD says, ActiveModel is the Rails 3 way.
In Rails 2.3 I am using this as a kluge:
class TablelessModel < ActiveRecord::Base
def self.columns() #columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
def save(validate = true)
validate ? valid? : true
end
end
I use that to e.g. validate contact forms that are not going to persist in any way. It's possible it could be extended for your specific purposes.

Related

Rails make model methods appear as attributes

I have a model connecting to a Postgres db.
class Person < ApplicationRecord
def say_id
"#{name} has id: #{id}"
end
end
I have some attributes id,name,email as well as the method above: say_id that can be accessed via:
person = Person.new
person.id => 1
person.say_id => "John has id: 1"
I would like to have the method 'say_id' listed as an attribute as well, now when running person.attributes, I'm only seeing: id, name, email
How can I have my method included as a listable information in full, as with person.attributes but which will include my method? A usecase would be for lazily just laying out all these fields in a table of the Person-object.
In Rails 5+ you can use the attributes api to create attributes that are not backed by a database column:
class Person < ApplicationRecord
attribute :foo
end
irb(main):002:0> Person.new.attributes
=> {"id"=>nil, "email"=>nil, "name"=>nil, "created_at"=>nil, "updated_at"=>nil, "foo"=>nil}
Unlike if you used attr_accessor these actually behave very much like database backed attributes.
You can then override the getter method if you wanted to:
class Person < ApplicationRecord
attribute :foo
def foo
"foo is #{super}"
end
end
irb(main):005:0> Person.new(foo: 'bar').foo
=> "foo is bar"
But for whatever you're doing its still not the right answer. You can get a list of the methods of an class by calling .instance_methods on a class:
irb(main):007:0> Person.instance_methods(false)
=> [:foo]
Passing false filters out inherited methods.

rails associations :autosave doesn't seem to working as expected

I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:
class Post < ActiveRecord::Base
has_many :comments,
:autosave => true,
:inverse_of => :post,
:dependent => :destroy
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
obj.text=val
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :inverse_of=>:comments
end
Now in the console, I test:
p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ...
p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated
Why not? What am I missing?
Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works
def comment=(val)
obj=comments.detect {|obj| obj.posted_at==Date.today}
obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
obj.text=val
end
But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.
Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
unless obj.new_record?
association(:comments).add_to_target(obj)
end
obj.text=val
end
I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.
If you change your method:
def comment=(val)
obj = comments.find_or_initialize_by(:posted_at => Date.today)
obj.text = val
puts "obj.object_id: #{obj.object_id} (#{obj.text})"
puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
obj.text
end
You'll see this:
p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)
So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:
def comment=(val)
obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
obj.text = val
end
John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:
class Post < ActiveRecord::Base
has_many :comments, ->{order "posted_at DESC"}, :autosave=>true, :inverse_of=>:post,:dependent=>:destroy
def comment=(val)
if comments.empty? || comments[0].posted_at != Date.today
comments.build(:posted_at=>Date.today, :text => val)
else
comments[0].text=val
end
end
end

Rails named_scope inheritance?

I'm trying to generalize some of my models by providing a common base model to inherit from that contains some mutual named_scope declarations and a filter method that activates that search for simpler querying on the controller side. This appears to be working when I run it in the console, but fails when in the controller:
# in the base model
class GenericModel < ActiveRecord::Base
named_scope :by_name, lambda { |name|
( name.blank? ) ? {} : { :conditions => [ "#{self.table_name}.name like ?", "%#{name}%" ] }
}
def filter(params)
res = []
res = self.by_name( (params[:name] or '') ) if params[:name]
return res
end
end
class MyModel < GenericModel
set_table_name 'my_models'
end
# works in in console!
>> params = { :name => 'jimmy' }
>> MyModel.filter(params)
=> [ <#MyModel ...>, ... ]
nil
# fails in controller
#model = MyModel.filter(params)
# ActiveRecord::StatementInvalid (Mysql::Error Unknown column 'generic_models.name' in where clause...)
Apparently the parent class' named_scope is being called when in rails, but works fine in rails console. Any ideas how to mend this? thanks.
That's a bit of a train-wreck because of the way ActiveRecord is trying to interpret what you're saying. Generally the first class derived from ActiveRecord::Base is used to define what the base table name is, and sub-classes of that are defined to use Single Table Inheritance (STI) by default. You're working around this by using set_table_name but, as is often the case, while it's possible to go against the grain in Rails, things often get messy.
You should be able to do this a lot more cleanly using a mixin as suggested by Beerlington.
module ByNameExtension
def self.extended(base)
# This method is called when a class extends with this module
base.send(:scope, :by_name, lambda { |name|
name.blank? ? nil : where("#{self.table_name}.name LIKE ?", "%#{name}%")
})
end
def filter(params)
params[:name].present? ? self.by_name(params[:name]) : [ ]
end
end
class MyModel < ActiveRecord::Base
# Load in class-level methods from module ByNameExtension
extend ByNameExtension
end
You should be able to keep your extensions contained to that module. If you want to clean this up even further, write an initializer that defines a method like scoped_by_name for ActiveRecord::Base that triggers this behavior:
class ActiveRecord::Base
def scoped_by_name
extend ByNameExtension
end
end
Then you can tag all classes that require this:
class MyModel < ActiveRecord::Base
scoped_by_name
end

ActiveRecord - replace model validation error with warning

I want to be able to replace a field error with a warning when saving/updating a model in rails. Basically I want to just write a wrapper around the validation methods that'll generate the error, save the model and perhaps be available in a warnings hash (which works just like the errors hash):
class Person < ActiveRecord::Base
# normal validation
validates_presence_of :name
# validation with warning
validates_numericality_of :age,
:only_integer => true,
:warning => true # <-- only warn
end
>>> p = Person.new(:name => 'john', :age => 2.2)
>>> p.save
=> true # <-- able to save to db
>>> p.warnings.map { |field, message| "#{field} - #{message}" }
["age - is not a number"] # <-- have access to warning content
Any idea how I could implement this? I was able to add :warning => false default value to ActiveRecord::Validations::ClassMethods::DEFAULT_VALIDATION_OPTIONS
By extending the module, but I'm looking for some insight on how to implement the rest. Thanks.
The validation_scopes gem uses some nice metaprogramming magic to give you all of the usual functionality of validations and ActiveRecord::Errors objects in contexts other than object.errors.
For example, you can say:
validation_scope :warnings do |s|
s.validates_presence_of :some_attr
end
The above validation will be triggered as usual on object.valid?, but won't block saves to the database on object.save if some_attr is not present. Any associated ActiveRecord::Errors objects will be found in object.warnings.
Validations specified in the usual manner without a scope will still behave as expected, blocking database saves and assigning error objects to object.errors.
The author has a brief description of the gem's development on his blog.
I don't know if it's ready for Rails 3, but this plugin does what you are looking for:
http://softvalidations.rubyforge.org/
Edited to add:
To update the basic functionality of this with ActiveModel I came up with the following:
#/config/initializer/soft_validate.rb:
module ActiveRecord
class Base
def warnings
#warnings ||= ActiveModel::Errors.new(self)
end
def complete?
warnings.clear
valid?
warnings.empty?
end
end
end
#/lib/soft_validate_validator.rb
class SoftValidateValidator < ActiveModel::EachValidator
def validate(record)
record.warnings.add_on_blank(attributes, options)
end
end
It adds a new Errors like object called warnings and a helper method complete?, and you can add it to a model like so:
class FollowupReport < ActiveRecord::Base
validates :suggestions, :soft_validate => true
end
I made my own gem to solve the problem for Rails 4.1+: https://github.com/s12chung/active_warnings
class BasicModel
include ActiveWarnings
attr_accessor :name
def initialize(name); #name = name; end
warnings do
validates :name, absence: true
end
end
model = BasicModel.new("some_name")
model.safe? # .invalid? equivalent, but for warnings
model.warnings # .errors equivalent

ActiveRecord serialize using JSON instead of YAML

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.

Resources