declare_schema 0.1.3 → 0.2.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5a1596f905dad5d9dd2f08787607c7b9adc742bbb54702bf4cf4aca812e6912
4
- data.tar.gz: 60ec900d6aeb5981d0dd2df7dc689ad01a243e3c1f5b575f33813f5196614c26
3
+ metadata.gz: 8ce85b3e273a189a4ab8b48e333c2f12b2aea4962c0c277ae3c289f1904cddd8
4
+ data.tar.gz: d12345f0a483effa2724e0cd315efa759b2ace23e82ef9a1ae2cdaae28aa2571
5
5
  SHA512:
6
- metadata.gz: b5dc90e36aeb33f00426de19f9df2b85d2bd40754d3bbd6bb8ff8ba410dda808139434d656e305cb9c13320e3cc4130613f06c06ef5abef7e34be4f3ed55e498
7
- data.tar.gz: 39e445fb41afbf2a91dcaa1271040ede91997abcd8e6237616f1e24983d5e88c5aa77c23bcd6c98c9b104f1c8ad68b9456c34a067a807044c2d72893f52e7b3b
6
+ metadata.gz: 84cfbdd55c37a0404b672fbbca5c12dfeee765b0582cdf369b8b3f95159cfb8f82eb3fb1e6149e830ba4fe4481d6df6ef4df45398e30985380ae6681c731f8a9
7
+ data.tar.gz: adeda560e6bb859104b78e3710fb94d438606cf42c10b199527e5d14545505dda6dde3206500a5f7ea38b79530952182954f5e3892779f9d96db9270d59a28f1
@@ -4,6 +4,13 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
5
  Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0] - Unreleased
8
+ ### Added
9
+ - Automatically eager_load! all Rails::Engines before generating migrations.
10
+
11
+ ### Changed
12
+ - Changed tests from rdoctest to rspec.
13
+
7
14
  ## [0.1.3] - Unreleased
8
15
  ### Changed
9
16
  - Updated the `always_ignore_tables` list in `Migrator` to access Rails metadata table names
@@ -18,6 +25,7 @@ using the appropriate Rails configuration attributes.
18
25
  ### Added
19
26
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
20
27
 
28
+ [0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
21
29
  [0.1.3]: https://github.com/Invoca/declare_schema/compare/v0.1.2...v0.1.3
22
30
  [0.1.2]: https://github.com/Invoca/declare_schema/compare/v0.1.1...v0.1.2
23
31
  [0.1.1]: https://github.com/Invoca/declare_schema/tree/v0.1.1
data/Gemfile CHANGED
@@ -18,6 +18,5 @@ gem 'rails', '~> 5.2', '>= 5.2.4.3'
18
18
  gem 'responders'
19
19
  gem 'rspec'
20
20
  gem 'rubocop'
21
- gem 'rubydoctest'
22
21
  gem 'sqlite3'
23
22
  gem 'yard'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.1.3)
4
+ declare_schema (0.2.0.pre.1)
5
5
  rails (>= 4.2)
6
6
 
7
7
  GEM
@@ -159,7 +159,6 @@ GEM
159
159
  rubocop-ast (0.4.2)
160
160
  parser (>= 2.7.1.4)
161
161
  ruby-progressbar (1.10.1)
162
- rubydoctest (1.1.5)
163
162
  sprockets (4.0.2)
164
163
  concurrent-ruby (~> 1.0)
165
164
  rack (> 1, < 3)
@@ -194,7 +193,6 @@ DEPENDENCIES
194
193
  responders
195
194
  rspec
196
195
  rubocop
197
- rubydoctest
198
196
  sqlite3
199
197
  yard
200
198
 
data/Rakefile CHANGED
@@ -14,7 +14,6 @@ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
14
14
  require 'declare_schema'
15
15
 
16
16
  RUBY = 'ruby'
17
- RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} -S rubydoctest"
18
17
  GEM_ROOT = __dir__
19
18
  TESTAPP_PATH = ENV['TESTAPP_PATH'] || File.join(Dir.tmpdir, 'declare_schema_testapp')
20
19
  BIN = File.expand_path('bin/declare_schema', __dir__)
@@ -24,13 +23,7 @@ task default: 'test:all'
24
23
  include Rake::DSL
25
24
 
26
25
  namespace "test" do
27
- task all: [:doctest, :spec]
28
-
29
- desc "Run the doctests"
30
- task :doctest do |_t|
31
- files = Dir['test/*.rdoctest'].sort.map { |f| File.expand_path(f) }.join(' ')
32
- system("#{RUBYDOCTEST} --trace --verbose #{files}") or exit(1)
33
- end
26
+ task all: :spec
34
27
 
35
28
  desc "Prepare a rails application for testing"
36
29
  task :prepare_testapp, :force do |_t, args|
@@ -38,19 +31,19 @@ namespace "test" do
38
31
  FileUtils.remove_entry_secure(TESTAPP_PATH, true)
