How to get a (Ruby) DataMapper custom type to work? - ruby

I have a SchoolDay class that represents a school day: it can tell you the date, the semester, the term, the week, and the day. It can generate a string like "Sem1 13A Fri". To store these objects in the database, I want them serialized as a string.
Here is my DataMapper custom type code. I've sort of scraped ideas from the code in dm-types because (disappointingly) there is no real documentation for creating custom types. Sorry it's long.
module DataMapper
class Property
class SchoolDay < DataMapper::Property::String
#load_as ::SchoolRecord::DomainObjects::SchoolDay
# Commented out: the 'load_as' method is not found
def load(value)
# Take a string from the database and load it. We need a calendar!
val = case value
when ::String then calendar.schoolday(value)
when ::SR::DO::SchoolDay then value
else
# fail
end
end
def dump(value)
# Store a SchoolDay value into the database as a string.
case value
when SR::DO::SchoolDay
sd = value
"Sem#{sd.semester} #{sd.weekstr} #{sd.day}"
when ::String
value
else
# fail
end
end
def typecast(value)
# I don't know what this is supposed to do -- that is, when and why it
# is called -- but I am aping the behaviour of the Regexp custom type,
# which, like this one, stores as a String and loads as something else.
load(value)
end
# private methods calendar() and error_message() omitted
end
end
end
This code works for reading from the (SQLite) database, but not for creating new rows. The error message is:
Schoolday must be of type String
The code that defines the DataMapper resource and tries to create the record is:
class LessonDescription
include DataMapper::Resource
property :id, Serial
property :schoolday, SchoolDay # "Sem1 3A Fri"
property :class_label, String # "10"
property :period, Integer # (0..6), 0 being before school
property :description, Text # "Completed yesterday's worksheet. hw:(4-07)"
end
# ...
ld = LessonDescription.create(
schoolday: #schoolday,
class_label: #class_label,
period: #period,
description: description
)
Here is the code for the Regexp datamapper type in the dm-types library. It's so simple!
module DataMapper
class Property
class Regexp < String
load_as ::Regexp # NOTE THIS LINE
def load(value)
::Regexp.new(value) unless value.nil?
end
def dump(value)
value.source unless value.nil?
end
def typecast(value)
load(value)
end
end
end
end
For some reason, I cannot use the load_as line in my code.
To summarise: I am trying to create a custom type that translates between a SchoolDay (domain object) and a String (database representation). The translation is easy, and I've copied the code structure primarily from the DataMapper Regexp type. But when I try to save a SchoolDay, it complains that I'm not giving it a string. Frustratingly, I can't use the "load_as" method that the built-in and custom types all use, even though I have the latest gem. I can't find the "load_as" method defined anywhere in the source code for DataMapper, either. But it's called!
Sorry for the ridiculous length. Any help would be greatly appreciated, as would a pointer to a guide for creating these things that I have somehow missed.

It seems that the current code of dm-types at github hasn't made it to any official release -- that's why load_as doesn't work in your example. But try to add this method:
module DataMapper
class Property
class SchoolDay < DataMapper::Property::String
def custom?
true
end
end
end
end
That's working here.

Related

Correct way to define virtual attributes on a Model for the keys in a JSON column in rails

