dm-core 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,132 @@
1
+ gem 'do_mysql', '=0.9.2'
2
+ require 'do_mysql'
3
+
4
+ module DataMapper
5
+ module Adapters
6
+ # Options:
7
+ # host, user, password, database (path), socket(uri query string), port
8
+ class MysqlAdapter < DataObjectsAdapter
9
+ module SQL
10
+ private
11
+
12
+ def supports_default_values?
13
+ false
14
+ end
15
+
16
+ def quote_table_name(table_name)
17
+ "`#{table_name.gsub('`', '``')}`"
18
+ end
19
+
20
+ def quote_column_name(column_name)
21
+ "`#{column_name.gsub('`', '``')}`"
22
+ end
23
+
24
+ def quote_column_value(column_value)
25
+ case column_value
26
+ when TrueClass then quote_column_value(1)
27
+ when FalseClass then quote_column_value(0)
28
+ else
29
+ super
30
+ end
31
+ end
32
+ end #module SQL
33
+
34
+ include SQL
35
+
36
+ # TODO: move to dm-more/dm-migrations
37
+ module Migration
38
+ # TODO: move to dm-more/dm-migrations (if possible)
39
+ def storage_exists?(storage_name)
40
+ statement = <<-EOS.compress_lines
41
+ SELECT COUNT(*)
42
+ FROM `information_schema`.`columns`
43
+ WHERE `table_schema` = ? AND `table_name` = ?
44
+ EOS
45
+
46
+ query(statement, db_name, storage_name).first > 0
47
+ end
48
+
49
+ # TODO: move to dm-more/dm-migrations (if possible)
50
+ def field_exists?(storage_name, field_name)
51
+ statement = <<-EOS.compress_lines
52
+ SELECT COUNT(*)
53
+ FROM `information_schema`.`columns`
54
+ WHERE `table_schema` = ? AND `table_name` = ? AND `column_name` = ?
55
+ EOS
56
+
57
+ query(statement, db_name, storage_name, field_name).first > 0
58
+ end
59
+
60
+ private
61
+
62
+ # TODO: move to dm-more/dm-migrations (if possible)
63
+ def db_name
64
+ @uri.path.split('/').last
65
+ end
66
+
67
+ module SQL
68
+ private
69
+
70
+ # TODO: move to dm-more/dm-migrations
71
+ def supports_serial?
72
+ true
73
+ end
74
+
75
+ # TODO: move to dm-more/dm-migrations
76
+ def create_table_statement(repository, model)
77
+ "#{super} ENGINE = InnoDB CHARACTER SET #{character_set} COLLATE #{collation}"
78
+ end
79
+
80
+ # TODO: move to dm-more/dm-migrations
81
+ def property_schema_hash(property, model)
82
+ schema = super
83
+ schema.delete(:default) if schema[:primitive] == 'TEXT'
84
+ schema
85
+ end
86
+
87
+ # TODO: move to dm-more/dm-migrations
88
+ def property_schema_statement(schema)
89
+ statement = super
90
+ statement << ' AUTO_INCREMENT' if supports_serial? && schema[:serial?]
91
+ statement
92
+ end
93
+
94
+ # TODO: move to dm-more/dm-migrations
95
+ def character_set
96
+ @character_set ||= show_variable('character_set_connection') || 'utf8'
97
+ end
98
+
99
+ # TODO: move to dm-more/dm-migrations
100
+ def collation
101
+ @collation ||= show_variable('collation_connection') || 'utf8_general_ci'
102
+ end
103
+
104
+ # TODO: move to dm-more/dm-migrations
105
+ def show_variable(name)
106
+ query('SHOW VARIABLES WHERE `variable_name` = ?', name).first.value rescue nil
107
+ end
108
+ end # module SQL
109
+
110
+ include SQL
111
+
112
+ module ClassMethods
113
+ # TypeMap for MySql databases.
114
+ #
115
+ # @return <DataMapper::TypeMap> default TypeMap for MySql databases.
116
+ #
117
+ # TODO: move to dm-more/dm-migrations
118
+ def type_map
119
+ @type_map ||= TypeMap.new(super) do |tm|
120
+ tm.map(Integer).to('INT').with(:size => 11)
121
+ tm.map(TrueClass).to('TINYINT').with(:size => 1) # TODO: map this to a BIT or CHAR(0) field?
122
+ tm.map(Object).to('TEXT')
123
+ end
124
+ end
125
+ end # module ClassMethods
126
+ end # module Migration
127
+
128
+ include Migration
129
+ extend Migration::ClassMethods
130
+ end # class MysqlAdapter
131
+ end # module Adapters
132
+ end # module DataMapper
@@ -0,0 +1,179 @@
1
+ gem 'do_postgres', '=0.9.2'
2
+ require 'do_postgres'
3
+
4
+ module DataMapper
5
+ module Adapters
6
+ class PostgresAdapter < DataObjectsAdapter
7
+ module SQL
8
+ private
9
+
10
+ def supports_returning?
11
+ true
12
+ end
13
+ end #module SQL
14
+
15
+ include SQL
16
+
17
+ # TODO: move to dm-more/dm-migrations (if possible)
18
+ module Migration
19
+ # TODO: move to dm-more/dm-migrations (if possible)
20
+ def storage_exists?(storage_name)
21
+ statement = <<-EOS.compress_lines
22
+ SELECT COUNT(*)
23
+ FROM "information_schema"."columns"
24
+ WHERE "table_name" = ? AND "table_schema" = current_schema()
25
+ EOS
26
+
27
+ query(statement, storage_name).first > 0
28
+ end
29
+
30
+ # TODO: move to dm-more/dm-migrations (if possible)
31
+ def field_exists?(storage_name, column_name)
32
+ statement = <<-EOS.compress_lines
33
+ SELECT COUNT(*)
34
+ FROM "pg_class"
35
+ JOIN "pg_attribute" ON "pg_class"."oid" = "pg_attribute"."attrelid"
36
+ WHERE "pg_attribute"."attname" = ? AND "pg_class"."relname" = ? AND "pg_attribute"."attnum" >= 0
37
+ EOS
38
+
39
+ query(statement, column_name, storage_name).first > 0
40
+ end
41
+
42
+ # TODO: move to dm-more/dm-migrations
43
+ def upgrade_model_storage(repository, model)
44
+ add_sequences(repository, model)
45
+ super
46
+ end
47
+
48
+ # TODO: move to dm-more/dm-migrations
49
+ def create_model_storage(repository, model)
50
+ add_sequences(repository, model)
51
+ without_notices { super }
52
+ end
53
+
54
+ # TODO: move to dm-more/dm-migrations
55
+ def destroy_model_storage(repository, model)
56
+ success = without_notices { super }
57
+ model.properties(repository.name).each do |property|
58
+ drop_sequence(repository, property) if property.serial?
59
+ end
60
+ success
61
+ end
62
+
63
+ protected
64
+
65
+ # TODO: move to dm-more/dm-migrations
66
+ def create_sequence(repository, property)
67
+ return if sequence_exists?(repository, property)
68
+ execute(create_sequence_statement(repository, property))
69
+ end
70
+
71
+ # TODO: move to dm-more/dm-migrations
72
+ def drop_sequence(repository, property)
73
+ without_notices { execute(drop_sequence_statement(repository, property)) }
74
+ end
75
+
76
+ module SQL
77
+ private
78
+
79
+ # TODO: move to dm-more/dm-migrations
80
+ def without_notices(&block)
81
+ # execute the block with NOTICE messages disabled
82
+ begin
83
+ execute('SET client_min_messages = warning')
84
+ yield
85
+ ensure
86
+ execute('RESET client_min_messages')
87
+ end
88
+ end
89
+
90
+ # TODO: move to dm-more/dm-migrations
91
+ def add_sequences(repository, model)
92
+ model.properties(repository.name).each do |property|
93
+ create_sequence(repository, property) if property.serial?
94
+ end
95
+ end
96
+
97
+ # TODO: move to dm-more/dm-migrations
98
+ def sequence_name(repository, property)
99
+ "#{property.model.storage_name(repository.name)}_#{property.field(repository.name)}_seq"
100
+ end
101
+
102
+ # TODO: move to dm-more/dm-migrations
103
+ def sequence_exists?(repository, property)
104
+ statement = <<-EOS.compress_lines
105
+ SELECT COUNT(*)
106
+ FROM "pg_class"
107
+ WHERE "relkind" = 'S' AND "relname" = ?
108
+ EOS
109
+
110
+ query(statement, sequence_name(repository, property)).first > 0
111
+ end
112
+
113
+ # TODO: move to dm-more/dm-migrations
114
+ def create_sequence_statement(repository, property)
115
+ "CREATE SEQUENCE #{quote_column_name(sequence_name(repository, property))}"
116
+ end
117
+
118
+ # TODO: move to dm-more/dm-migrations
119
+ def drop_sequence_statement(repository, property)
120
+ "DROP SEQUENCE IF EXISTS #{quote_column_name(sequence_name(repository, property))}"
121
+ end
122
+
123
+ # TODO: move to dm-more/dm-migrations
124
+ def property_schema_statement(schema)
125
+ statement = super
126
+
127
+ if schema.has_key?(:sequence_name)
128
+ statement << " DEFAULT nextval('#{schema[:sequence_name]}') NOT NULL"
129
+ end
130
+
131
+ statement
132
+ end
133
+
134
+ # TODO: move to dm-more/dm-migrations
135
+ def property_schema_hash(repository, property)
136
+ schema = super
137
+
138
+ if property.serial?
139
+ schema.delete(:default) # the sequence will be the default
140
+ schema[:sequence_name] = sequence_name(repository, property)
141
+ end
142
+
143
+ # TODO: see if TypeMap can be updated to set specific attributes to nil
144
+ # for different adapters. precision/scale are perfect examples for
145
+ # Postgres floats
146
+
147
+ # Postgres does not support precision and scale for Float
148
+ if property.primitive == Float
149
+ schema.delete(:precision)
150
+ schema.delete(:scale)
151
+ end
152
+
153
+ schema
154
+ end
155
+ end # module SQL
156
+
157
+ include SQL
158
+
159
+ module ClassMethods
160
+ # TypeMap for PostgreSQL databases.
161
+ #
162
+ # @return <DataMapper::TypeMap> default TypeMap for PostgreSQL databases.
163
+ #
164
+ # TODO: move to dm-more/dm-migrations
165
+ def type_map
166
+ @type_map ||= TypeMap.new(super) do |tm|
167
+ tm.map(DateTime).to('TIMESTAMP')
168
+ tm.map(Integer).to('INT4')
169
+ tm.map(Float).to('FLOAT8')
170
+ end
171
+ end
172
+ end # module ClassMethods
173
+ end # module Migration
174
+
175
+ include Migration
176
+ extend Migration::ClassMethods
177
+ end # class PostgresAdapter
178
+ end # module Adapters
179
+ end # module DataMapper
@@ -0,0 +1,105 @@
1
+ gem 'do_sqlite3', '=0.9.2'
2
+ require 'do_sqlite3'
3
+
4
+ module DataMapper
5
+ module Adapters
6
+ class Sqlite3Adapter < DataObjectsAdapter
7
+ module SQL
8
+ private
9
+
10
+ def quote_column_value(column_value)
11
+ case column_value
12
+ when TrueClass then quote_column_value('t')
13
+ when FalseClass then quote_column_value('f')
14
+ else
15
+ super
16
+ end
17
+ end
18
+ end # module SQL
19
+
20
+ include SQL
21
+
22
+ # TODO: move to dm-more/dm-migrations (if possible)
23
+ module Migration
24
+ # TODO: move to dm-more/dm-migrations (if possible)
25
+ def storage_exists?(storage_name)
26
+ query_table(storage_name).size > 0
27
+ end
28
+
29
+ # TODO: move to dm-more/dm-migrations (if possible)
30
+ def field_exists?(storage_name, column_name)
31
+ query_table(storage_name).any? do |row|
32
+ row.name == column_name
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # TODO: move to dm-more/dm-migrations (if possible)
39
+ def query_table(table_name)
40
+ query('PRAGMA table_info(?)', table_name)
41
+ end
42
+
43
+ module SQL
44
+ private
45
+
46
+ # TODO: move to dm-more/dm-migrations
47
+ def supports_serial?
48
+ sqlite_version >= '3.1.0'
49
+ end
50
+
51
+ # TODO: move to dm-more/dm-migrations
52
+ def create_table_statement(repository, model)
53
+ statement = <<-EOS.compress_lines
54
+ CREATE TABLE #{quote_table_name(model.storage_name(repository.name))}
55
+ (#{model.properties_with_subclasses(repository.name).map { |p| property_schema_statement(property_schema_hash(repository, p)) } * ', '}
56
+ EOS
57
+
58
+ # skip adding the primary key if one of the columns is serial. In
59
+ # SQLite the serial column must be the primary key, so it has already
60
+ # been defined
61
+ unless model.properties(repository.name).any? { |p| p.serial? }
62
+ if (key = model.properties(repository.name).key).any?
63
+ statement << ", PRIMARY KEY(#{key.map { |p| quote_column_name(p.field(repository.name)) } * ', '})"
64
+ end
65
+ end
66
+
67
+ statement << ')'
68
+ statement
69
+ end
70
+
71
+ # TODO: move to dm-more/dm-migrations
72
+ def property_schema_statement(schema)
73
+ statement = super
74
+ statement << ' PRIMARY KEY AUTOINCREMENT' if supports_serial? && schema[:serial?]
75
+ statement
76
+ end
77
+
78
+ # TODO: move to dm-more/dm-migrations
79
+ def sqlite_version
80
+ @sqlite_version ||= query('SELECT sqlite_version(*)').first
81
+ end
82
+ end # module SQL
83
+
84
+ include SQL
85
+
86
+ module ClassMethods
87
+ # TypeMap for SQLite 3 databases.
88
+ #
89
+ # @return <DataMapper::TypeMap> default TypeMap for SQLite 3 databases.
90
+ #
91
+ # TODO: move to dm-more/dm-migrations
92
+ def type_map
93
+ @type_map ||= TypeMap.new(super) do |tm|
94
+ tm.map(Integer).to('INTEGER')
95
+ tm.map(Class).to('VARCHAR')
96
+ end
97
+ end
98
+ end # module ClassMethods
99
+ end # module Migration
100
+
101
+ include Migration
102
+ extend Migration::ClassMethods
103
+ end # class Sqlite3Adapter
104
+ end # module Adapters
105
+ end # module DataMapper
@@ -0,0 +1,172 @@
1
+ dir = Pathname(__FILE__).dirname.expand_path / 'associations'
2
+
3
+ require dir / 'relationship'
4
+ require dir / 'relationship_chain'
5
+ require dir / 'many_to_many'
6
+ require dir / 'many_to_one'
7
+ require dir / 'one_to_many'
8
+ require dir / 'one_to_one'
9
+
10
+ module DataMapper
11
+ module Associations
12
+ include Assertions
13
+
14
+ class ImmutableAssociationError < RuntimeError
15
+ end
16
+
17
+ class UnsavedParentError < RuntimeError
18
+ end
19
+
20
+ def relationships(repository_name = default_repository_name)
21
+ @relationships ||= Hash.new { |h,k| h[k] = k == Repository.default_name ? {} : h[Repository.default_name].dup }
22
+ @relationships[repository_name]
23
+ end
24
+
25
+ def n
26
+ 1.0/0
27
+ end
28
+
29
+ ##
30
+ # A shorthand, clear syntax for defining one-to-one, one-to-many and
31
+ # many-to-many resource relationships.
32
+ #
33
+ # @example [Usage]
34
+ # * has 1, :friend # one friend
35
+ # * has n, :friends # many friends
36
+ # * has 1..3, :friends
37
+ # # many friends (at least 1, at most 3)
38
+ # * has 3, :friends
39
+ # # many friends (exactly 3)
40
+ # * has 1, :friend, :class_name => 'User'
41
+ # # one friend with the class name User
42
+ # * has 3, :friends, :through => :friendships
43
+ # # many friends through the friendships relationship
44
+ # * has n, :friendships => :friends
45
+ # # identical to above example
46
+ #
47
+ # @param cardinality [Integer, Range, Infinity]
48
+ # cardinality that defines the association type and constraints
49
+ # @param name <Symbol> the name that the association will be referenced by
50
+ # @param opts <Hash> an options hash
51
+ #
52
+ # @option :through[Symbol] A association that this join should go through to form
53
+ # a many-to-many association
54
+ # @option :class_name[String] The name of the class to associate with, if omitted
55
+ # then the association name is assumed to match the class name
56
+ # @option :remote_name[Symbol] In the case of a :through option being present, the
57
+ # name of the relationship on the other end of the :through-relationship
58
+ # to be linked to this relationship.
59
+ #
60
+ # @return [DataMapper::Association::Relationship] the relationship that was
61
+ # created to reflect either a one-to-one, one-to-many or many-to-many
62
+ # relationship
63
+ # @raise [ArgumentError] if the cardinality was not understood. Should be a
64
+ # Integer, Range or Infinity(n)
65
+ #
66
+ # @api public
67
+ def has(cardinality, name, options = {})
68
+
69
+ # NOTE: the reason for this fix is that with the ability to pass in two
70
+ # hashes into has() there might be instances where people attempt to
71
+ # pass in the options into the name part and not know why things aren't
72
+ # working for them.
73
+ if name.kind_of?(Hash)
74
+ name_through, through = name.keys.first, name.values.first
75
+ cardinality_string = cardinality.to_s == 'Infinity' ? 'n' : cardinality.inspect
76
+ warn("In #{self.name} 'has #{cardinality_string}, #{name_through.inspect} => #{through.inspect}' is deprecated. Use 'has #{cardinality_string}, #{name_through.inspect}, :through => #{through.inspect}' instead")
77
+ end
78
+
79
+ options = options.merge(extract_min_max(cardinality))
80
+ options = options.merge(extract_throughness(name))
81
+
82
+ # do not remove this. There is alot of confusion on people's
83
+ # part about what the first argument to has() is. For the record it
84
+ # is the min cardinality and max cardinality of the association.
85
+ # simply put, it constraints the number of resources that will be
86
+ # returned by the association. It is not, as has been assumed,
87
+ # the number of results on the left and right hand side of the
88
+ # reltionship.
89
+ if options[:min] == n && options[:max] == n
90
+ raise ArgumentError, 'Cardinality may not be n..n. The cardinality specifies the min/max number of results from the association', caller
91
+ end
92
+
93
+ klass = options[:max] == 1 ? OneToOne : OneToMany
94
+ klass = ManyToMany if options[:through] == DataMapper::Resource
95
+ relationship = klass.setup(options.delete(:name), self, options)
96
+
97
+ # Please leave this in - I will release contextual serialization soon
98
+ # which requires this -- guyvdb
99
+ # TODO convert this to a hook in the plugin once hooks work on class
100
+ # methods
101
+ self.init_has_relationship_for_serialization(relationship) if self.respond_to?(:init_has_relationship_for_serialization)
102
+
103
+ relationship
104
+ end
105
+
106
+ ##
107
+ # A shorthand, clear syntax for defining many-to-one resource relationships.
108
+ #
109
+ # @example [Usage]
110
+ # * belongs_to :user # many_to_one, :friend
111
+ # * belongs_to :friend, :class_name => 'User' # many_to_one :friends
112
+ #
113
+ # @param name [Symbol] The name that the association will be referenced by
114
+ # @see #has
115
+ #
116
+ # @return [DataMapper::Association::ManyToOne] The association created
117
+ # should not be accessed directly
118
+ #
119
+ # @api public
120
+ def belongs_to(name, options={})
121
+ relationship = ManyToOne.setup(name, self, options)
122
+ # Please leave this in - I will release contextual serialization soon
123
+ # which requires this -- guyvdb
124
+ # TODO convert this to a hook in the plugin once hooks work on class
125
+ # methods
126
+ self.init_belongs_relationship_for_serialization(relationship) if self.respond_to?(:init_belongs_relationship_for_serialization)
127
+
128
+ relationship
129
+ end
130
+
131
+ private
132
+
133
+ def extract_throughness(name)
134
+ assert_kind_of 'name', name, Hash, Symbol
135
+
136
+ case name
137
+ when Hash
138
+ unless name.keys.size == 1
139
+ raise ArgumentError, "name must have only one key, but had #{name.keys.size}", caller(2)
140
+ end
141
+
142
+ { :name => name.keys.first, :through => name.values.first }
143
+ when Symbol
144
+ { :name => name }
145
+ end
146
+ end
147
+
148
+ # A support method form converting Integer, Range or Infinity values into a
149
+ # { :min => x, :max => y } hash.
150
+ #
151
+ # @api private
152
+ def extract_min_max(constraints)
153
+ assert_kind_of 'constraints', constraints, Integer, Range unless constraints == n
154
+
155
+ case constraints
156
+ when Integer
157
+ { :min => constraints, :max => constraints }
158
+ when Range
159
+ if constraints.first > constraints.last
160
+ raise ArgumentError, "Constraint min (#{constraints.first}) cannot be larger than the max (#{constraints.last})"
161
+ end
162
+
163
+ { :min => constraints.first, :max => constraints.last }
164
+ when n
165
+ { :min => 0, :max => n }
166
+ end
167
+ end
168
+ end # module Associations
169
+
170
+ Model.append_extensions DataMapper::Associations
171
+
172
+ end # module DataMapper