sam-dm-core 0.9.6

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 (126) hide show
  1. data/.autotest +26 -0
  2. data/CONTRIBUTING +51 -0
  3. data/FAQ +92 -0
  4. data/History.txt +145 -0
  5. data/MIT-LICENSE +22 -0
  6. data/Manifest.txt +125 -0
  7. data/QUICKLINKS +12 -0
  8. data/README.txt +143 -0
  9. data/Rakefile +30 -0
  10. data/SPECS +63 -0
  11. data/TODO +1 -0
  12. data/lib/dm-core.rb +224 -0
  13. data/lib/dm-core/adapters.rb +4 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  15. data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
  16. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  19. data/lib/dm-core/associations.rb +199 -0
  20. data/lib/dm-core/associations/many_to_many.rb +147 -0
  21. data/lib/dm-core/associations/many_to_one.rb +107 -0
  22. data/lib/dm-core/associations/one_to_many.rb +309 -0
  23. data/lib/dm-core/associations/one_to_one.rb +61 -0
  24. data/lib/dm-core/associations/relationship.rb +218 -0
  25. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  26. data/lib/dm-core/auto_migrations.rb +113 -0
  27. data/lib/dm-core/collection.rb +638 -0
  28. data/lib/dm-core/dependency_queue.rb +31 -0
  29. data/lib/dm-core/hook.rb +11 -0
  30. data/lib/dm-core/identity_map.rb +45 -0
  31. data/lib/dm-core/is.rb +16 -0
  32. data/lib/dm-core/logger.rb +232 -0
  33. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  34. data/lib/dm-core/migrator.rb +29 -0
  35. data/lib/dm-core/model.rb +471 -0
  36. data/lib/dm-core/naming_conventions.rb +84 -0
  37. data/lib/dm-core/property.rb +673 -0
  38. data/lib/dm-core/property_set.rb +162 -0
  39. data/lib/dm-core/query.rb +625 -0
  40. data/lib/dm-core/repository.rb +159 -0
  41. data/lib/dm-core/resource.rb +637 -0
  42. data/lib/dm-core/scope.rb +58 -0
  43. data/lib/dm-core/support.rb +7 -0
  44. data/lib/dm-core/support/array.rb +13 -0
  45. data/lib/dm-core/support/assertions.rb +8 -0
  46. data/lib/dm-core/support/errors.rb +23 -0
  47. data/lib/dm-core/support/kernel.rb +7 -0
  48. data/lib/dm-core/support/symbol.rb +41 -0
  49. data/lib/dm-core/transaction.rb +267 -0
  50. data/lib/dm-core/type.rb +160 -0
  51. data/lib/dm-core/type_map.rb +80 -0
  52. data/lib/dm-core/types.rb +19 -0
  53. data/lib/dm-core/types/boolean.rb +7 -0
  54. data/lib/dm-core/types/discriminator.rb +34 -0
  55. data/lib/dm-core/types/object.rb +24 -0
  56. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  57. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  58. data/lib/dm-core/types/serial.rb +9 -0
  59. data/lib/dm-core/types/text.rb +10 -0
  60. data/lib/dm-core/version.rb +3 -0
  61. data/script/all +5 -0
  62. data/script/performance.rb +203 -0
  63. data/script/profile.rb +87 -0
  64. data/spec/integration/association_spec.rb +1371 -0
  65. data/spec/integration/association_through_spec.rb +203 -0
  66. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  67. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  68. data/spec/integration/associations/one_to_many_spec.rb +151 -0
  69. data/spec/integration/auto_migrations_spec.rb +398 -0
  70. data/spec/integration/collection_spec.rb +1069 -0
  71. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  72. data/spec/integration/dependency_queue_spec.rb +58 -0
  73. data/spec/integration/model_spec.rb +127 -0
  74. data/spec/integration/mysql_adapter_spec.rb +85 -0
  75. data/spec/integration/postgres_adapter_spec.rb +731 -0
  76. data/spec/integration/property_spec.rb +233 -0
  77. data/spec/integration/query_spec.rb +506 -0
  78. data/spec/integration/repository_spec.rb +57 -0
  79. data/spec/integration/resource_spec.rb +475 -0
  80. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  81. data/spec/integration/sti_spec.rb +208 -0
  82. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  83. data/spec/integration/transaction_spec.rb +75 -0
  84. data/spec/integration/type_spec.rb +271 -0
  85. data/spec/lib/logging_helper.rb +18 -0
  86. data/spec/lib/mock_adapter.rb +27 -0
  87. data/spec/lib/model_loader.rb +91 -0
  88. data/spec/lib/publicize_methods.rb +28 -0
  89. data/spec/models/vehicles.rb +34 -0
  90. data/spec/models/zoo.rb +47 -0
  91. data/spec/spec.opts +3 -0
  92. data/spec/spec_helper.rb +86 -0
  93. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  94. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  95. data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
  96. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  97. data/spec/unit/associations/many_to_many_spec.rb +17 -0
  98. data/spec/unit/associations/many_to_one_spec.rb +152 -0
  99. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  100. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  101. data/spec/unit/associations/relationship_spec.rb +71 -0
  102. data/spec/unit/associations_spec.rb +242 -0
  103. data/spec/unit/auto_migrations_spec.rb +111 -0
  104. data/spec/unit/collection_spec.rb +182 -0
  105. data/spec/unit/data_mapper_spec.rb +35 -0
  106. data/spec/unit/identity_map_spec.rb +126 -0
  107. data/spec/unit/is_spec.rb +80 -0
  108. data/spec/unit/migrator_spec.rb +33 -0
  109. data/spec/unit/model_spec.rb +339 -0
  110. data/spec/unit/naming_conventions_spec.rb +36 -0
  111. data/spec/unit/property_set_spec.rb +83 -0
  112. data/spec/unit/property_spec.rb +753 -0
  113. data/spec/unit/query_spec.rb +530 -0
  114. data/spec/unit/repository_spec.rb +93 -0
  115. data/spec/unit/resource_spec.rb +626 -0
  116. data/spec/unit/scope_spec.rb +142 -0
  117. data/spec/unit/transaction_spec.rb +493 -0
  118. data/spec/unit/type_map_spec.rb +114 -0
  119. data/spec/unit/type_spec.rb +119 -0
  120. data/tasks/ci.rb +68 -0
  121. data/tasks/dm.rb +63 -0
  122. data/tasks/doc.rb +20 -0
  123. data/tasks/gemspec.rb +23 -0
  124. data/tasks/hoe.rb +46 -0
  125. data/tasks/install.rb +20 -0
  126. metadata +216 -0
