declare_schema 0.1.0 → 0.2.0

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 (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