Creating a class and adding methods dynamically in Ruby - ruby

How do I go about creating a new class and adding a few methods to it without resorting to "eval"?
Here's what I'm trying to do; I'd like to transform this structure:
obj = [
{
:scope => 'account',
:fields => [
{ :title => 'title', :length => 64, :required => true },
{ :title => 'email', :length => 256, :required => true, :type => 'email' }
],
:before_save => Proc.new{
#do something here
},
},
{
:scope => 'product',
:fields => [
{ :title => 'title', :length => 64, :required => true },
{ :title => 'description', :length => 256, :required => true },
{ :title => 'cost', :required => true, :type => 'decimal' }
]
},
]
into this:
class Account
include DataMapper::Resource
property :id, Serial
property :title, String, :length => 64, :required => true
property :email, String, :length => 256, :required => true
def before_save
#do something here
end
end
...
Thanks!

As Andrew already said, there are different ways to create class dynamically. This could be one of them:
Say you start with one DM model (haven't used DM, taking the first one from the docs):
class Post
include DataMapper::Resource
property :id, Serial # An auto-increment integer key
property :title, String # A varchar type string, for short strings
property :body, Text # A text block, for longer string data.
property :created_at, DateTime # A DateTime, for any date you might like.
end
and you want to create it dynamically, from a metadata given in a hash of the form
{:ClassName => {:field1 => :Type1, :field2 => :Type2 ...}}
You could do:
require 'data_mapper'
models = {:Post => {
:id => :Serial,
:title => :String,
:body => :Text
}}
models.each do |name, fields|
klass = Class.new do
include DataMapper::Resource
fields.each do |field_name, field_type|
property(field_name, const_get(field_type))
end
end
Object.const_set(name, klass)
end
Key methods:
Class.new
Module#const_set

If you want to look at a real-world example, please consult the code in this library: https://github.com/apohllo/rod/blob/v0.7.x/lib/rod/model.rb#L410

Related

Why is one of my tables not being populated if I use DataMapper's "has n" and "belongs_to" methods?

I am getting to grips with DataMapper on sqlite3 at the moment. I have to models defined which are creating two tables: "companies" and "apps".
Each app belongs to a company and each company many apps. I want to represent this relationship in my models but I add the "has n" and "belongs_to" methods to each class, the App class stops working when call #create on a bunch of apps, they are not inserted into the database.
If I don't have the associations methods then everything works fine.
This is my DataMapper code:
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/app.db")
class Company
include DataMapper::Resource
property :id, Serial
property :company_name,
property :company_id, Text, :unique => true
has n, :apps
end
class App
include DataMapper::Resource
property :id, Serial
property :app_id, Integer
property :bundle_id, Text
property :game_name, Text
property :company_name, Text
property :created_at, DateTime
property :rank, Integer
belongs_to :company
end
DataMapper.finalize.auto_upgrade!
puts 'Database and tables created'
This is the code I am using to populate my tables
companies_in_chart.each do |company|
#add_company = Company.create(
:company_name => company["company_name"],
:company_id => company["company_id"]
)
end
puts "Inserted companies into database"
apps_arr.each do |app|
#new_app = App.create(
:app_id => app["app_id"],
:bundle_id => app["bundle_id"],
:game_name => app["game_name"],
:company_name => app["company_name"],
:created_at => app["DateTime"],
:rank => app["rank"]
)
end
puts "Inserted apps into database"
EDIT: New code
#Set up database and apps table
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/app.db")
class Company
include DataMapper::Resource
property :id, Serial
property :company_name, Text, :required => true, :lazy => false
property :company_id, Text, :required => true, :lazy => false, :unique => true
has n, :apps
end
class App
include DataMapper::Resource
property :id, Serial
property :app_id, Integer, :required => true
property :bundle_id, Text, :required => true, :lazy => false
property :game_name, Text, :required => true, :lazy => false
property :company_id, Text, :required => true, :lazy => false
property :created_at, DateTime
property :rank, Integer
belongs_to :company
end
DataMapper.finalize.auto_upgrade!
puts 'Database and tables created'
#Insert apps and companies into database
apps_arr.each do |app|
# Creates a new company based on app entry if the company does
# not exist in the companies table
#add_company = Company.create(
:company_name => app["company_name"],
:company_id => app["company_id"]
)
#add_app = App.create(
:app_id => app["app_id"],
:bundle_id => app["bundle_id"],
:game_name => app["game_name"],
:company_id => app["company_id"],
:created_at => app["DateTime"],
:rank => app["rank"]
)
end
puts "Inserted app and companies into database"
#company = Company.first
ap #company # => #<Company #id=1 #company_name="Rovio Entertainment Ltd" #company_id="rovio">
ap #company.apps # => [] --I was hoping it would return all of Rovio's apps in the database
apps not created cause you have to attach a company when creating app.
if you want to add apps not attached to any company, use this in you App model:
belongs_to :company, :required => false
to attach a company when creating app:
#Insert apps and companies into database
apps_arr.each do |app|
# Creates a new company based on app entry if the company does
# not exist in the companies table
company = Company.first_or_create(
:company_name => app["company_name"],
:company_id => app["company_id"]
)
app = App.first_or_create(
:company => company, # you missed this
:app_id => app["app_id"],
:bundle_id => app["bundle_id"],
:game_name => app["game_name"],
:company_id => app["company_id"],
:created_at => app["DateTime"],
:rank => app["rank"]
)
end
puts "Inserted app and companies into database"
I successfully replicated your code on CIBox and it runs perfectly.
See the code and live demo here
As you can see, it creates a company and attach it to created app.
Company.first.apps returns created app, so associations works correctly.

