whiteprint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Whiteprint
2
+ module HasWhiteprint
3
+ def has_whiteprint
4
+ send :include, ::Whiteprint::Model
5
+ end
6
+ end
7
+ end
@@ -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,3 @@
1
+ module Whiteprint
2
+ VERSION = '0.1.0'.freeze
3
+ 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