oracle-model-generator 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,17 @@
1
+ = 0.3.0 - 12-May-2011
2
+ * Added automatic test generation. In addition to the active record model file,
3
+ a test file will be generated that includes a series of stock tests that
4
+ are based on the model's validations. At the moment only test-unit is
5
+ supported, but I'll eventually add rspec.
6
+ * Added the -x/--tests command line option for the new test generation feature.
7
+ * The dependencies method now returns an array of dependencies for the given
8
+ table or view.
9
+ * The table method has been altered. It now returns just the plain, uppercased
10
+ name of the table. A new method called 'model' now returns the active record
11
+ model name.
12
+ * The default output file name will now chop any trailing 's' characters.
13
+ * More tests and comments added.
14
+
1
15
  = 0.2.1 - 3-May-2011
2
16
  * Fixed a bug where the omg binary blew up if you didn't have the dbi-dbrc
3
17
  library installed. It's supposed to be optional. Thanks go to Jason Roth
data/README CHANGED
@@ -49,6 +49,43 @@ The omg library will generate this:
49
49
  validates :country_id, :length => {:maximum => 2}
50
50
  end
51
51
 
52
+ It will also generate a corresponding test file using test-unit 2 by default.
53
+ For the above example you will see some tests like this:
54
+
55
+ class TC_Location < Test::Unit::TestCase
56
+ def setup
57
+ @location = Location.new
58
+ end
59
+
60
+ test 'table name is locations' do
61
+ assert_equal('locations', Location.table_name)
62
+ end
63
+
64
+ test 'primary key is location_id' do
65
+ assert_equal('location_id', Location.primary_key)
66
+ end
67
+
68
+ test 'location_id basic functionality' do
69
+ assert_respond_to(@location, :location_id)
70
+ assert_nothing_raised{ @location.location_id }
71
+ assert_kind_of(Numeric, @location.location_id)
72
+ end
73
+
74
+ test 'location_id must be a number' do
75
+ @location.location_id = 'test_string'
76
+ assert_false(@location.valid?)
77
+ assert_true(@location.errors[:location_id].include?('is not a number'))
78
+ end
79
+
80
+ test 'location_id cannot exceed the value 9999' do
81
+ @location.location_id = 10000
82
+ assert_false(@location.valid?)
83
+ assert_true(@location.errors[:location_id].include?('must be less than or equal to 9999'))
84
+ end
85
+
86
+ # ... and so on.
87
+ end
88
+
52
89
  = Requirements
53
90
  == Must Have
54
91
  * ruby-oci8
@@ -85,7 +122,7 @@ primary keys.
85
122
 
86
123
  = Future Plans
87
124
  Add support for views.
88
- Add automatic test suite generation.
125
+ Add automatic test suite generation for rspec.
89
126
  Explicitly set :foreign_key if using CPK in belongs_to relationships.
90
127
  The output could use a little formatting love.
91
128
 
data/bin/omg CHANGED
@@ -18,7 +18,8 @@ opts = Getopt::Long.getopts(
18
18
  ['--password', '-p', Getopt::REQUIRED],
19
19
  ['--database', '-d', Getopt::REQUIRED],
20
20
  ['--output', '-o', Getopt::REQUIRED],
21
- ['--rails', '-r', Getopt::REQUIRED]
21
+ ['--rails', '-r', Getopt::REQUIRED],
22
+ ['--tests', '-x', Getopt::OPTIONAL]
22
23
  )
23
24
 
24
25
  def help
@@ -33,6 +34,7 @@ def help
33
34
  -u, --user => The user used to establish a connection to the database.
34
35
  -p, --password => The password used to establish a connection to the database.
35
36
  -r, --rails => The version of rails you're using (2 or 3).
37
+ -x, --tests => Generate tests (the default).
36
38
 
37
39
  If no user or password are supplied, then OMG will attempt to glean that
38
40
  information using a combination of the database name and your .dbrc file.
@@ -44,6 +46,10 @@ def help
44
46
  for me to deal with all possible permutations. Note that the output file
45
47
  name is also used as the basis for the class name
46
48
 
49
+ If no tests option is specified then a test file, using test-unit 2, will
50
+ be generated that includes some basic tests to backup the builtin
51
+ validations. Legal options are 'testunit', 'rspec', or 'none'.
52
+
47
53
  Examples:
48
54
 
49
55
  # Create a User model for the users table (rails 3)
@@ -109,7 +115,13 @@ connection = OCI8.new(user, pass, opts['database'])
109
115
  omg = Oracle::Model::Generator.new(connection)
