declare_schema 0.1.1 → 0.3.0.pre.1
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.
- checksums.yaml +4 -4
- data/.travis.yml +37 -0
- data/CHANGELOG.md +36 -4
- data/Gemfile +0 -2
- data/Gemfile.lock +1 -4
- data/Rakefile +13 -20
- data/gemfiles/rails_4.gemfile +4 -7
- data/gemfiles/rails_5.gemfile +4 -7
- data/gemfiles/rails_6.gemfile +4 -7
- data/lib/declare_schema/model.rb +21 -23
- data/lib/declare_schema/model/field_spec.rb +1 -12
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
- data/lib/generators/declare_schema/migration/migrator.rb +57 -33
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/lib/generators/declare_schema/support/eval_template.rb +12 -3
- data/lib/generators/declare_schema/support/model.rb +77 -2
- data/spec/lib/declare_schema/api_spec.rb +125 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
- data/spec/lib/declare_schema/generator_spec.rb +57 -0
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
- data/spec/lib/declare_schema/migration_generator_spec.rb +735 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +31 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
- data/spec/spec_helper.rb +26 -0
- metadata +9 -11
- data/.jenkins/Jenkinsfile +0 -72
- data/.jenkins/ruby_build_pod.yml +0 -19
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
- data/test/api.rdoctest +0 -136
- data/test/generators.rdoctest +0 -60
- data/test/interactive_primary_key.rdoctest +0 -56
- data/test/migration_generator.rdoctest +0 -846
- data/test/migration_generator_comments.rdoctestDISABLED +0 -74
- 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).
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
|
@@ -394,14 +410,13 @@ module Generators
|
|
394
410
|
spec = model.field_specs[c]
|
395
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
|
396
412
|
change_spec = fk_field_options(model, c)
|
397
|
-
change_spec[:limit]
|
413
|
+
change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
|
398
414
|
::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
|
399
415
|
(spec.limit || col.limit)
|
400
416
|
change_spec[:precision] = spec.precision unless spec.precision.nil?
|
401
417
|
change_spec[:scale] = spec.scale unless spec.scale.nil?
|
402
418
|
change_spec[:null] = spec.null unless spec.null && col.null
|
403
419
|
change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
|
404
|
-
change_spec[:comment] = spec.comment unless spec.comment.nil? && (col.comment if col.respond_to?(:comment)).nil?
|
405
420
|
|
406
421
|
changes << "change_column :#{new_table_name}, :#{c}, " +
|
407
422
|
([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
|
@@ -501,8 +516,7 @@ module Generators
|
|
501
516
|
next if k == :null && v == true
|
502
517
|
end
|
503
518
|
|
504
|
-
next if k == :limit && type == :text &&
|
505
|
-
(!::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?
|
506
520
|
|
507
521
|
if k.is_a?(Symbol)
|
508
522
|
"#{k}: #{v.inspect}"
|
@@ -513,10 +527,20 @@ module Generators
|
|
513
527
|
end
|
514
528
|
|
515
529
|
def fk_field_options(model, field_name)
|
516
|
-
|
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)
|
517
532
|
parent_columns = connection.columns(parent_table) rescue []
|
518
|
-
|
519
|
-
|
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
|
+
|
520
544
|
{ limit: pk_limit }
|
521
545
|
else
|
522
546
|
{}
|
@@ -9,9 +9,18 @@ module DeclareSchema
|
|
9
9
|
private
|
10
10
|
|
11
11
|
def eval_template(template_name)
|
12
|
-
source
|
13
|
-
|
14
|
-
|
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
|
30
|
-
|
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
|
-
|
7
|
-
|
8
|
-
name :string, limit: 127
|
6
|
+
before do
|
7
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
9
8
|
|
10
|
-
|
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
|