├── spec
├── spec.opts
├── unit
│ ├── associations
│ │ ├── one_to_one_spec.rb
│ │ ├── many_to_many_spec.rb
│ │ ├── many_to_one_spec.rb
│ │ ├── one_to_many_spec.rb
│ │ └── relationship_spec.rb
│ ├── support
│ │ ├── string_spec.rb
│ │ ├── struct_spec.rb
│ │ ├── object_spec.rb
│ │ ├── inflection_spec.rb
│ │ └── blank_spec.rb
│ ├── adapters
│ │ └── adapter_shared_spec.rb
│ ├── naming_conventions_spec.rb
│ ├── types
│ │ ├── enum_spec.rb
│ │ └── flag_spec.rb
│ ├── repository_spec.rb
│ ├── property_set_spec.rb
│ ├── loaded_set_spec.rb
│ ├── type_spec.rb
│ ├── identity_map_spec.rb
│ ├── scope_spec.rb
│ ├── associations_spec.rb
│ ├── property_spec.rb
│ └── resource_spec.rb
├── lib
│ └── mock_adapter.rb
├── integration
│ ├── mysql_adapter_spec.rb
│ ├── data_objects_adapter_spec.rb
│ ├── repository_spec.rb
│ ├── type_spec.rb
│ ├── property_spec.rb
│ ├── sqlite3_adapter_spec.rb
│ └── association_spec.rb
└── spec_helper.rb
├── TODO
├── script
├── all
└── profile.rb
├── lib
├── data_mapper
│ ├── support
│ │ ├── pathname.rb
│ │ ├── struct.rb
│ │ ├── kernel.rb
│ │ ├── object.rb
│ │ ├── errors.rb
│ │ ├── symbol.rb
│ │ ├── blank.rb
│ │ ├── string.rb
│ │ └── inflection.rb
│ ├── adapters.rb
│ ├── types.rb
│ ├── types
│ │ ├── text.rb
│ │ ├── csv.rb
│ │ ├── yaml.rb
│ │ ├── enum.rb
│ │ └── flag.rb
│ ├── support.rb
│ ├── scope.rb
│ ├── associations
│ │ ├── many_to_many.rb
│ │ ├── one_to_one.rb
│ │ ├── many_to_one.rb
│ │ ├── one_to_many.rb
│ │ └── relationship.rb
│ ├── identity_map.rb
│ ├── adapters
│ │ ├── postgres_adapter.rb
│ │ ├── sqlite3_adapter.rb
│ │ └── mysql_adapter.rb
│ ├── naming_conventions.rb
│ ├── repository.rb
│ ├── property_set.rb
│ ├── loaded_set.rb
│ ├── type.rb
│ ├── associations.rb
│ ├── logger.rb
│ └── hook.rb
└── data_mapper.rb
├── .gitignore
├── QUICKLINKS
├── .autotest
├── MIT-LICENSE
├── Rakefile
├── FAQ
├── README
└── CHANGELOG
/spec/spec.opts:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | See: http://wiki.datamapper.org/doku.php?id=what_needs_to_be_done
--------------------------------------------------------------------------------
/script/all:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | ADAPTER=sqlite3 rake
3 | ADAPTER=mysql rake
4 | ADAPTER=postgresql rake
--------------------------------------------------------------------------------
/lib/data_mapper/support/pathname.rb:
--------------------------------------------------------------------------------
1 | class Pathname
2 | def /(path)
3 | (self + path).expand_path
4 | end
5 | end # class Pathname
6 |
--------------------------------------------------------------------------------
/lib/data_mapper/adapters.rb:
--------------------------------------------------------------------------------
1 | dir = Pathname(__FILE__).dirname.expand_path / 'adapters'
2 |
3 | require dir / 'abstract_adapter'
4 | require dir / 'data_objects_adapter'
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | log/*
3 | doc
4 | cov
5 | pkg
6 | .DS_Store
7 | www/output
8 | coverage/*
9 | *.db
10 | spec/integration/*.db*
11 | nbproject
12 | profile_results.*
13 | \#*
14 | TAGS
--------------------------------------------------------------------------------
/lib/data_mapper/types.rb:
--------------------------------------------------------------------------------
1 | dir = Pathname(__FILE__).dirname.expand_path / 'types'
2 |
3 | require dir / 'csv'
4 | require dir / 'enum'
5 | require dir / 'flag'
6 | require dir / 'text'
7 | require dir / 'yaml'
8 |
--------------------------------------------------------------------------------
/spec/unit/associations/one_to_one_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe "DataMapper::Associations::OneToOne" do
4 |
5 | it "should allow a declaration" do
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/struct.rb:
--------------------------------------------------------------------------------
1 | class Struct
2 | # Returns a hash containing the names and values for all instance variables in the Struct.
3 | def attributes
4 | h = {}
5 | each_pair { |k,v| h[k] = v }
6 | h
7 | end
8 | end # class Struct
9 |
--------------------------------------------------------------------------------
/spec/lib/mock_adapter.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Adapters
3 | class MockAdapter < DataMapper::Adapters::DataObjectsAdapter
4 |
5 | def create(repository, instance)
6 | instance
7 | end
8 |
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/unit/support/string_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe String do
4 | it 'should translate' do
5 | '%s is great!'.t('DataMapper').should == 'DataMapper is great!'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/kernel.rb:
--------------------------------------------------------------------------------
1 | module Kernel
2 | # Delegates to DataMapper::repository.
3 | # Will not overwrite if a method of the same name is pre-defined.
4 | def repository(*args, &block)
5 | DataMapper.repository(*args, &block)
6 | end
7 | end # module Kernel
8 |
--------------------------------------------------------------------------------
/spec/unit/support/struct_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe Struct do
4 |
5 | it "should have attributes" do
6 |
7 | s = Struct.new(:name).new('bob')
8 | s.attributes.should == { :name => 'bob' }
9 |
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/lib/data_mapper/types/text.rb:
--------------------------------------------------------------------------------
1 | # FIXME: can we alias this to the class Text if it isn't already defined?
2 | module DataMapper
3 | module Types
4 | class Text < DataMapper::Type
5 | primitive String
6 | size 65535
7 | lazy true
8 | end # class Text
9 | end # module Types
10 | end # module DataMapper
--------------------------------------------------------------------------------
/lib/data_mapper/support.rb:
--------------------------------------------------------------------------------
1 | dir = Pathname(__FILE__).dirname.expand_path / 'support'
2 |
3 | require dir / 'blank'
4 | require dir / 'errors'
5 | require dir / 'inflection'
6 | require dir / 'kernel'
7 | require dir / 'object'
8 | require dir / 'pathname'
9 | require dir / 'string'
10 | require dir / 'struct'
11 | require dir / 'symbol'
12 |
--------------------------------------------------------------------------------
/QUICKLINKS:
--------------------------------------------------------------------------------
1 | = Quick Links
2 |
3 | * Finders and CRUD - DataMapper::Persistence::ConvenienceMethods::ClassMethods
4 | * Properties - DataMapper::Property
5 | * Validations - Validatable
6 | * Migrations
7 | * FAQ[link:/files/FAQ.html]
8 | * Contact Us
9 | * Website - http://www.datamapper.org
10 | * Bug Reports - http://wm.lighthouseapp.com/projects/4819-datamapper/overview
11 | * IRC Channel - ##datamapper on irc.freenode.net
12 | * Mailing List - http://groups.google.com/group/datamapper/
--------------------------------------------------------------------------------
/lib/data_mapper/support/object.rb:
--------------------------------------------------------------------------------
1 | class Object
2 | @nested_constants = Hash.new do |h,k|
3 | klass = Object
4 | k.split('::').each do |c|
5 | klass = klass.const_get(c)
6 | end
7 | h[k] = klass
8 | end
9 |
10 | def self.recursive_const_get(nested_name)
11 | @nested_constants[nested_name]
12 | end
13 |
14 | unless instance_methods.include?('instance_variable_defined?')
15 | def instance_variable_defined?(method)
16 | instance_variables.include?(method.to_s)
17 | end
18 | end
19 | end # class Object
20 |
--------------------------------------------------------------------------------
/spec/unit/adapters/adapter_shared_spec.rb:
--------------------------------------------------------------------------------
1 |
2 | describe "a DataMapper Adapter", :shared => true do
3 |
4 | it "should initialize the connection uri" do
5 | new_adapter = @adapter.class.new(:default, URI.parse('some://uri/string'))
6 | new_adapter.instance_variable_get('@uri').to_s.should == URI.parse('some://uri/string').to_s
7 | end
8 |
9 | %w{create read update delete read_one read_set delete_set} .each do |meth|
10 | it "should have a #{meth} method" do
11 | @adapter.should respond_to(meth.intern)
12 | end
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/spec/unit/support/object_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe Object do
4 |
5 | it "should be able to get a recursive constant" do
6 | Object.recursive_const_get('DataMapper::Resource').should == DataMapper::Resource
7 | end
8 |
9 | it "should not cache unresolvable class string" do
10 | lambda { Object.recursive_const_get('Foo::Bar::Baz') }.should raise_error(NameError)
11 | Object.instance_variable_get(:@nested_constants).has_key?('Foo::Bar::Baz').should == false
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/errors.rb:
--------------------------------------------------------------------------------
1 | #Some useful errors types
2 | module DataMapper
3 | class ValidationError < StandardError; end
4 |
5 | class ObjectNotFoundError < StandardError; end
6 |
7 | class MaterializationError < StandardError; end
8 |
9 | class RepositoryNotSetupError < StandardError; end
10 |
11 | class IncompleteResourceError < StandardError; end
12 | end # module DataMapper
13 |
14 | class StandardError
15 | # Displays the specific error message and the backtrace associated with it.
16 | def display
17 | "#{message}\n\t#{backtrace.join("\n\t")}"
18 | end
19 | end # class StandardError
20 |
--------------------------------------------------------------------------------
/lib/data_mapper/types/csv.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Types
3 | class Csv < DataMapper::Type
4 | primitive String
5 | size 65535
6 | lazy true
7 |
8 | def self.load(value, property)
9 | case value
10 | when String then FasterCSV.parse(value)
11 | when Array then value
12 | else nil
13 | end
14 | end
15 |
16 | def self.dump(value, property)
17 | case value
18 | when Array then
19 | FasterCSV.generate do |csv|
20 | value.each { |row| csv << row }
21 | end
22 | when String then value
23 | else nil
24 | end
25 | end
26 | end # class Csv
27 | end # module Types
28 | end # module DataMapper
--------------------------------------------------------------------------------
/.autotest:
--------------------------------------------------------------------------------
1 | Autotest.add_hook :initialize do |at|
2 | ignore = %w[ .git burn www log plugins script tasks bin CHANGELOG FAQ MIT-LICENSE PERFORMANCE QUICKLINKS README ]
3 |
4 | unless ENV['AUTOTEST'] == 'integration'
5 | ignore << 'spec/integration'
6 | end
7 |
8 | ignore.each do |exception|
9 | at.add_exception(exception)
10 | end
11 |
12 | at.clear_mappings
13 |
14 | at.add_mapping(%r{^spec/.+_spec\.rb$}) do |filename,_|
15 | filename
16 | end
17 |
18 | at.add_mapping(%r{^lib/data_mapper/(.+)\.rb$}) do |_,match|
19 | [ "spec/unit/#{match[1]}_spec.rb" ] +
20 | at.files_matching(%r{^spec/integration/.+_spec\.rb$})
21 | end
22 |
23 | at.add_mapping(%r{^spec/spec_helper\.rb$}) do
24 | at.files_matching(%r{^spec/.+_spec\.rb$})
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/symbol.rb:
--------------------------------------------------------------------------------
1 | class Symbol
2 | def gt
3 | DataMapper::Query::Operator.new(self, :gt)
4 | end
5 |
6 | def gte
7 | DataMapper::Query::Operator.new(self, :gte)
8 | end
9 |
10 | def lt
11 | DataMapper::Query::Operator.new(self, :lt)
12 | end
13 |
14 | def lte
15 | DataMapper::Query::Operator.new(self, :lte)
16 | end
17 |
18 | def not
19 | DataMapper::Query::Operator.new(self, :not)
20 | end
21 |
22 | def eql
23 | DataMapper::Query::Operator.new(self, :eql)
24 | end
25 |
26 | def like
27 | DataMapper::Query::Operator.new(self, :like)
28 | end
29 |
30 | def in
31 | DataMapper::Query::Operator.new(self, :in)
32 | end
33 |
34 | def to_proc
35 | lambda { |value| value.send(self) }
36 | end
37 | end # class Symbol
38 |
--------------------------------------------------------------------------------
/lib/data_mapper/types/yaml.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | module DataMapper
4 | module Types
5 | class Yaml < DataMapper::Type
6 | primitive String
7 | size 65535
8 | lazy true
9 |
10 | def self.load(value, property)
11 | if value.nil?
12 | nil
13 | elsif value.is_a?(String)
14 | ::YAML.load(value)
15 | else
16 | raise ArgumentError.new("+value+ must be nil or a String")
17 | end
18 | end
19 |
20 | def self.dump(value, property)
21 | if value.nil?
22 | nil
23 | elsif value.is_a?(String) && value =~ /^---/
24 | value
25 | else
26 | ::YAML.dump(value)
27 | end
28 | end
29 | end # class Yaml
30 | end # module Types
31 | end # module DataMapper
--------------------------------------------------------------------------------
/lib/data_mapper/types/enum.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Types
3 | class Enum < DataMapper::Type(Fixnum)
4 |
5 | def self.flag_map
6 | @flag_map
7 | end
8 |
9 | def self.flag_map=(value)
10 | @flag_map = value
11 | end
12 |
13 | def self.new(*flags)
14 | enum = Enum.dup
15 | enum.flag_map = {}
16 |
17 | flags.each_with_index do |flag, i|
18 | enum.flag_map[i + 1] = flag
19 | end
20 |
21 | enum
22 | end
23 |
24 | def self.[](*flags)
25 | new(*flags)
26 | end
27 |
28 | def self.load(value)
29 | self.flag_map[value]
30 | end
31 |
32 | def self.dump(flag)
33 | self.flag_map.invert[flag]
34 | end
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/spec/unit/associations/many_to_many_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe "DataMapper::Associations::ManyToMany" do
4 |
5 | before do
6 | @adapter = DataMapper::Repository.adapters[:relationship_spec] || DataMapper.setup(:relationship_spec, 'mock://localhost')
7 | end
8 |
9 | it "should allow a declaration" do
10 |
11 | lambda do
12 | class Supplier
13 | many_to_many :manufacturers
14 | end
15 | end.should_not raise_error
16 | end
17 |
18 | describe DataMapper::Associations::ManyToMany::Instance do
19 | before do
20 | @this = mock("this")
21 | @that = mock("that")
22 | @relationship = mock("relationship")
23 | @association = DataMapper::Associations::ManyToMany::Instance.new(@relationship, @that, nil)
24 | end
25 |
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/blank.rb:
--------------------------------------------------------------------------------
1 | # blank? methods for several different class types
2 | class Object
3 | # Returns true if the object is nil or empty (if applicable)
4 | def blank?
5 | nil? || (respond_to?(:empty?) && empty?)
6 | end
7 | end # class Object
8 |
9 | class Numeric
10 | # Numerics can't be blank
11 | def blank?
12 | false
13 | end
14 | end # class Numeric
15 |
16 | class NilClass
17 | # Nils are always blank
18 | def blank?
19 | true
20 | end
21 | end # class NilClass
22 |
23 | class TrueClass
24 | # True is not blank.
25 | def blank?
26 | false
27 | end
28 | end # class TrueClass
29 |
30 | class FalseClass
31 | # False is always blank.
32 | def blank?
33 | true
34 | end
35 | end # class FalseClass
36 |
37 | class String
38 | # Strips out whitespace then tests if the string is empty.
39 | def blank?
40 | strip.empty?
41 | end
42 | end # class String
43 |
--------------------------------------------------------------------------------
/spec/unit/associations/many_to_one_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe "DataMapper::Associations::ManyToOne" do
4 |
5 | it "should allow a declaration" do
6 | lambda do
7 | class Vehicle
8 | many_to_one :manufacturer
9 | end
10 | end.should_not raise_error
11 | end
12 |
13 | describe DataMapper::Associations::ManyToOne::Instance do
14 | before do
15 | @child = mock("child")
16 | @parent = mock("parent")
17 | @relationship = mock("relationship")
18 | @association = DataMapper::Associations::ManyToOne::Instance.new(@relationship, @child)
19 | end
20 |
21 | describe "when the parent exists" do
22 | it "should attach the parent to the child" do
23 | @parent.should_receive(:new_record?).and_return(false)
24 | @relationship.should_receive(:attach_parent).with(@child, @parent)
25 |
26 | @association.parent = @parent
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/data_mapper/scope.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Scope
3 | def self.included(base)
4 | base.extend(ClassMethods)
5 | end
6 |
7 | module ClassMethods
8 | protected
9 |
10 | def with_scope(query, &block)
11 | # merge the current scope with the passed in query
12 | with_exclusive_scope(current_scope ? current_scope.merge(query) : query, &block)
13 | end
14 |
15 | def with_exclusive_scope(query, &block)
16 | query = DataMapper::Query.new(repository, self, query) if Hash === query
17 |
18 | scope_stack << query
19 |
20 | begin
21 | yield
22 | ensure
23 | scope_stack.pop
24 | end
25 | end
26 |
27 | private
28 |
29 | def scope_stack
30 | scope_stack_for = Thread.current[:dm_scope_stack] ||= Hash.new { |h,k| h[k] = [] }
31 | scope_stack_for[self]
32 | end
33 |
34 | def current_scope
35 | scope_stack.last
36 | end
37 | end # module ClassMethods
38 | end # module Scope
39 | end # module DataMapper
40 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2007 Sam Smoot
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/lib/data_mapper/types/flag.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Types
3 | class Flag < DataMapper::Type(Fixnum)
4 |
5 | def self.flag_map
6 | @flag_map
7 | end
8 |
9 | def self.flag_map=(value)
10 | @flag_map = value
11 | end
12 |
13 | def self.new(*flags)
14 | type = Flag.dup
15 | type.flag_map = {}
16 |
17 | flags.each_with_index do |flag, i|
18 | type.flag_map[2 ** i] = flag
19 | end
20 |
21 | type
22 | end
23 |
24 | def self.[](*flags)
25 | new(*flags)
26 | end
27 |
28 | def self.load(value)
29 | begin
30 | matches = []
31 |
32 | 0.upto((Math.log(value) / Math.log(2)).ceil) do |i|
33 | pow = 2 ** i
34 | matches << flag_map[pow] if value & pow == pow
35 | end
36 |
37 | matches.compact
38 | rescue TypeError, Errno::EDOM
39 | []
40 | end
41 | end
42 |
43 | def self.dump(*flags)
44 | flag_map.invert.values_at(*flags.flatten).compact.inject(0) {|sum, i| sum + i}
45 | end
46 | end
47 | end
48 | end
--------------------------------------------------------------------------------
/lib/data_mapper/associations/many_to_many.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Associations
3 | module ManyToMany
4 | private
5 | def many_to_many(name, options = {})
6 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
7 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options
8 |
9 | # TOOD: raise an exception if unknown options are passed in
10 |
11 | child_model_name = DataMapper::Inflection.demodulize(self.name)
12 | parent_model_name = options[:class_name] || DataMapper::Inflection.classify(name)
13 |
14 | relationships(repository.name)[name] = Relationship.new(
15 | name,
16 | options,
17 | repository.name,
18 | child_model_name,
19 | nil,
20 | parent_model_name,
21 | nil
22 | )
23 | relationships(repository.name)[name]
24 | end
25 |
26 | class Instance
27 | def initialize() end
28 |
29 | def save
30 | raise NotImplementedError
31 | end
32 |
33 | end # class Instance
34 | end # module ManyToMany
35 | end # module Associations
36 | end # module DataMapper
37 |
--------------------------------------------------------------------------------
/lib/data_mapper/support/string.rb:
--------------------------------------------------------------------------------
1 | class String
2 | # Overwrite this method to provide your own translations.
3 | def self.translate(value)
4 | translations[value] || value
5 | end
6 |
7 | def self.translations
8 | @translations ||= {}
9 | end
10 |
11 | # Matches any whitespace (including newline) and replaces with a single space
12 | # EXAMPLE:
13 | # < "SELECT name FROM users"
18 | def compress_lines(spaced = true)
19 | split($/).map { |line| line.strip }.join(spaced ? ' ' : '')
20 | end
21 |
22 | # Useful for heredocs - removes whitespace margin.
23 | def margin(indicator = nil)
24 | lines = self.dup.split($/)
25 |
26 | min_margin = 0
27 | lines.each do |line|
28 | if line =~ /^(\s+)/ && (min_margin == 0 || $1.size < min_margin)
29 | min_margin = $1.size
30 | end
31 | end
32 | lines.map { |line| line.sub(/^\s{#{min_margin}}/, '') }.join($/)
33 | end
34 |
35 | # Formats String for easy translation. Replaces an arbitrary number of
36 | # values using numeric identifier replacement.
37 | #
38 | # "%s %s %s" % %w(one two three) #=> "one two three"
39 | # "%3$s %2$s %1$s" % %w(one two three) #=> "three two one"
40 | def t(*values)
41 | self.class::translate(self) % values
42 | end
43 |
44 | def to_class
45 | ::Object::recursive_const_get(self)
46 | end
47 | end # class String
48 |
--------------------------------------------------------------------------------
/lib/data_mapper/identity_map.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 |
3 | # Tracks objects to help ensure that each object gets loaded only once.
4 | # See: http://www.martinfowler.com/eaaCatalog/identityMap.html
5 | class IdentityMap
6 | # Get a resource from the IdentityMap
7 | def get(key)
8 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key
9 |
10 | @cache[key]
11 | end
12 |
13 | alias [] get
14 |
15 | # Add a resource to the IdentityMap
16 | def set(key, resource)
17 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key
18 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource
19 |
20 | @second_level_cache.set(key, resource) if @second_level_cache
21 | @cache[key] = resource
22 | end
23 |
24 | alias []= set
25 |
26 | # Remove a resource from the IdentityMap
27 | def delete(key)
28 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key
29 |
30 | @second_level_cache.delete(key) if @second_level_cache
31 | @cache.delete(key)
32 | end
33 |
34 | private
35 |
36 | def initialize(second_level_cache = nil)
37 | @cache = if @second_level_cache = second_level_cache
38 | Hash.new { |h,key| h[key] = @second_level_cache.get(key) }
39 | else
40 | Hash.new
41 | end
42 | end
43 | end # class IdentityMap
44 | end # module DataMapper
45 |
--------------------------------------------------------------------------------
/spec/integration/mysql_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | begin
4 | gem 'do_mysql', '=0.9.0'
5 | require 'do_mysql'
6 |
7 | DataMapper.setup(:mysql, "mysql://localhost/dm_core_test")
8 |
9 | describe DataMapper::Adapters::DataObjectsAdapter do
10 | before :all do
11 | @adapter = repository(:mysql).adapter
12 | end
13 |
14 | describe "handling transactions" do
15 | before :all do
16 | @adapter.execute('DROP TABLE IF EXISTS sputniks')
17 | @adapter.execute('CREATE TABLE sputniks (id serial, name text) ENGINE = innodb')
18 | end
19 |
20 | before :each do
21 | @transaction = DataMapper::Adapters::Transaction.new(@adapter)
22 | end
23 |
24 | it "should rollback changes when #rollback_transaction is called" do
25 | @transaction.commit do |trans|
26 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')")
27 | trans.rollback
28 | end
29 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").empty?.should == true
30 | end
31 | it "should commit changes when #commit_transaction is called" do
32 | @transaction.commit do
33 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')")
34 | end
35 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").size.should == 1
36 | end
37 | end
38 |
39 | end
40 | rescue LoadError => e
41 | describe 'do_mysql' do
42 | it 'should be required' do
43 | fail "MySQL integration specs not run! Could not load do_mysql: #{e}"
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/unit/naming_conventions_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe "DataMapper::NamingConventions" do
4 | it "should coerce a string into the Underscored convention" do
5 | DataMapper::NamingConventions::Underscored.call('User').should == 'user'
6 | DataMapper::NamingConventions::Underscored.call('UserAccountSetting').should == 'user_account_setting'
7 | end
8 |
9 | it "should coerce a string into the UnderscoredAndPluralized convention" do
10 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('User').should == 'users'
11 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('UserAccountSetting').should == 'user_account_settings'
12 | end
13 |
14 | it "should coerce a string into the UnderscoredAndPluralized convention joining namespace with underscore" do
15 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('Model::User').should == 'model_users'
16 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('Model::UserAccountSetting').should == 'model_user_account_settings'
17 | end
18 |
19 | it "should coerce a string into the UnderscoredAndPluralizedWithoutModule convention" do
20 | DataMapper::NamingConventions::UnderscoredAndPluralizedWithoutModule.call('Model::User').should == 'users'
21 | DataMapper::NamingConventions::UnderscoredAndPluralizedWithoutModule.call('Model::UserAccountSetting').should == 'user_account_settings'
22 | end
23 |
24 | it "should coerce a string into the Yaml convention" do
25 | DataMapper::NamingConventions::Yaml.call('UserSetting').should == 'user_settings.yaml'
26 | DataMapper::NamingConventions::Yaml.call('User').should == 'users.yaml'
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/data_mapper/adapters/postgres_adapter.rb:
--------------------------------------------------------------------------------
1 | gem 'do_postgres', '=0.9.0'
2 | require 'do_postgres'
3 |
4 | module DataMapper
5 | module Adapters
6 |
7 | class PostgresAdapter < DataObjectsAdapter
8 |
9 | def begin_transaction(transaction)
10 | cmd = "BEGIN"
11 | transaction.connection_for(self).create_command(cmd).execute_non_query
12 | DataMapper.logger.debug("#{self}: #{cmd}")
13 | end
14 |
15 | def transaction_id(transaction)
16 | "#{transaction.id}:#{self.object_id}"
17 | end
18 |
19 | def commit_transaction(transaction)
20 | cmd = "COMMIT PREPARED '#{transaction_id(transaction)}'"
21 | transaction.connection_for(self).create_command(cmd).execute_non_query
22 | DataMapper.logger.debug("#{self}: #{cmd}")
23 | end
24 |
25 | def prepare_transaction(transaction)
26 | cmd = "PREPARE TRANSACTION '#{transaction_id(transaction)}'"
27 | transaction.connection_for(self).create_command(cmd).execute_non_query
28 | DataMapper.logger.debug("#{self}: #{cmd}")
29 | end
30 |
31 | def rollback_transaction(transaction)
32 | cmd = "ROLLBACK"
33 | transaction.connection_for(self).create_command(cmd).execute_non_query
34 | DataMapper.logger.debug("#{self}: #{cmd}")
35 | end
36 |
37 | def rollback_prepared_transaction(transaction)
38 | cmd = "ROLLBACK PREPARED '#{transaction_id(transaction)}'"
39 | transaction.connection.create_command(cmd).execute_non_query
40 | DataMapper.logger.debug("#{self}: #{cmd}")
41 | end
42 |
43 | def create_with_returning?; true; end
44 |
45 | end # class PostgresAdapter
46 |
47 | end # module Adapters
48 | end # module DataMapper
49 |
--------------------------------------------------------------------------------
/spec/unit/support/inflection_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe DataMapper::Inflection do
4 |
5 | it 'should pluralize a word' do
6 | 'car'.plural.should == 'cars'
7 | DataMapper::Inflection.pluralize('car').should == 'cars'
8 | end
9 |
10 | it 'should singularize a word' do
11 | "cars".singular.should == "car"
12 | DataMapper::Inflection.singularize('cars').should == 'car'
13 | end
14 |
15 | it 'should classify an underscored name' do
16 | DataMapper::Inflection.classify('data_mapper').should == 'DataMapper'
17 | end
18 |
19 | it 'should camelize an underscored name' do
20 | DataMapper::Inflection.camelize('data_mapper').should == 'DataMapper'
21 | end
22 |
23 | it 'should underscore a camelized name' do
24 | DataMapper::Inflection.underscore('DataMapper').should == 'data_mapper'
25 | end
26 |
27 | it 'should humanize names' do
28 | DataMapper::Inflection.humanize('employee_salary').should == 'Employee salary'
29 | DataMapper::Inflection.humanize('author_id').should == 'Author'
30 | end
31 |
32 | it 'should demodulize a module name' do
33 | DataMapper::Inflection.demodulize('DataMapper::Inflector').should == 'Inflector'
34 | end
35 |
36 | it 'should tableize a name (underscore with last word plural)' do
37 | DataMapper::Inflection.tableize('fancy_category').should == 'fancy_categories'
38 | DataMapper::Inflection.tableize('FancyCategory').should == 'fancy_categories'
39 | end
40 |
41 | it 'should create a fk name from a class name' do
42 | DataMapper::Inflection.foreign_key('Message').should == 'message_id'
43 | DataMapper::Inflection.foreign_key('Admin::Post').should == 'post_id'
44 | end
45 |
46 |
47 |
48 |
49 |
50 | end
51 |
--------------------------------------------------------------------------------
/script/profile.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require File.join(File.dirname(__FILE__), '..', 'lib', 'data_mapper')
4 |
5 | require 'ruby-prof'
6 |
7 | OUTPUT = DataMapper.root / 'profile_results.txt'
8 |
9 | SOCKET_FILE = Pathname.glob(%w[
10 | /opt/local/var/run/mysql5/mysqld.sock
11 | tmp/mysqld.sock
12 | tmp/mysql.sock
13 | ]).find(&:socket?)
14 |
15 | DataMapper::Logger.new(DataMapper.root / 'log' / 'dm.log', :debug)
16 | DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{SOCKET_FILE}")
17 |
18 | class Exhibit
19 | include DataMapper::Resource
20 |
21 | property :id, Fixnum, :serial => true
22 | property :name, String
23 | property :zoo_id, Fixnum
24 | property :notes, String, :lazy => true
25 | property :created_on, Date
26 | property :updated_at, DateTime
27 |
28 | end
29 |
30 | touch_attributes = lambda do |exhibits|
31 | [*exhibits].each do |exhibit|
32 | exhibit.id
33 | exhibit.name
34 | exhibit.created_on
35 | exhibit.updated_at
36 | end
37 | end
38 |
39 | # RubyProf, making profiling Ruby pretty since 1899!
40 | def profile(&b)
41 | result = RubyProf.profile &b
42 | printer = RubyProf::FlatPrinter.new(result)
43 | printer.print(OUTPUT.open('w+'))
44 | end
45 |
46 | profile do
47 | 10_000.times { touch_attributes[Exhibit.get(1)] }
48 |
49 | # repository(:default) do
50 | # 10_000.times { touch_attributes[Exhibit.get(1)] }
51 | # end
52 | #
53 | # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] }
54 | #
55 | # repository(:default) do
56 | # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] }
57 | # end
58 | #
59 | # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] }
60 | #
61 | # repository(:default) do
62 | # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] }
63 | # end
64 | end
65 |
66 | puts "Done!"
67 |
--------------------------------------------------------------------------------
/spec/integration/data_objects_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | gem 'do_sqlite3', '=0.9.0'
4 | require 'do_sqlite3'
5 |
6 | describe DataMapper::Adapters::DataObjectsAdapter do
7 |
8 | describe "when using transactions" do
9 |
10 | before :each do
11 | @adapter = DataMapper::Adapters::Sqlite3Adapter.new(:sqlite3, URI.parse("sqlite3://#{INTEGRATION_DB_PATH}"))
12 | @transaction = DataMapper::Adapters::Transaction.new(@adapter)
13 | @transaction.begin
14 | end
15 |
16 | describe "#close_connection" do
17 | it "should not close connections that are used for the current transaction" do
18 | @transaction.connection_for(@adapter).should_not_receive(:close)
19 | @transaction.within do
20 | @adapter.close_connection(@transaction.connection_for(@adapter))
21 | end
22 | end
23 | it "should still close connections that are not used for the current transaction" do
24 | conn2 = mock("connection2")
25 | conn2.should_receive(:close)
26 | @transaction.within do
27 | @adapter.close_connection(conn2)
28 | end
29 | end
30 | end
31 | it "should return a fresh connection on #create_connection_outside_transaction" do
32 | DataObjects::Connection.should_receive(:new).once.with(@adapter.uri)
33 | conn = @adapter.create_connection_outside_transaction
34 | end
35 | describe "#create_connection" do
36 | it "should return the connection for the transaction if within a transaction" do
37 | @transaction.within do
38 | @adapter.create_connection.should == @transaction.connection_for(@adapter)
39 | end
40 | end
41 | it "should return new connections if not within a transaction" do
42 | @adapter.create_connection.should_not == @transaction.connection_for(@adapter)
43 | end
44 | end
45 | end
46 |
47 | end
48 |
--------------------------------------------------------------------------------
/spec/unit/types/enum_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | include DataMapper::Types
4 |
5 | describe DataMapper::Types::Enum do
6 |
7 | describe ".new" do
8 | it "should create a Class" do
9 | Enum.new.should be_instance_of(Class)
10 | end
11 |
12 | it "should create unique a Class each call" do
13 | Enum.new.should_not == Enum.new
14 | end
15 |
16 | it "should use the arguments as the values in the @flag_map hash" do
17 | Enum.new(:first, :second, :third).flag_map.values.should == [:first, :second, :third]
18 | end
19 |
20 | it "should create incremental keys for the @flag_map hash, staring at 1" do
21 | Enum.new(:one, :two, :three, :four).flag_map.keys.should == (1..4).to_a
22 | end
23 | end
24 |
25 | describe ".[]" do
26 | it "should be an alias for the new method" do
27 | Enum.should_receive(:new).with(:uno, :dos, :tres)
28 | Enum[:uno, :dos, :tres]
29 | end
30 | end
31 |
32 | describe ".dump" do
33 | before(:each) do
34 | @enum = Enum[:first, :second, :third]
35 | end
36 |
37 | it "should return the key of the value match from the flag map" do
38 | @enum.dump(:first).should == 1
39 | @enum.dump(:second).should == 2
40 | @enum.dump(:third).should == 3
41 | end
42 |
43 | it "should return nil if there is no match" do
44 | @enum.dump(:zero).should be_nil
45 | end
46 | end
47 |
48 | describe ".load" do
49 | before(:each) do
50 | @enum = Enum[:uno, :dos, :tres]
51 | end
52 |
53 | it "should return the value of the key match from the flag map" do
54 | @enum.load(1).should == :uno
55 | @enum.load(2).should == :dos
56 | @enum.load(3).should == :tres
57 | end
58 |
59 | it "should return nil if there is no key" do
60 | @enum.load(-1).should be_nil
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/data_mapper/associations/one_to_one.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Associations
3 | module OneToOne
4 | private
5 | def one_to_one(name, options = {})
6 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
7 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options
8 |
9 | # TOOD: raise an exception if unknown options are passed in
10 |
11 | child_model_name = options[:class_name] || DataMapper::Inflection.classify(name)
12 | parent_model_name = DataMapper::Inflection.demodulize(self.name)
13 |
14 | relationships(repository.name)[name] = Relationship.new(
15 | DataMapper::Inflection.underscore(parent_model_name).to_sym,
16 | options,
17 | repository.name,
18 | child_model_name,
19 | nil,
20 | parent_model_name,
21 | nil
22 | )
23 |
24 | class_eval <<-EOS, __FILE__, __LINE__
25 | def #{name}
26 | #{name}_association.first
27 | end
28 |
29 | def #{name}=(child_resource)
30 | #{name}_association.clear
31 | #{name}_association << child_resource unless child_resource.nil?
32 | end
33 |
34 | private
35 |
36 | def #{name}_association
37 | @#{name}_association ||= begin
38 | relationship = self.class.relationships(repository.name)[:#{name}]
39 |
40 | association = Associations::OneToMany::Proxy.new(relationship, self) do |repository, relationship|
41 | repository.all(*relationship.to_child_query(self))
42 | end
43 |
44 | parent_associations << association
45 |
46 | association
47 | end
48 | end
49 | EOS
50 | relationships(repository.name)[name]
51 | end
52 |
53 | end # module HasOne
54 | end # module Associations
55 | end # module DataMapper
56 |
--------------------------------------------------------------------------------
/spec/unit/support/blank_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe Object do
4 | it 'should provide blank?' do
5 | Object.new.should respond_to(:blank?)
6 | end
7 |
8 | it 'should be blank if it is nil' do
9 | object = Object.new
10 | class << object
11 | def nil?; true end
12 | end
13 | object.should be_blank
14 | end
15 |
16 | it 'should be blank if it is empty' do
17 | {}.should be_blank
18 | [].should be_blank
19 | end
20 |
21 | it 'should not be blank if not nil or empty' do
22 | Object.new.should_not be_blank
23 | [nil].should_not be_blank
24 | { nil => 0 }.should_not be_blank
25 | end
26 | end
27 |
28 | describe Numeric do
29 | it 'should provide blank?' do
30 | 1.should respond_to(:blank?)
31 | end
32 |
33 | it 'should never be blank' do
34 | 1.should_not be_blank
35 | end
36 | end
37 |
38 | describe NilClass do
39 | it 'should provide blank?' do
40 | nil.should respond_to(:blank?)
41 | end
42 |
43 | it 'should always be blank' do
44 | nil.should be_blank
45 | end
46 | end
47 |
48 | describe TrueClass do
49 | it 'should provide blank?' do
50 | true.should respond_to(:blank?)
51 | end
52 |
53 | it 'should never be blank' do
54 | true.should_not be_blank
55 | end
56 | end
57 |
58 | describe FalseClass do
59 | it 'should provide blank?' do
60 | false.should respond_to(:blank?)
61 | end
62 |
63 | it 'should always be blank' do
64 | false.should be_blank
65 | end
66 | end
67 |
68 | describe String do
69 | it 'should provide blank?' do
70 | 'string'.should respond_to(:blank?)
71 | end
72 |
73 | it 'should be blank if empty' do
74 | ''.should be_blank
75 | end
76 |
77 | it 'should be blank if it only contains whitespace' do
78 | ' '.should be_blank
79 | " \r \n \t ".should be_blank
80 | end
81 |
82 | it 'should not be blank if it contains non-whitespace' do
83 | ' a '.should_not be_blank
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/data_mapper/naming_conventions.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 |
3 | # Use these modules to set naming conventions.
4 | # The default is UnderscoredAndPluralized.
5 | # You assign a naming convention like so:
6 | #
7 | # repository(:default).adapter.resource_naming_convention = NamingConventions::Underscored
8 | #
9 | # You can also easily assign a custom convention with a Proc:
10 | #
11 | # repository(:default).adapter.resource_naming_convention = lambda do |value|
12 | # 'tbl' + value.camelize(true)
13 | # end
14 | #
15 | # Or by simply defining your own module in NamingConventions that responds to ::call.
16 | #
17 | # NOTE: It's important to set the convention before accessing your models since the resource_names
18 | # are cached after first accessed. DataMapper.setup(name, uri) returns the Adapter for convenience,
19 | # so you can use code like this:
20 | #
21 | # adapter = DataMapper.setup(:default, "mock://localhost/mock")
22 | # adapter.resource_naming_convention = DataMapper::NamingConventions::Underscored
23 | module NamingConventions
24 |
25 | module UnderscoredAndPluralized
26 | def self.call(value)
27 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(value)).gsub('/','_')
28 | end
29 | end # module UnderscoredAndPluralized
30 |
31 | module UnderscoredAndPluralizedWithoutModule
32 | def self.call(value)
33 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(DataMapper::Inflection.demodulize(value)))
34 | end
35 | end # module UnderscoredAndPluralizedWithoutModule
36 |
37 | module Underscored
38 | def self.call(value)
39 | DataMapper::Inflection.underscore(value)
40 | end
41 | end # module Underscored
42 |
43 | module Yaml
44 | def self.call(value)
45 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(value)) + ".yaml"
46 | end
47 | end # module Yaml
48 |
49 | end # module NamingConventions
50 | end # module DataMapper
51 |
--------------------------------------------------------------------------------
/lib/data_mapper/adapters/sqlite3_adapter.rb:
--------------------------------------------------------------------------------
1 | gem 'do_sqlite3', '=0.9.0'
2 | require 'do_sqlite3'
3 |
4 | module DataMapper
5 | module Adapters
6 |
7 | class Sqlite3Adapter < DataObjectsAdapter
8 |
9 | def begin_transaction(transaction)
10 | cmd = "BEGIN"
11 | transaction.connection_for(self).create_command(cmd).execute_non_query
12 | DataMapper.logger.debug("#{self}: #{cmd}")
13 | end
14 |
15 | def commit_transaction(transaction)
16 | cmd = "COMMIT"
17 | transaction.connection_for(self).create_command(cmd).execute_non_query
18 | DataMapper.logger.debug("#{self}: #{cmd}")
19 | end
20 |
21 | def prepare_transaction(transaction)
22 | DataMapper.logger.debug("#{self}: #prepare_transaction called, but I don't know how... I hope the commit comes pretty soon!")
23 | end
24 |
25 | def rollback_transaction(transaction)
26 | cmd = "ROLLBACK"
27 | transaction.connection_for(self).create_command(cmd).execute_non_query
28 | DataMapper.logger.debug("#{self}: #{cmd}")
29 | end
30 |
31 | def rollback_prepared_transaction(transaction)
32 | cmd = "ROLLBACK"
33 | transaction.connection.create_command(cmd).execute_non_query
34 | DataMapper.logger.debug("#{self}: #{cmd}")
35 | end
36 |
37 | TYPES.merge!(
38 | :integer => 'INTEGER'.freeze,
39 | :string => 'TEXT'.freeze,
40 | :text => 'TEXT'.freeze,
41 | :class => 'TEXT'.freeze,
42 | :boolean => 'INTEGER'.freeze
43 | )
44 |
45 | def rewrite_uri(uri, options)
46 | new_uri = uri.dup
47 | new_uri.path = options[:path] || uri.path
48 |
49 | new_uri
50 | end
51 |
52 | protected
53 |
54 | def normalize_uri(uri_or_options)
55 | uri = super
56 | uri.path = File.join(Dir.pwd, File.dirname(uri.path), File.basename(uri.path)) unless File.exists?(uri.path)
57 | uri
58 | end
59 | end # class Sqlite3Adapter
60 |
61 | end # module Adapters
62 | end # module DataMapper
63 |
--------------------------------------------------------------------------------
/spec/unit/repository_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe DataMapper::Repository do
4 |
5 | before do
6 | @adapter = DataMapper::Repository.adapters[:repository_spec] || DataMapper.setup(:repository_spec, 'mock://localhost')
7 |
8 | class Vegetable
9 | include DataMapper::Resource
10 |
11 | property :id, Fixnum, :serial => true
12 | property :name, String
13 |
14 | end
15 | end
16 |
17 | describe "managing transactions" do
18 | it "should create a new Transaction with itself as argument when #transaction is called" do
19 | trans = mock("transaction")
20 | repo = repository
21 | DataMapper::Adapters::Transaction.should_receive(:new).once.with(repo).and_return(trans)
22 | repo.transaction.should == trans
23 | end
24 | end
25 |
26 | it "should provide persistance methods" do
27 | repository.should respond_to(:get)
28 | repository.should respond_to(:first)
29 | repository.should respond_to(:all)
30 | repository.should respond_to(:save)
31 | repository.should respond_to(:destroy)
32 | end
33 |
34 | it 'should call #create when #save is called on a new record' do
35 | repository = repository(:repository_spec)
36 | instance = Vegetable.new({:id => 1, :name => 'Potato'})
37 |
38 | @adapter.should_receive(:create).with(repository, instance).and_return(instance)
39 |
40 | repository.save(instance)
41 | end
42 |
43 | it 'should call #update when #save is called on an existing record' do
44 | repository = repository(:repository_spec)
45 | instance = Vegetable.new(:name => 'Potato')
46 | instance.instance_variable_set('@new_record', false)
47 |
48 | @adapter.should_receive(:update).with(repository, instance).and_return(instance)
49 |
50 | repository.save(instance)
51 | end
52 |
53 | it 'should provide default_name' do
54 | DataMapper::Repository.should respond_to(:default_name)
55 | end
56 |
57 | it 'should return :default for default_name' do
58 | DataMapper::Repository.default_name.should == :default
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/integration/repository_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | begin
4 | gem 'do_sqlite3', '=0.9.0'
5 | require 'do_sqlite3'
6 |
7 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}")
8 |
9 | describe DataMapper::Repository do
10 | describe "finders" do
11 | before do
12 | class SerialFinderSpec
13 | include DataMapper::Resource
14 |
15 | property :id, Fixnum, :serial => true
16 | property :sample, String
17 | end
18 |
19 | @adapter = repository(:sqlite3).adapter
20 |
21 | @adapter.execute(<<-EOS.compress_lines)
22 | CREATE TABLE "serial_finder_specs" (
23 | "id" INTEGER PRIMARY KEY,
24 | "sample" VARCHAR(50)
25 | )
26 | EOS
27 |
28 | # Why do we keep testing with Repository instead of the models directly?
29 | # Just because we're trying to target the code we're actualling testing
30 | # as much as possible.
31 | setup_repository = repository(:sqlite3)
32 | 100.times do
33 | setup_repository.save(SerialFinderSpec.new(:sample => rand.to_s))
34 | end
35 | end
36 |
37 | it "should return all available rows" do
38 | repository(:sqlite3).all(SerialFinderSpec, {}).should have(100).entries
39 | end
40 |
41 | it "should allow limit and offset" do
42 | repository(:sqlite3).all(SerialFinderSpec, { :limit => 50 }).should have(50).entries
43 |
44 | repository(:sqlite3).all(SerialFinderSpec, { :limit => 20, :offset => 40 }).map(&:id).should ==
45 | repository(:sqlite3).all(SerialFinderSpec, {})[40...60].map(&:id)
46 | end
47 |
48 | it "should lazy-load missing attributes" do
49 | sfs = repository(:sqlite3).all(SerialFinderSpec, { :fields => [:id], :limit => 1 }).first
50 | sfs.should be_a_kind_of(SerialFinderSpec)
51 | sfs.should_not be_a_new_record
52 |
53 | sfs.instance_variables.should_not include('@sample')
54 | sfs.sample.should_not be_nil
55 | end
56 |
57 | it "should translate an Array to an IN clause" do
58 | ids = repository(:sqlite3).all(SerialFinderSpec, { :limit => 10 }).map(&:id)
59 | results = repository(:sqlite3).all(SerialFinderSpec, { :id => ids })
60 |
61 | results.size.should == 10
62 | results.map(&:id).should == ids
63 | end
64 |
65 | after do
66 | @adapter.execute('DROP TABLE "serial_finder_specs"')
67 | end
68 |
69 | end
70 | end
71 | rescue LoadError
72 | warn "integration/repository_spec not run! Could not load do_sqlite3."
73 | end
74 |
--------------------------------------------------------------------------------
/spec/unit/types/flag_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | include DataMapper::Types
4 |
5 | describe DataMapper::Types::Flag do
6 |
7 | describe ".new" do
8 | it "should create a Class" do
9 | Flag.new.should be_instance_of(Class)
10 | end
11 |
12 | it "should create unique a Class each call" do
13 | Flag.new.should_not == Flag.new
14 | end
15 |
16 | it "should use the arguments as the values in the @flag_map hash" do
17 | Flag.new(:first, :second, :third).flag_map.values.should == [:first, :second, :third]
18 | end
19 |
20 | it "should create keys by the 2 power series for the @flag_map hash, staring at 1" do
21 | Flag.new(:one, :two, :three, :four, :five).flag_map.keys.should include(1, 2, 4, 8, 16)
22 | end
23 | end
24 |
25 | describe ".[]" do
26 | it "should be an alias for the new method" do
27 | Flag.should_receive(:new).with(:uno, :dos, :tres)
28 | Flag[:uno, :dos, :tres]
29 | end
30 | end
31 |
32 | describe ".dump" do
33 | before(:each) do
34 | @flag = Flag[:first, :second, :third, :fourth, :fifth]
35 | end
36 |
37 | it "should return the key of the value match from the flag map" do
38 | @flag.dump(:first).should == 1
39 | @flag.dump(:second).should == 2
40 | @flag.dump(:third).should == 4
41 | @flag.dump(:fourth).should == 8
42 | @flag.dump(:fifth).should == 16
43 | end
44 |
45 | it "should return a binary flag built from the key values of all matches" do
46 | @flag.dump(:first, :second).should == 3
47 | @flag.dump(:second, :fourth).should == 10
48 | @flag.dump(:first, :second, :third, :fourth, :fifth).should == 31
49 | end
50 |
51 | it "should return 0 if there is no match" do
52 | @flag.dump(:zero).should == 0
53 | end
54 | end
55 |
56 | describe ".load" do
57 | before(:each) do
58 | @flag = Flag[:uno, :dos, :tres, :cuatro, :cinco]
59 | end
60 |
61 | it "should return the value of the key match from the flag map" do
62 | @flag.load(1).should == [:uno]
63 | @flag.load(2).should == [:dos]
64 | @flag.load(4).should == [:tres]
65 | @flag.load(8).should == [:cuatro]
66 | @flag.load(16).should == [:cinco]
67 | end
68 |
69 | it "should return an array of all flags matches" do
70 | @flag.load(3).should include(:uno, :dos)
71 | @flag.load(10).should include(:dos, :cuatro)
72 | @flag.load(31).should include(:uno, :dos, :tres, :cuatro, :cinco)
73 | end
74 |
75 | it "should return an empty array if there is no key" do
76 | @flag.load(-1).should == []
77 | @flag.load(nil).should == []
78 | @flag.load(32).should == []
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/data_mapper/adapters/mysql_adapter.rb:
--------------------------------------------------------------------------------
1 | gem 'do_mysql', '=0.9.0'
2 | require 'do_mysql'
3 |
4 | module DataMapper
5 | module Adapters
6 |
7 | # Options:
8 | # host, user, password, database (path), socket(uri query string), port
9 | class MysqlAdapter < DataObjectsAdapter
10 |
11 | def begin_transaction(transaction)
12 | cmd = "XA START '#{transaction_id(transaction)}'"
13 | transaction.connection_for(self).create_command(cmd).execute_non_query
14 | DataMapper.logger.debug("#{self}: #{cmd}")
15 | end
16 |
17 | def transaction_id(transaction)
18 | "#{transaction.id}:#{self.object_id}"
19 | end
20 |
21 | def commit_transaction(transaction)
22 | cmd = "XA COMMIT '#{transaction_id(transaction)}'"
23 | transaction.connection_for(self).create_command(cmd).execute_non_query
24 | DataMapper.logger.debug("#{self}: #{cmd}")
25 | end
26 |
27 | def finalize_transaction(transaction)
28 | cmd = "XA END '#{transaction_id(transaction)}'"
29 | transaction.connection_for(self).create_command(cmd).execute_non_query
30 | DataMapper.logger.debug("#{self}: #{cmd}")
31 | end
32 |
33 | def prepare_transaction(transaction)
34 | finalize_transaction(transaction)
35 | cmd = "XA PREPARE '#{transaction_id(transaction)}'"
36 | transaction.connection_for(self).create_command(cmd).execute_non_query
37 | DataMapper.logger.debug("#{self}: #{cmd}")
38 | end
39 |
40 | def rollback_transaction(transaction)
41 | finalize_transaction(transaction)
42 | cmd = "XA ROLLBACK '#{transaction_id(transaction)}'"
43 | transaction.connection_for(self).create_command(cmd).execute_non_query
44 | DataMapper.logger.debug("#{self}: #{cmd}")
45 | end
46 |
47 | def rollback_prepared_transaction(transaction)
48 | cmd = "XA ROLLBACK '#{transaction_id(transaction)}'"
49 | transaction.connection.create_command(cmd).execute_non_query
50 | DataMapper.logger.debug("#{self}: #{cmd}")
51 | end
52 |
53 | private
54 |
55 | def quote_table_name(table_name)
56 | "`#{table_name}`"
57 | end
58 |
59 | def quote_column_name(column_name)
60 | "`#{column_name}`"
61 | end
62 |
63 | def rewrite_uri(uri, options)
64 | new_uri = uri.dup
65 | new_uri.host = options[:host] || uri.host
66 | new_uri.user = options[:user] || uri.user
67 | new_uri.password = options[:password] || uri.password
68 | new_uri.path = (options[:database] && "/" << options[:database]) || uri.path
69 | new_uri.port = options[:port] || uri.port
70 | new_uri.query = (options[:socket] && "socket=#{options[:socket]}") || uri.query
71 |
72 | new_uri
73 | end
74 |
75 | end # class MysqlAdapter
76 | end # module Adapters
77 | end # module DataMapper
78 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gem 'rspec', '>=1.1.3'
3 | require 'pathname'
4 | require 'spec'
5 | require 'fileutils'
6 |
7 | require Pathname(__FILE__).dirname.expand_path.parent + 'lib/data_mapper'
8 | require DataMapper.root / 'spec' / 'lib' / 'mock_adapter'
9 |
10 | gem 'rspec', '>=1.1.3'
11 |
12 | INTEGRATION_DB_PATH = DataMapper.root / 'spec' / 'integration' / 'integration_test.db'
13 |
14 | FileUtils.touch INTEGRATION_DB_PATH unless INTEGRATION_DB_PATH.exist?
15 |
16 | DataMapper.setup(:default, 'mock://localhost')
17 |
18 | # Determine log path.
19 | ENV['_'] =~ /(\w+)/
20 | log_path = DataMapper.root / 'log' / "#{$1 == 'opt' ? 'spec' : $1}.log"
21 | log_path.dirname.mkpath
22 |
23 | DataMapper::Logger.new(log_path, 0)
24 | at_exit { DataMapper.logger.close }
25 |
26 | class Article
27 | include DataMapper::Resource
28 |
29 | property :id, Fixnum, :serial => true
30 | property :blog_id, Fixnum
31 | property :created_at, DateTime
32 | property :author, String
33 | property :title, String
34 |
35 | class << self
36 | def property_by_name(name)
37 | properties(repository.name)[name]
38 | end
39 | end
40 | end
41 |
42 | class Comment
43 | include DataMapper::Resource
44 | end
45 |
46 | class NormalClass
47 | # should not include DataMapper::Resource
48 | end
49 |
50 | # ==========================
51 | # Used for Association specs
52 | class Vehicle
53 | include DataMapper::Resource
54 |
55 | property :id, Fixnum, :serial => true
56 | property :name, String
57 | end
58 |
59 | class Manufacturer
60 | include DataMapper::Resource
61 |
62 | property :id, Fixnum, :serial => true
63 | property :name, String
64 | end
65 |
66 | class Supplier
67 | include DataMapper::Resource
68 |
69 | property :id, Fixnum, :serial => true
70 | property :name, String
71 | end
72 |
73 | class Class
74 | def publicize_methods
75 | klass = class << self; self; end
76 |
77 | saved_private_class_methods = klass.private_instance_methods
78 | saved_protected_class_methods = klass.protected_instance_methods
79 | saved_private_instance_methods = self.private_instance_methods
80 | saved_protected_instance_methods = self.protected_instance_methods
81 |
82 | self.class_eval do
83 | klass.send(:public, *saved_private_class_methods)
84 | klass.send(:public, *saved_protected_class_methods)
85 | public(*saved_private_instance_methods)
86 | public(*saved_protected_instance_methods)
87 | end
88 |
89 | begin
90 | yield
91 | ensure
92 | self.class_eval do
93 | klass.send(:private, *saved_private_class_methods)
94 | klass.send(:protected, *saved_protected_class_methods)
95 | private(*saved_private_instance_methods)
96 | protected(*saved_protected_instance_methods)
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/integration/type_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | gem 'fastercsv', '>=1.2.3'
4 | require 'fastercsv'
5 |
6 | begin
7 | gem 'do_sqlite3', '=0.9.0'
8 | require 'do_sqlite3'
9 |
10 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}")
11 |
12 | describe DataMapper::Type do
13 |
14 | before do
15 |
16 | @adapter = repository(:sqlite3).adapter
17 | @adapter.execute("CREATE TABLE coconuts (id INTEGER PRIMARY KEY, faked TEXT, document TEXT, stuff TEXT)")
18 |
19 | module TypeTests
20 | class Impostor < DataMapper::Type
21 | primitive String
22 | end
23 |
24 | class Coconut
25 | include DataMapper::Resource
26 |
27 | storage_names[:sqlite3] = 'coconuts'
28 |
29 | property :id, Fixnum, :serial => true
30 | property :faked, Impostor
31 | property :document, DM::Csv
32 | property :stuff, DM::Yaml
33 | end
34 | end
35 |
36 | @document = <<-EOS.margin
37 | NAME, RATING, CONVENIENCE
38 | Freebird's, 3, 3
39 | Whataburger, 1, 5
40 | Jimmy John's, 3, 4
41 | Mignon, 5, 2
42 | Fuzi Yao's, 5, 1
43 | Blue Goose, 5, 1
44 | EOS
45 |
46 | @stuff = YAML::dump({ 'Happy Cow!' => true, 'Sad Cow!' => false })
47 | end
48 |
49 | it "should instantiate an object with custom types" do
50 | coconut = TypeTests::Coconut.new(:faked => 'bob', :document => @document, :stuff => @stuff)
51 | coconut.faked.should == 'bob'
52 | coconut.document.should be_a_kind_of(Array)
53 | coconut.stuff.should be_a_kind_of(Hash)
54 | end
55 |
56 | it "should CRUD an object with custom types" do
57 | repository(:sqlite3) do
58 | coconut = TypeTests::Coconut.new(:faked => 'bob', :document => @document, :stuff => @stuff)
59 | coconut.save.should be_true
60 | coconut.id.should_not be_nil
61 |
62 | fred = TypeTests::Coconut[coconut.id]
63 | fred.faked.should == 'bob'
64 | fred.document.should be_a_kind_of(Array)
65 | fred.stuff.should be_a_kind_of(Hash)
66 |
67 | texadelphia = ["Texadelphia", "5", "3"]
68 |
69 | # Figure out how to track these... possibly proxies? :-p
70 | document = fred.document.dup
71 | document << texadelphia
72 | fred.document = document
73 |
74 | stuff = fred.stuff.dup
75 | stuff['Manic Cow!'] = :maybe
76 | fred.stuff = stuff
77 |
78 | fred.save.should be_true
79 |
80 | # Can't call coconut.reload! since coconut.loaded_set isn't setup.
81 | mac = TypeTests::Coconut[fred.id]
82 | mac.document.last.should == texadelphia
83 | mac.stuff['Manic Cow!'].should == :maybe
84 | end
85 | end
86 |
87 | after do
88 | @adapter = repository(:sqlite3).adapter
89 | @adapter.execute("DROP TABLE coconuts")
90 | end
91 | end
92 | rescue LoadError
93 | warn "integration/type_spec not run! Could not load do_sqlite3."
94 | end
95 |
--------------------------------------------------------------------------------
/spec/unit/property_set_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | class Icon
4 | include DataMapper::Resource
5 |
6 | property :id, Fixnum, :serial => true
7 | property :name, String
8 | property :width, Fixnum, :lazy => true
9 | property :height, Fixnum, :lazy => true
10 | end
11 |
12 | class Boat
13 | include DataMapper::Resource
14 | property :name, String #not lazy
15 | property :text, DataMapper::Types::Text #Lazy by default
16 | property :notes, String, :lazy => true
17 | property :a1, String, :lazy => [:ctx_a,:ctx_c]
18 | property :a2, String, :lazy => [:ctx_a,:ctx_b]
19 | property :a3, String, :lazy => [:ctx_a]
20 | property :b1, String, :lazy => [:ctx_b]
21 | property :b2, String, :lazy => [:ctx_b]
22 | property :b3, String, :lazy => [:ctx_b]
23 | end
24 |
25 | describe DataMapper::PropertySet do
26 | before :each do
27 | @properties = Icon.properties(:default)
28 | end
29 |
30 | it "#slice should find properties" do
31 | @properties.slice(:name, 'width').should have(2).entries
32 | end
33 |
34 | it "#select should find properties" do
35 | @properties.select { |property| property.type == Fixnum }.should have(3).entries
36 | end
37 |
38 | it "#[] should find properties by name (Symbol or String)" do
39 | default_properties = [ :id, 'name', :width, 'height' ]
40 | @properties.each_with_index do |property,i|
41 | property.should == @properties[default_properties[i]]
42 | end
43 | end
44 |
45 | it "should provide defaults" do
46 | @properties.defaults.should have(2).entries
47 | @properties.should have(4).entries
48 | end
49 |
50 | it 'should add a property for lazy loading to the :default context if a context is not supplied' do
51 | Boat.properties(:default).lazy_context(:default).length.should == 2 # text & notes
52 | end
53 |
54 | it 'should return a list of contexts that a given field is in' do
55 | props = Boat.properties(:default)
56 | set = props.property_contexts(:a1)
57 | set.include?(:ctx_a).should == true
58 | set.include?(:ctx_c).should == true
59 | set.include?(:ctx_b).should == false
60 | end
61 |
62 | it 'should return a list of expanded fields that should be loaded with a given field' do
63 | props = Boat.properties(:default)
64 | set = props.lazy_load_context(:a2)
65 | expect = [:a1,:a2,:a3,:b1,:b2,:b3]
66 | expect.should == set.sort! {|a,b| a.to_s <=> b.to_s}
67 | end
68 |
69 | describe 'when dup\'ed' do
70 | it 'should duplicate the @entries ivar' do
71 | @properties.dup.entries.should_not equal(@properties.entries)
72 | end
73 |
74 | it 'should reinitialize @properties_for' do
75 | # force @properties_for to hold a property
76 | Icon.properties(:default)[:name].should_not be_nil
77 | @properties = Icon.properties(:default)
78 |
79 | @properties.instance_variable_get("@property_for").should_not be_empty
80 | @properties.dup.instance_variable_get("@property_for").should be_empty
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/spec/unit/loaded_set_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe "DataMapper::LoadedSet" do
4 |
5 | before :all do
6 | DataMapper.setup(:default, "mock://localhost/mock") unless DataMapper::Repository.adapters[:default]
7 | DataMapper.setup(:other, "mock://localhost/mock") unless DataMapper::Repository.adapters[:other]
8 |
9 | @cow = Class.new do
10 | include DataMapper::Resource
11 |
12 | property :name, String, :key => true
13 | property :age, Fixnum
14 | end
15 | end
16 |
17 | it "should return the right repository" do
18 | klass = Class.new do
19 | include DataMapper::Resource
20 | end
21 |
22 | DataMapper::LoadedSet.new(repository(:other), klass, []).repository.name.should == :other
23 | end
24 |
25 | it "should be able to add arbitrary objects" do
26 | properties = @cow.properties(:default)
27 | properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten]
28 |
29 | set = DataMapper::LoadedSet.new(DataMapper::repository(:default), @cow, properties_with_indexes)
30 | set.should respond_to(:reload)
31 |
32 | set.load(['Bob', 10])
33 | set.load(['Nancy', 11])
34 |
35 | results = set.entries
36 | results.should have(2).entries
37 |
38 | results.each do |cow|
39 | cow.instance_variables.should include('@name')
40 | cow.instance_variables.should include('@age')
41 | end
42 |
43 | bob, nancy = results[0], results[1]
44 |
45 | bob.name.should eql('Bob')
46 | bob.age.should eql(10)
47 | bob.should_not be_a_new_record
48 |
49 | nancy.name.should eql('Nancy')
50 | nancy.age.should eql(11)
51 | nancy.should_not be_a_new_record
52 |
53 | results.first.should == bob
54 | end
55 |
56 | end
57 |
58 | describe "DataMapper::LazyLoadedSet" do
59 |
60 | before :all do
61 | DataMapper.setup(:default, "mock://localhost/mock") unless DataMapper::Repository.adapters[:default]
62 |
63 | @cow = Class.new do
64 | include DataMapper::Resource
65 |
66 | property :name, String, :key => true
67 | property :age, Fixnum
68 | end
69 |
70 | properties = @cow.properties(:default)
71 | @properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten]
72 | end
73 |
74 | it "should raise an error if no block is provided" do
75 | lambda { set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) }.should raise_error
76 | end
77 |
78 | it "should make a materialization block" do
79 | set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) do |lls|
80 | lls.load(['Bob', 10])
81 | lls.load(['Nancy', 11])
82 | end
83 |
84 | results = set.entries
85 | results.size.should == 2
86 | end
87 |
88 | it "should be eachable" do
89 | set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) do |lls|
90 | lls.load(['Bob', 10])
91 | lls.load(['Nancy', 11])
92 | end
93 |
94 | set.each do |x|
95 | x.name.should be_a_kind_of(String)
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/data_mapper/associations/many_to_one.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Associations
3 | module ManyToOne
4 | def many_to_one(name, options = {})
5 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
6 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options
7 |
8 | # TOOD: raise an exception if unknown options are passed in
9 |
10 | child_model_name = DataMapper::Inflection.demodulize(self.name)
11 | parent_model_name = options[:class_name] || DataMapper::Inflection.classify(name)
12 |
13 | relationships(repository.name)[name] = Relationship.new(
14 | name,
15 | options,
16 | repository.name,
17 | child_model_name,
18 | nil,
19 | parent_model_name,
20 | nil
21 | )
22 |
23 | class_eval <<-EOS, __FILE__, __LINE__
24 | def #{name}
25 | #{name}_association.parent
26 | end
27 |
28 | def #{name}=(parent_resource)
29 | #{name}_association.parent = parent_resource
30 | end
31 |
32 | private
33 |
34 | def #{name}_association
35 | @#{name}_association ||= begin
36 | relationship = self.class.relationships(repository.name)[:#{name}]
37 |
38 | association = relationship.with_child(self, Instance) do |repository, child_key, parent_key, parent_model, child_resource|
39 | repository.all(parent_model, parent_key.to_query(child_key.get(child_resource))).first
40 | end
41 |
42 | child_associations << association
43 |
44 | association
45 | end
46 | end
47 | EOS
48 | relationships(repository.name)[name]
49 | end
50 |
51 | class Instance
52 | def parent
53 | @parent_resource ||= @parent_loader.call
54 | end
55 |
56 | def parent=(parent_resource)
57 | @parent_resource = parent_resource
58 |
59 | @relationship.attach_parent(@child_resource, @parent_resource) if @parent_resource.nil? || !@parent_resource.new_record?
60 | end
61 |
62 | def loaded?
63 | !defined?(@parent_resource)
64 | end
65 |
66 | def save
67 | if parent.new_record?
68 | repository(@relationship.repository_name).save(parent)
69 | @relationship.attach_parent(@child_resource, parent)
70 | end
71 | end
72 |
73 | private
74 |
75 | def initialize(relationship, child_resource, &parent_loader)
76 | # raise ArgumentError, "+relationship+ should be a DataMapper::Association::Relationship, but was #{relationship.class}", caller unless Relationship === relationship
77 | # raise ArgumentError, "+child_resource+ should be a DataMapper::Resource, but was #{child_resource.class}", caller unless Resource === child_resource
78 |
79 | @relationship = relationship
80 | @child_resource = child_resource
81 | @parent_loader = parent_loader
82 | end
83 | end # class Instance
84 | end # module ManyToOne
85 | end # module Associations
86 | end # module DataMapper
87 |
--------------------------------------------------------------------------------
/lib/data_mapper/repository.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | class Repository
3 | @adapters = {}
4 |
5 | def self.adapters
6 | @adapters
7 | end
8 |
9 | def self.context
10 | Thread.current[:dm_repository_contexts] ||= []
11 | end
12 |
13 | def self.default_name
14 | :default
15 | end
16 |
17 | attr_reader :name, :adapter
18 |
19 | def identity_map_get(model, key)
20 | @identity_maps[model][key]
21 | end
22 |
23 | def identity_map_set(resource)
24 | @identity_maps[resource.class][resource.key] = resource
25 | end
26 |
27 | # TODO: this should use current_scope too
28 | def get(model, key)
29 | @identity_maps[model][key] || @adapter.read(self, model, key)
30 | end
31 |
32 | def first(model, options)
33 | query = if current_scope = model.send(:current_scope)
34 | current_scope.merge(options.merge(:limit => 1))
35 | else
36 | Query.new(self, model, options.merge(:limit => 1))
37 | end
38 | @adapter.read_set(self, query).first
39 | end
40 |
41 | def all(model, options)
42 | query = if current_scope = model.send(:current_scope)
43 | current_scope.merge(options)
44 | else
45 | Query.new(self, model, options)
46 | end
47 | @adapter.read_set(self, query).entries
48 | end
49 |
50 | def save(resource)
51 | resource.child_associations.each { |a| a.save }
52 |
53 | success = if resource.new_record?
54 | if @adapter.create(self, resource)
55 | identity_map_set(resource)
56 | resource.instance_variable_set(:@new_record, false)
57 | resource.dirty_attributes.clear
58 | true
59 | else
60 | false
61 | end
62 | else
63 | if @adapter.update(self, resource)
64 | resource.dirty_attributes.clear
65 | true
66 | else
67 | false
68 | end
69 | end
70 |
71 | resource.parent_associations.each { |a| a.save }
72 | success
73 | end
74 |
75 | def destroy(resource)
76 | if @adapter.delete(self, resource)
77 | @identity_maps[resource.class].delete(resource.key)
78 | resource.instance_variable_set(:@new_record, true)
79 | resource.dirty_attributes.clear
80 | resource.class.properties(name).each do |property|
81 | resource.dirty_attributes << property if resource.attribute_loaded?(property.name)
82 | end
83 | true
84 | else
85 | false
86 | end
87 | end
88 |
89 | #
90 | # Produce a new Transaction for this Repository.
91 | #
92 | # ==== Returns
93 | # DataMapper::Adapters::Transaction:: A new Transaction (in state :none) that can be used to execute
94 | # code #with_transaction.
95 | #
96 | def transaction
97 | DataMapper::Adapters::Transaction.new(self)
98 | end
99 |
100 | def to_s
101 | "#"
102 | end
103 |
104 | private
105 |
106 | def initialize(name)
107 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
108 |
109 | @name = name
110 | @adapter = self.class.adapters[name]
111 | @identity_maps = Hash.new { |h,model| h[model] = IdentityMap.new }
112 | end
113 |
114 | end # class Repository
115 | end # module DataMapper
116 |
--------------------------------------------------------------------------------
/lib/data_mapper.rb:
--------------------------------------------------------------------------------
1 | # This file begins the loading sequence.
2 | #
3 | # Quick Overview:
4 | # * Requires set, fastthread, support libs, and base
5 | # * Sets the applications root and environment for compatibility with rails or merb
6 | # * Checks for the database.yml and loads it if it exists
7 | # * Sets up the database using the config from the yaml file or from the environment
8 | #
9 |
10 | # Require the basics...
11 | require 'date'
12 | require 'pathname'
13 | require 'rubygems'
14 | require 'set'
15 | require 'time'
16 | require 'uri'
17 | require 'yaml'
18 |
19 | begin
20 | require 'fastthread'
21 | rescue LoadError
22 | end
23 |
24 | # for Pathname /
25 | require File.expand_path(File.join(File.dirname(__FILE__), 'data_mapper', 'support', 'pathname'))
26 |
27 | dir = Pathname(__FILE__).dirname.expand_path / 'data_mapper'
28 |
29 | require dir / 'associations'
30 | require dir / 'hook'
31 | require dir / 'identity_map'
32 | require dir / 'loaded_set'
33 | require dir / 'logger'
34 | require dir / 'naming_conventions'
35 | require dir / 'property_set'
36 | require dir / 'query'
37 | require dir / 'repository'
38 | require dir / 'resource'
39 | require dir / 'scope'
40 | require dir / 'support'
41 | require dir / 'type'
42 | require dir / 'types'
43 | require dir / 'property'
44 | require dir / 'adapters'
45 |
46 | module DataMapper
47 | def self.root
48 | @root ||= Pathname(__FILE__).dirname.parent.expand_path
49 | end
50 |
51 | def self.setup(name, uri_or_options)
52 | raise ArgumentError, "+name+ must be a Symbol, but was #{name.class}", caller unless Symbol === name
53 |
54 | case uri_or_options
55 | when Hash
56 | adapter_name = uri_or_options[:adapter]
57 | when String, URI
58 | uri_or_options = URI.parse(uri_or_options) if String === uri_or_options
59 | adapter_name = uri_or_options.scheme
60 | else
61 | raise ArgumentError, "+uri_or_options+ must be a Hash, URI or String, but was #{uri_or_options.class}", caller
62 | end
63 |
64 | # TODO: use autoload to load the adapter on-the-fly when used
65 | class_name = DataMapper::Inflection.classify(adapter_name) + 'Adapter'
66 |
67 | unless Adapters::const_defined?(class_name)
68 | lib_name = "#{DataMapper::Inflection.underscore(adapter_name)}_adapter"
69 | begin
70 | require root / 'lib' / 'data_mapper' / 'adapters' / lib_name
71 | rescue LoadError
72 | require lib_name
73 | end
74 | end
75 |
76 | Repository.adapters[name] = Adapters::const_get(class_name).new(name, uri_or_options)
77 | end
78 |
79 | # ===Block Syntax:
80 | # Pushes the named repository onto the context-stack,
81 | # yields a new session, and pops the context-stack.
82 | #
83 | # results = DataMapper.repository(:second_database) do |current_context|
84 | # ...
85 | # end
86 | #
87 | # ===Non-Block Syntax:
88 | # Returns the current session, or if there is none,
89 | # a new Session.
90 | #
91 | # current_repository = DataMapper.repository
92 | def self.repository(name = nil) # :yields: current_context
93 | # TODO return context.last if last.name == name (arg)
94 | current_repository = if name
95 | Repository.new(name)
96 | else
97 | Repository.context.last || Repository.new(Repository.default_name)
98 | end
99 |
100 | return current_repository unless block_given?
101 |
102 | Repository.context << current_repository
103 |
104 | begin
105 | return yield(current_repository)
106 | ensure
107 | Repository.context.pop
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/spec/unit/associations/one_to_many_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe "DataMapper::Associations::OneToMany" do
4 |
5 | before do
6 | @class = Class.new do
7 | def self.name
8 | "Hannibal"
9 | end
10 |
11 | include DataMapper::Resource
12 |
13 | property :id, Fixnum
14 |
15 | send :one_to_many, :vehicles
16 | end
17 |
18 | @relationship = mock("relationship")
19 | end
20 |
21 | it "should install the association's methods" do
22 | victim = @class.new
23 |
24 | victim.should respond_to(:vehicles)
25 | end
26 |
27 | it "should work with classes inside modules"
28 |
29 | describe DataMapper::Associations::OneToMany::Proxy do
30 | describe "when loading" do
31 | def init
32 | DataMapper::Associations::OneToMany::Proxy.new(@relationship, nil) do |one, two|
33 | @tester.weee
34 | end
35 | end
36 |
37 | before do
38 | @tester = mock("tester")
39 | end
40 |
41 | it "should not load on initialize" do
42 | @tester.should_not_receive(:weee)
43 | init
44 | end
45 |
46 | it "should load when accessed" do
47 | @relationship.should_receive(:repository_name).and_return(:a_symbol)
48 | @tester.should_receive(:weee).and_return([])
49 | a = init
50 | a.entries
51 | end
52 | end
53 |
54 | describe "when adding an element" do
55 | before do
56 | @parent = mock("parent")
57 | @element = mock("element", :null_object => true)
58 | @association = DataMapper::Associations::OneToMany::Proxy.new(@relationship, @parent) do
59 | []
60 | end
61 | end
62 |
63 | describe "with a persisted parent" do
64 | it "should save the element" do
65 | @relationship.should_receive(:repository_name).and_return(:a_symbol)
66 | @parent.should_receive(:new_record?).and_return(false)
67 | @association.should_receive(:save_child).with(@element)
68 |
69 | @association << @element
70 |
71 | @association.instance_variable_get("@dirty_children").should be_empty
72 | end
73 | end
74 |
75 | describe "with a non-persisted parent" do
76 | it "should not save the element" do
77 | @relationship.should_receive(:repository_name).and_return(:a_symbol)
78 | @parent.should_receive(:new_record?).and_return(true)
79 | @association.should_not_receive(:save_child)
80 |
81 | @association << @element
82 |
83 | @association.instance_variable_get("@dirty_children").should_not be_empty
84 | end
85 |
86 | it "should save the element after the parent is saved" do
87 |
88 | end
89 |
90 | it "should add the parent's keys to the element after the parent is saved"
91 | end
92 | end
93 |
94 | describe "when deleting an element" do
95 | it "should delete the element from the database" do
96 |
97 | end
98 |
99 | it "should delete the element from the association"
100 |
101 | it "should erase the ex-parent's keys from the element"
102 | end
103 |
104 | describe "when deleting the parent" do
105 |
106 | end
107 |
108 |
109 | describe "with an unsaved parent" do
110 | describe "when deleting an element from an unsaved parent" do
111 | it "should remove the element from the association" do
112 |
113 | end
114 | end
115 | end
116 | end
117 |
118 | describe "when changing an element's parent" do
119 |
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/spec/unit/type_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe DataMapper::Type do
4 |
5 | before(:each) do
6 | class TestType < DataMapper::Type
7 | primitive String
8 | size 10
9 | end
10 |
11 | class TestType2 < DataMapper::Type
12 | primitive String
13 | size 10
14 |
15 | def self.load(value, property)
16 | value.reverse
17 | end
18 |
19 | def self.dump(value, property)
20 | value.reverse
21 | end
22 | end
23 |
24 | class TestResource
25 | include DataMapper::Resource
26 | end
27 |
28 | class TestType3 < DataMapper::Type
29 | primitive String
30 | size 10
31 | attr_accessor :property, :value
32 |
33 | def self.load(value, property)
34 | type = self.new
35 | type.property = property
36 | type.value = value
37 | type
38 | end
39 |
40 | def self.dump(value, property)
41 | value.value
42 | end
43 | end
44 | end
45 |
46 | it "should have the same PROPERTY_OPTIONS aray as DataMapper::Property" do
47 | # pending("currently there is no way to read PROPERTY_OPTIONS and aliases from DataMapper::Property. Also, some properties need to be defined as aliases instead of being listed in the PROPERTY_OPTIONS array")
48 | DataMapper::Type::PROPERTY_OPTIONS.should == DataMapper::Property::PROPERTY_OPTIONS
49 | end
50 |
51 | it "should create a new type based on String primitive" do
52 | TestType.primitive.should == String
53 | end
54 |
55 | it "should have size of 10" do
56 | TestType.size.should == 10
57 | end
58 |
59 | it "should have options hash exactly equal to options specified in custom type" do
60 | #ie. it should not include null elements
61 | TestType.options.should == { :size => 10, :length => 10 }
62 | end
63 |
64 | it "should have length aliased to size" do
65 | TestType.length.should == TestType.size
66 | end
67 |
68 | it "should pass through the value if load wasn't overriden" do
69 | TestType.load("test", nil).should == "test"
70 | end
71 |
72 | it "should pass through the value if dump wasn't overriden" do
73 | TestType.dump("test", nil).should == "test"
74 | end
75 |
76 | it "should not raise NotImplmenetedException if load was overriden" do
77 | TestType2.dump("helo", nil).should == "oleh"
78 | end
79 |
80 | it "should not raise NotImplmenetedException if dump was overriden" do
81 | TestType2.load("oleh", nil).should == "helo"
82 | end
83 |
84 | describe "using a custom type" do
85 | before do
86 | @property = DataMapper::Property.new TestResource, :name, TestType3, {}
87 | end
88 |
89 | it "should return a object of the same type" do
90 | TestType3.load("helo", @property).class.should == TestType3
91 | end
92 |
93 | it "should contain the property" do
94 | TestType3.load("helo", @property).property.should == @property
95 | end
96 |
97 | it "should contain the value" do
98 | TestType3.load("helo", @property).value.should == "helo"
99 | end
100 |
101 | it "should return the value" do
102 | obj = TestType3.load("helo", @property)
103 | TestType3.dump(obj, @property).should == "helo"
104 | end
105 | end
106 |
107 | describe "using def Type" do
108 | before do
109 | @class = Class.new(DataMapper::Type(String, :size => 20))
110 | end
111 |
112 | it "should be of the specified type" do
113 | @class.primitive.should == String
114 | end
115 |
116 | it "should have the right options set" do
117 | @class.size.should == 20
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/lib/data_mapper/property_set.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | class PropertySet
3 | include Enumerable
4 |
5 | def [](name)
6 | @property_for[name]
7 | end
8 |
9 | alias has_property? []
10 |
11 | def slice(*names)
12 | @property_for.values_at(*names)
13 | end
14 |
15 | def add(*properties)
16 | @entries.push(*properties)
17 | self
18 | end
19 |
20 | alias << add
21 |
22 | def length
23 | @entries.length
24 | end
25 |
26 | def empty?
27 | @entries.empty?
28 | end
29 |
30 | def each
31 | @entries.each { |property| yield property }
32 | self
33 | end
34 |
35 | def defaults
36 | @defaults ||= reject { |property| property.lazy? }
37 | end
38 |
39 | def key
40 | @key ||= select { |property| property.key? }
41 | end
42 |
43 | def inheritance_property
44 | @inheritance_property ||= detect { |property| property.type == Class }
45 | end
46 |
47 | def get(resource)
48 | map { |property| property.get(resource) }
49 | end
50 |
51 | def set(resource, values)
52 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource
53 | if Array === values
54 | raise ArgumentError, "+values+ must have a length of #{length}, but has #{values.length}", caller if values.length != length
55 | elsif !values.nil?
56 | raise ArgumentError, "+values+ must be nil or an Array, but was a #{values.class}", caller
57 | end
58 |
59 | each_with_index { |property,i| property.set(resource, values.nil? ? nil : values[i]) }
60 | end
61 |
62 | def property_contexts(name)
63 | contexts = []
64 | lazy_contexts.each do |context,property_names|
65 | contexts << context if property_names.include?(name)
66 | end
67 | contexts
68 | end
69 |
70 | def lazy_context(name)
71 | lazy_contexts[name]
72 | end
73 |
74 | def lazy_load_context(names)
75 | if Array === names
76 | raise ArgumentError, "+names+ cannot be an empty Array", caller if names.empty?
77 | elsif !(Symbol === names)
78 | raise ArgumentError, "+names+ must be a Symbol or an Array of Symbols, but was a #{names.class}", caller
79 | end
80 |
81 | result = []
82 |
83 | Array(names).each do |name|
84 | contexts = property_contexts(name)
85 | if contexts.empty?
86 | result << name # not lazy
87 | else
88 | result |= lazy_contexts.values_at(*contexts).flatten.uniq
89 | end
90 | end
91 | result
92 | end
93 |
94 | def to_query(values)
95 | Hash[ *zip(values).flatten ]
96 | end
97 |
98 | def inspect
99 | '#'
100 | end
101 |
102 | private
103 |
104 | def initialize(properties = [])
105 | raise ArgumentError, "+properties+ should be an Array, but was #{properties.class}", caller unless Array === properties
106 |
107 | @entries = properties
108 | @property_for = hash_for_property_for
109 | end
110 |
111 | def initialize_copy(orig)
112 | @entries = orig.entries.dup
113 | @property_for = hash_for_property_for
114 | end
115 |
116 | def hash_for_property_for
117 | Hash.new do |h,k|
118 | raise "Key must be a Symbol or String, but was #{k.class}" unless [String, Symbol].include?(k.class)
119 |
120 | ksym = k.to_sym
121 | if property = detect { |property| property.name == ksym }
122 | h[ksym] = h[k.to_s] = property
123 | end
124 | end
125 | end
126 |
127 | def lazy_contexts
128 | @lazy_contexts ||= Hash.new { |h,context| h[context] = [] }
129 | end
130 | end # class PropertySet
131 | end # module DataMapper
132 |
--------------------------------------------------------------------------------
/spec/unit/identity_map_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe "DataMapper::IdentityMap" do
4 | before(:all) do
5 | class Cow
6 | include DataMapper::Resource
7 | property :id, Fixnum, :key => true
8 | property :name, String
9 | end
10 |
11 | class Chicken
12 | include DataMapper::Resource
13 | property :name, String
14 | end
15 |
16 | class Pig
17 | include DataMapper::Resource
18 | property :id, Fixnum, :key => true
19 | property :composite, Fixnum, :key => true
20 | property :name, String
21 | end
22 | end
23 |
24 | it "should use a second level cache if created with on"
25 |
26 | it "should return nil on #get when it does not find the requested instance" do
27 | map = DataMapper::IdentityMap.new
28 | map.get([23]).should be_nil
29 | end
30 |
31 | it "should return an instance on #get when it finds the requested instance" do
32 | betsy = Cow.new({:id=>23,:name=>'Betsy'})
33 | map = DataMapper::IdentityMap.new
34 | map.set(betsy.key, betsy)
35 | map.get([23]).should == betsy
36 | end
37 |
38 | it "should store an instance on #set" do
39 | betsy = Cow.new({:id=>23,:name=>'Betsy'})
40 | map = DataMapper::IdentityMap.new
41 | map.set(betsy.key, betsy)
42 | map.get([23]).should == betsy
43 | end
44 |
45 | it "should store instances with composite keys on #set" do
46 | pig = Pig.new({:id=>1,:composite=>1,:name=> 'Pig'})
47 | piggy = Pig.new({:id=>1,:composite=>2,:name=>'Piggy'})
48 |
49 | map = DataMapper::IdentityMap.new
50 | map.set(pig.key, pig)
51 | map.set(piggy.key, piggy)
52 |
53 | map.get([1,1]).should == pig
54 | map.get([1,2]).should == piggy
55 | end
56 |
57 | it "should remove an instance on #delete" do
58 | betsy = Cow.new({:id=>23,:name=>'Betsy'})
59 | map = DataMapper::IdentityMap.new
60 | map.set(betsy.key, betsy)
61 | map.delete([23])
62 | map.get([23]).should be_nil
63 | end
64 | end
65 |
66 | describe "Second Level Caching" do
67 |
68 | before :all do
69 | @mock_class = Class.new do
70 | def get(key); raise NotImplementedError end
71 | def set(key, instance); raise NotImplementedError end
72 | def delete(key); raise NotImplementedError end
73 | end
74 | end
75 |
76 | it 'should expose a standard API' do
77 | cache = @mock_class.new
78 | cache.should respond_to(:get)
79 | cache.should respond_to(:set)
80 | cache.should respond_to(:delete)
81 | end
82 |
83 | it 'should provide values when the first level cache entry is empty' do
84 | cache = @mock_class.new
85 | key = %w[ test ]
86 |
87 | cache.should_receive(:get).with(key).once.and_return('resource')
88 |
89 | map = DataMapper::IdentityMap.new(cache)
90 | map.get(key).should == 'resource'
91 | end
92 |
93 | it 'should be set when the first level cache entry is set' do
94 | cache = @mock_class.new
95 | betsy = Cow.new(:id => 23, :name => 'Betsy')
96 |
97 | cache.should_receive(:set).with(betsy.key, betsy).once.and_return(betsy)
98 |
99 | map = DataMapper::IdentityMap.new(cache)
100 | map.set(betsy.key, betsy).should == betsy
101 | end
102 |
103 | it 'should be deleted when the first level cache entry is deleted' do
104 | cache = @mock_class.new
105 | betsy = Cow.new(:id => 23, :name => 'Betsy')
106 |
107 | cache.stub!(:set)
108 | cache.should_receive(:delete).with(betsy.key).once.and_return(betsy)
109 |
110 | map = DataMapper::IdentityMap.new(cache)
111 | map.set(betsy.key, betsy).should == betsy
112 | map.delete(betsy.key).should == betsy
113 | end
114 |
115 | it 'should not provide values when the first level cache entry is full' do
116 | cache = @mock_class.new
117 | betsy = Cow.new(:id => 23, :name => 'Betsy')
118 |
119 | cache.stub!(:set)
120 | cache.should_not_receive(:get)
121 |
122 | map = DataMapper::IdentityMap.new(cache)
123 | map.set(betsy.key, betsy).should == betsy
124 | map.get(betsy.key).should == betsy
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/lib/data_mapper/associations/one_to_many.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module DataMapper
4 | module Associations
5 | module OneToMany
6 | private
7 |
8 | def one_to_many(name, options = {})
9 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
10 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options
11 |
12 | # TOOD: raise an exception if unknown options are passed in
13 |
14 | child_model_name = options[:class_name] || DataMapper::Inflection.classify(name)
15 |
16 | relationships(repository.name)[name] = Relationship.new(
17 | DataMapper::Inflection.underscore(self.name).to_sym,
18 | options,
19 | repository.name,
20 | child_model_name,
21 | nil,
22 | self.name,
23 | nil
24 | )
25 |
26 | class_eval <<-EOS, __FILE__, __LINE__
27 | def #{name}
28 | #{name}_association
29 | end
30 |
31 | private
32 |
33 | def #{name}_association
34 | @#{name}_association ||= begin
35 | relationship = self.class.relationships(repository.name)[:#{name}]
36 |
37 | association = Proxy.new(relationship, self) do |repository, relationship|
38 | repository.all(*relationship.to_child_query(self))
39 | end
40 |
41 | parent_associations << association
42 |
43 | association
44 | end
45 | end
46 | EOS
47 |
48 | relationships(repository.name)[name]
49 | end
50 |
51 | class Proxy
52 | extend Forwardable
53 | include Enumerable
54 |
55 | def_instance_delegators :entries, :[], :size, :length, :first, :last
56 |
57 | def loaded?
58 | !defined?(@children_resources)
59 | end
60 |
61 | def clear
62 | each { |child_resource| delete(child_resource) }
63 | end
64 |
65 | def each(&block)
66 | children.each(&block)
67 | self
68 | end
69 |
70 | def children
71 | @children_resources ||= @children_loader.call(repository(@relationship.repository_name), @relationship)
72 | end
73 |
74 | def save
75 | @dirty_children.each do |child_resource|
76 | save_child(child_resource)
77 | end
78 | end
79 |
80 | def push(*child_resources)
81 | child_resources.each do |child_resource|
82 | if @parent_resource.new_record?
83 | @dirty_children << child_resource
84 | else
85 | save_child(child_resource)
86 | end
87 |
88 | children << child_resource
89 | end
90 |
91 | self
92 | end
93 |
94 | alias << push
95 |
96 | def delete(child_resource)
97 | deleted_resource = children.delete(child_resource)
98 | begin
99 | @relationship.attach_parent(deleted_resource, nil)
100 | repository(@relationship.repository_name).save(deleted_resource)
101 | rescue
102 | children << child_resource
103 | raise
104 | end
105 | end
106 |
107 | private
108 |
109 | def initialize(relationship, parent_resource, &children_loader)
110 | # raise ArgumentError, "+relationship+ should be a DataMapper::Association::Relationship, but was #{relationship.class}", caller unless Relationship === relationship
111 | # raise ArgumentError, "+parent_resource+ should be a DataMapper::Resource, but was #{parent_resource.class}", caller unless Resource === parent_resource
112 |
113 | @relationship = relationship
114 | @parent_resource = parent_resource
115 | @children_loader = children_loader
116 | @dirty_children = []
117 | end
118 |
119 | def save_child(child_resource)
120 | @relationship.attach_parent(child_resource, @parent_resource)
121 | repository(@relationship.repository_name).save(child_resource)
122 | end
123 | end # class Proxy
124 | end # module OneToMany
125 | end # module Associations
126 | end # module DataMapper
127 |
--------------------------------------------------------------------------------
/lib/data_mapper/associations/relationship.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 | module Associations
3 | class Relationship
4 |
5 | attr_reader :name, :repository_name, :options
6 |
7 | def child_key
8 | @child_key ||= begin
9 | model_properties = child_model.properties(@repository_name)
10 |
11 | child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
12 | # TODO: use something similar to DM::NamingConventions to determine the property name
13 | property_name ||= "#{@name}_#{parent_property.name}".to_sym
14 | model_properties[property_name] || child_model.property(property_name, parent_property.type)
15 | end
16 |
17 | PropertySet.new(child_key)
18 | end
19 | end
20 |
21 | def parent_key
22 | @parent_key ||= begin
23 | model_properties = parent_model.properties(@repository_name)
24 |
25 | parent_key = if @parent_properties
26 | model_properties.slice(*@parent_properties)
27 | else
28 | model_properties.key
29 | end
30 |
31 | PropertySet.new(parent_key)
32 | end
33 | end
34 |
35 |
36 | def to_child_query(parent)
37 | [child_model, child_key.to_query(parent_key.get(parent))]
38 | end
39 |
40 | def with_child(child_resource, association, &loader)
41 | association.new(self, child_resource) do
42 | yield repository(@repository_name), child_key, parent_key, parent_model, child_resource
43 | end
44 | end
45 |
46 | def with_parent(parent_resource, association, &loader)
47 | association.new(self, parent_resource) do
48 | yield repository(@repository_name), child_key, parent_key, child_model, parent_resource
49 | end
50 | end
51 |
52 | def attach_parent(child, parent)
53 | child_key.set(child, parent && parent_key.get(parent))
54 | end
55 |
56 | def parent_model
57 | @parent_model_name.to_class
58 | end
59 |
60 | def child_model
61 | @child_model_name.to_class
62 | end
63 |
64 | private
65 |
66 | # +child_model_name and child_properties refers to the FK, parent_model_name
67 | # and parent_properties refer to the PK. For more information:
68 | # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
69 | # I wash my hands of it!
70 |
71 | # FIXME: should we replace child_* and parent_* arguments with two
72 | # Arrays of Property objects? This would allow syntax like:
73 | #
74 | # belongs_to = DataMapper::Associations::Relationship.new(
75 | # :manufacturer,
76 | # :relationship_spec,
77 | # Vehicle.properties.slice(:manufacturer_id)
78 | # Manufacturer.properties.slice(:id)
79 | # )
80 | def initialize(name,options, repository_name, child_model_name, child_properties, parent_model_name, parent_properties, &loader)
81 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name
82 | raise ArgumentError, "+repository_name+ must be a Symbol, but was #{repository_name.class}", caller unless Symbol === repository_name
83 | raise ArgumentError, "+child_model_name+ must be a String, but was #{child_model_name.class}", caller unless String === child_model_name
84 | raise ArgumentError, "+child_properties+ must be an Array or nil, but was #{child_properties.class}", caller unless Array === child_properties || child_properties.nil?
85 | raise ArgumentError, "+parent_model_name+ must be a String, but was #{parent_model_name.class}", caller unless String === parent_model_name
86 | raise ArgumentError, "+parent_properties+ must be an Array or nil, but was #{parent_properties.class}", caller unless Array === parent_properties || parent_properties.nil?
87 |
88 | @name = name
89 | @options = options
90 | @repository_name = repository_name
91 | @child_model_name = child_model_name
92 | @child_properties = child_properties # may be nil
93 | @parent_model_name = parent_model_name
94 | @parent_properties = parent_properties # may be nil
95 | @loader = loader
96 | end
97 | end # class Relationship
98 | end # module Associations
99 | end # module DataMapper
100 |
--------------------------------------------------------------------------------
/lib/data_mapper/loaded_set.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module DataMapper
4 | class LoadedSet
5 | extend Forwardable
6 | include Enumerable
7 |
8 | def_instance_delegators :entries, :[], :size, :length, :first, :last
9 |
10 | attr_reader :repository
11 |
12 | def reload(options = {})
13 | query = Query.new(@repository, @model, keys.merge(:fields => @key_properties))
14 | query.update(options.merge(:reload => true))
15 | @repository.adapter.read_set(@repository, query)
16 | end
17 |
18 | def load(values, reload = false)
19 | model = if @inheritance_property_index
20 | values.at(@inheritance_property_index)
21 | else
22 | @model
23 | end
24 |
25 | resource = nil
26 |
27 | if @key_property_indexes
28 | key_values = values.values_at(*@key_property_indexes)
29 |
30 | if resource = @repository.identity_map_get(model, key_values)
31 | self << resource
32 | return resource unless reload
33 | else
34 | resource = model.allocate
35 | self << resource
36 | @key_properties.zip(key_values).each do |property,key_value|
37 | resource.instance_variable_set(property.instance_variable_name, key_value)
38 | end
39 | resource.instance_variable_set(:@new_record, false)
40 | @repository.identity_map_set(resource)
41 | end
42 | else
43 | resource = model.allocate
44 | self << resource
45 | resource.instance_variable_set(:@new_record, false)
46 | resource.readonly!
47 | end
48 |
49 | @properties_with_indexes.each_pair do |property, i|
50 | resource.instance_variable_set(property.instance_variable_name, values.at(i))
51 | end
52 |
53 | self
54 | end
55 |
56 | def add(resource)
57 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource
58 | @resources << resource
59 | resource.loaded_set = self
60 | end
61 |
62 | alias << add
63 |
64 | def merge(*resources)
65 | resources.each { |resource| add(resource) }
66 | self
67 | end
68 |
69 | def delete(resource)
70 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource
71 | @resources.delete(resource)
72 | end
73 |
74 | def entries
75 | @resources.dup
76 | end
77 |
78 | def each(&block)
79 | entries.each { |entry| yield entry }
80 | self
81 | end
82 |
83 | private
84 |
85 | # +properties_with_indexes+ is a Hash of Property and values Array index pairs.
86 | # { Property<:id> => 1, Property<:name> => 2, Property<:notes> => 3 }
87 | def initialize(repository, model, properties_with_indexes)
88 | raise ArgumentError, "+repository+ must be a DataMapper::Repository, but was #{repository.class}", caller unless Repository === repository
89 | raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource", caller unless Resource > model
90 |
91 | @repository = repository
92 | @model = model
93 | @properties_with_indexes = properties_with_indexes
94 | @resources = []
95 |
96 | if inheritance_property = @model.inheritance_property(@repository.name)
97 | @inheritance_property_index = @properties_with_indexes[inheritance_property]
98 | end
99 |
100 | if (@key_properties = @model.key(@repository.name)).all? { |key| @properties_with_indexes.include?(key) }
101 | @key_property_indexes = @properties_with_indexes.values_at(*@key_properties)
102 | end
103 | end
104 |
105 | def keys
106 | entry_keys = @resources.map { |resource| resource.key }
107 |
108 | keys = {}
109 | @key_properties.zip(entry_keys.transpose).each do |property,values|
110 | keys[property] = values
111 | end
112 | keys
113 | end
114 | end # class LoadedSet
115 |
116 | class LazyLoadedSet < LoadedSet
117 | def entries
118 | @loader[self]
119 |
120 | class << self
121 | def entries
122 | super
123 | end
124 | end
125 |
126 | super
127 | end
128 |
129 | private
130 |
131 | def initialize(*args, &block)
132 | raise "LazyLoadedSets require a materialization block. Use a LoadedSet instead." unless block_given?
133 | super
134 | @loader = block
135 | end
136 | end # class LazyLoadedSet
137 | end # module DataMapper
138 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'pathname'
4 | require 'rubygems'
5 | require 'rake'
6 | require Pathname('spec/rake/spectask')
7 | require Pathname('rake/rdoctask')
8 | require Pathname('rake/gempackagetask')
9 | require Pathname('rake/contrib/rubyforgepublisher')
10 |
11 | ROOT = Pathname(__FILE__).dirname.expand_path
12 |
13 | Pathname.glob(ROOT + 'tasks/**/*.rb') { |t| require t }
14 |
15 | task :default => 'dm:spec'
16 | task :spec => 'dm:spec'
17 |
18 | namespace :spec do
19 | task :unit => 'dm:spec:unit'
20 | task :integration => 'dm:spec:integration'
21 | end
22 |
23 | desc 'Remove all package, rdocs and spec products'
24 | task :clobber_all => %w[ clobber_package clobber_rdoc dm:clobber_spec ]
25 |
26 |
27 |
28 | namespace :dm do
29 | def run_spec(name, files, rcov = true)
30 | Spec::Rake::SpecTask.new(name) do |t|
31 | t.spec_opts << '--format' << 'specdoc' << '--colour'
32 | t.spec_opts << '--loadby' << 'random'
33 | t.spec_files = Pathname.glob(ENV['FILES'] || files)
34 | t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : rcov
35 | t.rcov_opts << '--exclude' << 'spec,environment.rb'
36 | t.rcov_opts << '--text-summary'
37 | t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
38 | t.rcov_opts << '--only-uncovered'
39 | end
40 | end
41 |
42 | desc "Run all specifications"
43 | run_spec('spec', ROOT + 'spec/**/*_spec.rb')
44 |
45 | namespace :spec do
46 | desc "Run unit specifications"
47 | run_spec('unit', ROOT + 'spec/unit/**/*_spec.rb', false)
48 |
49 | desc "Run integration specifications"
50 | run_spec('integration', ROOT + 'spec/integration/**/*_spec.rb', false)
51 | end
52 |
53 | desc "Run comparison with ActiveRecord"
54 | task :perf do
55 | load Pathname.glob(ROOT + 'script/performance.rb')
56 | end
57 |
58 | desc "Profile DataMapper"
59 | task :profile do
60 | load Pathname.glob(ROOT + 'script/profile.rb')
61 | end
62 | end
63 |
64 | PACKAGE_VERSION = '0.9.0'
65 |
66 | PACKAGE_FILES = [
67 | 'README',
68 | 'FAQ',
69 | 'QUICKLINKS',
70 | 'CHANGELOG',
71 | 'MIT-LICENSE',
72 | '*.rb',
73 | 'lib/**/*.rb',
74 | 'spec/**/*.{rb,yaml}',
75 | 'tasks/**/*',
76 | 'plugins/**/*'
77 | ].collect { |pattern| Pathname.glob(pattern) }.flatten.reject { |path| path.to_s =~ /(\/db|Makefile|\.bundle|\.log|\.o)\z/ }
78 |
79 | DOCUMENTED_FILES = PACKAGE_FILES.reject do |path|
80 | path.directory? || path.to_s.match(/(?:^spec|\/spec|\/swig\_)/)
81 | end
82 |
83 | PROJECT = "dm-core"
84 |
85 | desc 'List all package files'
86 | task :ls do
87 | puts PACKAGE_FILES
88 | end
89 |
90 | desc "Generate Documentation"
91 | rd = Rake::RDocTask.new do |rdoc|
92 | rdoc.rdoc_dir = 'doc'
93 | rdoc.title = "DataMapper -- An Object/Relational Mapper for Ruby"
94 | rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
95 | rdoc.rdoc_files.include(*DOCUMENTED_FILES.map { |file| file.to_s })
96 | end
97 |
98 | gem_spec = Gem::Specification.new do |s|
99 | s.platform = Gem::Platform::RUBY
100 | s.name = PROJECT
101 | s.summary = "An Object/Relational Mapper for Ruby"
102 | s.description = "Faster, Better, Simpler."
103 | s.version = PACKAGE_VERSION
104 |
105 | s.authors = "Sam Smoot"
106 | s.email = "ssmoot@gmail.com"
107 | s.rubyforge_project = PROJECT
108 | s.homepage = "http://datamapper.org"
109 |
110 | s.files = PACKAGE_FILES.map { |f| f.to_s }
111 |
112 | s.require_path = "lib"
113 | s.requirements << "none"
114 | s.add_dependency("data_objects", ">=0.9.0")
115 | s.add_dependency("english", ">=0.2.0")
116 | s.add_dependency("rspec", ">=1.1.3")
117 |
118 | s.has_rdoc = true
119 | s.rdoc_options << "--line-numbers" << "--inline-source" << "--main" << "README"
120 | s.extra_rdoc_files = DOCUMENTED_FILES.map { |f| f.to_s }
121 | end
122 |
123 | Rake::GemPackageTask.new(gem_spec) do |p|
124 | p.gem_spec = gem_spec
125 | p.need_tar = true
126 | p.need_zip = true
127 | end
128 |
129 | desc "Publish to RubyForge"
130 | task :rubyforge => [ :rdoc, :gem ] do
131 | Rake::SshDirPublisher.new("#{ENV['RUBYFORGE_USER']}@rubyforge.org", "/var/www/gforge-projects/#{PROJECT}", 'doc').upload
132 | end
133 |
134 | desc "Install #{PROJECT}"
135 | task :install => :package do
136 | sh %{sudo gem install pkg/#{PROJECT}-#{PACKAGE_VERSION}}
137 | end
138 |
139 | if RUBY_PLATFORM.match(/mswin32|cygwin|mingw|bccwin/)
140 | namespace :dev do
141 | desc 'Install for development (for windows)'
142 | task :winstall => :gem do
143 | system %{gem install --no-rdoc --no-ri -l pkg/#{PROJECT}-#{PACKAGE_VERSION}.gem}
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/spec/unit/scope_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | DataMapper.setup(:mock, "mock:///mock.db")
4 |
5 | describe DataMapper::Scope do
6 | after do
7 | Article.publicize_methods do
8 | Article.scope_stack.clear # reset the stack before each spec
9 | end
10 | end
11 |
12 | describe '.with_scope' do
13 | it 'should be protected' do
14 | klass = class << Article; self; end
15 | klass.should be_protected_method_defined(:with_scope)
16 | end
17 |
18 | it 'should set the current scope for the block when given a Hash' do
19 | Article.publicize_methods do
20 | Article.with_scope :blog_id => 1 do
21 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :blog_id => 1)
22 | end
23 | end
24 | end
25 |
26 | it 'should set the current scope for the block when given a DataMapper::Query' do
27 | Article.publicize_methods do
28 | Article.with_scope query = DataMapper::Query.new(repository(:mock), Article) do
29 | Article.current_scope.should == query
30 | end
31 | end
32 | end
33 |
34 | it 'should set the current scope for an inner block, merged with the outer scope' do
35 | Article.publicize_methods do
36 | Article.with_scope :blog_id => 1 do
37 | Article.with_scope :author => 'dkubb' do
38 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :blog_id => 1, :author => 'dkubb')
39 | end
40 | end
41 | end
42 | end
43 |
44 | it 'should reset the stack on error' do
45 | Article.publicize_methods do
46 | Article.current_scope.should be_nil
47 | lambda {
48 | Article.with_scope(:blog_id => 1) { raise 'There was a problem!' }
49 | }.should raise_error(RuntimeError)
50 | Article.current_scope.should be_nil
51 | end
52 | end
53 | end
54 |
55 | describe '.with_exclusive_scope' do
56 | it 'should be protected' do
57 | klass = class << Article; self; end
58 | klass.should be_protected_method_defined(:with_exclusive_scope)
59 | end
60 |
61 | it 'should set the current scope for an inner block, ignoring the outer scope' do
62 | Article.publicize_methods do
63 | Article.with_scope :blog_id => 1 do
64 | Article.with_exclusive_scope :author => 'dkubb' do
65 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :author => 'dkubb')
66 | end
67 | end
68 | end
69 | end
70 |
71 | it 'should reset the stack on error' do
72 | Article.publicize_methods do
73 | Article.current_scope.should be_nil
74 | lambda {
75 | Article.with_exclusive_scope(:blog_id => 1) { raise 'There was a problem!' }
76 | }.should raise_error(RuntimeError)
77 | Article.current_scope.should be_nil
78 | end
79 | end
80 | end
81 |
82 | describe '.scope_stack' do
83 | it 'should be private' do
84 | klass = class << Article; self; end
85 | klass.should be_private_method_defined(:scope_stack)
86 | end
87 |
88 | it 'should provide an Array' do
89 | Article.publicize_methods do
90 | Article.scope_stack.should be_kind_of(Array)
91 | end
92 | end
93 |
94 | it 'should be the same in a thread' do
95 | Article.publicize_methods do
96 | Article.scope_stack.object_id.should == Article.scope_stack.object_id
97 | end
98 | end
99 |
100 | it 'should be different in each thread' do
101 | Article.publicize_methods do
102 | a = Thread.new { Article.scope_stack }
103 | b = Thread.new { Article.scope_stack }
104 |
105 | a.value.object_id.should_not == b.value.object_id
106 | end
107 | end
108 | end
109 |
110 | describe '.current_scope' do
111 | it 'should be private' do
112 | klass = class << Article; self; end
113 | klass.should be_private_method_defined(:current_scope)
114 | end
115 |
116 | it 'should return nil if the scope stack is empty' do
117 | Article.publicize_methods do
118 | Article.scope_stack.should be_empty
119 | Article.current_scope.should be_nil
120 | end
121 | end
122 |
123 | it 'should return the last element of the scope stack' do
124 | Article.publicize_methods do
125 | query = DataMapper::Query.new(repository(:mock), Article)
126 | Article.scope_stack << query
127 | Article.current_scope.object_id.should == query.object_id
128 | end
129 | end
130 | end
131 |
132 | # TODO: specify the behavior of finders (all, first, get, []) when scope is in effect
133 | end
134 |
--------------------------------------------------------------------------------
/spec/unit/associations/relationship_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2 |
3 | describe DataMapper::Associations::Relationship do
4 |
5 | before do
6 | @adapter = DataMapper::Repository.adapters[:relationship_spec] || DataMapper.setup(:relationship_spec, 'mock://localhost')
7 | end
8 |
9 | it "should describe an association" do
10 | belongs_to = DataMapper::Associations::Relationship.new(
11 | :manufacturer,
12 | {},
13 | :relationship_spec,
14 | 'Vehicle',
15 | [ :manufacturer_id ],
16 | 'Manufacturer',
17 | nil
18 | )
19 |
20 | belongs_to.should respond_to(:name)
21 | belongs_to.should respond_to(:repository_name)
22 | belongs_to.should respond_to(:child_key)
23 | belongs_to.should respond_to(:parent_key)
24 | end
25 |
26 | it "should map properties explicitly when an association method passes them in its options" do
27 | repository_name = :relationship_spec
28 |
29 | belongs_to = DataMapper::Associations::Relationship.new(
30 | :manufacturer,
31 | {},
32 | repository_name,
33 | 'Vehicle',
34 | [ :manufacturer_id ],
35 | 'Manufacturer',
36 | [ :id ]
37 | )
38 |
39 | belongs_to.name.should == :manufacturer
40 | belongs_to.repository_name.should == repository_name
41 |
42 | belongs_to.child_key.should be_a_kind_of(DataMapper::PropertySet)
43 | belongs_to.parent_key.should be_a_kind_of(DataMapper::PropertySet)
44 |
45 | belongs_to.child_key.to_a.should == Vehicle.properties(repository_name).slice(:manufacturer_id)
46 | belongs_to.parent_key.to_a.should == Manufacturer.properties(repository_name).key
47 | end
48 |
49 | it "should infer properties when options aren't passed" do
50 | repository_name = :relationship_spec
51 |
52 | has_many = DataMapper::Associations::Relationship.new(
53 | :models,
54 | {},
55 | repository_name,
56 | 'Vehicle',
57 | nil,
58 | 'Manufacturer',
59 | nil
60 | )
61 |
62 | has_many.name.should == :models
63 | has_many.repository_name.should == repository_name
64 |
65 | has_many.child_key.should be_a_kind_of(DataMapper::PropertySet)
66 | has_many.parent_key.should be_a_kind_of(DataMapper::PropertySet)
67 |
68 | has_many.child_key.to_a.should == Vehicle.properties(repository_name).slice(:models_id)
69 | has_many.parent_key.to_a.should == Manufacturer.properties(repository_name).key
70 | end
71 |
72 | it "should generate child properties with a safe subset of the parent options" do
73 | pending
74 | # For example, :size would be an option you'd want a generated child Property to copy,
75 | # but :serial or :key obviously not. So need to take a good look at Property::OPTIONS to
76 | # see what applies and what doesn't.
77 | end
78 |
79 | end
80 |
81 | __END__
82 | class LazyLoadedSet < LoadedSet
83 |
84 | def initialize(*args, &b)
85 | super(*args)
86 | @on_demand_loader = b
87 | end
88 |
89 | def each
90 | @on_demand_loader[self]
91 | class << self
92 | def each
93 | super
94 | end
95 | end
96 |
97 | super
98 | end
99 |
100 | end
101 |
102 | set = LazyLoadedSet.new(repository, Zoo, { Property<:id> => 1, Property<:name> => 2, Property<:notes> => 3 }) do |lls|
103 | connection = create_connection
104 | command = connection.create_command("SELECT id, name, notes FROM zoos")
105 | command.set_types([Fixnum, String, String])
106 | reader = command.execute_reader
107 |
108 | while(reader.next!)
109 | lls.load(reader.values)
110 | end
111 |
112 | reader.close
113 | connection.close
114 | end
115 |
116 | class AssociationSet
117 |
118 | def initialize(relationship)
119 | @relationship = relationship
120 | end
121 |
122 | def each
123 | # load some stuff
124 | end
125 |
126 | def <<(value)
127 | # add some stuff and track it.
128 | end
129 | end
130 |
131 | class Vehicle
132 | belongs_to :manufacturer
133 |
134 | def manufacturer
135 | manufacturer_association_set.first
136 | end
137 |
138 | def manufacturer=(value)
139 | manufacturer_association_set.set(value)
140 | end
141 |
142 | private
143 | # This is all class-evaled code defined by belongs_to:
144 | def manufacturer_association_set
145 | @manufacturer_association_set ||= AssociationSet.new(
146 | self.class.associations(repository.name)[:manufacturer]
147 | ) do |set|
148 | # This block is the part that will change between different associations.
149 |
150 | # Parent is the Array of PK properties remember.
151 | resource = set.relationship.parent.first.resource
152 |
153 | resource.all(resource.key => self.loaded_set.keys)
154 | end
155 | end
156 |
157 | end
158 |
--------------------------------------------------------------------------------
/lib/data_mapper/type.rb:
--------------------------------------------------------------------------------
1 | module DataMapper
2 |
3 | # :include:/QUICKLINKS
4 | #
5 | # = Types
6 | # Provides means of writing custom types for properties. Each type is based
7 | # on a ruby primitive and handles its own serialization and materialization,
8 | # and therefore is responsible for providing those methods.
9 | #
10 | # To see complete list of supported types, see documentation for
11 | # DataMapper::Property::TYPES
12 | #
13 | # == Defining new Types
14 | # To define a new type, subclass DataMapper::Type, pick ruby primitive, and
15 | # set the options for this type.
16 | #
17 | # class MyType < DataMapper::Type
18 | # primitive String
19 | # size 10
20 | # end
21 | #
22 | # Following this, you will be able to use MyType as a type for any given
23 | # property. If special materialization and serialization is required,
24 | # override the class methods
25 | #
26 | # class MyType < DataMapper::Type
27 | # primitive String
28 | # size 10
29 | #
30 | # def self.dump(value, property)
31 | #
32 | # end
33 | #
34 | # def self.load(value)
35 | #
36 | # end
37 | # end
38 | class Type
39 | PROPERTY_OPTIONS = [
40 | :public, :protected, :private, :accessor, :reader, :writer,
41 | :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
42 | :format, :index, :check, :ordinal, :auto_validation, :validates, :unique,
43 | :lock, :track
44 | ]
45 |
46 | PROPERTY_OPTION_ALIASES = {
47 | :size => [ :length ]
48 | }
49 |
50 | class << self
51 |
52 | def configure(primitive_type, options)
53 | @_primitive_type = primitive_type
54 | @_options = options
55 |
56 | def self.inherited(base)
57 | base.primitive @_primitive_type
58 |
59 | @_options.each do |k, v|
60 | base.send(k, v)
61 | end
62 | end
63 |
64 | self
65 | end
66 |
67 | # The Ruby primitive type to use as basis for this type. See
68 | # DataMapper::Property::TYPES for list of types.
69 | #
70 | # ==== Parameters
71 | # primitive::
72 | # The class for the primitive. If nil is passed in, it returns the
73 | # current primitive
74 | #
75 | # ==== Returns
76 | # Class:: if the param is nil, return the current primitive.
77 | #
78 | # @public
79 | def primitive(primitive = nil)
80 | return @primitive if primitive.nil?
81 |
82 | @primitive = primitive
83 | end
84 |
85 | #load DataMapper::Property options
86 | PROPERTY_OPTIONS.each do |property_option|
87 | self.class_eval <<-EOS, __FILE__, __LINE__
88 | def #{property_option}(arg = nil)
89 | return @#{property_option} if arg.nil?
90 |
91 | @#{property_option} = arg
92 | end
93 | EOS
94 | end
95 |
96 | #create property aliases
97 | PROPERTY_OPTION_ALIASES.each do |property_option, aliases|
98 | aliases.each do |ali|
99 | self.class_eval <<-EOS, __FILE__, __LINE__
100 | alias #{ali} #{property_option}
101 | EOS
102 | end
103 | end
104 |
105 | # Gives all the options set on this type
106 | #
107 | # ==== Returns
108 | # Hash:: with all options and their values set on this type
109 | #
110 | # @public
111 | def options
112 | options = {}
113 | PROPERTY_OPTIONS.each do |method|
114 | next if (value = send(method)).nil?
115 | options[method] = value
116 | end
117 | options
118 | end
119 | end
120 |
121 | # Stub instance method for dumping
122 | #
123 | # ==== Parameters
124 | # value