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.
@@ -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