dr_nic_magic_models 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/Rakefile +9 -9
  2. data/lib/base.rb +10 -95
  3. data/lib/connection_adapters/abstract/schema_statements.rb +0 -0
  4. data/lib/connection_adapters/abstract_adapter.rb +32 -0
  5. data/lib/connection_adapters/mysql_adapter.rb +42 -0
  6. data/lib/connection_adapters/postgresql_adapter.rb +45 -0
  7. data/lib/dr_nic_magic_models/schema.rb +210 -8
  8. data/lib/dr_nic_magic_models/validations.rb +35 -18
  9. data/lib/dr_nic_magic_models/version.rb +2 -2
  10. data/lib/dr_nic_magic_models.rb +13 -2
  11. data/lib/module.rb +0 -18
  12. data/lib/rails.rb +19 -0
  13. data/lib/schema_dumper.rb +0 -0
  14. data/scripts/txt2js +57 -0
  15. data/test/connections/native_postgresql/connection.rb +14 -0
  16. data/test/fixtures/adjectives.yml +3 -0
  17. data/test/fixtures/adjectives_fun_users.yml +3 -0
  18. data/test/fixtures/db_definitions/mysql.drop.sql +4 -30
  19. data/test/fixtures/db_definitions/mysql.sql +30 -2
  20. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  21. data/test/fixtures/db_definitions/postgresql.sql +56 -0
  22. data/test/fixtures/fun_users.yml +8 -1
  23. data/test/fixtures/group_tag.yml +3 -1
  24. data/test/fixtures/groups.yml +9 -1
  25. data/test/foreign_keys_test.rb +0 -0
  26. data/test/fun_user_plus.rb +2 -0
  27. data/test/invisible_model_access_test.rb +35 -7
  28. data/test/invisible_model_assoc_test.rb +34 -19
  29. data/test/invisible_model_classes_test.rb +5 -1
  30. data/website/index.html +24 -4
  31. data/website/index.txt +9 -1
  32. data/website/template.js +3 -0
  33. data/website/template.rhtml +10 -1
  34. data/website/version-raw.js +3 -0
  35. data/website/version-raw.txt +1 -0
  36. data/website/version.js +4 -0
  37. data/website/version.txt +3 -0
  38. metadata +32 -19
  39. data/lib/dr_nic_magic_models/associations.rb +0 -12
data/Rakefile CHANGED
@@ -18,7 +18,7 @@ RUBY_FORGE_PROJECT = "magicmodels"
18
18
  RUBY_FORGE_USER = "nicwilliams"
19
19
 
20
20
  PKG_FILES = FileList[
21
- "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "[A-Z]*", "install.rb", "Rakefile"
21
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "scripts/**/*", "[A-Z]*", "install.rb", "Rakefile"
22
22
  ].exclude(/\bCVS\b|~$/)
23
23
 
24
24
 
@@ -28,7 +28,7 @@ task :test => [ :test_mysql ]
28
28
 
29
29
  # Run the unit tests
30
30
 
