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,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