結局書いた。堅いオブジェクト用DSL
http://d.hatena.ne.jp/yssk22/20080803#1217768127 の続き。なんだかんだで、クライアントから受け取るJSON(XML,form-url-encoded)を正しく解釈し、よきに計らってくれるものが必要。こんなドメインオブジェクトは必要でしょうか。私は必要でした(w
to_hash, from_hash でハッシュ互換になったところがポイント。これで、JSONでもXMLでも何でもこい、です(その代わり動作速度は微妙かと。。。。あとは、ActiveRecord で、暗黙の型変換が走るようなので*1、それにならって実装しましたが、object(:is_aオプションなしの場合はHash)と array の取り扱いは少々工夫?。ハッシュが渡されてもオブジェクトに復元可能であれば復元するように努力します。arrayも同様に配列に復元可能であれば復元しようと努力します。
あとで svn 上にコミットするけれど、とりあえずの使い方とソースをバックアップ代わりにべたばり。
すくなくとも、CouchObject::Persistableよりは自分の意図するとおりに動作するようにはしたつもり。次はCouchResource::Validations ですかね。
つかいかた。
>> class DomainObject >> include CouchResource::Struct >> class NestedDomainObject >> include CouchResource::Struct >> string :subtitle >> end >> string :title >> array :users >> object :meta_tags >> object :subobjects, :is_a => NestedDomainObject >> end => nil >> a = DomainObject.new => #<DomainObject:0x1123f94> >> a.title = "hoge" => "hoge" >> a.users = ["yssk22"] => ["yssk22"] >> a.meta_tags = { :keyword => [:hoge, :fuga], :description => "hogehgoe" } => {:keyword=>[:hoge, :fuga], :description=>"hogehgoe"} >> a.subobjects = DomainObject::NestedDomainObject.new => #<DomainObject::NestedDomainObject:0x5ba3b0> >> a.subobjects.subtitle = "hoge" => "hoge" >> a.to_hash => {"title"=>"hoge\n", "subobjects"=>{"class"=>"DomainObject::NestedDomainObject", "subtitle"=>"hoge"}, "class"=>"DomainObject", "users"=>["yssk22"], "meta_tags"=>{"description"=>"hogehgoe", "keyword"=>[:hoge, :fuga]}} >> b = DomainObject.from_hash(a.to_hash) => #<DomainObject:0x12ede74 @attributes={"title"=>"hoge\n", "subobjects"=>#<DomainObject::NestedDomainObject:0x12e99dc @attributes={"subtitle"=>"hoge"}>, "users"=>["yssk22"], "meta_tags"=>{"description"=>"hogehgoe", "keyword"=>[:hoge, :fuga]}}> >> b.title => "hoge\n" >>
ソース。plugin/couch_resource/lib/couch_resource/struct.rb に置くことにした。
require 'rubygems' require 'active_support' require 'json' module CouchResource module Struct def self.included(base) base.send(:extend, ClassMethods) base.send(:include, InstanceMethods) end module ClassMethods def string(name, option={}) option[:is_a] = :string register_attribute_member(name, option) define_attribute_accessor(name, option) end def number(name, option={}) option[:is_a] = :number register_attribute_member(name, option) define_attribute_accessor(name, option) end def boolean(name, option={}) option[:is_a] = :boolean register_attribute_member(name, option) define_attribute_accessor(name, option) method = <<-EOS def #{name}? get_attribute(:#{name}) end EOS class_eval(method, __FILE__, __LINE__) end def array(name, option={}) option[:is_a] = :array register_attribute_member(name, option) define_attribute_accessor(name, option) end def object(name, option={}) unless option.has_key?(:is_a) option[:is_a] = :hash end register_attribute_member(name, option) define_attribute_accessor(name, option) end def from_hash(hash) hash ||= {} instance = self.new (read_inheritable_attribute(:attribute_members) || {}).each do |name, option| instance.set_attribute(name, hash[name]) end instance end private def register_attribute_member(name, option = {}) attribute_members = read_inheritable_attribute(:attribute_members) attribute_members ||= HashWithIndifferentAccess.new({}) attribute_members[name] = option write_inheritable_attribute(:attribute_members, attribute_members) end def define_attribute_accessor(name, option={}) define_attribute_read_accessor(name, option) define_attribute_write_accessor(name, option) end def define_attribute_read_accessor(name, option={}) method = <<-EOS def #{name} get_attribute(:#{name}) end EOS class_eval(method, __FILE__, __LINE__) end def define_attribute_write_accessor(name, option={}) method = <<-EOS def #{name}=(value) set_attribute(:#{name}, value) end EOS class_eval(method, __FILE__, __LINE__) end end module InstanceMethods def set_attribute(name, value) attribute_members = self.class.read_inheritable_attribute(:attribute_members) || {} if attribute_members.has_key?(name) option = attribute_members[name] @attributes ||= HashWithIndifferentAccess.new({}) if value.nil? @attributes[name] = nil else klass = option[:is_a] parsed_value = case klass when :string, :number, :boolean, :array, :hash self.send("parse_for_#{klass}_attributes", value) else self.send("parse_for_object_attributes", value, klass) end @attributes[name] = parsed_value end end end def get_attribute(name) @attributes ||= HashWithIndifferentAccess.new({}) @attributes[name] end def to_hash hash = HashWithIndifferentAccess.new({ :class => self.class.name }) (self.class.read_inheritable_attribute(:attribute_members) || {}).each do |name, option| klass = option[:is_a] value = get_attribute(name) case klass when :string, :number, :boolean, :array, :hash hash[name] = value else if value hash[name] = value.to_hash else hash[name] = nil end end end hash end private def parse_for_string_attributes(value) value.is_a?(String) ? value : value.to_s end def parse_for_number_attributes(value) if value.is_a?(Numeric) value else v = value.to_s if v =~ /^\d+$/ v.to_i else v.to_f end end end def parse_for_boolean_attributes(value) value && true end def parse_for_array_attributes(value) if value.is_a?(Array) value else if value.respond_to?(:to_a) value.to_a else [value] end end end def parse_for_hash_attributes(value) if value.is_a?(Hash) value else if value.respond_to?(:to_hash) value.to_hash else raise TypeError.new end end end def parse_for_object_attributes(value, klass) if value.is_a?(klass) value elsif value.is_a?(Hash) klass.from_hash(value) else raise TypeError.new end end end end end
*1:"integer"の列に文字列を入れるとto_iが呼ばれて値がセットされる