whiteprint 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +251 -0
- data/Rakefile +55 -0
- data/lib/tasks/blueprint.rake +5 -0
- data/lib/whiteprint.rb +99 -0
- data/lib/whiteprint/adapters/active_record.rb +193 -0
- data/lib/whiteprint/adapters/active_record/has_and_belongs_to_many.rb +22 -0
- data/lib/whiteprint/adapters/active_record/migration.rb +17 -0
- data/lib/whiteprint/adapters/test.rb +20 -0
- data/lib/whiteprint/attributes.rb +207 -0
- data/lib/whiteprint/base.rb +95 -0
- data/lib/whiteprint/config.rb +19 -0
- data/lib/whiteprint/explanation.rb +73 -0
- data/lib/whiteprint/has_whiteprint.rb +7 -0
- data/lib/whiteprint/migrator.rb +66 -0
- data/lib/whiteprint/model.rb +25 -0
- data/lib/whiteprint/railtie.rb +24 -0
- data/lib/whiteprint/transform.rb +35 -0
- data/lib/whiteprint/version.rb +3 -0
- data/test/cases/active_record_test.rb +13 -0
- data/test/cases/attributes_test.rb +62 -0
- data/test/cases/blueprint_test.rb +125 -0
- data/test/cases/changes_tree_test.rb +51 -0
- data/test/cases/explanation_test.rb +32 -0
- data/test/cases/migrator_test.rb +70 -0
- data/test/models/car.rb +8 -0
- data/test/models/user.rb +8 -0
- data/test/schema.rb +11 -0
- data/test/test_helper.rb +21 -0
- data/vendor/active_support/concern.rb +142 -0
- metadata +270 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Migrator
|
3
|
+
class << self
|
4
|
+
def eager_load!
|
5
|
+
return unless Whiteprint.config.eager_load
|
6
|
+
|
7
|
+
Rails.application.eager_load! if defined?(Rails)
|
8
|
+
|
9
|
+
[*Whiteprint.config.eager_load_paths.uniq].each do |path|
|
10
|
+
Gem.find_files(path).each do |file|
|
11
|
+
load file
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def explanations
|
17
|
+
Whiteprint.changed_whiteprints.map.with_index do |whiteprint, index|
|
18
|
+
whiteprint.explanation(index + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def interactive(input: $stdin, output: $stdout, migrate_input: $stdin, migrate_output: $stdout)
|
23
|
+
# TODO: Clean up
|
24
|
+
|
25
|
+
eager_load!
|
26
|
+
cli = HighLine.new input, output
|
27
|
+
|
28
|
+
if number_of_changes == 0
|
29
|
+
cli.say('Whiteprint detected no changes')
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
cli.say "Whiteprint has detected <%= color('#{number_of_changes}', :bold, :white) %> changes to your models."
|
34
|
+
explanations.each do |explanation|
|
35
|
+
cli.say explanation
|
36
|
+
end
|
37
|
+
|
38
|
+
cli.choose do |menu|
|
39
|
+
menu.header = 'Migrations'
|
40
|
+
menu.prompt = 'How would you like to process these changes?'
|
41
|
+
menu.choice('In one migration') { migrate_at_once(input: migrate_input, output: migrate_output) }
|
42
|
+
menu.choice('In separate migrations') { cli.say 'Bar' }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def migrate_at_once(input: $stdin, output: $stdout)
|
47
|
+
# TODO: Clean up
|
48
|
+
|
49
|
+
cli = HighLine.new input, output
|
50
|
+
name = cli.ask 'How would you like to name this migration?'
|
51
|
+
Whiteprint.changed_whiteprints
|
52
|
+
.group_by(&:transformer)
|
53
|
+
.map do |adapter, whiteprints|
|
54
|
+
adapter.generate_migration(name, whiteprints.map(&:changes_tree))
|
55
|
+
end
|
56
|
+
|
57
|
+
ActiveRecord::Migration.verbose = true
|
58
|
+
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths)
|
59
|
+
end
|
60
|
+
|
61
|
+
def number_of_changes
|
62
|
+
Whiteprint.changed_whiteprints.size
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def whiteprint(**options, &block)
|
7
|
+
return @_whiteprint unless block
|
8
|
+
|
9
|
+
@_whiteprint ||= ::Whiteprint.new(self, **options)
|
10
|
+
@_whiteprint.execute(&block)
|
11
|
+
end
|
12
|
+
alias_method :schema, :whiteprint
|
13
|
+
|
14
|
+
def inherited(base)
|
15
|
+
whiteprint.clone_to(base) if whiteprint
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(model)
|
21
|
+
Whiteprint.models += [model]
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
class << self
|
4
|
+
def whiteprint_config
|
5
|
+
::Whiteprint.config do |c|
|
6
|
+
c.eager_load = true
|
7
|
+
c.migration_path = Rails.root.join(ActiveRecord::Migrator.migrations_path)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "whiteprint.config_for_rails" do
|
13
|
+
::Whiteprint.config do |c|
|
14
|
+
c.eager_load = true
|
15
|
+
c.migration_path = Rails.root.join(ActiveRecord::Migrator.migrations_path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
rake_tasks do
|
20
|
+
# whiteprint_config
|
21
|
+
load "tasks/whiteprint.rake"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
class Transform < Parslet::Transform
|
3
|
+
class << self
|
4
|
+
def create_rule(name, **expression)
|
5
|
+
define_singleton_method name do |&block|
|
6
|
+
rule(expression, &block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def table_expression
|
11
|
+
{
|
12
|
+
table_name: simple(:table_name),
|
13
|
+
attributes: subtree(:attributes)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def attribute_expression
|
18
|
+
{
|
19
|
+
name: simple(:name),
|
20
|
+
type: simple(:type),
|
21
|
+
options: subtree(:options)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
create_rule :create_table, table_exists: false, has_id: true, **table_expression
|
27
|
+
create_rule :create_table_without_id, table_exists: false, has_id: false, **table_expression
|
28
|
+
create_rule :change_table, table_exists: true, **table_expression
|
29
|
+
|
30
|
+
create_rule :added_attribute, kind: :added, **attribute_expression
|
31
|
+
create_rule :changed_attribute, kind: :changed, **attribute_expression
|
32
|
+
create_rule :removed_attribute, kind: :removed, **attribute_expression
|
33
|
+
create_rule :added_timestamps, kind: :added, type: :timestamps
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ActiveRecordTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@persisted_attributes = User.whiteprint.persisted_attributes
|
6
|
+
end
|
7
|
+
|
8
|
+
test 'the active_record adapter can read the persisted attributes from the database' do
|
9
|
+
assert_equal Whiteprint::Attribute.new(name: :name, type: :string, default: 'John').for_persisted, @persisted_attributes.name
|
10
|
+
assert_equal Whiteprint::Attribute.new(name: :age, type: :integer, default: 0).for_persisted, @persisted_attributes.age
|
11
|
+
assert_equal Whiteprint::Attribute.new(name: :date_of_birth, type: :date).for_persisted, @persisted_attributes.date_of_birth
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AttributesTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@attribute = Whiteprint::Attribute.new type: :integer, default: 10
|
6
|
+
|
7
|
+
@attributes = Whiteprint::Attributes.new
|
8
|
+
|
9
|
+
@attributes.add name: 'name', type: :string, default: 'John'
|
10
|
+
@attributes.add name: 'age', type: :integer
|
11
|
+
@attributes.add name: 'height', type: :integer, default: 180
|
12
|
+
@attributes.add name: 'date_of_birth', type: :date
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'an attribute can be asked whether it has a certain key or keys' do
|
16
|
+
assert_equal true, @attribute.has?(:type)
|
17
|
+
assert_equal true, @attribute.has?(:type, :default)
|
18
|
+
assert_equal false, @attribute.has?(:type, :foo)
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'an attribute can be asked whether it has a certain values' do
|
22
|
+
assert_equal true, @attribute.has?(type: :integer)
|
23
|
+
assert_equal false, @attribute.has?(type: :string)
|
24
|
+
assert_equal true, @attribute.has?(type: :integer, default: 10)
|
25
|
+
assert_equal true, @attribute.has?(:type, default: 10)
|
26
|
+
assert_equal false, @attribute.has?(:type, default: 11)
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'attributes can be queried' do
|
30
|
+
assert_equal @attributes.to_h.slice(:name, :height), @attributes.where(:default).to_h
|
31
|
+
assert_equal @attributes.to_h.slice(:name), @attributes.where(default: 'John').to_h
|
32
|
+
assert_equal @attributes.to_h.slice(:age, :height), @attributes.where(type: :integer).to_h
|
33
|
+
assert_equal @attributes.to_h.slice(:name, :date_of_birth), @attributes.not(type: :integer).to_h
|
34
|
+
assert_equal @attributes.to_h.slice(:name), @attributes.where(:default).not(type: :integer).to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'attributes can be diffed' do
|
38
|
+
diff = @attributes.where(:default).diff(@attributes)
|
39
|
+
|
40
|
+
assert_equal @attributes.to_h.slice(:age, :date_of_birth), diff[:added].to_h
|
41
|
+
assert_equal({}, diff[:removed].to_h)
|
42
|
+
assert_equal({}, diff[:changed].to_h)
|
43
|
+
|
44
|
+
diff = @attributes.diff(@attributes.where(:default))
|
45
|
+
|
46
|
+
assert_equal({}, diff[:added].to_h)
|
47
|
+
assert_equal @attributes.to_h.slice(:age, :date_of_birth), diff[:removed].to_h
|
48
|
+
assert_equal({}, diff[:changed].to_h)
|
49
|
+
|
50
|
+
diff_attributes = Whiteprint::Attributes.new
|
51
|
+
|
52
|
+
diff_attributes.add name: 'name', type: :string, default: 'Joe'
|
53
|
+
diff_attributes.add name: 'weight', type: :integer
|
54
|
+
diff_attributes.add name: 'height', type: :integer, default: 160
|
55
|
+
|
56
|
+
diff = @attributes.diff(diff_attributes)
|
57
|
+
|
58
|
+
assert_equal diff_attributes.to_h.slice(:weight), diff[:added].to_h
|
59
|
+
assert_equal @attributes.to_h.slice(:age, :date_of_birth), diff[:removed].to_h
|
60
|
+
assert_equal diff_attributes.to_h.slice(:name, :height), diff[:changed].to_h
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class WhiteprintTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@model = Class.new
|
6
|
+
@whiteprint = ::Whiteprint::Base.new(@model)
|
7
|
+
end
|
8
|
+
|
9
|
+
test 'a whiteprint is tied to a model' do
|
10
|
+
assert_equal @model, @whiteprint.model
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'a whiteprint is initialized with an empty set of attributes' do
|
14
|
+
assert_equal({}, @whiteprint.attributes.to_h)
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'a whiteprint saves attributes with a type and options' do
|
18
|
+
@whiteprint.string :name, default: 'John'
|
19
|
+
@whiteprint.integer 'age'
|
20
|
+
|
21
|
+
assert_equal({ name: :name, type: :string, default: 'John' }, @whiteprint.attributes.name.to_h)
|
22
|
+
assert_equal({ name: :age, type: :integer }, @whiteprint.attributes.age.to_h)
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'attributes can be accessed like a hash with indifferent access, but they can also be accessed as methods' do
|
26
|
+
@whiteprint.string :name, default: 'John'
|
27
|
+
|
28
|
+
assert_equal :string, @whiteprint.attributes['name'][:type]
|
29
|
+
assert_equal :string, @whiteprint.attributes.name.type
|
30
|
+
assert_equal 'John', @whiteprint.attributes[:name]['default']
|
31
|
+
assert_equal 'John', @whiteprint.attributes.name.default
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class WhiteprintModelTest < ActiveSupport::TestCase
|
36
|
+
test 'a model responds to whiteprint (and schema) if Whiteprint::Model is included' do
|
37
|
+
model = Class.new do
|
38
|
+
include Whiteprint::Model
|
39
|
+
end
|
40
|
+
assert_respond_to model, :whiteprint
|
41
|
+
assert_respond_to model, :schema
|
42
|
+
end
|
43
|
+
|
44
|
+
test 'if a model inherits from ActiveRecord::Base has_whiteprint does the same as including Whiteprint::Model' do
|
45
|
+
model = Class.new(ActiveRecord::Base) do
|
46
|
+
has_whiteprint
|
47
|
+
end
|
48
|
+
|
49
|
+
assert model < Whiteprint::Model
|
50
|
+
end
|
51
|
+
|
52
|
+
test 'a model can add attribtues to its whiteprint by passing the whiteprint method a block' do
|
53
|
+
model = Class.new do
|
54
|
+
include Whiteprint::Model
|
55
|
+
|
56
|
+
whiteprint do
|
57
|
+
string :name, default: 'John'
|
58
|
+
integer :age
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
assert_instance_of ::Whiteprint::Base, model.whiteprint
|
63
|
+
assert_equal({ name: :name, type: :string, default: 'John' }, model.whiteprint.attributes.name.to_h)
|
64
|
+
assert_equal({ name: :age, type: :integer }, model.whiteprint.attributes.age.to_h)
|
65
|
+
end
|
66
|
+
|
67
|
+
test "attributes can also be added to a model's whiteprint via composition" do
|
68
|
+
concern = Module.new do
|
69
|
+
extend ActiveSupport::Concern
|
70
|
+
include Whiteprint::Model
|
71
|
+
|
72
|
+
included do
|
73
|
+
whiteprint do
|
74
|
+
string :name, default: 'John'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
model = Class.new do
|
80
|
+
include Whiteprint::Model
|
81
|
+
include concern
|
82
|
+
|
83
|
+
whiteprint do
|
84
|
+
integer :age
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
assert_equal({ name: :name, type: :string, default: 'John' }, model.whiteprint.attributes.name.to_h)
|
89
|
+
assert_equal({ name: :age, type: :integer }, model.whiteprint.attributes.age.to_h)
|
90
|
+
end
|
91
|
+
|
92
|
+
test 'an adapter can be set by the user or is automatically determined if possible' do
|
93
|
+
model = Class.new do
|
94
|
+
include Whiteprint::Model
|
95
|
+
|
96
|
+
whiteprint(adapter: :active_record) do
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
assert_instance_of ::Whiteprint::Adapters::ActiveRecord, model.whiteprint
|
101
|
+
|
102
|
+
model = Class.new(ActiveRecord::Base) do
|
103
|
+
include Whiteprint::Model
|
104
|
+
|
105
|
+
whiteprint do
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
assert_instance_of ::Whiteprint::Adapters::ActiveRecord, model.whiteprint
|
110
|
+
|
111
|
+
model = Class.new(ActiveRecord::Base) do
|
112
|
+
include Whiteprint::Model
|
113
|
+
|
114
|
+
whiteprint(adapter: :base) do
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
assert_instance_of ::Whiteprint::Base, model.whiteprint
|
119
|
+
end
|
120
|
+
|
121
|
+
def teardown
|
122
|
+
Whiteprint.models = []
|
123
|
+
Whiteprint::Migrator.eager_load!
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ChangesTreeTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@model = Class.new do
|
6
|
+
include Whiteprint::Model
|
7
|
+
|
8
|
+
whiteprint(adapter: :test) do
|
9
|
+
string :name, default: 'John'
|
10
|
+
integer :age, default: 0
|
11
|
+
date :date_of_birth
|
12
|
+
|
13
|
+
persisted do
|
14
|
+
string :name
|
15
|
+
integer :age, default: 0
|
16
|
+
integer :weight
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.table_name
|
21
|
+
'persons'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.table_exists?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'the test adapter can set its persisted attributes with a block' do
|
31
|
+
assert_equal Whiteprint::Attribute.new(name: :name, type: :string), @model.whiteprint.persisted_attributes.name
|
32
|
+
assert_equal Whiteprint::Attribute.new(name: :age, type: :integer, default: 0), @model.whiteprint.persisted_attributes.age
|
33
|
+
assert_equal Whiteprint::Attribute.new(name: :weight, type: :integer), @model.whiteprint.persisted_attributes.weight
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'a whiteprint can generate a changes_tree with all the differences between the persisted attributes and the actual attributes' do
|
37
|
+
changes_tree = @model.whiteprint.changes_tree
|
38
|
+
|
39
|
+
attributes = [
|
40
|
+
{ name: :date_of_birth, type: :date, options: {}, kind: :added },
|
41
|
+
{ name: :name, type: :string, options: { default: 'John' }, kind: :changed },
|
42
|
+
{ name: :weight, type: :integer, options: {}, kind: :removed }
|
43
|
+
]
|
44
|
+
assert_equal({ table_name: 'persons', table_exists: true, attributes: attributes }, changes_tree)
|
45
|
+
end
|
46
|
+
|
47
|
+
def teardown
|
48
|
+
Whiteprint.models = []
|
49
|
+
Whiteprint::Migrator.eager_load!
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ExplanationTest < ActiveSupport::TestCase
|
4
|
+
test 'the changes whiteprint is about to process can be visualized in a table' do
|
5
|
+
user_explanation = <<-TXT.sub(/\n$/, '')
|
6
|
+
+--------+---------------+---------+------------------+-------------------+------------------------+
|
7
|
+
| 1. Make changes to users |
|
8
|
+
+--------+---------------+---------+------------------+-------------------+------------------------+
|
9
|
+
| action | name | type | type (currently) | options | options (currently) |
|
10
|
+
+--------+---------------+---------+------------------+-------------------+------------------------+
|
11
|
+
| change | name | string | string | {:default=>"Joe"} | {:default=>"John"} |
|
12
|
+
| change | age | integer | integer | {:default=>10} | {:default=>0} |
|
13
|
+
| remove | date_of_birth | | | | |
|
14
|
+
+--------+---------------+---------+------------------+-------------------+------------------------+
|
15
|
+
TXT
|
16
|
+
|
17
|
+
car_explanation = <<-TXT.sub(/\n$/, '')
|
18
|
+
+---------------------------+------------------------+---------------------------------------------+
|
19
|
+
| 1. Create a new table cars |
|
20
|
+
+---------------------------+------------------------+---------------------------------------------+
|
21
|
+
| name | type | options |
|
22
|
+
+---------------------------+------------------------+---------------------------------------------+
|
23
|
+
| brand | string | {:default=>"BMW"} |
|
24
|
+
| price | decimal | {:precision=>5, :scale=>10} |
|
25
|
+
| timestamps | | |
|
26
|
+
+---------------------------+------------------------+---------------------------------------------+
|
27
|
+
TXT
|
28
|
+
|
29
|
+
assert_equal user_explanation, User.whiteprint.explanation.to_s
|
30
|
+
assert_equal car_explanation, Car.whiteprint.explanation.to_s
|
31
|
+
end
|
32
|
+
end
|