declare_schema 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +37 -0
  3. data/CHANGELOG.md +28 -4
  4. data/Gemfile +0 -2
  5. data/Gemfile.lock +1 -4
  6. data/README.md +59 -2
  7. data/Rakefile +13 -20
  8. data/gemfiles/rails_4.gemfile +4 -7
  9. data/gemfiles/rails_5.gemfile +4 -7
  10. data/gemfiles/rails_6.gemfile +4 -7
  11. data/lib/declare_schema/model.rb +0 -1
  12. data/lib/declare_schema/model/field_spec.rb +4 -14
  13. data/lib/declare_schema/version.rb +1 -1
  14. data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
  15. data/lib/generators/declare_schema/migration/migrator.rb +58 -38
  16. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
  17. data/lib/generators/declare_schema/support/eval_template.rb +12 -3
  18. data/lib/generators/declare_schema/support/model.rb +77 -2
  19. data/spec/lib/declare_schema/api_spec.rb +125 -0
  20. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
  21. data/spec/lib/declare_schema/generator_spec.rb +57 -0
  22. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
  23. data/spec/lib/declare_schema/migration_generator_spec.rb +686 -0
  24. data/spec/lib/declare_schema/prepare_testapp.rb +29 -0
  25. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
  26. data/spec/spec_helper.rb +26 -0
  27. metadata +9 -12
  28. data/.jenkins/Jenkinsfile +0 -72
  29. data/.jenkins/ruby_build_pod.yml +0 -19
  30. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
  31. data/test/api.rdoctest +0 -136
  32. data/test/doc-only.rdoctest +0 -76
  33. data/test/generators.rdoctest +0 -60
  34. data/test/interactive_primary_key.rdoctest +0 -56
  35. data/test/migration_generator.rdoctest +0 -846
  36. data/test/migration_generator_comments.rdoctestDISABLED +0 -74
  37. data/test/prepare_testapp.rb +0 -15
@@ -96,6 +96,20 @@ module Generators
96
96
  def connection
97
97
  ActiveRecord::Base.connection
98
98
  end
99
+
100
+ def fix_native_types(types)
101
+ case connection.class.name
102
+ when /mysql/i
103
+ types[:integer][:limit] ||= 11
104
+ types[:text][:limit] ||= 0xffff
105
+ types[:binary][:limit] ||= 0xffff
106
+ end
107
+ types
108
+ end
109
+
110
+ def native_types
111
+ @native_types ||= fix_native_types(connection.native_database_types)
112
+ end
99
113
  end
100
114
 
101
115
  def initialize(ambiguity_resolver = {})
@@ -106,37 +120,29 @@ module Generators
106
120
 
107
121
  attr_accessor :renames
108
122
 
123
+ # TODO: Add an application callback (maybe an initializer in a special group?) that
124
+ # the application can use to load other models that live in the database, to support DeclareSchema migrations
125
+ # for them.
109
126
  def load_rails_models
127
+ ActiveRecord::Migration.verbose = false
128
+
110
129
  Rails.application.eager_load!
130
+ Rails::Engine.subclasses.each(&:eager_load!)
111
131
  end
112
132
 
113
133
  # Returns an array of model classes that *directly* extend
114
134
  # ActiveRecord::Base, excluding anything in the CGI module
115
135
  def table_model_classes
116
136
  load_rails_models
117
- ActiveRecord::Base.send(:descendants).reject { |c| (c.base_class != c) || c.name.starts_with?("CGI::") }
137
+ ActiveRecord::Base.send(:descendants).select do |klass|
138
+ klass.base_class == klass && !klass.name.starts_with?("CGI::")
139
+ end
118
140
  end
119
141
 
120
142
  def connection
121
143
  self.class.connection
122
144
  end
123
145
 
124
- class << self
125
- def fix_native_types(types)
126
- case connection.class.name
127
- when /mysql/i
128
- types[:integer][:limit] ||= 11
129
- types[:text][:limit] ||= 0xffff
130
- types[:binary][:limit] ||= 0xffff
131
- end
132
- types
133
- end
134
-
135
- def native_types
136
- @native_types ||= fix_native_types(connection.native_database_types)
137
- end
138
- end
139
-
140
146
  def native_types