Retrieving nested records in Sequel

I'm trying to retrieve data in a nested form from the following two tables (in SQLite)
DB = Sequel.sqlite('database.sqlite')
DB.create_table? :artists do
primary_key :id
String :name
end
DB.create_table? :albums do
primary_key :id
String :title
foreign_key :artist_id,
:artists,
:key => :id
end
artists = DB[:artists]
albums = DB[:albums]
id1 = artists.insert(:name => 'Mike')
id2 = artists.insert(:name => 'John')
albums.insert(:title => 'Only You', :artist_id => id1 )
albums.insert(:title => 'Only Us', :artist_id => id1 )
albums.insert(:title => 'Only Me', :artist_id => id2 )
The output I'm trying to get -
[
{
:id => 1,
:name => 'Mike'
:albums => [
{
:id => 1,
:title => 'Only You'
},
{
:id => 2,
:title => 'Only Us'
}
]
},
{
:id => 2,
:name => 'John'
:albums => [
{
:id => 3,
:title => 'Only Me'
}
]
}
]
I've tried 'eager' loading -
class Artist < Sequel::Model(:artists)
one_to_many :db[:albums], :key => :artist_id
end
class Album < Sequel::Model(:albums)
many_to_one :artist, :key => :artist_id
end
Artist.eager(:albums).all{ |a| p a }
But that didn't work.
Can anyone point me in the right direction?
Artist.eager(:albums).all does eagerly load the albums, but {|a| p a} is not going to show the albums (as Sequel::Model#inspect only shows values for the current model, not any associated objects). Use {|a| p [a, a.albums]} to see that the albums are already loaded.
If you want to produce the hash you described:
Artist.eager(:albums).all.map do |a|
a.values.merge(:albums=>a.albums.map{|al| al.values})
end
You can add a method to Artist to output it the way you want it
class Artist < Sequel::Model(:artists)
one_to_many :albums, :key => :artist_id
def my_hash
to_hash.merge!(
{
:albums => albums.map{|a|
a.to_hash.reject!{ |k,v|
k==:artist_id
}
}
}
)
end
end
class Album < Sequel::Model(:albums)
many_to_one :artist, :key => :artist_id
end
records = Artist.all.map{ |a| a.my_hash }
p records
Instead of using reject! it would be cleaner to add a my_hash method the Album to return a hash without the :artist_id, but you get the idea. This outputs:
[
{
:albums=>[
{
:title=>"Only You",
:id=>1
},
{
:title=>"Only Us",
:id=>2
}
],
:name=>"Mike",
:id=>1
},
{
:albums=>[
{
:title=>"Only Me",
:id=>3
}
],
:name=>"John",
:id=>2
}
]

Not null fields for ActiveAttr

I'm having issues enforcing a field to not be nil within ActiveAttr::Model.
Is there an elegant way of enforcing this constraint within the model instead of defining it in the controller? Or am I testing incorrectly for the scenario?
Model:
class Message
include ActiveAttr::Model
attribute :name, :presence => true, :allow_nil => false, :allow_blank => false
attribute :email, :presence => true
attribute :content, :presence => true
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
end
RSpec:
module MessageSpecHelper
def valid_message_attributes
{
:name => "Walter White",
:email => "walter#hailtheking.com",
:content => "I am the one who knocks"
}
end
end
it "should have error on name (alternate with nil)" do
#message.attributes = valid_message_attributes.except(:name => nil)
#message.should have(1).error_on(:name)
end

FasterCSV importer to DataMapper model - No rows inserted

I have a model (called Test):
property :id, Serial
property :title, String, :length => 255, :required => true
property :description, String, :length => 255, :required => true
property :brand, String, :length => 255, :required => true
property :link, String, :length => 255, :required => true
property :image_link, String, :length => 255, :required => true
property :price, String, :length => 255, :required => true
property :condition, String, :length => 255, :required => true
property :product_type, String, :length => 255, :required => true
I am importing data from a tab delimited file, using FasterCSV,
FasterCSV.foreach("test.txt", {:headers => true, :quote_char=>'"', :col_sep =>'/t'}) do |row_data|
row_data = Test.first_or_new(
'title' => :title,
'description' => :supplier,
'brand' => :brand,
'link' => :link,
'image_link' => :image_link,
'price' => :price,
'condition' => :condition,
'product_type' => :product_type
)
row_data.save
end
No errors appear, when I run the importer. Nothing appears inserted in SQLite table.
Am i missing something obvious? (The table exists within the target database, and the field names are the same as the headers from my file.
Update 2014/11/19: FasterCSV has been removed. Ruby standard library CSV should now be used intead. Just replace all occurrences of FasterCSV with CSV
There's two problem i guess
the delimiter you intended to use was rather "\t" than '/t'
you're not using the row_data to populate the datamapper object
This should work better:
FasterCSV.foreach("test.txt", {:headers => true, :quote_char=>'"', :col_sep =>"\t"}) do |row_data|
new_record = Test.first_or_new(
'title' => row_data['title'],
'description' => row_data['supplier'],
'brand' => row_data['brand'],
'link' => row_data['link'],
'image_link' => row_data['image_link'],
'price' => row_data['price'],
'condition' => row_data['condition'],
'product_type' => row_data['product_type']
)
new_record.save
end

How do I work with checkboxes with DataMapper and Sinatra?

I'm trying to make a simple room management service. The rooms have these properties:
class Room
include DataMapper::Resource
validates_is_unique :number
property :id, Serial
property :number, Integer
property :guest, String
property :status, Enum[ :free, :occupied ], :default => :free
end
Then I create a new room like this
post '/new' do
content_type :json
#room = Room.new :guest => params[:guest],
:number => params[:number],
:status => params[:status]
if #room.save
{ :number => #room.number, :guest => #room.guest, :status => #room.status }.to_json
end
end
through this haml form
%form#new_message{:action => '/new', :method => 'post'}
%p
%input{:type => "text", :id => "number", :name => "number"}
%input{:type => "text", :id => "guest", :name => "guest"}
%input{:type => "checkbox", :id => "status", :name => "status", :value => "occupied"}
%input{:type => "submit", :value => "post"}
When the box is checked the :status is "occupied" but when I leave it unchecked the object won't save. I thought it would work since it is defaulted to "free" but no...
For whatever stupid reason, checkboxes do not get submitted if they are not clicked. This means they are not in the hash that hits your app. When you say :status => params[:status] you are really saying :status => nil. Since you have set a value, it checks that against your enum, and nil is not in your enum, so it fails validations. (based on how you are using this, doesn't it seem like it should be a boolean called either "occupied" or "available" ?)
Anyway, you could either explicitly set it to free, or not set it at all, and let the default take care of it. That is what I opted for when checking it, by moving it into a mass assignment. The code I used is below.
require 'rubygems'
require 'sinatra'
require 'haml'
require 'dm-core'
require 'dm-validations'
require 'dm-types'
require 'dm-migrations'
require 'sqlite3'
configure do
class Room
include DataMapper::Resource
validates_uniqueness_of :number
property :id, Serial
property :number, Integer
property :guest, String
property :status, Enum[ :free, :occupied ], :default => :free
end
set :sessions , true
DataMapper::Logger.new($stdout, :debug)
DataMapper.setup( :default , "sqlite3://#{Dir.pwd}/development.sqlite3" )
DataMapper.finalize
DataMapper.auto_upgrade!
end
get '/' do
#rooms = Room.all
haml :index
end
post '/new' do
p params
#room = Room.new params[:room]
if #room.save
session[:flash] = "room reserved"
redirect '/'
else
session[:flash] = #room.errors.to_a
redirect '/new'
end
end
get '/new' do
haml :new
end
__END__
##layout
!!!
%html
#flash
= session[:flash].inspect
= yield
##new
%form#new_message{:action => '/new', :method => 'post' , :name => 'room' }
%p
%input{:type => "text", :id => "number", :name => "room[number]"}
%input{:type => "text", :id => "guest", :name => "room[guest]"}
%input{:type => "checkbox", :id => "status", :name => "room[status]", :value => "occupied"}
%input{:type => "submit", :value => "post"}
##index
%table
- #rooms.each do |room|
%tr
%td= room.number
%td= room.guest
%td= room.status
View the HTML source of your web-form! There should be a hidden field which sets the unchecked checkboxes to '0' as the default , in case nobody checks them...
see also:
http://railscasts.com/episodes/17-habtm-checkboxes?autoplay=true (towards the end)

Resources