110
116
  omg.generate(table || view, view)
111
117
 
112
- ofile = opts['o'] || table + '.rb'
118
+ ofile = opts['o']
119
+
120
+ unless ofile
121
+ ofile = omg.table.downcase
122
+ ofile.chop! if ofile[-1].chr.upcase == 'S'
123
+ ofile += '.rb'
124
+ end
113
125
 
114
126
  # Default to Rails 3
115
127
  rails = (opts['rails'] && opts['rails'].to_i) || 3
@@ -120,7 +132,7 @@ if rails < 2 || rails > 3
120
132
  end
121
133
 
122
134
  File.open(ofile, 'w') do |fh|
123
- fh.puts "class #{omg.table} < ActiveRecord::Base"
135
+ fh.puts "class #{omg.model} < ActiveRecord::Base"
124
136
  fh.puts " set_table_name :#{table}"
125
137
 
126
138
  if omg.primary_keys.size > 1
@@ -135,7 +147,7 @@ File.open(ofile, 'w') do |fh|
135
147
  fh.puts " belongs_to :#{table.downcase}"
136
148
  }
137
149
 
138
- fh.puts "\n # Validations\n\n"
150
+ fh.puts "\n # Validations\n"
139
151
 
140
152
  if rails == 2
141
153
  # Character fields, size
@@ -185,7 +197,18 @@ File.open(ofile, 'w') do |fh|
185
197
  when 'char', 'varchar', 'varchar2'
186
198
  validation = "validates :#{col.name.downcase}, "
187
199
  validation << ":length => {:maximum => #{col.data_size}}"
188
- validation << ", :presence => #{!col.nullable?}" unless col.nullable?
200
+
201
+ format_str = ",\n :format => { :with => /[:alpha]/, :message => 'is not a string'"
202
+
203
+ if col.nullable?
204
+ format_str += ", :if => :#{col.name.downcase}? }"
205
+ else
206
+ format_str += " }"
207
+ end
208
+
209
+ validation << format_str
210
+ validation << ",\n :presence => #{!col.nullable?}" unless col.nullable?
211
+ validation << "\n\n"
189
212
  when 'number'
190
213
  max = "9" * col.precision
191
214
  max.insert(col.precision - col.scale, ".") if col.scale > 0
@@ -197,14 +220,14 @@ File.open(ofile, 'w') do |fh|
197
220
  end
198
221
 
199
222
  validation << ":numericality => {"
200
- validation << "\n :less_than_or_equal_to => #{max}, "
201
- validation << "\n :greater_than_or_equal_to => -#{max}"
223
+ validation << "\n :less_than_or_equal_to => #{max}, "
224
+ validation << "\n :greater_than_or_equal_to => -#{max}"
202
225
 
203
226
  if col.scale == 0
204
- validation << ",\n :only_integer => true"
227
+ validation << ",\n :only_integer => true"
205
228
  end
206
229
 
207
- validation << "\n }"
230
+ validation << "\n }\n\n"
208
231
  end
209
232
 
210
233
  fh.puts " #{validation}"
@@ -238,4 +261,160 @@ File.open(ofile, 'w') do |fh|
238
261
  fh.puts "end"
239
262
  end
240
263
 