141
147
  self.class.native_types
142
148
  end
@@ -217,14 +223,24 @@ module Generators
217
223
  end
218
224
  end
219
225
 
220
- def always_ignore_tables
221
- # TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
222
- if defined?(CGI::Session::ActiveRecordStore::Session) &&
223
- defined?(ActionController::Base) &&
224
- ActionController::Base.session_store == CGI::Session::ActiveRecordStore
225
- sessions_table = CGI::Session::ActiveRecordStore::Session.table_name
226
- end
227
- ['schema_info', 'schema_migrations', 'simple_sesions', sessions_table].compact
226
+ def self.always_ignore_tables
227
+ sessions_table =
228
+ begin
229
+ if defined?(CGI::Session::ActiveRecordStore::Session) &&
230
+ defined?(ActionController::Base) &&
231
+ ActionController::Base.session_store == CGI::Session::ActiveRecordStore
232
+ CGI::Session::ActiveRecordStore::Session.table_name
233
+ end
234
+ rescue
235
+ nil
236
+ end
237
+
238
+ [
239
+ 'schema_info',
240
+ ActiveRecord::Base.try(:schema_migrations_table_name) || 'schema_migrations',
241
+ ActiveRecord::Base.try(:internal_metadata_table_name) || 'ar_internal_metadata',
242
+ sessions_table
243
+ ].compact
228
244
  end
229
245
 
230
246
  def generate
@@ -246,7 +262,7 @@ module Generators
246
262
  model_table_names = models_by_table_name.keys
247
263
 
248
264
  to_create = model_table_names - db_tables
249
- to_drop = db_tables - model_table_names - always_ignore_tables
265
+ to_drop = db_tables - model_table_names - self.class.always_ignore_tables
250
266
  to_change = model_table_names
251
267
  to_rename = extract_table_renames!(to_create, to_drop)
252
268
 
@@ -295,10 +311,6 @@ module Generators
295
311
  down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
296
312
 
297
313
  [up, down]
298
- rescue Exception => ex
299
- puts "Caught exception: #{ex}"
300
- puts ex.backtrace.join("\n")
301
- raise
302
314
  end
303
315
 
304
316
  def create_table(model)
@@ -398,14 +410,13 @@ module Generators
398
410
  spec = model.field_specs[c]
399
411
  if spec.different_to?(col) # TODO: DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
400
412
  change_spec = fk_field_options(model, c)
401
- change_spec[:limit] = spec.limit if (spec.sql_type != :text ||
413
+ change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
402
414
  ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
403
415
  (spec.limit || col.limit)
404
416
  change_spec[:precision] = spec.precision unless spec.precision.nil?
405
417
  change_spec[:scale] = spec.scale unless spec.scale.nil?
406
418
  change_spec[:null] = spec.null unless spec.null && col.null
407
419
  change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
408
- change_spec[:comment] = spec.comment unless spec.comment.nil? && (col.comment if col.respond_to?(:comment)).nil?
409
420
 
410
421
  changes << "change_column :#{new_table_name}, :#{c}, " +
411
422
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -473,7 +484,7 @@ module Generators
473
484
  return [[], []] if Migrator.disable_indexing
474
485
 
475
486
  new_table_name = model.table_name
476
- existing_fks = DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
487
+ existing_fks = ::DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
477
488
  model_fks = model.constraint_specs
478
489
  add_fks = model_fks - existing_fks
479
490
  drop_fks = existing_fks - model_fks
@@ -505,8 +516,7 @@ module Generators
505
516
  next if k == :null && v == true
506
517
  end
507
518
 
508
- next if k == :limit && type == :text &&
509
- (!::DeclareSchema::Model::FieldSpec.mysql_text_limits? || v == ::DeclareSchema::Model::FieldSpec::MYSQL_LONGTEXT_LIMIT)
519
+ next if k == :limit && type == :text && !::DeclareSchema::Model::FieldSpec.mysql_text_limits?
510
520
 
511
521
  if k.is_a?(Symbol)
512
522
  "#{k}: #{v.inspect}"
@@ -517,10 +527,20 @@ module Generators
517
527
  end
518
528
 
519
529
  def fk_field_options(model, field_name)
520
- if (foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }) && (parent_table = foreign_key.parent_table_name)
530
+ foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }
531
+ if foreign_key && (parent_table = foreign_key.parent_table_name)
521
532
  parent_columns = connection.columns(parent_table) rescue []
