whiteprint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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