@@ -0,0 +1,136 @@
1
+ gem 'do_mysql', '>=0.9.5'
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`.`tables`
43
+ WHERE `table_type` = 'BASE TABLE'
44
+ AND `table_schema` = ?
45
+ AND `table_name` = ?
46
+ EOS
47
+
48
+ query(statement, db_name, storage_name).first > 0
49
+ end
50
+
51
+ # TODO: move to dm-more/dm-migrations (if possible)
52
+ def field_exists?(storage_name, field_name)
53
+ statement = <<-EOS.compress_lines
54
+ SELECT COUNT(*)
55
+ FROM `information_schema`.`columns`
56
+ WHERE `table_schema` = ?
57
+ AND `table_name` = ?
58
+ AND `column_name` = ?
59
+ EOS
60
+
61
+ query(statement, db_name, storage_name, field_name).first > 0
62
+ end
63
+
64
+ private
65
+
66
+ # TODO: move to dm-more/dm-migrations (if possible)
67
+ def db_name
68
+ @uri.path.split('/').last
69
+ end
70
+
71
+ module SQL
72
+ private
73
+
74
+ # TODO: move to dm-more/dm-migrations
75
+ def supports_serial?
76
+ true
77
+ end
78
+
79
+ # TODO: move to dm-more/dm-migrations
80
+ def create_table_statement(repository, model)
81
+ "#{super} ENGINE = InnoDB CHARACTER SET #{character_set} COLLATE #{collation}"
82
+ end
83
+
84
+ # TODO: move to dm-more/dm-migrations
85
+ def property_schema_hash(property, model)
86
+ schema = super
87
+ schema.delete(:default) if schema[:primitive] == 'TEXT'
88
+ schema
89
+ end
90
+
91
+ # TODO: move to dm-more/dm-migrations
92
+ def property_schema_statement(schema)
93
+ statement = super
94
+ statement << ' AUTO_INCREMENT' if supports_serial? && schema[:serial?]
95
+ statement
96
+ end
97
+
98
+ # TODO: move to dm-more/dm-migrations
99
+ def character_set
100
+ @character_set ||= show_variable('character_set_connection') || 'utf8'
101
+ end
102
+
103
+ # TODO: move to dm-more/dm-migrations
104
+ def collation
105
+ @collation ||= show_variable('collation_connection') || 'utf8_general_ci'
106
+ end
107
+
108
+ # TODO: move to dm-more/dm-migrations
109
+ def show_variable(name)
110
+ query('SHOW VARIABLES WHERE `variable_name` = ?', name).first.value rescue nil
111
+ end
112
+ end # module SQL
113
+
114
+ include SQL
115
+
116
+ module ClassMethods
117
+ # TypeMap for MySql databases.
118
+ #
119
+ # @return <DataMapper::TypeMap> default TypeMap for MySql databases.
120
+ #
121
+ # TODO: move to dm-more/dm-migrations
122
+ def type_map
123
+ @type_map ||= TypeMap.new(super) do |tm|
124
+ tm.map(Integer).to('INT').with(:size => 11)
125
+ tm.map(TrueClass).to('TINYINT').with(:size => 1) # TODO: map this to a BIT or CHAR(0) field?
126
+ tm.map(Object).to('TEXT')
127
+ end
128
+ end
129
+ end # module ClassMethods
130
+ end # module Migration
131
+
132
+ include Migration
133
+ extend Migration::ClassMethods
134
+ end # class MysqlAdapter
135
+ end # module Adapters
136
+ end # module DataMapper
@@ -0,0 +1,188 @@
1
+ gem 'do_postgres', '>=0.9.5'
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" = ?
25
+ AND "table_schema" = current_schema()
26
+ EOS
27
+
28
+ query(statement, storage_name).first > 0
29
+ end
30
+
31
+ # TODO: move to dm-more/dm-migrations (if possible)
32
+ def field_exists?(storage_name, column_name)
33
+ statement = <<-EOS.compress_lines
34
+ SELECT COUNT(*)
35
+ FROM "pg_class"
36
+ JOIN "pg_attribute" ON "pg_class"."oid" = "pg_attribute"."attrelid"
37
+ WHERE "pg_attribute"."attname" = ?
38
+ AND "pg_class"."relname" = ?
39
+ AND "pg_attribute"."attnum" >= 0
40
+ EOS
41
+
42
+ query(statement, column_name, storage_name).first > 0
43
+ end
44
+
45
+ # TODO: move to dm-more/dm-migrations
46
+ def upgrade_model_storage(repository, model)
47
+ add_sequences(repository, model)
48
+ super
49
+ end
50
+
51
+ # TODO: move to dm-more/dm-migrations
52
+ def create_model_storage(repository, model)
53
+ add_sequences(repository, model)
54
+ without_notices { super }
55
+ end
56
+
57
+ # TODO: move to dm-more/dm-migrations
58
+ def destroy_model_storage(repository, model)
59
+ return true unless storage_exists?(model.storage_name(repository.name))
60
+ success = without_notices { super }
61
+ model.properties(repository.name).each do |property|
62
+ drop_sequence(repository, property) if property.serial?
63
+ end
64
+ success
65
+ end
66
+
67
+ protected
68
+
69
+ # TODO: move to dm-more/dm-migrations
70
+ def create_sequence(repository, property)
71
+ return if sequence_exists?(repository, property)
72
+ execute(create_sequence_statement(repository, property))
73
+ end
74
+
75
+ # TODO: move to dm-more/dm-migrations
76
+ def drop_sequence(repository, property)
77
+ without_notices { execute(drop_sequence_statement(repository, property)) }
78
+ end
79
+
80
+ module SQL
81
+ private
82
+
83
+ # TODO: move to dm-more/dm-migrations
84
+ def drop_table_statement(repository, model)
85
+ "DROP TABLE #{quote_table_name(model.storage_name(repository.name))}"
86
+ end
87
+
88
+ # TODO: move to dm-more/dm-migrations
89
+ def without_notices(&block)
90
+ # execute the block with NOTICE messages disabled
91
+ begin
92
+ execute('SET client_min_messages = warning')
93
+ yield
94
+ ensure
95
+ execute('RESET client_min_messages')
96
+ end
97
+ end
98
+
99
+ # TODO: move to dm-more/dm-migrations
100
+ def add_sequences(repository, model)
101
+ model.properties(repository.name).each do |property|
102
+ create_sequence(repository, property) if property.serial?
103
+ end
104
+ end
105
+
106
+ # TODO: move to dm-more/dm-migrations
107
+ def sequence_name(repository, property)
108
+ "#{property.model.storage_name(repository.name)}_#{property.field(repository.name)}_seq"
109
+ end
110
+
111
+ # TODO: move to dm-more/dm-migrations
112
+ def sequence_exists?(repository, property)
113
+ statement = <<-EOS.compress_lines
114
+ SELECT COUNT(*)
115
+ FROM "pg_class"
116
+ WHERE "relkind" = 'S' AND "relname" = ?
117
+ EOS
118
+
119
+ query(statement, sequence_name(repository, property)).first > 0
120
+ end
121
+
122
+ # TODO: move to dm-more/dm-migrations
123
+ def create_sequence_statement(repository, property)
124
+ "CREATE SEQUENCE #{quote_column_name(sequence_name(repository, property))}"
125
+ end
126
+
127
+ # TODO: move to dm-more/dm-migrations
128
+ def drop_sequence_statement(repository, property)
129
+ "DROP SEQUENCE IF EXISTS #{quote_column_name(sequence_name(repository, property))}"
130
+ end
131
+
132
+ # TODO: move to dm-more/dm-migrations
133
+ def property_schema_statement(schema)
134
+ statement = super
135
+
136
+ if schema.has_key?(:sequence_name)
137
+ statement << " DEFAULT nextval('#{schema[:sequence_name]}') NOT NULL"
138
+ end
139
+
140
+ statement
141
+ end
142
+
143
+ # TODO: move to dm-more/dm-migrations
144
+ def property_schema_hash(repository, property)
145
+ schema = super
146
+
147
+ if property.serial?
148
+ schema.delete(:default) # the sequence will be the default
149
+ schema[:sequence_name] = sequence_name(repository, property)
150
+ end
151
+
152
+ # TODO: see if TypeMap can be updated to set specific attributes to nil
153
+ # for different adapters. precision/scale are perfect examples for
154
+ # Postgres floats
155
+
156
+ # Postgres does not support precision and scale for Float
157
+ if property.primitive == Float
158
+ schema.delete(:precision)
159
+ schema.delete(:scale)
160
+ end
161
+
162
+ schema
163
+ end
164
+ end # module SQL
165
+
166
+ include SQL
167
+
168
+ module ClassMethods
169
+ # TypeMap for PostgreSQL databases.
170
+ #
171
+ # @return <DataMapper::TypeMap> default TypeMap for PostgreSQL databases.
172
+ #
173
+ # TODO: move to dm-more/dm-migrations
174
+ def type_map
175
+ @type_map ||= TypeMap.new(super) do |tm|
176
+ tm.map(DateTime).to('TIMESTAMP')
177
+ tm.map(Integer).to('INT4')
178
+ tm.map(Float).to('FLOAT8')
179
+ end
180
+ end
181
+ end # module ClassMethods
182
+ end # module Migration
183
+
184
+ include Migration
185
+ extend Migration::ClassMethods
186
+ end # class PostgresAdapter
187
+ end # module Adapters
188
+ end # module DataMapper
@@ -0,0 +1,105 @@
1
+ gem 'do_sqlite3', '>=0.9.5'
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 ## This cannot be private for current migrations
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,199 @@
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
+ # Returns all relationships that are many-to-one for this model.
21
+ #
22
+ # Used to find the relationships that require properties in any Repository.
23
+ #
24
+ # Example:
25
+ # class Plur
26
+ # include DataMapper::Resource
27
+ # def self.default_repository_name
28
+ # :plur_db
29
+ # end
30
+ # repository(:plupp_db) do
31
+ # has 1, :plupp
32
+ # end
33
+ # end
34
+ #
35
+ # This resource has a many-to-one to the Plupp resource residing in the :plupp_db repository,
36
+ # but the Plur resource needs the plupp_id property no matter what repository itself lives in,
37
+ # ie we need to create that property when we migrate etc.
38
+ #
39
+ # Used in DataMapper::Model.properties_with_subclasses
40
+ #
41
+ # @api private
42
+ def many_to_one_relationships
43
+ relationships unless @relationships # needs to be initialized!
44
+ @relationships.values.collect do |rels| rels.values end.flatten.select do |relationship| relationship.child_model == self end
45
+ end
46
+
47
+ def relationships(repository_name = default_repository_name)
48
+ @relationships ||= Hash.new { |h,k| h[k] = k == Repository.default_name ? {} : h[Repository.default_name].dup }
49
+ @relationships[repository_name]
50
+ end
51
+
52
+ def n
53
+ 1.0/0
54
+ end
55
+
56
+ ##
57
+ # A shorthand, clear syntax for defining one-to-one, one-to-many and
58
+ # many-to-many resource relationships.
59
+ #
60
+ # @example [Usage]
61
+ # * has 1, :friend # one friend
62
+ # * has n, :friends # many friends
63
+ # * has 1..3, :friends
64
+ # # many friends (at least 1, at most 3)
65
+ # * has 3, :friends
66
+ # # many friends (exactly 3)
67
+ # * has 1, :friend, :class_name => 'User'
68
+ # # one friend with the class name User
69
+ # * has 3, :friends, :through => :friendships
70
+ # # many friends through the friendships relationship
71
+ # * has n, :friendships => :friends
72
+ # # identical to above example
73
+ #
74
+ # @param cardinality [Integer, Range, Infinity]
75
+ # cardinality that defines the association type and constraints
76
+ # @param name <Symbol> the name that the association will be referenced by
77
+ # @param opts <Hash> an options hash
78
+ #
79
+ # @option :through[Symbol] A association that this join should go through to form
80
+ # a many-to-many association
81
+ # @option :class_name[String] The name of the class to associate with, if omitted
82
+ # then the association name is assumed to match the class name
83
+ # @option :remote_name[Symbol] In the case of a :through option being present, the
84
+ # name of the relationship on the other end of the :through-relationship
85
+ # to be linked to this relationship.
86
+ #
87
+ # @return [DataMapper::Association::Relationship] the relationship that was
88
+ # created to reflect either a one-to-one, one-to-many or many-to-many
89
+ # relationship
90
+ # @raise [ArgumentError] if the cardinality was not understood. Should be a
91
+ # Integer, Range or Infinity(n)
92
+ #
93
+ # @api public
94
+ def has(cardinality, name, options = {})
95
+
96
+ # NOTE: the reason for this fix is that with the ability to pass in two
97
+ # hashes into has() there might be instances where people attempt to
98
+ # pass in the options into the name part and not know why things aren't
99
+ # working for them.
100
+ if name.kind_of?(Hash)
101
+ name_through, through = name.keys.first, name.values.first
102
+ cardinality_string = cardinality.to_s == 'Infinity' ? 'n' : cardinality.inspect
103
+ 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")
104
+ end
105
+
106
+ options = options.merge(extract_min_max(cardinality))
107
+ options = options.merge(extract_throughness(name))
108
+
109
+ # do not remove this. There is alot of confusion on people's
110
+ # part about what the first argument to has() is. For the record it
111
+ # is the min cardinality and max cardinality of the association.
112
+ # simply put, it constraints the number of resources that will be
113
+ # returned by the association. It is not, as has been assumed,
114
+ # the number of results on the left and right hand side of the
115
+ # reltionship.
116
+ if options[:min] == n && options[:max] == n
117
+ raise ArgumentError, 'Cardinality may not be n..n. The cardinality specifies the min/max number of results from the association', caller
118
+ end
119
+
120
+ klass = options[:max] == 1 ? OneToOne : OneToMany
121
+ klass = ManyToMany if options[:through] == DataMapper::Resource
122
+ relationship = klass.setup(options.delete(:name), self, options)
123
+
124
+ # Please leave this in - I will release contextual serialization soon
125
+ # which requires this -- guyvdb
126
+ # TODO convert this to a hook in the plugin once hooks work on class
127
+ # methods
128
+ self.init_has_relationship_for_serialization(relationship) if self.respond_to?(:init_has_relationship_for_serialization)
129
+
130
+ relationship
131
+ end
132
+
133
+ ##
134
+ # A shorthand, clear syntax for defining many-to-one resource relationships.
135
+ #
136
+ # @example [Usage]
137
+ # * belongs_to :user # many_to_one, :friend
138
+ # * belongs_to :friend, :class_name => 'User' # many_to_one :friends
139
+ #
140
+ # @param name [Symbol] The name that the association will be referenced by
141
+ # @see #has
142
+ #
143
+ # @return [DataMapper::Association::ManyToOne] The association created
144
+ # should not be accessed directly
145
+ #
146
+ # @api public
147
+ def belongs_to(name, options={})
148
+ relationship = ManyToOne.setup(name, self, options)
149
+ # Please leave this in - I will release contextual serialization soon
150
+ # which requires this -- guyvdb
151
+ # TODO convert this to a hook in the plugin once hooks work on class
152
+ # methods
153
+ self.init_belongs_relationship_for_serialization(relationship) if self.respond_to?(:init_belongs_relationship_for_serialization)
154
+
155
+ relationship
156
+ end
157
+
158
+ private
159
+
160
+ def extract_throughness(name)
161
+ assert_kind_of 'name', name, Hash, Symbol
162
+
163
+ case name
164
+ when Hash
165
+ unless name.keys.size == 1
166
+ raise ArgumentError, "name must have only one key, but had #{name.keys.size}", caller(2)
167
+ end
168
+
169
+ { :name => name.keys.first, :through => name.values.first }
170
+ when Symbol
171
+ { :name => name }
172
+ end
173
+ end
174
+
175
+ # A support method form converting Integer, Range or Infinity values into a
176
+ # { :min => x, :max => y } hash.
177
+ #
178
+ # @api private
179
+ def extract_min_max(constraints)
180
+ assert_kind_of 'constraints', constraints, Integer, Range unless constraints == n
181
+
182
+ case constraints
183
+ when Integer
184
+ { :min => constraints, :max => constraints }
185
+ when Range
186
+ if constraints.first > constraints.last
187
+ raise ArgumentError, "Constraint min (#{constraints.first}) cannot be larger than the max (#{constraints.last})"
188
+ end
189
+
190
+ { :min => constraints.first, :max => constraints.last }
191
+ when n
192
+ { :min => 0, :max => n }
193
+ end
194
+ end
195
+ end # module Associations
196
+
197
+ Model.append_extensions DataMapper::Associations
198
+
199
+ end # module DataMapper