31
- for adapter in %w( mysql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
31
+ for adapter in %w( mysql postgresql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
32
32
  Rake::TestTask.new("test_#{adapter}") { |t|
33
33
  t.libs << "test" << "test/connections/native_#{adapter}"
34
34
  t.pattern = "test/*_test{,_#{adapter}}.rb"
@@ -42,7 +42,9 @@ desc 'Build the MySQL test databases'
42
42
  task :build_mysql_databases do
43
43
  puts File.join(SCHEMA_PATH, 'mysql.sql')
44
44
  %x( mysqladmin -u root create "#{PKG_NAME}_unittest" )
45
- %x( mysql -u root "#{PKG_NAME}_unittest" < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
45
+ cmd = "mysql -u root #{PKG_NAME}_unittest < \"#{File.join(SCHEMA_PATH, 'mysql.sql')}\""
46
+ puts "#{cmd}\n"
47
+ %x( #{cmd} )
46
48
  end
47
49
 
48
50
  desc 'Drop the MySQL test databases'
@@ -55,13 +57,13 @@ task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases]
55
57
 
56
58
  desc 'Build the PostgreSQL test databases'
57
59
  task :build_postgresql_databases do
58
- %x( createdb "#{PKG_NAME}_unittest" )
59
- %x( psql "#{PKG_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} )
60
+ %x( createdb -U postgres "#{PKG_NAME}_unittest" )
61
+ %x( psql "#{PKG_NAME}_unittest" postgres -f "#{File.join(SCHEMA_PATH, 'postgresql.sql')}" )
60
62
  end
61
63
 
62
64
  desc 'Drop the PostgreSQL test databases'
63
65
  task :drop_postgresql_databases do
64
- %x( dropdb "#{PKG_NAME}_unittest" )
66
+ %x( dropdb -U postgres "#{PKG_NAME}_unittest" )
65
67
  end
66
68
 
67
69
  desc 'Rebuild the PostgreSQL test databases'
@@ -98,8 +100,6 @@ spec = Gem::Specification.new do |s|
98
100
  dist_dirs.each do |dir|
99
101
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
100
102
  end
101
-
102
- s.add_dependency('activerecord', '>= 1.14.3' + PKG_BUILD)
103
103
 
104
104
  s.require_path = 'lib'
105
105
  s.autorequire = 'dr_nic_magic_models'
@@ -156,4 +156,4 @@ task :release => [ :package ] do
156
156
  puts release_command
157
157
  system(release_command)
158
158
  end
159
- end
159
+ end
data/lib/base.rb CHANGED
@@ -1,101 +1,16 @@
1
- # Only needs to be an Include module for the generated classes, not for all ARs
1
+ #TODO: Use :dependent for FK cascade?
2
+
2
3
  module ActiveRecord
3
4
  class Base
4
5
  class << self
5
- alias old_new new
6
- def new(*args)
7
- obj = old_new(*args)
8
- unless self.methods.include? 'generate_validations'
9
- self.send(:include, DrNicMagicModels::Validations)
10
- self.generate_validations
11
- end
12
- obj
13
- end
14
- alias old_allocate allocate
15
- def allocate
16
- obj = old_allocate
17
- unless self.methods.include? 'generate_validations'
18
- self.send(:include, DrNicMagicModels::Validations)
19
- self.generate_validations
20
- end
21
- obj
22
- end
23
-
24
- # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
25
- # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
26
- # Invoice.reflect_on_association(:line_items).macro # returns :has_many
27
- def reflect_on_association(association)
28
- unless reflections[association]
29
- # See if an assocation can be generated
30
- self.new.send(association) rescue nil
31
- end
32
- reflections[association].is_a?(ActiveRecord::Reflection::AssociationReflection) ? reflections[association] : nil
33
- end
34
- end
35
-
36
- class_eval do
37
- alias :normal_method_missing :method_missing
38
- def method_missing(method, *args, &block)
39
- if unknown_method? method
40
- result = find_belongs_to method, *args, &block
41
- result = find_has_some method, *args, &block if not result
42
- result = find_has_some_indirect method, *args, &block if not result
43
- return result if result
44
- end
45
- add_known_unknown method
46
- normal_method_missing(method, *args, &block)
47
- end
48
-
49
- def add_known_unknown(method)
50
- @known_unknowns ||= {}
51
- @known_unknowns[method] = true
52
- end
53
-
54
- def unknown_method?(method)
55
- @known_unknowns.nil? or @known_unknowns.include? method
56
- end
57
-
58
- def find_belongs_to(method, *args, &block)
59
- foreign_key = self.class.columns.select {|column| column.name == method.to_s.foreign_key}.first
60
- return add_belongs_to(method, *args, &block) if foreign_key
61
- end
62
6
 
63
- def add_belongs_to(method, *args, &block)
64
- self.class.send 'belongs_to', method
65
- self.send(method, *args, &block)
66
- end
67
-
68
- def find_has_some(method, *args, &block)
69
- klass = Module.const_get method.to_s.downcase.singularize.camelize rescue nil
70
- foreign_key = klass.columns.select {|column| column.name == self.class.name.foreign_key}.first if klass
71
- return add_has_some(method, *args, &block) if foreign_key
72
- end
73
-
74
- def add_has_some(method, *args, &block)
75
- _method = method.to_s
76
- association = _method.singularize == _method ? 'has_one' : 'has_many'
77
- self.class.send association, method
78
- self.send(method, *args, &block)
79
- end
80
-
81
- def find_has_some_indirect(method, *args, &block)
82
- klass = Module.const_get method.to_s.downcase.singularize.camelize rescue return
83
- join_table = nil
84
- self.connection.tables.each do |table|
85
- unless [self.class.table_name, klass.table_name].include? table
86
- columns = self.connection.columns(table).map(&:name)
87
- join_table = table if columns.include?(self.class.to_s.foreign_key) and columns.include?(klass.to_s.foreign_key)
88
- end
89
- break if join_table
90
- end
91
- return add_has_some_through(join_table, method, *args, &block) if join_table
92
- end
93
-
94
- def add_has_some_through(join_table, method, *args, &block)
95
- self.class.send 'has_many', method, :through => join_table.to_sym
96
- self.send(method, *args, &block)
97
- end
98
-
7
+ public
8
+
9
+ def get_unique_index_columns
10
+ self.connection.indexes(self.table_name, "#{self.name} Indexes").select { |index| index.unique && index.columns.size == 1 }.map{ |index| index.columns.first }
11
+ end
99
12
  end
13
+
14
+
100
15
  end
101
- end
16
+ end
@@ -0,0 +1,32 @@
1
+
2
+ module ActiveRecord
3
+ module ConnectionAdapters # :nodoc:
4
+
5
+ # Generic holder for foreign key constraint meta-data from the database schema.
6
+ class ForeignKeyConstraint < Struct.new(:name, :table, :foreign_key, :reference_table, :reference_column, :on_update, :on_delete)
7
+ end
8
+
9
+ class AbstractAdapter
10
+
11
+ # Does this adapter support the ability to fetch foreign key information?
12
+ # Backend specific, as the abstract adapter always returns +false+.
13
+ def supports_fetch_foreign_keys?
14
+ false
15
+ end
16
+
17
+ def foreign_key_constraints(table, name = nil)
18
+ raise NotImplementedError, "foreign_key_constraints is not implemented for #{self.class}"
19
+ end
20
+
21
+ def remove_foreign_key_constraint(table_name, constraint_name)
22
+ raise NotImplementedError, "rename_table is not implemented for #{self.class}"
23
+ end
24
+
25
+ protected
26
+ def symbolize_foreign_key_constraint_action(constraint_action)
27
+ return nil if constraint_action.nil?
28
+ constraint_action.downcase.gsub(/\s/, '_').to_sym
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class MysqlAdapter < AbstractAdapter
6
+ def supports_fetch_foreign_keys?
7
+ true
8
+ end
9
+
10
+ def foreign_key_constraints(table, name = nil)
11
+ constraints = []
12
+ execute("SHOW CREATE TABLE #{table}", name).each do |row|
13
+ row[1].each do |create_line|
14
+ if create_line.strip =~ /CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^`]+)`\) REFERENCES `([^`]+)` \(`([^`]+)`\)([^,]*)/
15
+ constraint = ForeignKeyConstraint.new(Regexp.last_match(1), table, Regexp.last_match(2), Regexp.last_match(3), Regexp.last_match(4), nil, nil)
16
+
17
+ constraint_params = {}
18
+
19
+ unless Regexp.last_match(5).nil?
20
+ Regexp.last_match(5).strip.split('ON ').each do |param|
21
+ constraint_params[Regexp.last_match(1).upcase] = Regexp.last_match(2).strip.upcase if param.strip =~ /([^ ]+) (.+)/
22
+ end
23
+ end
24
+
25
+ constraint.on_update = symbolize_foreign_key_constraint_action(constraint_params['UPDATE']) if constraint_params.include? 'UPDATE'
26
+ constraint.on_delete = symbolize_foreign_key_constraint_action(constraint_params['DELETE']) if constraint_params.include? 'DELETE'
27
+
28
+ constraints << constraint
29
+ end
30
+ end
31
+ end
32
+
33
+ constraints
34
+ end
35
+
36
+ def remove_foreign_key_constraint(table_name, constraint_name)
37
+ execute "ALTER TABLE #{table_name} DROP FOREIGN KEY #{constraint_name}"
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class PostgreSQLAdapter < AbstractAdapter
6
+
7
+ def supports_fetch_foreign_keys?
8
+ true
9
+ end
10
+
11
+ def foreign_key_constraints(table, name = nil)
12
+
13
+
14
+ sql = "SELECT conname, pg_catalog.pg_get_constraintdef(oid) AS consrc FROM pg_catalog.pg_constraint WHERE contype='f' "
15
+ sql += "AND conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='#{table}')"
16
+
17
+ result = query(sql, name)
18
+
19
+ keys = []
20
+ re = /(?i)^FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)(?: ON UPDATE (\w+))?(?: ON DELETE (\w+))?$/
21
+ result.each do |row|
22
+ # pg_catalog.pg_get_constraintdef returns a string like this:
23
+ # FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
24
+ if match = re.match(row[1])
25
+
26
+ keys << ForeignKeyConstraint.new(row[0],
27
+ table,
28
+ match[1],
29
+ match[2],
30
+ match[3],
31
+ symbolize_foreign_key_constraint_action(match[4]),
32
+ symbolize_foreign_key_constraint_action(match[5]))
33
+ end
34
+ end
35
+
36
+ keys
37
+ end
38
+
39
+ def remove_foreign_key_constraint(table_name, constraint_name)
40
+ execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint_name}"
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -1,23 +1,225 @@
1
1
  module DrNicMagicModels
2
+
2
3
  class Schema
3
4
  class << self
5
+
6
+ # all in lower case please
7
+ ReservedTables = [:schema_info, :sessions]
8
+ @@models = nil
9
+
10
+ def logger
11
+ @@logger ||= DrNicMagicModels::Logger
12
+ end
13
+
4
14
  def models
5
- @@models ||= nil
6
- load_schema if @@models.blank?
15
+ load_schema if @@models.nil?
7
16
  @@models
8
17
  end
9
18
 
10
- def reset
11
- @@models = nil
19
+ def fks_on_table(table_name)
20
+ load_schema if @@models.nil?
21
+ @@fks_by_table[table_name.to_s] || []
12
22
  end
13
23
 
24
+ # active record only support 2 column link tables, otherwise use a model table, has_many and through
25
+ def is_link_table?(table_name)
26
+ load_schema if @@models.nil?
27
+ return @@link_tables[table_name] if ! @@link_tables[table_name].nil?
28
+ column_names = @conn.columns(table_name).map{|x| x.name }
29
+ @@link_tables[table_name] = ! column_names.include?("id") && column_names.length == 2 && column_names.select { |x| x =~ /_id$/ } == column_names
30
+ return @@link_tables[table_name]
31
+ end
32
+
33
+ def link_tables_for_class(klass)
34
+ load_schema if @@models.nil?
35
+ end
36
+
14
37
  def load_schema
15
- if conn = ActiveRecord::Base.connection
16
- @@models = DrNicMagicModels::ModelHash.new
17
- @@models.merge! conn.tables.inject({}) {|model_list,table_name| model_list[ActiveRecord::Base.class_name(table_name)] = table_name; model_list}
38
+
39
+ return if ! @@models.nil?
40
+
41
+ raise "No database connection" if !(@conn = ActiveRecord::Base.connection)
42
+
43
+ @@models = ModelHash.new
44
+ @@tables = Hash.new
45
+ @@fks_by_table = Hash.new
46
+ @@link_tables = Hash.new
47
+
48
+ tables = @conn.tables.sort
49
+
50
+ # Work out which tables are in the model and which aren't
51
+ tables.each do |table_name|
52
+
53
+ # deal with reserved tables & link_tables && other stray id-less tables
54
+ next if ReservedTables.include?(table_name.downcase.to_sym) || is_link_table?(table_name) || ! @conn.columns(table_name).map{ |x| x.name}.include?("id")
55
+
56
+ # a model table then...
57
+ model_class_name = ActiveRecord::Base.class_name(table_name)
58
+
59
+ logger.debug "Got a model table: #{table_name} => class #{model_class_name}"
60
+
61
+ @@models[model_class_name] = table_name
62
+ @@tables[table_name] = model_class_name
63
+
64
+ # create by MAGIC!
65
+ begin
66
+ klass = model_class_name.constantize
67
+ rescue Exception => e
68
+ # create our class!
69
+ class_def = <<-end_eval
70
+ class #{model_class_name} < ActiveRecord::Base
71
+ set_table_name('#{table_name}')
72
+ end
73
+ end_eval
74
+ eval(class_def, TOPLEVEL_BINDING)
75
+ klass = model_class_name.constantize
76
+ end
77
+
78
+ # magic up some validation
79
+ klass.send(:extend, DrNicMagicModels::Validations)
80
+ klass.send(:generate_validations)
81
+
18
82
  end
83
+
84
+ # Process FKs?
85
+ if @conn.supports_fetch_foreign_keys?
86
+
87
+ tables.each do |table_name|
88
+ logger.debug "Getting FKs for #{table_name}"
89
+ @@fks_by_table[table_name] = Array.new
90
+ @conn.foreign_key_constraints(table_name).each do |fk|
91
+ logger.debug "Got one: #{fk}"
92
+ @@fks_by_table[table_name].push(fk)
93
+ end # do each fk
94
+
95
+ end # each table
96
+ end
97
+
98
+ # Try to work out our link tables now...
99
+ @@models.keys.sort.each{|klass| process_table(@@models[klass.to_s])}
100
+ @@link_tables.keys.sort.each{|table_name| process_link_table(table_name) if @@link_tables[table_name]}
101
+
102
+
19
103
  end
20
- end
104
+
105
+ def process_table(table_name)
106
+
107
+ logger.debug "Processing model table #{table_name}"
108
+
109
+ # ok, so let's look at the foreign keys on the table...
110
+ belongs_to_klass = @@tables[table_name].constantize rescue return
111
+
112
+ processed_columns = Hash.new
113
+
114
+ fks_on_table(table_name).each do |fk|
115
+ logger.debug "Found FK column by suffix _id [#{fk.foreign_key}]"
116
+ has_some_klass = Inflector.classify(fk.reference_table).constantize rescue next
117
+ processed_columns[fk.foreign_key] = { :has_some_klass => has_some_klass }
118
+ processed_columns[fk.foreign_key].merge! add_has_some_belongs_to(belongs_to_klass, fk.foreign_key, has_some_klass) rescue next
119
+ end
120
+
121
+ column_names = @conn.columns(table_name).map{ |x| x.name}
122
+ column_names.each do |column_name|
123
+ next if not column_name =~ /_id$/
124
+ logger.debug "Found FK column by suffix _id [#{column_name}]"
125
+ if processed_columns.key?(column_name)
126
+ logger.debug "Skipping, already processed"
127
+ next
128
+ end
129
+ has_some_klass = Inflector.classify(column_name.sub(/_id$/,"")).constantize rescue next
130
+ processed_columns[column_name] = { :has_some_klass => has_some_klass }
131
+ processed_columns[column_name].merge! add_has_some_belongs_to(belongs_to_klass, column_name, has_some_klass) rescue next
132
+ end
133
+
134
+ #TODO: what if same classes in table?
135
+
136
+ # is this a link table with attributes? (has_many through?)
137
+ return if processed_columns.keys.length < 2
138
+
139
+ processed_columns.keys.each do |key1|
140
+ processed_columns.keys.each do |key2|
141
+ next if key1 == key2
142
+ logger.debug "\n*** #{processed_columns[key1][:has_some_class]}.send 'has_many', #{processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym}, :through => #{processed_columns[key2][:has_some_name]}\n\n"
143
+ processed_columns[key1][:has_some_class].send 'has_many', processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym, :through => processed_columns[key2][:has_some_name].to_sym
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+
150
+ def add_has_some_belongs_to(belongs_to_klass, belongs_to_fk, has_some_klass)
151
+
152
+ logger.debug "Trying to add a #{belongs_to_klass} belongs_to #{has_some_klass}..."
153
+
154
+ # so this is a belongs_to & has_some style relationship...
155
+ # is it a has_many, or a has_one? Well, let's assume a has_one has a unique index on the column please... good db design, haha!
156
+ unique = belongs_to_klass.get_unique_index_columns.include?(belongs_to_fk)
157
+ belongs_to_name = belongs_to_fk.sub(/_id$/, '').to_sym
158
+
159
+ logger.debug "\n*** #{belongs_to_klass}.send 'belongs_to', #{belongs_to_name}, :class_name => #{has_some_klass}, :foreign_key => #{belongs_to_fk}\n"
160
+ belongs_to_klass.send(:belongs_to, belongs_to_name, :class_name => has_some_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
161
+
162
+ # work out if we need a prefix
163
+ has_some_name = ((unique ? belongs_to_klass.table_name.singularize : belongs_to_klass.table_name.pluralize) + (belongs_to_name.to_s == has_some_klass.table_name.singularize ? "" : "_as_"+belongs_to_name.to_s)).downcase.to_sym
164
+ method = unique ? :has_one : :has_many
165
+ logger.debug "\n*** #{has_some_klass}.send(#{method}, #{has_some_name}, :class_name => #{belongs_to_klass.to_s}, :foreign_key => #{belongs_to_fk.to_sym})\n\n"
166
+ has_some_klass.send(method, has_some_name, :class_name => belongs_to_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
167
+
168
+ return { :method => method, :belongs_to_name => belongs_to_name, :has_some_name => has_some_name, :has_some_class => has_some_klass }
169
+
170
+ end
171
+
172
+ def process_link_table(table_name)
173
+
174
+ logger.debug "Processing link table #{table_name}"
175
+
176
+ classes_map = Hash.new
177
+ column_names = @conn.columns(table_name).map{ |x| x.name}
178
+
179
+ # use foreign keys first
180
+ fks_on_table(table_name).each do |fk|
181
+ logger.debug "Processing fk: #{fk}"
182
+ klass = Inflector.classify(fk.reference_table).constantize rescue logger.debug("Cannot find model #{class_name} for table #{fk.reference_table}") && return
183
+ classes_map[fk.foreign_key] = klass
184
+ end
185
+
186
+ logger.debug "Got #{classes_map.keys.length} references from FKs"
187
+
188
+ if classes_map.keys.length < 2
189
+
190
+ #Fall back on good ol _id recognition
191
+
192
+ column_names.each do |column_name|
193
+
194
+ # check we haven't processed by fks already
195
+ next if ! classes_map[column_name].nil?
196
+ referenced_table = column_name.sub(/_id$/, '')
197
+
198
+ begin
199
+ klass = Inflector.classify(referenced_table).constantize
200
+ # fall back on FKs here
201
+ if ! klass.nil?
202
+ classes_map[column_name] = klass
203
+ end
204
+ rescue
205
+ end
206
+ end
207
+ end
208
+
209
+ # not detected the link table?
210
+ logger.debug "Got #{classes_map.keys.length} references"
211
+ logger.debug "Cannot detect both tables referenced in link table" && return if classes_map.keys.length != 2
212
+
213
+ logger.debug "Adding habtm relationship"
214
+
215
+ logger.debug "\n*** #{classes_map[column_names[0]]}.send 'has_and_belongs_to_many', #{column_names[1].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[1]].to_s}, :join_table => #{table_name.to_sym}\n"
216
+ logger.debug "\n*** #{classes_map[column_names[1]]}.send 'has_and_belongs_to_many', #{column_names[0].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[0]].to_s}, :join_table => #{table_name.to_sym}\n\n"
217
+
218
+ classes_map[column_names[0]].send 'has_and_belongs_to_many', column_names[1].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[1]].to_s, :join_table => table_name.to_sym
219
+ classes_map[column_names[1]].send 'has_and_belongs_to_many', column_names[0].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[0]].to_s, :join_table => table_name.to_sym
220
+
221
+ end
222
+ end
21
223
  end
22
224
 
23
225
  class ModelHash < Hash
@@ -1,26 +1,43 @@
1
1
  module DrNicMagicModels
2
2
  module Validations
3
- def self.append_features(base)
4
- super
5
- base.extend(ClassMethods)
6
- base.generate_validations
3
+
4
+ def generate_validations
5
+
6
+ logger = DrNicMagicModels::Logger
7
+
8
+ # Code reworked from http://www.redhillconsulting.com.au/rails_plugins.html
9
+ # Thanks Red Hill Consulting for using an MIT licence :o)
10
+
11
+ # NOT NULL constraints
12
+ self.columns.reject { |column| column.name =~ /(?i)^(((created|updated)_(at|on))|position|type|id)$/ }.each do |column|
7
13
 
8
- # Currently only invoked on generated classes
9
- # How to include and invoke on all ARs? - hook into class loading??
10
- end
11
-
12
- module ClassMethods
13
- def generate_validations
14
- unless @@generated_validations ||= false
15
- @@generated_validations = true
16
- column_names = self.columns.select {|column| !column.null and !column.primary}.map {|column| column.name.to_sym}
17
- add_validation :validates_presence_of, column_names
14
+ if column.type == :integer
15
+ logger.debug "validates_numericality_of #{column.name}, :allow_nil => true, :only_integer => true"
16
+ self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
17
+ elsif column.number?
18
+ logger.debug "validates_numericality_of #{column.name}, :allow_nil => true"
19
+ self.validates_numericality_of column.name, :allow_nil => true
20
+ elsif column.text? && column.limit
21
+ logger.debug "validates_length_of #{column.name}, :allow_nil => true, :maximum => #{column.limit}"
22
+ self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
18
23
  end
19
- end
20
24
 
21
- def add_validation(validation, column_names)
22
- self.send validation, *column_names
25
+ # Active record seems to interpolate booleans anyway to either true, false or nil...
26
+ if column.type == :boolean
27
+ logger.debug "validates_inclusion_of #{column.name}, :in => [true, false], :allow_nil => #{column.null}, :message => ActiveRecord::Errors.default_error_messages[:blank]"
28
+ self.validates_inclusion_of column.name, :in => [true, false], :allow_nil => column.null, :message => ActiveRecord::Errors.default_error_messages[:blank]
29
+ elsif !column.null
30
+ logger.debug "validates_presence_of #{column.name}"
31
+ self.validates_presence_of column.name
32
+ end
23
33
  end
24
- end
34
+
35
+ # Single-column UNIQUE indexes
36
+ get_unique_index_columns.each do |col|
37
+ logger.debug "validates_uniqueness_of #{col}"
38
+ self.validates_uniqueness_of col
39
+ end
40
+
41
+ end
25
42
  end
26
43
  end
@@ -1,8 +1,8 @@
1
1
  module DrNicMagicModels #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 7
5
- TINY = 1
4
+ MINOR = 8
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -3,7 +3,6 @@ $:.unshift(File.dirname(__FILE__)) unless
3
3
 
4
4
  unless defined?(ActiveRecord)
5
5
  begin
6
- $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
7
6
  require 'active_record'
8
7
  rescue LoadError
9
8
  require 'rubygems'
@@ -11,7 +10,19 @@ unless defined?(ActiveRecord)
11
10
  end
12
11
  end
13
12
 
13
+ module DrNicMagicModels
14
+ Logger = RAILS_DEFAULT_LOGGER rescue Logger.new(STDERR)
15
+ end
16
+
14
17
  require 'dr_nic_magic_models/schema'
15
18
  require 'dr_nic_magic_models/validations'
16
- require 'module'
17
19
  require 'base'
20
+ require 'rails' rescue nil
21
+ require 'connection_adapters/abstract_adapter'
22
+ require 'connection_adapters/mysql_adapter'
23
+ require 'connection_adapters/postgresql_adapter'
24
+
25
+ # load the schema
26
+ DrNicMagicModels::Schema.load_schema
27
+
28
+
data/lib/module.rb CHANGED
@@ -1,18 +0,0 @@
1
- class Module
2
- alias :normal_const_missing :const_missing
3
-
4
- def const_missing(class_id)
5
- begin
6
- return normal_const_missing(class_id)
7
- rescue
8
- end
9
- unless table_name = DrNicMagicModels::Schema.models[class_id]
10
- raise NameError.new("uninitialized constant #{class_id}") if DrNicMagicModels::Schema.models.enquired? class_id
11
- end
12
- klass_code = lambda {klass_code}
13
- klass = Class.new ActiveRecord::Base, &klass_code
14
- const_set class_id, klass
15
- klass.set_table_name table_name
16
- klass
17
- end
18
- end
data/lib/rails.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Dependencies #:nodoc:#
2
+
3
+ @@models_dir = File.expand_path(File.join(RAILS_ROOT,'app','models'))
4
+
5
+ # don't reload models... it doesn't work anyway, not sure why they haven't done this?
6
+ # submit as patch?
7
+ alias require_or_load_old require_or_load
8
+ def require_or_load(file_name, *args)
9
+ file_name = $1 if file_name =~ /^(.*)\.rb$/
10
+ expanded = File.expand_path(file_name)
11
+ old_mechanism = mechanism
12
+ if expanded =~ /^#{@@models_dir}/
13
+ RAILS_DEFAULT_LOGGER.debug "*** Not reloading #{file_name}"
14
+ Dependencies.mechanism = :require
15
+ end
16
+ require_or_load_old(file_name, *args)
17
+ mechanism = old_mechanism
18
+ end
19
+ end
File without changes