522
- pk_column = parent_columns.find { |column| column.name == "id" } # right now foreign keys assume id is the target
523
- pk_limit = pk_column ? pk_column.cast_type.limit : 8
533
+ pk_limit =
534
+ if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
535
+ if Rails::VERSION::MAJOR <= 4
536
+ pk_column.cast_type.limit
537
+ else
538
+ pk_column.limit
539
+ end
540
+ else
541
+ 8
542
+ end
543
+
524
544
  { limit: pk_limit }
525
545
  else
526
546
  {}
@@ -1,4 +1,4 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR > 4 ? '[4.2]' : '' %>
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration<%= ('[4.2]' if Rails::VERSION::MAJOR >= 5) %>
2
2
  def self.up
3
3
  <%= @up %>
4
4
  end
@@ -9,9 +9,18 @@ module DeclareSchema
9
9
  private
10
10
 
11
11
  def eval_template(template_name)
12
- source = File.expand_path(find_in_source_paths(template_name))
13
- context = instance_eval('binding')
14
- ERB.new(::File.binread(source), trim_mode: '-').result(context)
12
+ source = File.expand_path(find_in_source_paths(template_name))
13
+ erb = ERB.new(::File.read(source).force_encoding(Encoding::UTF_8), trim_mode: '>')
14
+ erb.filename = source
15
+ begin
16
+ erb.result(binding)
17
+ rescue Exception => ex
18
+ raise ex.class, <<~EOS
19
+ #{ex.message}
20
+ #{erb.src}
21
+ #{ex.backtrace.join("\n ")}
22
+ EOS
23
+ end
15
24
  end
16
25
  end
17
26
  end
@@ -4,6 +4,39 @@ require_relative './eval_template'
4
4
 
5
5
  module DeclareSchema
6
6
  module Support
7
+ class IndentedBuffer
8
+ def initialize(indent: 0)
9
+ @string = +""
10
+ @indent = indent
11
+ @column = 0
12
+ @indent_amount = 2
13
+ end
14
+
15
+ def to_string
16
+ @string
17
+ end
18
+
19
+ def indent!
20
+ @indent += @indent_amount
21
+ yield
22
+ @indent -= @indent_amount
23
+ end
24
+
25
+ def newline!
26
+ @column = 0
27
+ @string << "\n"
28
+ end
29
+
30
+ def <<(str)
31
+ if (difference = @indent - @column) > 0
32
+ @string << ' ' * difference
33
+ end
34
+ @column += difference
35
+ @string << str
36
+ newline!
37
+ end
38
+ end
39
+
7
40
  module Model
8
41
  class << self
9
42
  def included(base)
@@ -26,9 +59,51 @@ module DeclareSchema
26
59
 
27
60
  def inject_declare_schema_code_into_model_file
