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,22 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Adapters
|
3
|
+
class ActiveRecord::HasAndBelongsToMany < ActiveRecord
|
4
|
+
def self.applicable?(_)
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
def changes_tree
|
9
|
+
return {} if model.table_exists?
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def changes?
|
14
|
+
!model.table_exists?
|
15
|
+
end
|
16
|
+
|
17
|
+
def transformer
|
18
|
+
self.class.parent
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Adapters
|
3
|
+
class ActiveRecord::Migration < Whiteprint::Transform
|
4
|
+
create_rule :removed_default, kind: :removed_default, name: simple(:name)
|
5
|
+
|
6
|
+
create_table { " create_table :#{table_name} do |t|\n#{attributes.join("\n")}\n end\n" }
|
7
|
+
create_table_without_id { " create_table :#{table_name}, id: false do |t|\n#{attributes.join("\n")}\n end\n" }
|
8
|
+
change_table { " change_table :#{table_name} do |t|\n#{attributes.join("\n")}\n end\n" }
|
9
|
+
|
10
|
+
added_attribute { " t.#{type} :#{name}, #{options}" }
|
11
|
+
changed_attribute { " t.change :#{name}, :#{type}, #{options}" }
|
12
|
+
removed_attribute { " t.remove :#{name}" }
|
13
|
+
removed_default { " t.change_default :#{name}, nil" }
|
14
|
+
added_timestamps { ' t.timestamps' }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Adapters
|
3
|
+
class Test < ::Whiteprint::Base
|
4
|
+
class << self
|
5
|
+
def applicable?(_model)
|
6
|
+
false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def persisted_attributes
|
11
|
+
@_persisted_whiteprint.attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def persisted(&block)
|
15
|
+
@_persisted_whiteprint ||= Whiteprint::Base.new(@model)
|
16
|
+
@_persisted_whiteprint.instance_eval(&block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
class Attribute
|
3
|
+
def initialize(persisted: nil, model: nil, **options)
|
4
|
+
@model = model
|
5
|
+
@options = options
|
6
|
+
@persisted = persisted
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
to_h == other.to_h
|
11
|
+
end
|
12
|
+
|
13
|
+
def has?(*keys, **conditions)
|
14
|
+
keys.none? do |key|
|
15
|
+
@options.values_at(*key).compact.empty?
|
16
|
+
end && conditions.all? do |key, value|
|
17
|
+
[*@options[key]] & [*value] != []
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def persisted_options
|
22
|
+
@options.select do |key, value|
|
23
|
+
Whiteprint.config.persisted_attribute_options.keys.include?(key) &&
|
24
|
+
!(key == :default && value.is_a?(Symbol))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
@options
|
30
|
+
end
|
31
|
+
|
32
|
+
def merge(options)
|
33
|
+
self.class.new(persisted: @persisted, **@options, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def for_meta(instance)
|
37
|
+
::Whiteprint.config.meta_attribute_options.map do |option|
|
38
|
+
{option => send("meta_#{option}", instance)}
|
39
|
+
end.inject(&:merge).compact
|
40
|
+
end
|
41
|
+
|
42
|
+
def for_persisted(**config)
|
43
|
+
return merge(config) if @persisted
|
44
|
+
self.class.new(persisted: true, name: @options[:name], type: @options[:type], options: persisted_options, **config)
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](name)
|
48
|
+
@options[name.to_sym]
|
49
|
+
end
|
50
|
+
|
51
|
+
def meta_enum(instance)
|
52
|
+
_enum = if enum.is_a?(Symbol)
|
53
|
+
instance.send(enum)
|
54
|
+
else
|
55
|
+
enum
|
56
|
+
end
|
57
|
+
|
58
|
+
return _enum if _enum.is_a?(Hash)
|
59
|
+
|
60
|
+
_enum.map do |value|
|
61
|
+
{value => value}
|
62
|
+
end.inject(&:merge)
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing(name)
|
66
|
+
if name.to_s.starts_with?('meta_')
|
67
|
+
self[name.to_s.remove(/^meta_/)]
|
68
|
+
else
|
69
|
+
self[name]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class AttributeScope
|
75
|
+
def initialize(scope, model: nil)
|
76
|
+
@scope = scope
|
77
|
+
@model = model
|
78
|
+
@selects = []
|
79
|
+
@rejects = []
|
80
|
+
end
|
81
|
+
|
82
|
+
def where(*keys, **conditions)
|
83
|
+
@selects << proc do |_, attribute|
|
84
|
+
attribute.has?(*keys, **conditions)
|
85
|
+
end
|
86
|
+
Attributes.new(filter, model: @model)
|
87
|
+
end
|
88
|
+
|
89
|
+
def not(*keys, **conditions)
|
90
|
+
@rejects << proc do |_, attribute|
|
91
|
+
attribute.has?(*keys, **conditions)
|
92
|
+
end
|
93
|
+
Attributes.new(filter, model: @model)
|
94
|
+
end
|
95
|
+
|
96
|
+
def filter
|
97
|
+
select = proc { |scope, condition| scope.select(&condition) }
|
98
|
+
reject = proc { |scope, condition| scope.reject(&condition) }
|
99
|
+
|
100
|
+
scope = @selects.inject(@scope, &select)
|
101
|
+
@rejects.inject(scope, &reject)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Attributes
|
106
|
+
def initialize(attributes = nil, model: nil)
|
107
|
+
attributes = Hash[attributes] if attributes.is_a?(Array)
|
108
|
+
@attributes = (attributes || {}).dup
|
109
|
+
@model = model
|
110
|
+
end
|
111
|
+
|
112
|
+
def add(name:, type:, **options)
|
113
|
+
@attributes[name.to_sym] = Attribute.new(name: name.to_sym, type: type.to_sym, model: @model, **options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def as_json(*args)
|
117
|
+
super['attributes']
|
118
|
+
end
|
119
|
+
|
120
|
+
def diff(diff, type: nil)
|
121
|
+
# TODO: Clean up
|
122
|
+
added = diff.slice(*(diff.keys - keys))
|
123
|
+
changed = diff.to_diff_a(type) - to_diff_a(type) - added.to_diff_a(type)
|
124
|
+
changed = Attributes.new(Hash[changed].map { |key, options| { key => Attribute.new(persisted: true, model: @model, **options) } }.inject(&:merge), model: @model)
|
125
|
+
removed = slice(*(keys - diff.keys))
|
126
|
+
|
127
|
+
{ added: added, changed: changed, removed: removed }
|
128
|
+
end
|
129
|
+
|
130
|
+
def where(*keys, **conditions)
|
131
|
+
AttributeScope.new(@attributes, model: @model).where(*keys, **conditions)
|
132
|
+
end
|
133
|
+
|
134
|
+
def not(*keys, **conditions)
|
135
|
+
AttributeScope.new(@attributes, model: @model).not(*keys, **conditions)
|
136
|
+
end
|
137
|
+
|
138
|
+
def keys
|
139
|
+
@attributes.keys
|
140
|
+
end
|
141
|
+
|
142
|
+
def slice(*keys)
|
143
|
+
where(name: keys)
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_h
|
147
|
+
@attributes
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_a
|
151
|
+
@attributes.values
|
152
|
+
end
|
153
|
+
|
154
|
+
def for_persisted
|
155
|
+
persisted_scope = self.not(virtual: true)
|
156
|
+
persisted_scope.to_h.each do |name, attribute|
|
157
|
+
persisted_scope.to_h[name] = attribute.for_persisted
|
158
|
+
end
|
159
|
+
persisted_scope
|
160
|
+
end
|
161
|
+
|
162
|
+
def for_meta(instance)
|
163
|
+
where(::Whiteprint.config.meta_attribute_options).to_h.map do |key, attribute|
|
164
|
+
{key => attribute.for_meta(instance)}
|
165
|
+
end.inject(&:merge)
|
166
|
+
end
|
167
|
+
|
168
|
+
def for_serializer
|
169
|
+
self.not(type: :references).not(private: true).keys
|
170
|
+
end
|
171
|
+
|
172
|
+
def for_permitted
|
173
|
+
self.not(readonly: true).not(name: [:updated_at, :created_at]).to_h.map do |name, attribute|
|
174
|
+
if attribute.array
|
175
|
+
{name => []}
|
176
|
+
elsif attribute.type == :has_and_belongs_to_many
|
177
|
+
{"#{name.to_s.singularize}_ids" => []}
|
178
|
+
elsif attribute.type == :references
|
179
|
+
"#{attribute.name}_id"
|
180
|
+
else
|
181
|
+
name
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def for_permitted_json
|
187
|
+
self.not(readonly: true).where(type: [:json, :jsonb]).keys
|
188
|
+
end
|
189
|
+
|
190
|
+
def to_diff_a(type)
|
191
|
+
if type
|
192
|
+
to_h.map { |name, attr| [name, attr.send("for_#{type}").to_h] }
|
193
|
+
else
|
194
|
+
to_h.map { |name, attr| [name, attr.to_h] }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def [](name)
|
199
|
+
return unless name
|
200
|
+
@attributes[name.to_sym]
|
201
|
+
end
|
202
|
+
|
203
|
+
def method_missing(name)
|
204
|
+
self[name]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
class Base
|
3
|
+
attr_accessor :model, :attributes, :configs, :options
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def applicable?(_)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_plugins
|
11
|
+
Whiteprint.config.plugins.each do |plugin|
|
12
|
+
include Whiteprint.plugins[plugin]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(model, **_options)
|
18
|
+
singleton_class.send :load_plugins
|
19
|
+
|
20
|
+
self.model = model
|
21
|
+
self.options = _options
|
22
|
+
self.attributes = Attributes.new(nil, model: model)
|
23
|
+
self.configs = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute(&block)
|
27
|
+
self.configs << block
|
28
|
+
instance_eval(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def explanation(index = 1, width = 100)
|
32
|
+
return unless changes?
|
33
|
+
table = Terminal::Table.new(Explanation.apply(self, index, width: width))
|
34
|
+
table.render
|
35
|
+
table
|
36
|
+
rescue => e
|
37
|
+
explanation(index, width + 10)
|
38
|
+
end
|
39
|
+
|
40
|
+
def changes_tree
|
41
|
+
changes = persisted_attributes.diff(attributes.not(virtual: true), type: :persisted)
|
42
|
+
|
43
|
+
attributes = changes.flat_map do |kind, changed_attributes|
|
44
|
+
changed_attributes.to_a.map do |attribute|
|
45
|
+
next if attribute.virtual
|
46
|
+
attribute.for_persisted(kind: kind).to_h
|
47
|
+
end.compact
|
48
|
+
end
|
49
|
+
|
50
|
+
table_name_without_schema = table_name.split('.').last
|
51
|
+
|
52
|
+
{ table_name: table_name_without_schema, table_exists: table_exists?, attributes: attributes }
|
53
|
+
end
|
54
|
+
|
55
|
+
def changes?
|
56
|
+
changes = persisted_attributes.diff(attributes, type: :persisted)
|
57
|
+
changes.any? do |_, attributes|
|
58
|
+
!attributes.to_a.reject(&:virtual).empty?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def clone_to(model)
|
63
|
+
clone = ::Whiteprint.new(model, **self.options)
|
64
|
+
self.configs.each do |config|
|
65
|
+
clone.execute(&config)
|
66
|
+
end
|
67
|
+
model.instance_variable_set :@_whiteprint, clone
|
68
|
+
Whiteprint.models += [model]
|
69
|
+
end
|
70
|
+
|
71
|
+
def persisted_attributes
|
72
|
+
Whiteprint::Attributes.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_model(model)
|
76
|
+
self.model = model
|
77
|
+
end
|
78
|
+
|
79
|
+
def table_name
|
80
|
+
model.table_name
|
81
|
+
end
|
82
|
+
|
83
|
+
def table_exists?
|
84
|
+
model.table_exists?
|
85
|
+
end
|
86
|
+
|
87
|
+
def transformer
|
88
|
+
self.class
|
89
|
+
end
|
90
|
+
|
91
|
+
def method_missing(type, name, **options)
|
92
|
+
@attributes.add name: name.to_sym, type: type, **options
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
def self.config(&block)
|
3
|
+
return Config unless block
|
4
|
+
Config.instance_exec(Config, &block)
|
5
|
+
end
|
6
|
+
|
7
|
+
module Config
|
8
|
+
class << self
|
9
|
+
attr_accessor :default_adapter, :persisted_attribute_options, :eager_load, :eager_load_paths,
|
10
|
+
:migration_path, :meta_attribute_options, :plugins
|
11
|
+
|
12
|
+
def plugin(name)
|
13
|
+
self.plugins << name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
self.plugins = []
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Whiteprint
|
2
|
+
module Explanation
|
3
|
+
class << self
|
4
|
+
def apply(whiteprint, index, width: 100)
|
5
|
+
@table = { title: "#{index}. ", rows: [], style: { width: width } }
|
6
|
+
@whiteprint = whiteprint
|
7
|
+
transformer.new.apply(whiteprint.changes_tree)
|
8
|
+
@table
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def helpers
|
14
|
+
type_was = lambda do |name|
|
15
|
+
@whiteprint.persisted_attributes[name][:type].to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
options_was = lambda do |name|
|
19
|
+
@whiteprint.persisted_attributes[name][:options].to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
table_exists = @whiteprint.changes_tree[:table_exists]
|
23
|
+
|
24
|
+
[type_was, options_was, table_exists]
|
25
|
+
end
|
26
|
+
|
27
|
+
def transformer
|
28
|
+
table, type_was, options_was, table_exists = @table, *helpers
|
29
|
+
|
30
|
+
Class.new(Whiteprint::Transform) do
|
31
|
+
create_table do
|
32
|
+
table[:headings] = %w(name type options)
|
33
|
+
table[:title] += "Create a new table #{table_name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table_without_id do
|
37
|
+
table[:headings] = %w(name type options)
|
38
|
+
table[:title] += "Create a new table #{table_name} (without id)"
|
39
|
+
end
|
40
|
+
|
41
|
+
change_table do
|
42
|
+
table[:headings] = ['action', 'name', 'type', 'type (currently)', 'options', 'options (currently)']
|
43
|
+
table[:title] += "Make changes to #{table_name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
added_attribute do
|
47
|
+
table[:rows] << if table_exists
|
48
|
+
['added', name, type, nil, options.to_s, nil]
|
49
|
+
else
|
50
|
+
[name, type, options.to_s]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
added_timestamps do
|
55
|
+
table[:rows] << if table_exists
|
56
|
+
['added', 'timestamps', nil, nil, nil, nil]
|
57
|
+
else
|
58
|
+
['timestamps', nil, nil]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
changed_attribute do
|
63
|
+
table[:rows] << ['change', name, type, type_was[name], options.to_s, options_was[name]]
|
64
|
+
end
|
65
|
+
|
66
|
+
removed_attribute do
|
67
|
+
table[:rows] << ['remove', name, nil, nil, nil, nil]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|