264
+ if opts['x'].nil? || opts['x'] == 'testunit'
265
+ test_file = "test_#{ofile}"
266
+ File.open(test_file, "w") do |fh|
267
+ setup_var = omg.table.downcase
268
+ setup_var.chop! if setup_var[-1].chr.downcase == 's'
269
+ instance_var = "@#{setup_var}"
270
+
271
+ # This bit is necessary to force test-unit 2 instead of using the library
272
+ # that ships as part of the Ruby standard library.
273
+ fh.puts "require 'rubygems'"
274
+ fh.puts "gem 'test-unit'"
275
+ fh.puts "require 'test/unit'\n\n"
276
+
277
+ fh.puts "class TC_#{omg.model} < Test::Unit::TestCase\n"
278
+ fh.puts " def setup"
279
+ fh.puts " #{instance_var} = #{omg.model}.new"
280
+ fh.puts " end\n\n"
281
+
282
+ fh.puts " test 'table name is #{omg.table.downcase}' do"
283
+ fh.puts " assert_equal('#{omg.table.downcase}', #{omg.model}.table_name)"
284
+ fh.puts " end\n\n"
285
+
286
+ if omg.primary_keys.size > 1
287
+ test_string = " test 'primary keys are #{omg.primary_keys.join(', ')}' do"
288
+ fh.puts test_string
289
+ fh.puts " assert_equal('#{omg.primary_keys.join(', ')}', #{omg.model}.primary_keys)"
290
+ else
291
+ test_string = " test 'primary key is #{omg.primary_keys.first}' do"
292
+ fh.puts test_string
293
+ fh.puts " assert_equal('#{omg.primary_keys.first}', #{omg.model}.primary_key)"
294
+ end
295
+
296
+ fh.puts " end\n"
297
+
298
+ omg.column_info.each{ |col|
299
+ data_type = col.data_type.to_s
300
+ column = col.name.downcase
301
+
302
+ fh.puts "\n test '#{column} basic functionality' do"
303
+ fh.puts " assert_respond_to(#{instance_var}, :#{column})"
304
+ fh.puts " assert_nothing_raised{ #{instance_var}.#{column} }"
305
+
306
+ case data_type
307
+ when 'char', 'varchar', 'varchar2'
308
+ if col.nullable?
309
+ fh.puts " assert_kind_of([NilClass, String], #{instance_var}.#{column})"
310
+ else
311
+ fh.puts " assert_kind_of(String, #{instance_var}.#{column})"
312
+ end
313
+ when 'number'
314
+ if col.nullable?
315
+ fh.puts " assert_kind_of([NilClass, Numeric], #{instance_var}.#{column})"
316
+ else
317
+ fh.puts " assert_kind_of(Numeric, #{instance_var}.#{column})"
318
+ end
319
+ when 'date'
320
+ if col.nullable?
321
+ fh.puts " assert_kind_of([NilClass, DateTime, Time], #{instance_var}.#{column})"
322
+ else
323
+ fh.puts " assert_kind_of([DateTime, Time], #{instance_var}.#{column})"
324
+ end
325
+ end
326
+
327
+ fh.puts " end\n"
328
+
329
+ case data_type
330
+ when 'char', 'varchar', 'varchar2'
331
+ test_title = "\n test '#{column} must be a string"
332
+ test_title += " if present" if col.nullable?
333
+ test_title += "' do"
334
+
335
+ fh.puts test_title
336
+ fh.puts " #{instance_var}.#{column} = #{rand(100)}"
337
+ fh.puts " assert_false(#{instance_var}.valid?)"
338
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?('is not a string'))"
339
+ fh.puts " end\n"
340
+
341
+ max_len = col.data_size
342
+ err_msg = "is too long (maximum is #{max_len} characters)"
343
+
344
+ fh.puts "\n test '#{column} cannot exceed #{max_len} characters' do"
345
+ fh.puts " #{instance_var}.#{column} = 'a' * #{max_len + 1}"
346
+ fh.puts " assert_false(#{instance_var}.valid?)"
347
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?('#{err_msg}'))"
348
+ fh.puts " end\n"
349
+ when 'number'
350
+ test_title = "\n test '#{column} must be a number"
351
+ test_title += " if present" if col.nullable?
352
+ test_title += "' do"
353
+
354
+ fh.puts test_title
355
+ fh.puts " #{instance_var}.#{column} = 'test_string'"
356
+ fh.puts " assert_false(#{instance_var}.valid?)"
357
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?('is not a number'))"
358
+ fh.puts " end\n"
359
+
360
+ max = "9" * col.precision
361
+ max.insert(col.precision - col.scale, ".") if col.scale > 0
362
+ err_msg = "must be less than or equal to #{max}"
363
+
364
+ fh.puts "\n test '#{column} cannot exceed the value #{max}' do"
365
+
366
+ if col.scale > 0
367
+ fh.puts " #{instance_var}.#{column} = #{max.to_f.round}"
368
+ else
369
+ fh.puts " #{instance_var}.#{column} = #{max.to_i + 1}"
370
+ end
371
+
372
+ fh.puts " assert_false(#{instance_var}.valid?)"
373
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?('#{err_msg}'))"
374
+ fh.puts " end\n"
375
+
376
+ err_msg = "must be greater than or equal to -#{max}"
377
+
378
+ fh.puts "\n test '#{column} cannot be less than the value -#{max}' do"
379
+
380
+ if col.scale > 0
381
+ fh.puts " #{instance_var}.#{column} = -#{max.to_f.round}"
382
+ else
383
+ fh.puts " #{instance_var}.#{column} = -#{max.to_i + 1}"
384
+ end
385
+
386
+ fh.puts " assert_false(#{instance_var}.valid?)"
387
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?('#{err_msg}'))"
388
+ fh.puts " end\n"
389
+ end
390
+
391
+ unless col.nullable?
392
+ err_msg = "can't be blank"
393
+ fh.puts "\n test '#{column} cannot be nil' do"
394
+ fh.puts " #{instance_var}.#{column} = nil"
395
+ fh.puts " assert_false(#{instance_var}.valid?)"
396
+ fh.puts " assert_true(#{instance_var}.errors[:#{column}].include?(\"#{err_msg}\"))"
397
+ fh.puts " end\n"
398
+ end
399
+ }
400
+
401
+ fh.puts "\n def teardown"
402
+ fh.puts " @#{setup_var} = nil"
403
+ fh.puts " end"
404
+
405
+ fh.puts "end"
406
+ end
407
+ puts "\nTest file '#{test_file}' generated\n"
408
+ else
409
+ if opts['x'] == 'rspec'
410
+ puts "\nrspec not yet supported for generating tests.\n"
411
+ elsif opts['x'] != 'none'
412
+ puts "\nInvalid option to -x/--tests: " + opts['x']
413
+ else
414
+ puts "\nSkipping test file genertion.\n"
415
+ end
416
+ end
417
+
418
+ omg.connection.logoff if omg.connection
419
+
241
420
  puts "\nFile '#{ofile}' generated\n\n"