28
61
  gsub_file(model_path, / # attr_accessible :title, :body\n/m, "")
29
- inject_into_class model_path, class_name do
30
- eval_template('model_injection.rb.erb')
62
+ inject_into_class(model_path, class_name) do
63
+ declare_model_fields_and_associations
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def declare_model_fields_and_associations
70
+ buffer = ::DeclareSchema::Support::IndentedBuffer.new(indent: 2)
71
+ buffer.newline!
72
+ buffer << 'fields do'
73
+ buffer.indent! do
74
+ field_attributes.each do |attribute|
75
+ decl = "%-#{max_attribute_length}s" % attribute.name + ' ' +
76
+ attribute.type.to_sym.inspect +
77
+ case attribute.type.to_s
78
+ when 'string'
79
+ ', limit: 255'
80
+ else
81
+ ''
82
+ end
83
+ buffer << decl
84
+ end
85
+ if options[:timestamps]
86
+ buffer.newline!
87
+ buffer << 'timestamps'
88
+ end
89
+ end
90
+ buffer << 'end'
91
+
92
+ if bts.any?
93
+ buffer.newline!
94
+ bts.each do |bt|
95
+ buffer << "belongs_to #{bt.to_sym.inspect}"
96
+ end
31
97
  end
98
+ if hms.any?
99
+ buffer.newline
100
+ hms.each do |hm|
101
+ buffer << "has_many #{hm.to_sym.inspect}, dependent: :destroy"
102
+ end
103
+ end
104
+ buffer.newline!
105
+
106
+ buffer.to_string
32
107
  end
33
108
 
34
109
  protected
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'rails/generators'
5
+
6
+ RSpec.describe 'DeclareSchema API' do
7
+ before do
8
+ load File.expand_path('prepare_testapp.rb', __dir__)
9
+ end
10
+
11
+ describe 'example models' do
12
+ it 'generates a model' do
13
+ expect(system("bundle exec rails generate declare_schema:model advert title:string body:text")).to be_truthy
14
+
15
+ # The above will generate the test, fixture and a model file like this:
16
+ # model_declaration = Rails::Generators.invoke('declare_schema:model', ['advert2', 'title:string', 'body:text'])
17
+ # expect(model_declaration.first).to eq([["Advert"], nil, "app/models/advert.rb", nil,
18
+ # [["AdvertTest"], "test/models/advert_test.rb", nil, "test/fixtures/adverts.yml"]])
19
+
20
+ expect(File.read("#{TESTAPP_PATH}/app/models/advert.rb")).to eq(<<~EOS)
21
+ class Advert < #{active_record_base_class}
22
+
23
+ fields do
24
+ title :string, limit: 255
25
+ body :text
26
+ end
27
+
28
+ end
29
+ EOS
30
+ system("rm -rf #{TESTAPP_PATH}/app/models/advert2.rb #{TESTAPP_PATH}/test/models/advert2.rb #{TESTAPP_PATH}/test/fixtures/advert2.rb")
31
+
32
+ # The migration generator uses this information to create a migration.
33
+ # The following creates and runs the migration:
34
+
35
+ expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
36
+
37
+ # We're now ready to start demonstrating the API
38
+
39
+ Rails.application.config.autoload_paths += ["#{TESTAPP_PATH}/app/models"]
40
+
41
+ $LOAD_PATH << "#{TESTAPP_PATH}/app/models"
42
+
43
+ unless Rails::VERSION::MAJOR >= 6
44
+ # TODO: get this to work on Travis for Rails 6
45
+ Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
46
+ end
47
+
48
+ require 'advert'
49
+
50
+ ## The Basics
51
+
52
+ # The main feature of DeclareSchema, aside from the migration generator, is the ability to declare rich types for your fields. For example, you can declare that a field is an email address, and the field will be automatically validated for correct email address syntax.
53
+
54
+ ### Field Types
55
+
56
+ # Field values are returned as the type you specify.
57
+
58
+ Advert.destroy_all
59
+
60
+ a = Advert.new(body: "This is the body", id: 1, title: "title")
61
+ expect(a.body).to eq("This is the body")
62
+
63
+ # This also works after a round-trip to the database
64
+
65
+ a.save!
66
+ expect(a.reload.body).to eq("This is the body")
67
+
68
+ ## Names vs. Classes
69
+
70
+ ## Model extensions
71
+
72
+ # DeclareSchema adds a few features to your models.
73
+
74
+ ### `Model.attr_type`
75
+
76
+ # Returns the type (i.e. class) declared for a given field or attribute
77
+
78
+ Advert.connection.schema_cache.clear!
79
+ Advert.reset_column_information
80
+
81
+ expect(Advert.attr_type(:title)).to eq(String)
82
+ expect(Advert.attr_type(:body)).to eq(String)
83
+
84
+ ## Field validations
85
+
86
+ # DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
87
+
88
+ ### Required fields
89
+
90
+ # The `:required` argument to a field gives a `validates_presence_of`:
91
+
92
+ class AdvertWithRequiredTitle < ActiveRecord::Base
93
+ self.table_name = 'adverts'
94
+
95
+ fields do
96
+ title :string, :required, limit: 255
97
+ end
98
+ end
99
+
100
+ a = AdvertWithRequiredTitle.new
101
+ expect(a.valid? || a.errors.full_messages).to eq(["Title can't be blank"])
102
+ a.id = 2
103
+ a.body = "hello"
104
+ a.title = "Jimbo"
105
+ a.save!
106
+
107
+ ### Unique fields
108
+
109
+ # The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
110
+
111
+ class AdvertWithUniqueTitle < ActiveRecord::Base
112
+ self.table_name = 'adverts'
113
+
114
+ fields do
115
+ title :string, :unique, limit: 255
116
+ end
117
+ end
118
+
119
+ a = AdvertWithUniqueTitle.new :title => "Jimbo", id: 3, body: "hello"
120
+ expect(a.valid? || a.errors.full_messages).to eq(["Title has already been taken"])
121
+ a.title = "Sambo"
122
+ a.save!
123
+ end
124
+ end
125
+ end
@@ -3,11 +3,15 @@
3
3
  require_relative '../../../lib/declare_schema/field_declaration_dsl'
4
4
 
5
5
  RSpec.describe DeclareSchema::FieldDeclarationDsl do
6
- class TestModel < ActiveRecord::Base
7
- fields do
8
- name :string, limit: 127
6
+ before do
7
+ load File.expand_path('prepare_testapp.rb', __dir__)
9
8
 
10
- timestamps
9
+ class TestModel < ActiveRecord::Base
10
+ fields do
11
+ name :string, limit: 127
12
+
13
+ timestamps
14
+ end
11
15
  end
12
16
  end
13
17
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'DeclareSchema Migration Generator' do
4
+ before do
5
+ load File.expand_path('prepare_testapp.rb', __dir__)
6
+ end
7
+
8
+ it "generates nested models" do
9
+ Rails::Generators.invoke('declare_schema:model', %w[alpha/beta one:string two:integer])
10
+
11
+ expect(File.exist?('app/models/alpha/beta.rb')).to be_truthy
12
+
13
+ expect(File.read('app/models/alpha/beta.rb')).to eq(<<~EOS)
14
+ class Alpha::Beta < #{active_record_base_class}
15
+
16
+ fields do
17
+ one :string, limit: 255
18
+ two :integer
19
+ end
20
+
21
+ end
22
+ EOS
23
+
24
+ expect(File.read('app/models/alpha.rb')).to eq(<<~EOS)
25
+ module Alpha
26
+ def self.table_name_prefix
27
+ 'alpha_'
28
+ end
29
+ end
30
+ EOS
31
+
32
+ expect(File.read('test/models/alpha/beta_test.rb')).to eq(<<~EOS)
33
+ require 'test_helper'
34
+
35
+ class Alpha::BetaTest < ActiveSupport::TestCase
36
+ # test "the truth" do
37
+ # assert true
38
+ # end
39
+ end
40
+ EOS
41
+
42
+ expect(File.exist?('test/fixtures/alpha/beta.yml')).to be_truthy
43
+
44
+ $LOAD_PATH << "#{TESTAPP_PATH}/app/models"
45
+
46
+ expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
47
+
48
+ expect(File.exist?('db/schema.rb')).to be_truthy
49
+
50
+ expect(File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")).to be_truthy
51
+
52
+ module Alpha; end
53
+ require 'alpha/beta'
54
+
55
+ expect { Alpha::Beta }.to_not raise_exception
56
+ end
57
+ end