diff --git a/dm-core.gemspec b/dm-core.gemspec index 1cdbb6d5..c3622f16 100644 --- a/dm-core.gemspec +++ b/dm-core.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Dan Kubb"] - s.date = %q{2010-03-18} + s.date = %q{2010-03-27} s.description = %q{Faster, Better, Simpler.} s.email = %q{dan.kubb@gmail.com} s.extra_rdoc_files = [ @@ -82,6 +82,7 @@ Gem::Specification.new do |s| "lib/dm-core/support/lazy_array.rb", "lib/dm-core/support/logger.rb", "lib/dm-core/support/naming_conventions.rb", + "lib/dm-core/support/subject.rb", "lib/dm-core/transaction.rb", "lib/dm-core/type.rb", "lib/dm-core/types/boolean.rb", @@ -137,7 +138,10 @@ Gem::Specification.new do |s| "spec/semipublic/adapters/sqlite3_adapter_spec.rb", "spec/semipublic/adapters/sqlserver_adapter_spec.rb", "spec/semipublic/adapters/yaml_adapter_spec.rb", + "spec/semipublic/associations/many_to_many_spec.rb", "spec/semipublic/associations/many_to_one_spec.rb", + "spec/semipublic/associations/one_to_many_spec.rb", + "spec/semipublic/associations/one_to_one_spec.rb", "spec/semipublic/associations/relationship_spec.rb", "spec/semipublic/associations_spec.rb", "spec/semipublic/collection_spec.rb", @@ -150,6 +154,7 @@ Gem::Specification.new do |s| "spec/semipublic/resource_spec.rb", "spec/semipublic/shared/condition_shared_spec.rb", "spec/semipublic/shared/resource_shared_spec.rb", + "spec/semipublic/shared/subject_shared_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/unit/array_spec.rb", @@ -213,7 +218,10 @@ Gem::Specification.new do |s| "spec/semipublic/adapters/sqlite3_adapter_spec.rb", "spec/semipublic/adapters/sqlserver_adapter_spec.rb", "spec/semipublic/adapters/yaml_adapter_spec.rb", + "spec/semipublic/associations/many_to_many_spec.rb", "spec/semipublic/associations/many_to_one_spec.rb", + "spec/semipublic/associations/one_to_many_spec.rb", + "spec/semipublic/associations/one_to_one_spec.rb", "spec/semipublic/associations/relationship_spec.rb", "spec/semipublic/associations_spec.rb", "spec/semipublic/collection_spec.rb", @@ -226,6 +234,7 @@ Gem::Specification.new do |s| "spec/semipublic/resource_spec.rb", "spec/semipublic/shared/condition_shared_spec.rb", "spec/semipublic/shared/resource_shared_spec.rb", + "spec/semipublic/shared/subject_shared_spec.rb", "spec/spec_helper.rb", "spec/unit/array_spec.rb", "spec/unit/hash_spec.rb", diff --git a/lib/dm-core.rb b/lib/dm-core.rb index 7d607cca..f6a6b33f 100644 --- a/lib/dm-core.rb +++ b/lib/dm-core.rb @@ -66,7 +66,7 @@ module ActiveSupport require 'dm-core/support/assertions' require 'dm-core/support/lazy_array' require 'dm-core/support/hook' - +require 'dm-core/support/subject' require 'dm-core/model' require 'dm-core/model/descendant_set' diff --git a/lib/dm-core/associations/many_to_one.rb b/lib/dm-core/associations/many_to_one.rb index 7ed8d639..70706270 100644 --- a/lib/dm-core/associations/many_to_one.rb +++ b/lib/dm-core/associations/many_to_one.rb @@ -114,6 +114,9 @@ def resource_for(source, other_query = nil) def get(source, other_query = nil) lazy_load(source) unless loaded?(source) + # set the default if it is not already set + set(source, default_for(source)) unless loaded?(source) + resource = get!(source) if other_query.nil? || query_for(source, other_query).conditions.matches?(resource) resource diff --git a/lib/dm-core/associations/one_to_many.rb b/lib/dm-core/associations/one_to_many.rb index 8b1c5400..2f8663bc 100644 --- a/lib/dm-core/associations/one_to_many.rb +++ b/lib/dm-core/associations/one_to_many.rb @@ -44,7 +44,7 @@ def collection_for(source, other_query = nil) collection.source = source # make the collection empty if the source is not saved - collection.replace([]) unless source.saved? + collection.replace(default_for(source)) unless source.saved? collection end @@ -70,6 +70,11 @@ def set(source, targets) get!(source).replace(targets) end + # @api semipublic + def default_for(source) + Array(super) + end + private # @api semipublic diff --git a/lib/dm-core/associations/one_to_one.rb b/lib/dm-core/associations/one_to_one.rb index 419ce100..7d1bf510 100644 --- a/lib/dm-core/associations/one_to_one.rb +++ b/lib/dm-core/associations/one_to_one.rb @@ -6,6 +6,9 @@ class Relationship < Associations::Relationship superclass.send("#{visibility}_instance_methods", false).each do |method| undef_method method unless method.to_s == 'initialize' end + + # remove mixed in methods + undef_method *DataMapper::Subject.send("#{visibility}_instance_methods", false) end # Loads (if necessary) and returns association target @@ -13,7 +16,9 @@ class Relationship < Associations::Relationship # # @api semipublic def get(source, other_query = nil) - return unless loaded?(source) || valid_source?(source) + unless loaded?(source) || valid_source?(source) + set(source, default_for(source)) + end relationship.get(source, other_query).first end @@ -26,6 +31,11 @@ def set(source, target) relationship.set(source, [ target ].compact).first end + # @api semipublic + def default_for(source) + relationship.default_for(source).first + end + # @api public def kind_of?(klass) super || relationship.kind_of?(klass) diff --git a/lib/dm-core/associations/relationship.rb b/lib/dm-core/associations/relationship.rb index 14bb3019..5f07b78e 100644 --- a/lib/dm-core/associations/relationship.rb +++ b/lib/dm-core/associations/relationship.rb @@ -7,8 +7,9 @@ module Associations # with methods like get and set overridden. class Relationship include DataMapper::Assertions + include Subject - OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse, :reader_visibility, :writer_visibility ].to_set + OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse, :reader_visibility, :writer_visibility, :default ].to_set # Relationship name # @@ -447,6 +448,7 @@ def initialize(name, child_model, parent_model, options = {}) @max = @options[:max] @reader_visibility = @options.fetch(:reader_visibility, :public) @writer_visibility = @options.fetch(:writer_visibility, :public) + @default = @options.fetch(:default, nil) # TODO: normalize the @query to become :conditions => AndOperation # - Property/Relationship/Path should be left alone diff --git a/lib/dm-core/property.rb b/lib/dm-core/property.rb index 9919e819..1c70eb94 100644 --- a/lib/dm-core/property.rb +++ b/lib/dm-core/property.rb @@ -290,6 +290,7 @@ module DataMapper # see SingleTableInheritance for more on how to use Class columns. class Property include DataMapper::Assertions + include Subject extend Deprecate extend Equalizer @@ -514,7 +515,7 @@ def get(resource) if loaded?(resource) get!(resource) else - set(resource, default? ? default_for(resource) : nil) + set(resource, default_for(resource)) end end @@ -687,38 +688,6 @@ def load(value) typecast(value) end - # Returns a default value of the - # property for given resource. - # - # When default value is a callable object, - # it is called with resource and property passed - # as arguments. - # - # @param [Resource] resource - # the model instance for which the default is to be set - # - # @return [Object] - # the default value of this property for +resource+ - # - # @api semipublic - def default_for(resource) - if @default.respond_to?(:call) - @default.call(resource, self) - else - @default.try_dup - end - end - - # Returns true if the property has a default value - # - # @return [Boolean] - # true if the property has a default value - # - # @api semipublic - def default? - @options.key?(:default) - end - # Returns given value unchanged for core types and # uses +dump+ method of the property type for custom types. # diff --git a/lib/dm-core/resource.rb b/lib/dm-core/resource.rb index 58a7f68e..135eadd8 100644 --- a/lib/dm-core/resource.rb +++ b/lib/dm-core/resource.rb @@ -894,10 +894,12 @@ def child_collections # @api private def _create # set defaults for new resource - properties.each do |property| - unless property.serial? || property.loaded?(self) - property.set(self, property.default_for(self)) - end + (properties | relationships.values).each do |subject| + next if subject.respond_to?(:serial?) && subject.serial? || + subject.loaded?(self) || + !subject.default? + + subject.set(self, subject.default_for(self)) end @_repository = repository diff --git a/lib/dm-core/support/subject.rb b/lib/dm-core/support/subject.rb new file mode 100644 index 00000000..dda4410a --- /dev/null +++ b/lib/dm-core/support/subject.rb @@ -0,0 +1,33 @@ +module DataMapper + module Subject + # Returns a default value of the subject for given resource + # + # When default value is a callable object, it is called with resource + # and subject passed as arguments. + # + # @param [Resource] resource + # the model instance for which the default is to be set + # + # @return [Object] + # the default value of this subject for +resource+ + # + # @api semipublic + def default_for(resource) + if @default.respond_to?(:call) + @default.call(resource, self) + else + @default.try_dup + end + end + + # Returns true if the subject has a default value + # + # @return [Boolean] + # true if the subject has a default value + # + # @api semipublic + def default? + @options.key?(:default) + end + end +end diff --git a/spec/semipublic/associations/many_to_many_spec.rb b/spec/semipublic/associations/many_to_many_spec.rb new file mode 100644 index 00000000..beb56822 --- /dev/null +++ b/spec/semipublic/associations/many_to_many_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) + +describe 'One to Many Associations' do + before :all do + module ::Blog + class Article + include DataMapper::Resource + + property :title, String, :key => true + property :body, Text, :required => true + end + end + + @article_model = Blog::Article + end + + supported_by :all do + before :all do + @article = @article_model.new(:title => 'DataMapper Rocks!', :body => 'TSIA') + end + + describe 'acts like a subject' do + before do + n = @article_model.n + + @subject_without_default = @article_model.has(n, :without_default, @article_model, :through => DataMapper::Resource) + @subject_with_default = @article_model.has(n, :with_default, @article_model, :through => DataMapper::Resource, :default => [ @article ]) + @subject_with_default_callable = @article_model.has(n, :with_default_callable, @article_model, :through => DataMapper::Resource, :default => lambda { |resource, relationship| [ @article ] }) + + @subject_without_default_value = [] + @subject_with_default_value = [ @article ] + @subject_with_default_callable_value = [ @article ] + + @resource = @article_model.new + end + + it_should_behave_like 'A semipublic Subject' + end + end +end diff --git a/spec/semipublic/associations/many_to_one_spec.rb b/spec/semipublic/associations/many_to_one_spec.rb index 810034bf..06a0cb9d 100644 --- a/spec/semipublic/associations/many_to_one_spec.rb +++ b/spec/semipublic/associations/many_to_one_spec.rb @@ -32,5 +32,21 @@ class ::Comment end it_should_behave_like 'A semipublic Resource' + + describe 'acts like a subject' do + before do + @subject_without_default = @user_model.belongs_to(:without_default, @user_model) + @subject_with_default = @user_model.belongs_to(:with_default, @user_model, :default => @user) + @subject_with_default_callable = @user_model.belongs_to(:with_default_callable, @user_model, :default => lambda { |resource, relationship| @user }) + + @subject_without_default_value = nil + @subject_with_default_value = @user + @subject_with_default_callable_value = @user + + @resource = @user_model.new + end + + it_should_behave_like 'A semipublic Subject' + end end end diff --git a/spec/semipublic/associations/one_to_many_spec.rb b/spec/semipublic/associations/one_to_many_spec.rb new file mode 100644 index 00000000..c0d40636 --- /dev/null +++ b/spec/semipublic/associations/one_to_many_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) + +describe 'One to Many Associations' do + before :all do + module ::Blog + class Article + include DataMapper::Resource + + property :title, String, :key => true + property :body, Text, :required => true + end + end + + @article_model = Blog::Article + end + + supported_by :all do + before :all do + @article = @article_model.new(:title => 'DataMapper Rocks!', :body => 'TSIA') + end + + describe 'acts like a subject' do + before do + n = @article_model.n + + @subject_without_default = @article_model.has(n, :without_default, @article_model) + @subject_with_default = @article_model.has(n, :with_default, @article_model, :default => [ @article ]) + @subject_with_default_callable = @article_model.has(n, :with_default_callable, @article_model, :default => lambda { |resource, relationship| [ @article ] }) + + @subject_without_default_value = [] + @subject_with_default_value = [ @article ] + @subject_with_default_callable_value = [ @article ] + + @resource = @article_model.new + end + + it_should_behave_like 'A semipublic Subject' + end + end +end diff --git a/spec/semipublic/associations/one_to_one_spec.rb b/spec/semipublic/associations/one_to_one_spec.rb new file mode 100644 index 00000000..2a0bf861 --- /dev/null +++ b/spec/semipublic/associations/one_to_one_spec.rb @@ -0,0 +1,38 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) + +describe 'One to One Associations' do + before :all do + module ::Blog + class Article + include DataMapper::Resource + + property :title, String, :key => true + property :body, Text, :required => true + end + end + + @article_model = Blog::Article + end + + supported_by :all do + before :all do + @article = @article_model.new(:title => 'DataMapper Rocks!', :body => 'TSIA') + end + + describe 'acts like a subject' do + before do + @subject_without_default = @article_model.has(1, :without_default, @article_model) + @subject_with_default = @article_model.has(1, :with_default, @article_model, :default => @article) + @subject_with_default_callable = @article_model.has(1, :with_default_callable, @article_model, :default => lambda { |resource, relationship| @article }) + + @subject_without_default_value = nil + @subject_with_default_value = @article + @subject_with_default_callable_value = @article + + @resource = @article_model.new + end + + it_should_behave_like 'A semipublic Subject' + end + end +end diff --git a/spec/semipublic/property_spec.rb b/spec/semipublic/property_spec.rb index e8bd6c5d..3d3d1e3a 100644 --- a/spec/semipublic/property_spec.rb +++ b/spec/semipublic/property_spec.rb @@ -607,4 +607,20 @@ class Author @model.properties[:name].options[:field].should be_nil end end + + describe 'acts like a subject' do + before do + @subject_without_default = @model.property(:without_default, Integer) + @subject_with_default = @model.property(:with_default, Integer, :default => 1) + @subject_with_default_callable = @model.property(:with_default_callable, Integer, :default => lambda { |resource, property| 1 }) + + @subject_without_default_value = nil + @subject_with_default_value = 1 + @subject_with_default_callable_value = 1 + + @resource = @model.new + end + + it_should_behave_like 'A semipublic Subject' + end end diff --git a/spec/semipublic/shared/subject_shared_spec.rb b/spec/semipublic/shared/subject_shared_spec.rb new file mode 100644 index 00000000..f65304ce --- /dev/null +++ b/spec/semipublic/shared/subject_shared_spec.rb @@ -0,0 +1,47 @@ +share_examples_for 'A semipublic Subject' do + describe '#default?' do + describe 'with a default' do + subject { @subject_with_default.default? } + + it { should be_true } + end + + describe 'without a default' do + subject { @subject_without_default.default? } + + it { should be_false } + end + end + + describe '#default_for' do + describe 'without a default' do + subject { @subject_without_default.default_for(@resource) } + + it { should == @subject_without_default_value } + + it 'should be used as a default for the subject accessor' do + should == @resource.__send__(@subject_without_default.name) + end + end + + describe 'with a default value' do + subject { @subject_with_default.default_for(@resource) } + + it { should == @subject_with_default_value } + + it 'should be used as a default for the subject accessor' do + should == @resource.__send__(@subject_with_default.name) + end + end + + describe 'with a default value responding to #call' do + subject { @subject_with_default_callable.default_for(@resource) } + + it { should == @subject_with_default_callable_value } + + it 'should be used as a default for the subject accessor' do + should == @resource.__send__(@subject_with_default_callable.name) + end + end + end +end