In my rails model I have a JSON column which stores some meta information.
This is to be entered bu the user from a form.
Since the keys of the JSON column are not attributes of the model I cannot use them directly in form_for instead I need to define a virtual attribute.
Since this number of virtual attributes could grow to be arbitrarily lengthy I would like to use meta programming to define the attributes.
I did try the answer in this question however when I use the constant in my model I get an error saying that the constant is undefined. So I added the symbols for the keys in an array directly and iterate over them in the module. When I do this I get an error that says stack level too deep.
Please can someone help me out here?
If you are using PostgreSQL specific columns like hstore or json simply use store_accessor instead to generate the accessor methods. Be aware that these columns use a string keyed hash and do not allow access using a symbol.
class Model < ActiveRecord::Base
store_accessor :my_json_column, [ :key_1, :key_2, key_3 ]
end
What it doing under the hood? It has define write\read helper methods:
def store_accessor(store_attribute, *keys)
keys = keys.flatten
_store_accessors_module.module_eval do
keys.each do |key|
define_method("#{key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
define_method(key) do
read_store_attribute(store_attribute, key)
end
end
end
# .....
store
I figured it out. I return the attribute as a key of the JSON column and it works fine now.
# lib/virtuals.rb
module Virtuals
%W(key_1 key_2 key_3).each do |attr|
define_method(attr) do
self.my_json_column[attr]
end
define_method("#{attr}=") do |val|
self.my_json_column[attr] = val
end
end
end
In my Model i just need to include that above module and it works fine in the form_for and updates correctly as well.

Can i convert an attribute from active record from Big Decimal to Integer?

I have an attribute coming back from Active Record that is a Big Decimal. The database column's data is numeric(18,0) and I'm not authorized to change it. i would like to be able to convert the attribute into an Integer with no decimal precision, but i'm not having any luck. I know that I can convert the value into something else by using big_decimal.to_i, however I am hoping there is a way to handle this within the Active Record Model possibly after_initialize so that I don't have to worry about conversion anywhere else in my code. Any help would be appreciated, thanks.
If you're stuck with a legacy schema you can always work around that by adding a wrapper method:
class MyModel < ActiveRecord::Base
def int_column
value = read_attribute(:big_decimal)
# Preserve `nil` values and avoid converting to zero.
value and value.to_i
end
def int_column=(value)
write_attribute(:big_decimal, value)
end
end
That gives you a method for reading/writing from that column using an alternate name.
You can always alias the current method and then override it.
Let's say the attribute is charge
class Transaction < ActiveRecord::Base
# right now #charge returns #<BigDecimal:7fb6c285fe28,'0.0',9(18)>
# now we can alias the current method #charge
alias_method :big_decimal_charge, :charge
# now they both return #<BigDecimal:7fb6c285fe28,'0.0',9(18)>
# and we can redefine #charge, based on the alias
def charge
big_decimal_charge.to_i
end
end
I wouldn't imagine there would be a need to change the setter.

Issue loading classes order EDIT: works, although some odd behavior along the way

I'm working on a project to recreate some of the functionality of ActiveRecord. Here's the portion that isn't working
module Associations
def belongs_to(name, params)
self.class.send(:define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
self.class.send(:define_method, :other_table_name) do |other_class|
other_class.table_name
end
.
.
.
o_c = other_class(name, params)
#puts this and other (working) values in a query
query = <<-SQL
...
SQL
#sends it off with db.execute(query)...
I'm building towards this testing file:
require 'all_files' #holds SQLClass & others
pets_db_file_name = File.expand_path(File.join(File.dirname(__FILE__), "pets.db"))
DBConnection.open(pets_db_file_name)
#class Person
#end
class Pet < SQLClass
set_table_name("pets")
set_attrs(:id, :name, :owner_id)
belongs_to :person, :class_name => "Person", :primary_key => :id, :foreign_key => :owner_id
end
class Person < SQLClass
set_table_name("people")
set_attrs(:id, :name)
has_many :pets, :foreign_key => :owner_id
end
.
.
.
Without any changes I received
.../active_support/inflector/methods.rb:230:in `block in constantize': uninitialized constant Person (NameError)
Just to make sure that it was an issue with the order of loading the classes in the file I began the file with the empty Person class, which, as predicted gave me
undefined method `table_name' for Person:Class (NoMethodError)
Since this is a learning project I don't want to change the test to make my code work (open all the classes, set all the tables/attributes then reopen them them for belongs_to. But, I'm stuck on how else to proceed.)
EDIT SQLClass:
class SQLClass < AssignmentClass
extend SearchMod
extend Associations
def self.set_table_name(table_name)
#table_name = table_name
end
def self.table_name
#table_name
end
#some more methods for finding rows, and creating new rows in existing tables
And the relevant part of AssignmentClass uses send on attr_accessor to give functionality to set_attrs and makes sure that before you initialize a new instance of a class all the names match what was set using set_attrs.
This highlights an important difference between dynamic, interpreted Ruby (et al) and static, compiled languages like Java/C#/C++. In Java, the compiler runs over all your source files, finds all the class/method definitions, and matches them up with usages. Ruby doesn't work like this -- a class "comes into existence" after executing its class block. Before that, the Ruby interpreter doesn't know anything about it.
In your test file, you define Pet first. Within the definition of Pet, you have belongs_to :person. belongs_to does :person.constantize, attempting to get the class object for Person. But Person doesn't exist yet! Its definition comes later in the test file.
There are a couple ways I can think that you could try to resolve this:
One would be to do what Rails does: define each class in its own file, and make the file names conform to some convention. Override constant_missing, and make it automatically load the file which defines the missing class. This will make load order problems resolve themselves automatically.
Another solution would be to make belongs_to lazy. Rather than looking up the Person class object immediately, it could just record the fact that there is an association between Pet and Person. When someone tries to call pet.person, use a missing_method hook to actually define the method. (Presumably, by that time all the class definitions will have been executed.)
Another way would be do something like:
define_method(belongs_to) do
belongs_to_class = belongs_to.constantize
self.class.send(:define_method, belongs_to) do
# put actual definition here
end
self.send(belongs_to)
end
This code is not tested, it's just to give you an idea! Though it's a pretty mind-bending idea, perhaps. Basically, you define a method which redefines itself the first time it is called. Just like using method_missing, this allows you to delay the class lookup until the first time the method is actually used.
If I can say one more thing: though you say you don't want to "overload" method_missing, I don't think that's as much of a problem as you think. It's just a matter of extracting code into helper methods to keep the definition of method_missing manageable. Maybe something like:
def method_missing(name,*a,&b)
if has_belongs_to_association?(name)
invoke_belongs_to_association(name,a,b)
elsif has_has_many_association?(name)
invoke_has_many_association(name,a,b)
# more...
else
super
end
end
Progress! Inspired by Alex D's suggestion to use method_missing to delay the creation I instead used define_methodto create a method for the name, like so:
define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
define_method(:other_table_name) do |other_class|
other_class.table_name
end
#etc
define_method(name) do #|params| turns out I didn't need to pass in `params` at all but:
#p "---#{params} (This is line 31: when testing this out I got the strangest error
#.rb:31:in `block in belongs_to': wrong number of arguments (0 for 1) (ArgumentError)
#if anyone can explain this I would be grateful.
#I had declared an #params class instance variable and a getter for it,
#but nothing that should make params require an argument
f_k = foreign_key(name, params)
p f_k
o_c = other_class(name, params)
o_t_n = other_table_name(o_c)
p_k = primary_key(params)
query = <<-SQL
SELECT *
FROM #{o_t_n}
WHERE #{p_k} = ?
SQL
row = DBConnection.execute(query, self.send(f_k))
o_c.parse_all(row)
end

Adding #to_yaml to DataMapper models

I am using DataMapper for Database access. My goal is to send the models to an webservice as read-only object. This is my current try:
class User
include DataMapper::Resource
def to_yaml(opts = {})
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
mini_me.to_yaml(opts)
end
....
end
YAML::ENGINE.yamler = 'psych'
u = User.get("hulk")
p u.to_yaml
# => "--- !ruby/object:OpenStruct\ntable:\n :uid: hulk\n :uidNumber: 1000\n :gidNumber: 1001\n :email: hulk#example.com\n :dn: uid=hulk,ou=People,o=example\n :name: Hulk\n :displayName: Hulk\n :description: Hulk\n :homeDirectory: /home/hulk\n :accountFlags: ! '[U ]'\n :sambaSID: S-1-5-21-......\nmodifiable: true\n"
p [ u ].to_yaml # TypeError: can't dump anonymous class Class
Any ideas how to make this work and get rid of the exception?
Thanks,
krissi
Using to_yaml is deprecated in Psych, and from my testing it seems to be actually broken in cases like this.
When you call to_yaml directly on your object, your method gets called and you get the result you expect. When you call it on the array containing your object, Psych serializes it but doesn’t correctly handle your to_yaml method, and ends up falling back onto the default serialization. In your case this results in an attempt to serialize an anonymous Class which causes the error.
To fix this, you should use the encode_with method instead. If it’s important that the serialized form is tagged as an OpenStruct object in the generated yaml you can use the represent_object (that first nil parameter doesn’t seem to be used):
def encode_with(coder)
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
coder.represent_object(nil, mini_me)
end
If you were just using OpenStruct for convenience, an alternative could be something like:
def encode_with(coder)
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
coder[var.to_s.gsub(/^#/, '')]= instance_variable_get(var)
end
end
Note that Datamapper has its own serializer plugin that provides yaml serialization for models, it might be worth looking into.

How to create a new DataMapper type?

I was following the guide from the DataMapper site, and this is what I have:
module DataMapper
class Property
class SymbolStore < String
def dump(v)
v.to_s
end
def load(v)
v.to_sym
end
def typcast_to_primitive(v)
v.to_s
end
end
end
end
But, I'm getting strange behaviour. When I try to set a value that is a SymbolStore, it comes back as a string. However, if I load a record, it comes back properly, as a symbol. From my experiments, load only seems to be called when a record is loaded, and not when I'm trying to get the value of something I've just set.
In the real world, I should not need to get a value I have just set, but this problem does not make sense.
Try it with typecast_to_primitive (you had a typo).

Resources