@@ -3,18 +3,43 @@ require 'oci8'
3
3
  module Oracle
4
4
  module Model
5
5
  class Generator
6
- VERSION = '0.2.1'
6
+ # The version of the oracle-model-generator library
7
+ VERSION = '0.3.0'
7
8
 
9
+ # The raw OCI8 connection.
8
10
  attr_reader :connection
11
+
12
+ # An array of hashes that contain per-column constraint information.
9
13
  attr_reader :constraints
14
+
15
+ # An array of foreign key names.
10
16
  attr_reader :foreign_keys
17
+
18
+ # An array of parent tables that the table has a foreign key
19
+ # relationship with.
11
20
  attr_reader :belongs_to
21
+
22
+ # The table name associated with the generator.
12
23
  attr_reader :table
24
+
25
+ # The name of the active record model to be generated.
26
+ attr_reader :model
27
+
28
+ # Boolean indicating whether the generator is for a regular table or a view.
13
29
  attr_reader :view
30
+
31
+ # A list of dependencies for the table.
14
32
  attr_reader :dependencies
33
+
34
+ # An array of raw OCI::Metadata::Column objects.
15
35
  attr_reader :column_info
36
+
37
+ # An array of primary keys for the column. May contain one or more values.
16
38
  attr_reader :primary_keys
17
39
 
40
+ # Creates and returns a new Oracle::Model::Generator object. It accepts
41
+ # an Oracle::Connection object, which is what OCI8.new returns.
42
+ #
18
43
  # Example:
19
44
  #
20
45
  # connection = Oracle::Connection.new(user, password, database)
@@ -30,6 +55,7 @@ module Oracle
30
55
  @belongs_to = []
31
56
  @column_info = []
32
57
  @table = nil
58
+ @model = nil
33
59
  end
34
60
 
35
61
  # Generates an Oracle::Model::Generator object for +table+. If this is
@@ -38,20 +64,33 @@ module Oracle
38
64
  # This method does not actually generate a file of any sort. It merely
39
65
  # sets instance variables which you can then use in your own class/file
40
66
  # generation programs.
67
+ #--
68
+ # Makes a best guess as to the model name. I'm not going to put too much
69
+ # effort into this. It's much easier for you to hand edit a class name
70
+ # than it is for me to parse English.
41
71
  #
42
72
  def generate(table, view = false)
43
- @table = table.split('_').map{ |e| e.downcase.capitalize }.join
73
+ @table = table.upcase
74
+ @model = table.split('_').map{ |e| e.downcase.capitalize }.join
44
75
  @view = view
45
- get_constraints(table) unless view
46
- get_foreign_keys unless view
47
- get_column_info(table) unless view
76
+
77
+ # Remove trailing 's'
78
+ @model.chop! if @model[-1].chr.upcase == 'S'
79
+
80
+ unless view
81
+ get_constraints
82
+ get_foreign_keys
83
+ get_column_info
84
+ end
85
+
48
86
  get_primary_keys
87
+ get_dependencies
49
88
  end
50
89
 
51
90
  private
52
91
 