39
32
  sh %(#{BIN} new #{TESTAPP_PATH} --skip-wizard --skip-bundle)
40
33
  FileUtils.chdir TESTAPP_PATH
41
- sh %(bundle install)
42
- sh %(echo "" >> Gemfile)
43
- sh %(echo "gem 'irt', :group => :development" >> Gemfile) # to make the bundler happy
44
- sh %(echo "gem 'therubyracer'" >> Gemfile)
45
- sh %(echo "gem 'kramdown'" >> Gemfile)
46
- sh %(echo "" > app/models/.gitignore) # because git reset --hard would rm the dir
47
- rm %(.gitignore) # we need to reset everything in a testapp
48
- sh %(git init && git add . && git commit -m "initial commit")
49
- puts %(The testapp has been created in '#{TESTAPP_PATH}')
34
+ sh "bundle install"
35
+ sh "(echo '';
36
+ echo \"gem 'irt', :group => :development\";
37
+ echo \"gem 'therubyracer'\";
38
+ echo \"gem 'kramdown'\") > Gemfile"
39
+ sh "echo '' > app/models/.gitignore" # because git reset --hard would rm the dir
40
+ rm ".gitignore" # we need to reset everything in a testapp
41
+ sh "git init && git add . && git commit -m \"initial commit\""
42
+ puts "The testapp has been created in '#{TESTAPP_PATH}'"
50
43
  else
51
- FileUtils.chdir TESTAPP_PATH
52
- sh %(git add .)
53
- sh %(git reset --hard -q HEAD)
44
+ FileUtils.chdir(TESTAPP_PATH)
45
+ sh "git add ."
46
+ sh "git reset --hard -q HEAD"
54
47
  end
55
48
  end
56
49
  end
@@ -11,7 +11,6 @@ gem "rails", "~> 4.2"
11
11
  gem "responders"
12
12
  gem "rspec"
13
13
  gem "rubocop"
14
- gem "rubydoctest"
15
14
  gem "sqlite3", "~> 1.3.0"
16
15
  gem "yard"
17
16
 
@@ -11,7 +11,6 @@ gem "rails", "~> 5.2"
11
11
  gem "responders"
12
12
  gem "rspec"
13
13
  gem "rubocop"
14
- gem "rubydoctest"
15
14
  gem "sqlite3"
16
15
  gem "yard"
17
16
 
@@ -11,7 +11,6 @@ gem "rails", "~> 6.0"
11
11
  gem "responders"
12
12
  gem "rspec"
13
13
  gem "rubocop"
14
- gem "rubydoctest"
15
14
  gem "sqlite3"
16
15
  gem "yard"
17
16
 
@@ -117,7 +117,6 @@ module DeclareSchema
117
117
  end
118
118
  column_options = {}
119
119
  column_options[:null] = options.delete(:null) || false
120
- column_options[:comment] = options.delete(:comment) if options.has_key?(:comment)
121
120
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
122
121
  column_options[:limit] = options.delete(:limit) if options.has_key?(:limit)
123
122
 
@@ -102,10 +102,6 @@ module DeclareSchema
102
102
  @options[:default]
103
103
  end
104
104
 
105
- def comment
106
- @options[:comment]
107
- end
108
-
109
105
  def same_type?(col_spec)
110
106
  type = sql_type
111
107
  normalized_type = TYPE_SYNONYMS[type] || type
@@ -115,13 +111,6 @@ module DeclareSchema
115
111
 
116
112
  def different_to?(col_spec)
117
113
  !same_type?(col_spec) ||
118
- # we should be able to use col_spec.comment, but col_spec has
119
- # a nil table_name for some strange reason.
120
- (model.table_exists? &&
121
- ActiveRecord::Base.respond_to?(:column_comment) &&
122
- !(col_comment = ActiveRecord::Base.column_comment(col_spec.name, model.table_name)).nil? &&
123
- col_comment != comment
124
- ) ||
125
114
  begin
126
115
  native_type = native_types[type]
127
116
  check_attributes = [:null, :default]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0.pre.1"
5
5
  end
@@ -3,7 +3,7 @@
3
3
  require 'rails/generators/migration'
4
4
  require 'rails/generators/active_record'
5
5
  require 'generators/declare_schema/support/thor_shell'
6
- require_relative '../../../declare_schema/model/field_spec'
6
+ require 'declare_schema/model/field_spec'
7
7
 
8
8
  module DeclareSchema
9
9
  class MigrationGenerator < Rails::Generators::Base
@@ -86,7 +86,7 @@ module DeclareSchema
86
86
  @down = down
87
87
  @migration_class_name = final_migration_name.camelize
88
88
 
89
- migration_template 'migration.rb.erb', "db/migrate/#{final_migration_name.underscore}.rb"
89
+ migration_template('migration.rb.erb', "db/migrate/#{final_migration_name.underscore}.rb")
90
90
  if action == 'm'
91
91
  case Rails::VERSION::MAJOR
92
92
  when 4
@@ -118,14 +118,13 @@ module DeclareSchema
118
118
  ActiveRecord::Migrator.new(:up, migrations, ActiveRecord::SchemaMigration).pending_migrations
119
119
  end
120
120
 
121
- if pending_migrations.any?
122
- say "You have #{pending_migrations.size} pending migration#{'s' if pending_migrations.size > 1}:"
123
- pending_migrations.each do |pending_migration|
124
- say format(' %4d %s', pending_migration.version, pending_migration.name)
121
+ pending_migrations.any?.tap do |any|
122
+ if any
123
+ say "You have #{pending_migrations.size} pending migration#{'s' if pending_migrations.size > 1}:"
124
+ pending_migrations.each do |pending_migration|
125
+ say format(' %4d %s', pending_migration.version, pending_migration.name)
126
+ end
125
127
  end
126
- true
127
- else
128
- false
129
128
  end
130
129
  end
131
130
 
@@ -139,10 +138,10 @@ module DeclareSchema
139
138
  loop do
140
139
  if rename_to_choices.empty?
141
140
  say "\nCONFIRM DROP! #{kind_str} #{name_prefix}#{t}"
142
- resp = ask("Enter 'drop #{t}' to confirm or press enter to keep:")
143
- if resp.strip == "drop #{t}"
141
+ resp = ask("Enter 'drop #{t}' to confirm or press enter to keep:").strip
142
+ if resp == "drop #{t}"
144
143
  break
145
- elsif resp.strip.empty?
144
+ elsif resp.empty?
146
145
  to_drop.delete(t)
147
146
  break
148
147
  else
@@ -151,8 +150,7 @@ module DeclareSchema
151
150
  else
152
151
  say "\nDROP, RENAME or KEEP?: #{kind_str} #{name_prefix}#{t}"
153
152
  say "Rename choices: #{to_create * ', '}"
154
- resp = ask "Enter either 'drop #{t}' or one of the rename choices or press enter to keep:"
155
- resp = resp.strip
153
+ resp = ask("Enter either 'drop #{t}' or one of the rename choices or press enter to keep:").strip
156
154
 
157
155
  if resp == "drop #{t}"
158
156
  # Leave things as they are
@@ -183,3 +181,11 @@ module DeclareSchema
183
181
  end
184
182
  end
185
183
  end
184
+
185
+ module Generators
186
+ module DeclareSchema
187
+ module Migration
188
+ MigrationGenerator = ::DeclareSchema::MigrationGenerator
189
+ end
190
+ end
191
+ end
@@ -106,15 +106,23 @@ module Generators
106
106
 
107
107
  attr_accessor :renames
108
108
 
109
+ # TODO: Add an application callback (maybe an initializer in a special group?) that
110
+ # the application can use to load other models that live in the database, to support DeclareSchema migrations
111
+ # for them.
109
112
  def load_rails_models
113
+ ActiveRecord::Migration.verbose = false
114
+
110
115
  Rails.application.eager_load!
116
+ Rails::Engine.subclasses.each(&:eager_load!)
111
117
  end
112
118
 
113
119
  # Returns an array of model classes that *directly* extend
114
120
  # ActiveRecord::Base, excluding anything in the CGI module
115
121
  def table_model_classes
116
122
  load_rails_models
117
- ActiveRecord::Base.send(:descendants).reject { |c| (c.base_class != c) || c.name.starts_with?("CGI::") }
123
+ ActiveRecord::Base.send(:descendants).select do |klass|
124
+ klass.base_class == klass && !klass.name.starts_with?("CGI::")
125
+ end
118
126
  end
119
127
 
120
128
  def connection
@@ -217,7 +225,7 @@ module Generators
217
225
  end
218
226
  end
219
227
 
220
- def always_ignore_tables
228
+ def self.always_ignore_tables
221
229
  sessions_table =
222
230
  begin
223
231
  if defined?(CGI::Session::ActiveRecordStore::Session) &&
@@ -231,8 +239,8 @@ module Generators
231
239
 
232
240
  [
233
241
  'schema_info',
234
- ActiveRecord::Base.schema_migrations_table_name,
235
- ActiveRecord::Base.internal_metadata_table_name,
242
+ ActiveRecord::Base.try(:schema_migrations_table_name) || 'schema_migrations',
243
+ ActiveRecord::Base.try(:internal_metadata_table_name) || 'ar_internal_metadata',
236
244
  sessions_table
237
245
  ].compact
238
246
  end
@@ -256,7 +264,7 @@ module Generators
256
264
  model_table_names = models_by_table_name.keys
257
265
 
258
266
  to_create = model_table_names - db_tables
259
- to_drop = db_tables - model_table_names - always_ignore_tables
267
+ to_drop = db_tables - model_table_names - self.class.always_ignore_tables
260
268
  to_change = model_table_names
261
269
  to_rename = extract_table_renames!(to_create, to_drop)
262
270
 
@@ -411,7 +419,6 @@ module Generators
411
419
  change_spec[:scale] = spec.scale unless spec.scale.nil?
412
420
  change_spec[:null] = spec.null unless spec.null && col.null
413
421
  change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
414
- change_spec[:comment] = spec.comment unless spec.comment.nil? && (col.comment if col.respond_to?(:comment)).nil?
415
422
 
416
423
  changes << "change_column :#{new_table_name}, :#{c}, " +
417
424
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -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