whiteprint 0.1.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.
- 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
|