Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of git@github.com:sam/dm-core

* 'master' of git@github.com:sam/dm-core:
  Associations may now be added inside a repository do ... end block
  Fixed repository(...) do ... end inside a Resource class declaration.
  Made Resource::ClassMethods#find_by_sql slightly more practically useful
  Added Resource::ClassMethods#find_by_sql.
  Added better documentation for Resource.including_classes.
  Enabled DataMapper::Resource to know what classes have included it.
  Resolved ticket #210.
  Resolved ticket #212.
  Added specs for Property type casting
  Fixed failing specs
  Fixes stupid thing I did.
  Assorted fixes.
  Added documentation for the new property parameter
  Added support to pass property information own to custom types in the dump/load
  • Loading branch information...
commit 5a3189ce02a26318923075bad789f6adc3be5e89 2 parents cee40c0 + c5bbdd0
@wycats wycats authored
View
138 lib/data_mapper/adapters/data_objects_adapter.rb
@@ -9,6 +9,77 @@
module DataMapper
+ module Resource
+
+ module ClassMethods
+ #
+ # Find instances by manually providing SQL
+ #
+ # ==== Parameters
+ # <String>:: An SQL query to execute
+ # <Array>:: An Array containing a String (being the SQL query to execute) and the parameters to the query.
+ # example: ["SELECT name FROM users WHERE id = ?", id]
+ # <DataMapper::Query>:: A prepared Query to execute.
+ # <Hash>:: An options hash.
+ #
+ # A String, Array or Query is required.
+ #
+ # ==== Options (the options hash)
+ # :repository<Symbol>:: The name of the repository to execute the query in. Defaults to self.default_repository_name.
+ # :reload<Boolean>:: Whether to reload any instances found that allready exist in the identity map. Defaults to false.
+ # :properties<Array>:: The Properties of the instance that the query loads. Must contain DataMapper::Properties. Defaults to self.properties.
+ #
+ # ==== Returns
+ # LoadedSet:: The instance matched by the query.
+ #
+ # ==== Example
+ # MyClass.find_by_sql(["SELECT id FROM my_classes WHERE county = ?", selected_county], :properties => MyClass.property[:id], :repository => :county_repo)
+ #
+ # -
+ # @public
+ def find_by_sql(*args)
+ sql = nil
+ query = nil
+ params = []
+ properties = nil
+ do_reload = false
+ repository_name = default_repository_name
+ args.each do |arg|
+ if arg.is_a?(String)
+ sql = arg
+ elsif arg.is_a?(Array)
+ sql = arg.first
+ params = arg[1..-1]
+ elsif arg.is_a?(DataMapper::Query)
+ query = arg
+ elsif arg.is_a?(Hash)
+ repository_name = arg.delete(:repository) if arg.include?(:repository)
+ properties = Array(arg.delete(:properties)) if arg.include?(:properties)
+ do_reload = arg.delete(:reload) if arg.include?(:reload)
+ raise "unknown options to #find_by_sql: #{arg.inspect}" unless arg.empty?
+ end
+ end
+
+ the_repository = repository(repository_name)
+ raise "#find_by_sql only available for Repositories served by a DataObjectsAdapter" unless the_repository.adapter.is_a?(DataMapper::Adapters::DataObjectsAdapter)
+
+ if query
+ sql = the_repository.adapter.query_read_statement(query)
+ params = query.fields
+ end
+
+ raise "#find_by_sql requires a query of some kind to work" unless sql
+
+ properties ||= self.properties
+
+ DataMapper.logger.debug { "FIND_BY_SQL: #{sql} PARAMETERS: #{params.inspect}" }
+
+ repository.adapter.read_set_with_sql(repository, self, properties, sql, params, do_reload)
+ end
+ end
+
+ end
+
module Adapters
# You must inherit from the DoAdapter, and implement the
@@ -122,20 +193,24 @@ def read(repository, resource, key)
def update(repository, resource)
properties = resource.dirty_attributes
-
- sql = update_statement(resource.class, properties)
- values = properties.map { |property| resource.instance_variable_get(property.instance_variable_name) }
- parameters = (values + resource.key)
- DataMapper.logger.debug { "UPDATE: #{sql} PARAMETERS: #{parameters.inspect}" }
-
- connection = create_connection
- command = connection.create_command(sql)
-
- affected_rows = command.execute_non_query(*parameters).to_i
-
- close_connection(connection)
-
- affected_rows == 1
+
+ if properties.empty?
+ return false
+ else
+ sql = update_statement(resource.class, properties)
+ values = properties.map { |property| resource.instance_variable_get(property.instance_variable_name) }
+ parameters = (values + resource.key)
+ DataMapper.logger.debug { "UPDATE: #{sql} PARAMETERS: #{parameters.inspect}" }
+
+ connection = create_connection
+ command = connection.create_command(sql)
+
+ affected_rows = command.execute_non_query(*parameters).to_i
+
+ close_connection(connection)
+
+ affected_rows == 1
+ end
end
def delete(repository, resource)
@@ -151,15 +226,24 @@ def delete(repository, resource)
affected_rows == 1
end
- # Methods dealing with finding stuff by some query parameters
- def read_set(repository, query)
- properties = query.fields
-
+ #
+ # used by find_by_sql and read_set
+ #
+ # ==== Parameters
+ # repository<DataMapper::Repository>:: The repository to read from.
+ # model<Object>:: The class of the instances to read.
+ # properties<Array>:: The properties to read. Must contain Symbols, Strings or DM::Properties.
+ # sql<String>:: The query to execute.
+ # parameters<Array>:: The conditions to the query.
+ # do_reload<Boolean>:: Whether to reload objects already found in the identity map.
+ #
+ # ==== Returns
+ # LoadedSet:: A set of the found instances.
+ #
+ def read_set_with_sql(repository, model, properties, sql, parameters, do_reload)
properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten]
- set = LoadedSet.new(repository, query.model, properties_with_indexes)
+ set = LoadedSet.new(repository, model, properties_with_indexes)
- sql = query_read_statement(query)
- parameters = query.parameters
DataMapper.logger.debug { "READ_SET: #{sql} PARAMETERS: #{parameters.inspect}" }
connection = create_connection
@@ -169,7 +253,7 @@ def read_set(repository, query)
reader = command.execute_reader(*parameters)
while(reader.next!)
- set.add(reader.values, query.reload?)
+ set.add(reader.values, do_reload)
end
reader.close
@@ -183,6 +267,16 @@ def read_set(repository, query)
set
end
+ # Methods dealing with finding stuff by some query parameters
+ def read_set(repository, query)
+ read_set_with_sql(repository,
+ query.model,
+ query.fields,
+ query_read_statement(query),
+ query.parameters,
+ query.reload?)
+ end
+
def delete_set(repository, query)
raise NotImplementedError
end
View
2  lib/data_mapper/associations/many_to_one.rb
@@ -13,7 +13,7 @@ def many_to_one(name, options = {})
relationships[name] = Relationship.new(
name,
- options[:repository_name] || repository.name,
+ repository.name,
child_model_name,
nil,
parent_model_name,
View
2  lib/data_mapper/associations/one_to_many.rb
@@ -15,7 +15,7 @@ def one_to_many(name, options = {})
relationships[name] = Relationship.new(
DataMapper::Inflection.underscore(parent_model_name).to_sym,
- options[:repository_name] || repository.name,
+ repository.name,
child_model_name,
nil,
parent_model_name,
View
2  lib/data_mapper/associations/one_to_one.rb
@@ -12,7 +12,7 @@ def one_to_one(name, options = {})
relationships[name] = Relationship.new(
DataMapper::Inflection.underscore(parent_model_name).to_sym,
- options[:repository_name] || repository.name,
+ repository.name,
child_model_name,
nil,
parent_model_name,
View
18 lib/data_mapper/hook.rb
@@ -8,8 +8,16 @@ module ClassMethods
def before(target_method, method_sym = nil, &block)
install_hook :before, target_method, method_sym, &block
end
+
+ def after(target_method, method_sym = nil, &block)
+ install_hook :after, target_method, method_sym, &block
+ end
def install_hook(type, name, method_sym = nil, &block)
+ raise ArgumentError.new("You need to pass 2 arguments to \"#{type}\".") if ! block_given? and method_sym.nil?
+ raise ArgumentError.new("target_method should be a symbol") unless name.is_a?(Symbol)
+ raise ArgumentError.new("method_sym should be a symbol") if method_sym && ! method_sym.is_a?(Symbol)
+
(hooks[name][type] ||= []) << if block
new_meth_name = "__hooks_#{type}_#{quote_method(name)}_#{hooks[name][type].length}".to_sym
define_method new_meth_name, block
@@ -21,14 +29,16 @@ def install_hook(type, name, method_sym = nil, &block)
class_eval define_advised_method(name), __FILE__, __LINE__
end
+ # FIXME Return the method value
def define_advised_method(name)
args = args_for(hooks[name][:old_method] ||= instance_method(name))
<<-EOD
def #{name}(#{args})
#{inline_hooks(name, :before, args)}
- #{inline_call(name, args)}
+ retval = #{inline_call(name, args)}
#{inline_hooks(name, :after, args)}
+ retval
end
EOD
end
@@ -80,11 +90,7 @@ def hooks
end
def quote_method(name)
- name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_')
- end
-
- def after(target_method, method_sym = nil, &block)
- install_hook :after, target_method, method_sym, &block
+ name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
end
end
end # module Hook
View
67 lib/data_mapper/property.rb
@@ -110,7 +110,7 @@ module DataMapper
# property :body, DataMapper::Types::Text # Is lazily loaded by default
# end
#
-# If you want to over-ride the lazy loading on any field you can set it to a
+# If you want to over-ride the lazy loading on any field you can set it to a
# context or false to disable it with the :lazy option. Contexts allow multipule
# lazy properties to be loaded at one time. If you set :lazy to true, it is placed
# in the :default context
@@ -230,6 +230,18 @@ def field
@field ||= @options.fetch(:field, repository.adapter.field_naming_convention.call(name))
end
+ def hash
+ return @model.hash + @name.hash
+ end
+
+ def eql?(o)
+ if o.is_a?(Property)
+ return o.model == @model && o.name == @name
+ else
+ return false
+ end
+ end
+
def lazy?
@lazy
end
@@ -258,33 +270,29 @@ def set(value, resource)
resource[@name] = value
end
- def inspect
- "#<Property:#{@model}:#{@name}>"
- end
-
def typecast(value)
- if type == TrueClass
- value == true || value == "true"
- elsif type == String
- value.to_s
- elsif [Float, Fixnum, BigDecimal].include?(type)
- value.to_f
- elsif type == DateTime
- Time.parse(value)
- elsif type == Date
- Date.parse(value)
- elsif type == Class
- Object.recursive_const_get(value)
- else
- value
+ return value if type === value || value.nil?
+
+ if type == TrueClass then true == value || 'true' == value
+ elsif type == String then value.to_s
+ elsif type == Float then value.to_f
+ elsif type == Fixnum then value.to_i
+ elsif type == BigDecimal then BigDecimal.new(value.to_s)
+ elsif type == DateTime then DateTime.parse(value.to_s)
+ elsif type == Date then Date.parse(value.to_s)
+ elsif type == Class then Object.recursive_const_get(value)
end
end
+ def inspect
+ "#<Property:#{@model}:#{@name}>"
+ end
+
private
- def initialize(model, name, type, options)
- raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource" unless Resource === model
- raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}" unless Symbol === name
+ def initialize(model, name, type, options = {})
+ raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource" unless Resource === model
+ raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}" unless Symbol === name
raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}" unless TYPES.include?(type) || (type.respond_to?(:ancestors) && type.ancestors.include?(DataMapper::Type) && TYPES.include?(type.primitive))
if (unknown_options = options.keys - PROPERTY_OPTIONS).any?
@@ -329,16 +337,9 @@ def create_getter
@model.class_eval <<-EOS, __FILE__, __LINE__
#{reader_visibility}
def #{@getter}
- unless @new_record || defined?(#{@instance_variable_name})
- if @loaded_set
- @loaded_set.reload!(:fields => self.class.properties(self.class.repository.name).lazy_load_context(#{name.inspect}))
- end
- end
- #{custom? ? "#{@type.inspect}.load(#{@instance_variable_name})" : @instance_variable_name}
+ self[#{name.inspect}]
end
EOS
- rescue SyntaxError
- raise SyntaxError, name
end
# defines the setter for the property
@@ -346,13 +347,9 @@ def create_setter
@model.class_eval <<-EOS, __FILE__, __LINE__
#{writer_visibility}
def #{name}=(value)
- #{lock? ? "@shadow_#{name} = #{@instance_variable_name}" : ''}
- dirty_attributes << self.class.properties(repository.name)[#{name.inspect}]
- #{@instance_variable_name} = #{custom? ? "#{@type.inspect}.dump(value)" : 'value'}
+ self[#{name.inspect}] = value
end
EOS
- rescue SyntaxError
- raise SyntaxError, name
end
end # class Property
end # module DataMapper
View
35 lib/data_mapper/property_set.rb
@@ -6,6 +6,8 @@ def [](name)
@property_for[name]
end
+ alias has_property? []
+
def slice(*names)
@property_for.values_at(*names)
end
@@ -25,7 +27,7 @@ def empty?
@entries.empty?
end
- def each(&block)
+ def each
@entries.each { |property| yield property }
self
end
@@ -96,22 +98,25 @@ def inspect
private
- def initialize(properties = [], &block)
+ def initialize(properties = [])
raise ArgumentError, "+properties+ should be an Array, but was #{properties.class}", caller unless Array === properties
- @entries = properties
- @property_for = Hash.new do |h,k|
- case k
- when Symbol
- if property = detect { |property| property.name == k }
- h[k.to_s] = h[k] = property
- end
- when String
- if property = detect { |property| property.name.to_s == k }
- h[k] = h[k.to_sym] = property
- end
- else
- raise "Key must be a Symbol or String, but was #{k.class}"
+ @entries = properties
+ @property_for = hash_for_property_for
+ end
+
+ def initialize_copy(orig)
+ @entries = orig.entries.dup
+ @property_for = hash_for_property_for
+ end
+
+ def hash_for_property_for
+ Hash.new do |h,k|
+ raise "Key must be a Symbol or String, but was #{k.class}" unless [String, Symbol].include?(k.class)
+
+ ksym = k.to_sym
+ if property = detect { |property| property.name == ksym }
+ h[ksym] = h[k.to_s] = property
end
end
end
View
4 lib/data_mapper/repository.rb
@@ -89,6 +89,10 @@ def destroy(resource)
end
end
+ def to_s
+ "#<DataMapper::Repository:#{@name}>"
+ end
+
private
def initialize(name)
View
31 lib/data_mapper/resource.rb
@@ -1,3 +1,4 @@
+require 'set'
require __DIR__ + 'support/string'
require __DIR__ + 'property_set'
require __DIR__ + 'property'
@@ -9,6 +10,8 @@ module DataMapper
module Resource
+ @@including_classes = Set.new
+
# +----------------------
# Resource module methods
@@ -17,6 +20,18 @@ def self.included(base)
base.extend DataMapper::Associations
base.send(:include, DataMapper::Hook)
base.send(:include, DataMapper::Scope)
+ @@including_classes << base
+ end
+
+ # Return all classes that include the DataMapper::Resource module
+ #
+ # ==== Returns
+ # Set:: A Set containing the including classes
+ #
+ # -
+ # @public
+ def self.including_classes
+ @@including_classes
end
def self.dependencies
@@ -45,7 +60,7 @@ def [](name)
end
value = instance_variable_get(ivar_name)
- property.custom? ? property.type.load(value) : value
+ property.custom? ? property.type.load(value, property) : value
end
def []=(name, value)
@@ -57,7 +72,8 @@ def []=(name, value)
end
dirty_attributes << property
- instance_variable_set(ivar_name, property.custom? ? property.type.dump(value) : property.typecast(value))
+
+ instance_variable_set(ivar_name, property.custom? ? property.type.dump(value, property) : property.typecast(value))
end
def repository
@@ -103,7 +119,7 @@ def attribute_loaded?(name)
end
def dirty_attributes
- @dirty_attributes ||= []
+ @dirty_attributes ||= Set.new
end
def dirty?
@@ -144,12 +160,11 @@ def attributes
# Mass-assign mapped fields.
def attributes=(values_hash)
values_hash.each_pair do |k,v|
- setter = k.to_s.sub(/\?\z/, '')
+ setter = "#{k.to_s.sub(/\?\z/, '')}="
# We check #public_methods and not Class#public_method_defined? to
# account for singleton methods.
if public_methods.include?(setter)
- self[setter] = v
- # send(setter, v)
+ send(setter, v)
end
end
end
@@ -216,8 +231,8 @@ def default_repository_name
Repository.default_name
end
- def repository(repository_name = default_repository_name)
- DataMapper.repository(repository_name)
+ def repository(repository_name = default_repository_name, &block)
+ DataMapper.repository(repository_name, &block)
end
def storage_name(repository_name = default_repository_name)
View
12 lib/data_mapper/type.rb
@@ -27,7 +27,7 @@ module DataMapper
# primitive String
# size 10
#
- # def self.dump(value)
+ # def self.dump(value, property)
# <work some magic>
# end
#
@@ -123,14 +123,16 @@ def options
# ==== Parameters
# value<Object, nil>::
# The value to dump
+ # property<Property, nil>::
+ # The property the type is being used by
#
# ==== Returns
# Object:: Dumped object
#
#
# @public
- def self.dump(value)
- value
+ def self.dump(value, property)
+ value
end
# Stub instance method for loading
@@ -138,13 +140,15 @@ def self.dump(value)
# ==== Parameters
# value<Object, nil>::
# The value to serialize
+ # property<Property, nil>::
+ # The property the type is being used by
#
# ==== Returns
# Object:: Serialized object. Must be the same type as the ruby primitive
#
#
# @public
- def self.load(value)
+ def self.load(value, property)
value
end
View
4 lib/data_mapper/types/csv.rb
@@ -5,7 +5,7 @@ class Csv < DataMapper::Type
size 65535
lazy true
- def self.load(value)
+ def self.load(value, property)
case value
when String then FasterCSV.parse(value)
when Array then value
@@ -13,7 +13,7 @@ def self.load(value)
end
end
- def self.dump(value)
+ def self.dump(value, property)
case value
when Array then
FasterCSV.generate do |csv|
View
4 lib/data_mapper/types/yaml.rb
@@ -7,7 +7,7 @@ class Yaml < DataMapper::Type
size 65535
lazy true
- def self.load(value)
+ def self.load(value, property)
if value.nil?
nil
elsif value.is_a?(String)
@@ -17,7 +17,7 @@ def self.load(value)
end
end
- def self.dump(value)
+ def self.dump(value, property)
if value.nil?
nil
elsif value.is_a?(String) && value =~ /^---/
View
41 spec/integration/association_spec.rb
@@ -21,7 +21,9 @@ class Yard
property :id, Fixnum, :serial => true
property :name, String
- many_to_one :engine, :repository_name => :sqlite3
+ repository(:sqlite3) do
+ many_to_one :engine
+ end
end
class Pie
@@ -37,7 +39,9 @@ class Sky
property :id, Fixnum, :serial => true
property :name, String
- one_to_one :pie, :repository_name => :sqlite3
+ repository(:sqlite3) do
+ one_to_one :pie
+ end
end
class Host
@@ -46,7 +50,9 @@ class Host
property :id, Fixnum, :serial => true
property :name, String
- one_to_many :slices, :repository_name => :sqlite3
+ repository(:sqlite3) do
+ one_to_many :slices
+ end
end
class Slice
@@ -55,7 +61,9 @@ class Slice
property :id, Fixnum, :serial => true
property :name, String
- many_to_one :host, :repository_name => :sqlite3
+ repository(:sqlite3) do
+ many_to_one :host, :repository_name => :sqlite3
+ end
end
describe DataMapper::Associations do
@@ -282,6 +290,31 @@ class Slice
s.host.id.should == 10
end
+ # describe '#through' do
+ # before(:all) do
+ # class Cake
+ # property :id, Fixnum, :serial => true
+ # property :name, String
+ # has :slices, 1..n
+ # end
+ #
+ # @adapter.execute(<<-EOS.compress_lines)
+ # CREATE TABLE "cakes" (
+ # "id" INTEGER PRIMARY KEY,
+ # "name" VARCHAR(50)
+ # )
+ # EOS
+ #
+ # @adapter.execute('INSERT INTO "cakes" ("id", "name") values (?, ?)', 1, 'cake1', 1)
+ # @adapter.execute('INSERT INTO "cakes" ("id", "name") values (?, ?)', 2, 'cake2', 1)
+ #
+ # class Slice
+ # has :cake, n..1
+ # end
+ # end
+ #
+ # end
+
after do
@adapter.execute('DROP TABLE "slices"')
@adapter.execute('DROP TABLE "hosts"')
View
1  spec/spec_helper.rb
@@ -2,6 +2,7 @@
require 'pathname'
require 'rubygems'
require 'spec'
+require 'fileutils'
# for __DIR__
require Pathname(__FILE__).dirname.expand_path.parent + 'lib/data_mapper/support/kernel'
View
91 spec/unit/adapters/data_objects_adapter_spec.rb
@@ -12,6 +12,88 @@
it_should_behave_like 'a DataMapper Adapter'
+ describe "#find_by_sql" do
+
+ before do
+ class Plupp
+ include DataMapper::Resource
+ property :id, Fixnum, :key => true
+ property :name, String
+ end
+ end
+
+ it "should be added to DataMapper::Resource::ClassMethods" do
+ DataMapper::Resource::ClassMethods.instance_methods.include?("find_by_sql").should == true
+ Plupp.methods.include?("find_by_sql").should == true
+ end
+
+ describe "when called" do
+
+ before do
+ @reader = mock("reader")
+ @reader.stub!(:next!).and_return(false)
+ @reader.stub!(:close)
+ @connection = mock("connection")
+ @connection.stub!(:close)
+ @command = mock("command")
+ @adapter = Plupp.repository.adapter
+ @repository = Plupp.repository
+ @repository.stub!(:adapter).and_return(@adapter)
+ @adapter.stub!(:create_connection).and_return(@connection)
+ @adapter.should_receive(:is_a?).any_number_of_times.with(DataMapper::Adapters::DataObjectsAdapter).and_return(true)
+ end
+
+ it "should accept a single String argument with or without options hash" do
+ @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps").and_return(@command)
+ @command.should_receive(:set_types).twice.with([Fixnum, String])
+ @command.should_receive(:execute_reader).twice.and_return(@reader)
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
+ Plupp.find_by_sql("SELECT * FROM plupps")
+ Plupp.find_by_sql("SELECT * FROM plupps", :repository => :plupp_repo)
+ end
+
+ it "should accept an Array argument with or without options hash" do
+ @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps WHERE plur = ?").and_return(@command)
+ @command.should_receive(:set_types).twice.with([Fixnum, String])
+ @command.should_receive(:execute_reader).twice.with("my pretty plur").and_return(@reader)
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
+ Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"])
+ Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"], :repository => :plupp_repo)
+ end
+
+ it "should accept a Query argument with or without options hash" do
+ @connection.should_receive(:create_command).twice.with("SELECT \"name\" FROM \"plupps\" WHERE (\"name\" = ?)").and_return(@command)
+ @command.should_receive(:set_types).twice.with([Fixnum, String])
+ @command.should_receive(:execute_reader).twice.with(Plupp.properties[:name]).and_return(@reader)
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
+ Plupp.find_by_sql(DataMapper::Query.new(Plupp, "name" => "my pretty plur", :fields => ["name"]))
+ Plupp.find_by_sql(DataMapper::Query.new(Plupp, "name" => "my pretty plur", :fields => ["name"]), :repository => :plupp_repo)
+ end
+
+ it "requires a Repository that is a DataObjectsRepository to work" do
+ non_do_adapter = mock("non do adapter")
+ non_do_repo = mock("non do repo")
+ non_do_repo.stub!(:adapter).and_return(non_do_adapter)
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(non_do_repo)
+ Proc.new do
+ Plupp.find_by_sql(:repository => :plupp_repo)
+ end.should raise_error(Exception, /DataObjectsAdapter/)
+ end
+
+ it "requires some kind of query to work at all" do
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
+ Proc.new do
+ Plupp.find_by_sql(:repository => :plupp_repo)
+ end.should raise_error(Exception, /requires a query/)
+ end
+
+ end
+
+ end
+
describe '#execute' do
before do
@mock_command = mock('Command', :execute_non_query => nil)
@@ -176,6 +258,15 @@ class LittleBox
end
end
+ describe "#update" do
+ it 'should not try to update if there are no dirty attributes' do
+ repository = mock("repository")
+ resource = mock("resource")
+ resource.stub!(:dirty_attributes).and_return({})
+ @adapter.update(repository, resource).should == false
+ end
+ end
+
describe "#create_statement_with_returning" do
it 'should generate a SQL statement for all fields' do
View
61 spec/unit/hook_spec.rb
@@ -185,7 +185,22 @@ def a_hook
end
@class.new.a_method
- end end
+ end
+
+ it "the advised method should still return its normal value" do
+ @class.class_eval do
+ def returner
+ 1
+ end
+
+ after :returner do
+ 2
+ end
+ end
+
+ @class.new.returner.should == 1
+ end
+ end
it 'should allow the use of before and after together' do
tester = mock("tester")
@@ -239,10 +254,12 @@ def a_hook
@class.new.a_method?
end
- it "should allow advising methods ending in ? or ! when passing methods as advices" do
+ it "should allow advising methods ending in ?, ! or = when passing methods as advices" do
tester = mock("tester")
- tester.should_receive(:before).ordered.once
+ tester.should_receive(:before_bang).ordered.once
tester.should_receive(:method!).ordered.once
+ tester.should_receive(:before_eq).ordered.once
+ tester.should_receive(:method_eq).ordered.once
tester.should_receive(:method?).ordered.once
tester.should_receive(:after).ordered.once
@@ -255,12 +272,22 @@ def a_hook
tester.method?
end
+ define_method :a_method= do |value|
+ tester.method_eq
+ end
+
define_method :before_a_method_bang do
- tester.before
+ tester.before_bang
end
before :a_method!, :before_a_method_bang
+ define_method :before_a_method_eq do
+ tester.before_eq
+ end
+
+ before :a_method=, :before_a_method_eq
+
define_method :after_a_method_question do
tester.after
end
@@ -269,6 +296,32 @@ def a_hook
end
@class.new.a_method!
+ @class.new.a_method = 1
@class.new.a_method?
end
+
+ it "should complain when only one argument is passed" do
+ lambda do
+ @class.class_eval do
+ before :a_method
+ after :a_method
+ end
+ end.should raise_error(ArgumentError)
+ end
+
+ it "should complain when target_method is not a symbol" do
+ lambda do
+ @class.class_eval do
+ before "target", :something
+ end
+ end.should raise_error(ArgumentError)
+ end
+
+ it "should complain when method_sym is not a symbol" do
+ lambda do
+ @class.class_eval do
+ before :target, "something"
+ end
+ end.should raise_error(ArgumentError)
+ end
end
View
15 spec/unit/property_set_spec.rb
@@ -66,4 +66,19 @@ class Boat
expect = [:a1,:a2,:a3,:b1,:b2,:b3]
expect.should == set.sort! {|a,b| a.to_s <=> b.to_s}
end
+
+ describe 'when dup\'ed' do
+ it 'should duplicate the @entries ivar' do
+ @properties.dup.entries.should_not equal(@properties.entries)
+ end
+
+ it 'should reinitialize @properties_for' do
+ # force @properties_for to hold a property
+ Icon.properties(:default)[:name].should_not be_nil
+ @properties = Icon.properties(:default)
+
+ @properties.instance_variable_get("@property_for").should_not be_empty
+ @properties.dup.instance_variable_get("@property_for").should be_empty
+ end
+ end
end
View
88 spec/unit/property_spec.rb
@@ -18,6 +18,16 @@ class Tomato
end
end
+ it "should evaluate two similar properties as equal" do
+ p1 = DataMapper::Property.new(Zoo, :name, String, { :size => 30 })
+ p2 = DataMapper::Property.new(Zoo, :name, String, { :size => 30 })
+ p3 = DataMapper::Property.new(Zoo, :title, String, { :size => 30 })
+ p1.eql?(p2).should == true
+ p1.hash.should == p2.hash
+ p1.eql?(p3).should == false
+ p1.hash.should_not == p3.hash
+ end
+
it "should create a String property" do
property = DataMapper::Property.new(Zoo, :name, String, { :size => 30 })
@@ -43,7 +53,6 @@ class Name < DataMapper::Type
options[:size].should == 50
end
-
it "should determine nullness" do
DataMapper::Property.new(Tomato,:botanical_name,String,{:nullable => true}).options[:nullable].should == true
end
@@ -146,4 +155,81 @@ class Tomahto
DataMapper::Property.new(Zoo, :fortune, DataMapper::Types::Text, {}).primitive.should == String
end
+ it 'should provide typecast' do
+ DataMapper::Property.new(Zoo, :name, String).should respond_to(:typecast)
+ end
+
+ it 'should pass through the value if it is the same type when typecasting' do
+ value = 'San Diego'
+ property = DataMapper::Property.new(Zoo, :name, String)
+ property.typecast(value).object_id.should == value.object_id
+ end
+
+ it 'should pass through the value nil when typecasting' do
+ property = DataMapper::Property.new(Zoo, :string, String)
+ property.typecast(nil).should == nil
+ end
+
+ it 'should typecast value (true) for a TrueClass property' do
+ property = DataMapper::Property.new(Zoo, :true_class, TrueClass)
+ property.typecast(true).should == true
+ end
+
+ it 'should typecast value ("true") for a TrueClass property' do
+ property = DataMapper::Property.new(Zoo, :true_class, TrueClass)
+ property.typecast('true').should == true
+ end
+
+ it 'should typecast value for a String property' do
+ property = DataMapper::Property.new(Zoo, :string, String)
+ property.typecast(0).should == '0'
+ end
+
+ it 'should typecast value for a Float property' do
+ property = DataMapper::Property.new(Zoo, :float, Float)
+ property.typecast('0.0').should == 0.0
+ end
+
+ it 'should typecast value for a Fixnum property' do
+ property = DataMapper::Property.new(Zoo, :fixnum, Fixnum)
+ property.typecast('0').should == 0
+ end
+
+ it 'should typecast value for a BigDecimal property' do
+ property = DataMapper::Property.new(Zoo, :big_decimal, BigDecimal)
+ property.typecast(0.0).should == BigDecimal.new('0.0')
+ end
+
+ it 'should typecast value for a DateTime property' do
+ property = DataMapper::Property.new(Zoo, :date_time, DateTime)
+ property.typecast('2000-01-01 00:00:00').should == DateTime.new(2000, 1, 1, 0, 0, 0)
+ end
+
+ it 'should typecast value for a Date property' do
+ property = DataMapper::Property.new(Zoo, :date, Date)
+ property.typecast('2000-01-01').should == Date.new(2000, 1, 1)
+ end
+
+ it 'should typecast value for a Class property' do
+ property = DataMapper::Property.new(Zoo, :class, Class)
+ property.typecast('Zoo').should == Zoo
+ end
+
+ it 'should pass through the value for an Object property' do
+ value = 'a ruby object'
+ property = DataMapper::Property.new(Zoo, :object, Object)
+ property.typecast(value).object_id.should == value.object_id
+ end
+
+ it 'should provide inspect' do
+ DataMapper::Property.new(Zoo, :name, String).should respond_to(:inspect)
+ end
+
+ it 'should return an abbreviated representation of the property when inspected' do
+ DataMapper::Property.new(Zoo, :name, String).inspect.should == '#<Property:Zoo:name>'
+ end
+
+ it 'should raise a SyntaxError when the name contains invalid characters' do
+ lambda { DataMapper::Property.new(Zoo, :"with space", TrueClass) }.should raise_error(SyntaxError)
+ end
end
View
35 spec/unit/resource_spec.rb
@@ -28,12 +28,24 @@ class Planet
property :core, String, :private => true
property :type, Class
- # An example of how to scope a property to a specific repository.
- # Un-specced currently.
- # repository(:legacy) do
- # property :name, String
- # end
+ repository(:legacy) do
+ property :cowabunga, String
+ end
end
+
+ class Moon
+ end
+ end
+
+ it "should hold repository-specific properties" do
+ Planet.properties(:legacy).should have_property(:cowabunga)
+ Planet.properties.should_not have_property(:cowabunga)
+ end
+
+ it "should track the classes that include it" do
+ DataMapper::Resource.including_classes.clear
+ Moon.class_eval do include(DataMapper::Resource) end
+ DataMapper::Resource.including_classes.should == Set.new([Moon])
end
it "should return an instance of the created object" do
@@ -115,6 +127,17 @@ class Planet
pluto.attribute_dirty?(:age).should be_true
end
+ it 'should overwite old dirty attributes with new ones' do
+ pluto = Planet.new(:name => 'Pluto', :age => 500_000)
+ pluto.dirty_attributes.size.should == 2
+ pluto.attribute_dirty?(:name).should be_true
+ pluto.attribute_dirty?(:age).should be_true
+ pluto.name = "pluto"
+ pluto.dirty_attributes.size.should == 2
+ pluto.attribute_dirty?(:name).should be_true
+ pluto.attribute_dirty?(:age).should be_true
+ end
+
it 'should provide a key' do
Planet.new.should respond_to(:key)
end
@@ -192,7 +215,7 @@ class Planet
it '.properties should return an PropertySet' do
Planet.properties(:legacy).should be_kind_of(DataMapper::PropertySet)
- Planet.properties(:legacy).should have(5).entries
+ Planet.properties(:legacy).should have(6).entries
end
it 'should provide key' do
View
56 spec/unit/type_spec.rb
@@ -15,14 +15,35 @@ class TestType2 < DataMapper::Type
primitive String
size 10
- def self.load(value)
+ def self.load(value, property)
value.reverse
end
- def self.dump(value)
+ def self.dump(value, property)
value.reverse
end
end
+
+ class TestResource
+ include DataMapper::Resource
+ end
+
+ class TestType3 < DataMapper::Type
+ primitive String
+ size 10
+ attr_accessor :property, :value
+
+ def self.load(value, property)
+ type = self.new
+ type.property = property
+ type.value = value
+ type
+ end
+
+ def self.dump(value, property)
+ value.value
+ end
+ end
end
it "should have the same PROPERTY_OPTIONS aray as DataMapper::Property" do
@@ -48,19 +69,42 @@ def self.dump(value)
end
it "should pass through the value if load wasn't overriden" do
- TestType.load("test").should == "test"
+ TestType.load("test", nil).should == "test"
end
it "should pass through the value if dump wasn't overriden" do
- TestType.dump("test").should == "test"
+ TestType.dump("test", nil).should == "test"
end
it "should not raise NotImplmenetedException if load was overriden" do
- TestType2.dump("helo").should == "oleh"
+ TestType2.dump("helo", nil).should == "oleh"
end
it "should not raise NotImplmenetedException if dump was overriden" do
- TestType2.load("oleh").should == "helo"
+ TestType2.load("oleh", nil).should == "helo"
+ end
+
+ describe "using a custom type" do
+ before do
+ @property = DataMapper::Property.new TestResource, :name, TestType3, {}
+ end
+
+ it "should return a object of the same type" do
+ TestType3.load("helo", @property).class.should == TestType3
+ end
+
+ it "should contain the property" do
+ TestType3.load("helo", @property).property.should == @property
+ end
+
+ it "should contain the value" do
+ TestType3.load("helo", @property).value.should == "helo"
+ end
+
+ it "should return the value" do
+ obj = TestType3.load("helo", @property)
+ TestType3.dump(obj, @property).should == "helo"
+ end
end
describe "using def Type" do
Please sign in to comment.
Something went wrong with that request. Please try again.