53
- def get_column_info(table_name)
54
- table = @connection.describe_table(table_name)
92
+ def get_column_info
93
+ table = @connection.describe_table(@table)
55
94
  table.columns.each{ |col| @column_info << col }
56
95
  end
57
96
 
@@ -107,14 +146,14 @@ module Oracle
107
146
 
108
147
  # Get a list of constraints for a given table.
109
148
  #
110
- def get_constraints(table_name)
149
+ def get_constraints
111
150
  sql = %Q{
112
151
  select *
113
152
  from all_cons_columns a, all_constraints b
114
153
  where a.owner = b.owner
115
154
  and a.constraint_name = b.constraint_name
116
155
  and a.table_name = b.table_name
117
- and b.table_name = '#{table_name.upcase}'
156
+ and b.table_name = '#{@table}'
118
157
  }
119
158
 
120
159
  begin
@@ -127,6 +166,25 @@ module Oracle
127
166
  end
128
167
  end
129
168
 
169
+ # An array of hashes indicating objects that are dependent on the table.
170
+ #
171
+ def get_dependencies
172
+ sql = %Q{
173
+ select *
174
+ from all_dependencies dep
175
+ where referenced_name = '#{@table}'
176
+ }
177
+
178
+ begin
179
+ cursor = @connection.exec(sql)
180
+ while rec = cursor.fetch_hash
181
+ @dependencies << rec
182
+ end
183
+ ensure
184
+ cursor.close if cursor
185
+ end
186
+ end
187
+
130
188
  end
131
189
  end
132
190
  end
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = 'oracle-model-generator'
5
- gem.version = '0.2.1'
5
+ gem.version = '0.3.0'
6
6
  gem.author = 'Daniel J. Berger'
7
7
  gem.license = 'Artistic 2.0'
8
8
  gem.email = 'djberg96@gmail.com'
@@ -1,3 +1,10 @@
1
+ #############################################################################
2
+ # test_oracle_model_generator.rb
3
+ #
4
+ # Test suite for the oracle-model-generator library. For testing purposes
5
+ # I'm using the 'hr' database that comes as part of the Oracle Express
6
+ # edition which you can download from oracle.com. Adjust as necessary.
7
+ #############################################################################
1
8
  require 'rubygems'
2
9
  gem 'test-unit'
3
10
  require 'test/unit'
@@ -13,7 +20,7 @@ class TC_Oracle_Model_Generator < Test::Unit::TestCase
13
20
  end
14
21
 
15
22
  test "version number is correct" do
16
- assert_equal('0.2.1', Oracle::Model::Generator::VERSION)
23
+ assert_equal('0.3.0', Oracle::Model::Generator::VERSION)
17
24
  end
18
25
 
19
26
  test "constructor accepts an oci8 connection object" do
@@ -26,11 +33,33 @@ class TC_Oracle_Model_Generator < Test::Unit::TestCase
26
33
  end
27
34
 
28
35
  test "generate method works with a table name or view" do
29
- assert_nothing_raised{ @generator = Oracle::Model::Generator.new(@connection) }
36
+ @generator = Oracle::Model::Generator.new(@connection)
30
37
  assert_nothing_raised{ @generator.generate('employees') }
31
38
  assert_nothing_raised{ @generator.generate('emp_details_view', true) }
32
39
  end
33
40
 
41
+ test "model method returns active record model name" do
42
+ @generator = Oracle::Model::Generator.new(@connection)
43
+ @generator.generate('emp_details_view', true)
44
+ assert_respond_to(@generator, :model)
45
+ assert_equal('EmpDetailsView', @generator.model)
46
+ end
47
+
48
+ test "table method returns uppercased table name passed to generate method" do
49
+ @generator = Oracle::Model::Generator.new(@connection)
50
+ @generator.generate('emp_details_view', true)
51
+ assert_respond_to(@generator, :table)
52
+ assert_equal('EMP_DETAILS_VIEW', @generator.table)
53
+ end
54
+
55
+ test "dependencies method returns an array of hashes" do
56
+ @generator = Oracle::Model::Generator.new(@connection)
57
+ @generator.generate('employees', true)
58
+ assert_respond_to(@generator, :dependencies)
59
+ assert_kind_of(Array, @generator.dependencies)
60
+ assert_kind_of(Hash, @generator.dependencies.first)
61
+ end
62
+
34
63
  def teardown
35
64
  @username = nil
36
65
  @password = nil
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oracle-model-generator
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 1
10
- version: 0.2.1
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Daniel J. Berger
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-03 00:00:00 -06:00
18
+ date: 2011-05-12 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency