brick 1.0.17 → 1.0.20
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 +4 -4
- data/lib/brick/config.rb +1 -1
- data/lib/brick/extensions.rb +347 -74
- data/lib/brick/frameworks/rails/engine.rb +140 -109
- data/lib/brick/join_array.rb +227 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +36 -2
- data/lib/generators/brick/install_generator.rb +10 -10
- metadata +3 -2
@@ -7,10 +7,12 @@ module Brick
|
|
7
7
|
# paths['app/models'] << 'lib/brick/frameworks/active_record/models'
|
8
8
|
config.brick = ActiveSupport::OrderedOptions.new
|
9
9
|
ActiveSupport.on_load(:before_initialize) do |app|
|
10
|
+
is_development = (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
|
10
11
|
::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
|
11
|
-
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers,
|
12
|
-
::Brick.
|
13
|
-
::Brick.
|
12
|
+
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, is_development)
|
13
|
+
require 'brick/join_array' if ::Brick.enable_controllers?
|
14
|
+
::Brick.enable_views = app.config.brick.fetch(:enable_views, is_development)
|
15
|
+
::Brick.enable_routes = app.config.brick.fetch(:enable_routes, is_development)
|
14
16
|
::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
|
15
17
|
|
16
18
|
# Specific database tables and views to omit when auto-creating models
|
@@ -43,7 +45,7 @@ module Brick
|
|
43
45
|
# ====================================
|
44
46
|
# Dynamically create generic templates
|
45
47
|
# ====================================
|
46
|
-
if ::Brick.enable_views?
|
48
|
+
if ::Brick.enable_views?
|
47
49
|
ActionView::LookupContext.class_exec do
|
48
50
|
alias :_brick_template_exists? :template_exists?
|
49
51
|
def template_exists?(*args, **options)
|
@@ -51,14 +53,12 @@ module Brick
|
|
51
53
|
# Need to return true if we can fill in the blanks for a missing one
|
52
54
|
# args will be something like: ["index", ["categories"]]
|
53
55
|
model = args[1].map(&:camelize).join('::').singularize.constantize
|
54
|
-
if (
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
)
|
61
|
-
instance_variable_set(:@_brick_model, model)
|
56
|
+
if is_template_exists = model && (
|
57
|
+
['index', 'show'].include?(args.first) || # Everything has index and show
|
58
|
+
# Only CUD stuff has create / update / destroy
|
59
|
+
(!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
|
60
|
+
)
|
61
|
+
@_brick_model = model
|
62
62
|
end
|
63
63
|
end
|
64
64
|
is_template_exists
|
@@ -66,55 +66,43 @@ module Brick
|
|
66
66
|
|
67
67
|
alias :_brick_find_template :find_template
|
68
68
|
def find_template(*args, **options)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
exclude_hms[hmt.last.name] = nil
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
92
|
-
table_options = ::Brick.relations.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
93
|
-
hms_columns = +'' # Used for 'index'
|
94
|
-
hms_headers = hms.each_with_object([]) do |hm, s|
|
95
|
-
next if exclude_hms.key?((hm_assoc = hm.last).name)
|
96
|
-
|
97
|
-
if args.first == 'index'
|
98
|
-
hm_fk_name = if hm_assoc.options[:through]
|
99
|
-
associative = associatives[hm_assoc.name]
|
100
|
-
"'#{associative.name}.#{associative.foreign_key}'"
|
101
|
-
else
|
102
|
-
hm_assoc.foreign_key
|
103
|
-
end
|
104
|
-
hms_columns << if hm_assoc.macro == :has_many
|
69
|
+
return _brick_find_template(*args, **options) unless @_brick_model
|
70
|
+
|
71
|
+
model_name = @_brick_model.name
|
72
|
+
pk = @_brick_model.primary_key
|
73
|
+
obj_name = model_name.underscore
|
74
|
+
table_name = model_name.pluralize.underscore
|
75
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
76
|
+
hms_columns = +'' # Used for 'index'
|
77
|
+
hms_headers = hms.each_with_object([]) do |hm, s|
|
78
|
+
hm_assoc = hm.last
|
79
|
+
if args.first == 'index'
|
80
|
+
hm_fk_name = if hm_assoc.options[:through]
|
81
|
+
associative = associatives[hm_assoc.name]
|
82
|
+
"'#{associative.name}.#{associative.foreign_key}'"
|
83
|
+
else
|
84
|
+
hm_assoc.foreign_key
|
85
|
+
end
|
86
|
+
hms_columns << if hm_assoc.macro == :has_many
|
105
87
|
"<td>
|
106
|
-
<%=
|
88
|
+
<%= ct = #{obj_name}._br_#{hm.first}_ct
|
89
|
+
link_to \"#\{ct\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct.zero? %>
|
107
90
|
</td>\n"
|
108
|
-
|
91
|
+
else # has_one
|
109
92
|
"<td>
|
110
93
|
<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
|
111
94
|
</td>\n"
|
112
|
-
|
113
|
-
end
|
114
|
-
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
95
|
+
end
|
115
96
|
end
|
97
|
+
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
98
|
+
end
|
116
99
|
|
117
|
-
|
100
|
+
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
101
|
+
# %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
|
102
|
+
# environment or whatever, then get either the controllers or routes list instead
|
103
|
+
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
104
|
+
.each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
|
105
|
+
css = +"<style>
|
118
106
|
table {
|
119
107
|
border-collapse: collapse;
|
120
108
|
margin: 25px 0;
|
@@ -163,6 +151,23 @@ a.big-arrow {
|
|
163
151
|
font-size: 2.5em;
|
164
152
|
text-decoration: none;
|
165
153
|
}
|
154
|
+
.wide-input {
|
155
|
+
display: block;
|
156
|
+
overflow: hidden;
|
157
|
+
}
|
158
|
+
.wide-input input[type=text] {
|
159
|
+
width: 100%;
|
160
|
+
}
|
161
|
+
.dimmed {
|
162
|
+
background-color: #C0C0C0;
|
163
|
+
}
|
164
|
+
input[type=submit] {
|
165
|
+
background-color: #004998;
|
166
|
+
color: #FFF;
|
167
|
+
}
|
168
|
+
.right {
|
169
|
+
text-align: right;
|
170
|
+
}
|
166
171
|
</style>
|
167
172
|
<% def is_bcrypt?(val)
|
168
173
|
val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
|
@@ -171,10 +176,16 @@ def hide_bcrypt(val)
|
|
171
176
|
is_bcrypt?(val) ? '(hidden)' : val
|
172
177
|
end %>"
|
173
178
|
|
174
|
-
|
179
|
+
if ['index', 'show', 'update'].include?(args.first)
|
180
|
+
# Example: <% bts = { "site_id" => [:site, Site, "id"], "study_id" => [:study, Study, "id"], "study_country_id" => [:study_country, StudyCountry, "id"], "user_id" => [:user, User, "id"], "role_id" => [:role, Role, "id"] } %>
|
181
|
+
css << "<% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
|
182
|
+
end
|
183
|
+
|
184
|
+
script = "<script>
|
175
185
|
var schemaSelect = document.getElementById(\"schema\");
|
186
|
+
var brickSchema;
|
176
187
|
if (schemaSelect) {
|
177
|
-
|
188
|
+
brickSchema = changeout(location.href, \"_brick_schema\");
|
178
189
|
if (brickSchema) {
|
179
190
|
[... document.getElementsByTagName(\"A\")].forEach(function (a) { a.href = changeout(a.href, \"_brick_schema\", brickSchema); });
|
180
191
|
}
|
@@ -184,6 +195,17 @@ if (schemaSelect) {
|
|
184
195
|
location.href = changeout(location.href, \"_brick_schema\", this.value);
|
185
196
|
});
|
186
197
|
}
|
198
|
+
[... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
|
199
|
+
if (brickSchema)
|
200
|
+
form.action = changeout(form.action, \"_brick_schema\", brickSchema);
|
201
|
+
form.addEventListener('submit', function (ev) {
|
202
|
+
[... ev.target.getElementsByTagName(\"SELECT\")].forEach(function (select) {
|
203
|
+
if (select.value === \"^^^brick_NULL^^^\")
|
204
|
+
select.value = null;
|
205
|
+
});
|
206
|
+
return true;
|
207
|
+
});
|
208
|
+
});
|
187
209
|
|
188
210
|
var tblSelect = document.getElementById(\"tbl\");
|
189
211
|
if (tblSelect) {
|
@@ -213,9 +235,8 @@ function changeout(href, param, value) {
|
|
213
235
|
return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
|
214
236
|
}
|
215
237
|
</script>"
|
216
|
-
|
217
|
-
|
218
|
-
when 'index'
|
238
|
+
inline = case args.first
|
239
|
+
when 'index'
|
219
240
|
"#{css}
|
220
241
|
<p style=\"color: green\"><%= notice %></p>#{"
|
221
242
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -223,9 +244,8 @@ function changeout(href, param, value) {
|
|
223
244
|
<h1>#{model_name.pluralize}</h1>
|
224
245
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
225
246
|
<table id=\"#{table_name}\">
|
226
|
-
<thead><tr>#{
|
227
|
-
<%
|
228
|
-
@#{table_name}.columns.map(&:name).each do |col| %>
|
247
|
+
<thead><tr>#{'<th></th>' if pk}
|
248
|
+
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
229
249
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
230
250
|
<th>
|
231
251
|
<% if (bt = bts[col]) %>
|
@@ -241,16 +261,16 @@ function changeout(href, param, value) {
|
|
241
261
|
<tbody>
|
242
262
|
<% @#{table_name}.each do |#{obj_name}| %>
|
243
263
|
<tr>#{"
|
244
|
-
<td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk
|
264
|
+
<td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
|
245
265
|
<% #{obj_name}.attributes.each do |k, val| %>
|
246
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
266
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
247
267
|
<td>
|
248
268
|
<% if (bt = bts[k]) %>
|
249
|
-
<%#
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
269
|
+
<%# binding.pry if bt.first == :user %>
|
270
|
+
<% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
|
271
|
+
<% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
|
272
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
273
|
+
<%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
|
254
274
|
<% else %>
|
255
275
|
<%= hide_bcrypt(val) %>
|
256
276
|
<% end %>
|
@@ -265,46 +285,70 @@ function changeout(href, param, value) {
|
|
265
285
|
|
266
286
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
|
267
287
|
#{script}"
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
288
|
+
when 'show', 'update'
|
289
|
+
"#{css}
|
290
|
+
<p style=\"color: green\"><%= notice %></p>#{"
|
291
|
+
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
292
|
+
<select id=\"tbl\">#{table_options}</select>
|
293
|
+
<h1>#{model_name}: <%= (obj = @#{obj_name}&.first)&.brick_descrip || controller_name %></h1>
|
294
|
+
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
295
|
+
<% if obj %>
|
296
|
+
<%= # path_options = [obj.#{pk}]
|
297
|
+
# path_options << { '_brick_schema': } if
|
298
|
+
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
299
|
+
form_for(obj) do |f| %>
|
276
300
|
<table>
|
277
|
-
<%
|
278
|
-
@#{obj_name}.first.attributes.each do |k, val| %>
|
301
|
+
<% @#{obj_name}.first.attributes.each do |k, val| %>
|
279
302
|
<tr>
|
280
303
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
281
304
|
<th class=\"show-field\">
|
282
305
|
<% if (bt = bts[k])
|
283
306
|
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
307
|
+
bt_name = bt[1].name
|
284
308
|
# %%% Only do this if the user has permissions to edit this bt field
|
285
|
-
|
286
|
-
|
309
|
+
if bt.length < 4
|
310
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
311
|
+
bt[1].order(:#{pk}).each { |obj| option_detail << [obj.brick_descrip, obj.#{pk}] }
|
312
|
+
end %>
|
313
|
+
BT <%= \"#\{bt.first\}-\" unless bt_name.underscore == bt.first.to_s %><%= bt_name %>
|
287
314
|
<% else %>
|
288
315
|
<%= k %>
|
289
316
|
<% end %>
|
290
317
|
</th>
|
291
318
|
<td>
|
292
|
-
<% if (bt = bts[k]) # bt_obj.brick_descrip
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
<%=
|
297
|
-
<% else
|
298
|
-
|
319
|
+
<% if (bt = bts[k]) # bt_obj.brick_descrip
|
320
|
+
html_options = { prompt: \"Select #\{bt_name\}\" }
|
321
|
+
html_options[:class] = 'dimmed' unless val %>
|
322
|
+
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
323
|
+
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
324
|
+
<% else case #{model_name}.column_for_attribute(k).type
|
325
|
+
when :string, :text %>
|
326
|
+
<% if is_bcrypt?(val) # || .readonly? %>
|
327
|
+
<%= hide_bcrypt(val) %>
|
328
|
+
<% else %>
|
329
|
+
<div class=\"wide-input\"><%= f.text_field k.to_sym %></div>
|
330
|
+
<% end %>
|
331
|
+
<% when :boolean %>
|
332
|
+
<%= f.check_box k.to_sym %>
|
333
|
+
<% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
|
334
|
+
# What happens when keys are UUID?
|
335
|
+
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
336
|
+
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
337
|
+
# ActiveUUID gem created a new :uuid type %>
|
338
|
+
<%= val %>
|
339
|
+
<% when :binary, :primary_key %>
|
340
|
+
<% end %>
|
299
341
|
<% end %>
|
300
342
|
</td>
|
301
343
|
</tr>
|
302
|
-
|
344
|
+
<% end %>
|
345
|
+
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
303
346
|
</table>
|
304
347
|
<% end %>
|
305
348
|
|
306
349
|
#{hms_headers.map do |hm|
|
307
350
|
next unless (pk = hm.first.klass.primary_key)
|
351
|
+
|
308
352
|
"<table id=\"#{hm_name = hm.first.name.to_s}\">
|
309
353
|
<tr><th>#{hm.last}</th></tr>
|
310
354
|
<% collection = @#{obj_name}.first.#{hm_name}
|
@@ -317,21 +361,19 @@ function changeout(href, param, value) {
|
|
317
361
|
<% end %>
|
318
362
|
<% end %>
|
319
363
|
</table>" end.join}
|
364
|
+
<% end %>
|
320
365
|
#{script}"
|
321
366
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
else
|
328
|
-
_brick_find_template(*args, **options)
|
329
|
-
end
|
367
|
+
end
|
368
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
369
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
370
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
371
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
330
372
|
end
|
331
373
|
end
|
332
374
|
end
|
333
375
|
|
334
|
-
if ::Brick.enable_routes?
|
376
|
+
if ::Brick.enable_routes?
|
335
377
|
ActionDispatch::Routing::RouteSet.class_exec do
|
336
378
|
# In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
|
337
379
|
prepend ::Brick::RouteSet
|
@@ -341,17 +383,6 @@ function changeout(href, param, value) {
|
|
341
383
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
342
384
|
# go make sure we've loaded additional references (virtual foreign keys).
|
343
385
|
::Brick.load_additional_references
|
344
|
-
|
345
|
-
# Find associative tables that can be set up for has_many :through
|
346
|
-
::Brick.relations.each do |_key, tbl|
|
347
|
-
tbl_cols = tbl[:cols].keys
|
348
|
-
fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
|
349
|
-
# Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
|
350
|
-
# foreign keys then it can act as an associative table and thus be used with has_many :through.
|
351
|
-
if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
|
352
|
-
fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
|
353
|
-
end
|
354
|
-
end
|
355
386
|
end
|
356
387
|
end
|
357
388
|
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Brick
|
2
|
+
# JoinArray and JoinHash
|
3
|
+
#
|
4
|
+
# These JOIN-related collection classes -- JoinArray and its related "partner in crime" JoinHash -- both interact to
|
5
|
+
# more easily build out nested sets of hashes and arrays to be used with ActiveRecord's .joins() method. For example,
|
6
|
+
# if there is an Order, Customer, and Employee model, and Order belongs_to :customer and :employee, then from the
|
7
|
+
# perspective of Order all these three could be JOINed together by referencing the two belongs_to association names:
|
8
|
+
#
|
9
|
+
# Order.joins([:customer, :employee])
|
10
|
+
#
|
11
|
+
# and from the perspective of Employee it would instead use a hash like this, using the has_many :orders association
|
12
|
+
# and the :customer belongs_to:
|
13
|
+
#
|
14
|
+
# Employee.joins({ orders: :customer })
|
15
|
+
#
|
16
|
+
# (in both cases the same three tables are being JOINed, the two approaches differ just based on their starting standpoint.)
|
17
|
+
# These utility classes are designed to make building out any goofy linkages like this pretty simple in a few ways:
|
18
|
+
# ** if the same association is requested more than once then no duplicates.
|
19
|
+
# ** If a bunch of intermediary associations are referenced leading up to a final one then all of them get automatically built
|
20
|
+
# out and added along the way, without any having to previously exist.
|
21
|
+
# ** If one reference was made previously and now another neighbouring one is called for, then what used to be a simple symbol
|
22
|
+
# is automatically graduated into an array so that both members can be held. For instance, if with the Order example above
|
23
|
+
# there was also a LineItem model that belongs_to Order, then let's say you start from LineItem and want to now get all 4
|
24
|
+
# related models. You could start by going through :order to :employee like this:
|
25
|
+
#
|
26
|
+
# line_item_joins = JoinArray.new
|
27
|
+
# line_item_joins[:order] = :employee
|
28
|
+
# => { order: :employee }
|
29
|
+
#
|
30
|
+
# and then add in the reference to :customer like this:
|
31
|
+
#
|
32
|
+
# line_item_joins[:order] = :customer
|
33
|
+
# => { order: [:employee, :customer] }
|
34
|
+
#
|
35
|
+
# and then carry on incrementally building out more JOINs in whatever sequence makes the best sense. This bundle of nested
|
36
|
+
# stuff can then be used to query ActiveRecord like this:
|
37
|
+
#
|
38
|
+
# LineItem.joins(line_item_joins)
|
39
|
+
|
40
|
+
class JoinArray < Array
|
41
|
+
attr_reader :parent, :orig_parent, :parent_key
|
42
|
+
alias _brick_set []=
|
43
|
+
|
44
|
+
def [](*args)
|
45
|
+
if !(key = args[0]).is_a?(Symbol)
|
46
|
+
super
|
47
|
+
else
|
48
|
+
idx = -1
|
49
|
+
# Whenever a JoinHash has a value of a JoinArray with a single member then it is a wrapper, usually for a Symbol
|
50
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
51
|
+
case matching
|
52
|
+
when ::Brick::JoinHash
|
53
|
+
matching[key]
|
54
|
+
when ::Brick::JoinArray
|
55
|
+
matching.first
|
56
|
+
else
|
57
|
+
::Brick::JoinHash.new.tap do |child|
|
58
|
+
child.instance_variable_set(:@parent, self)
|
59
|
+
child.instance_variable_set(:@parent_key, key) # %%% Use idx instead of key?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def []=(*args)
|
66
|
+
::Brick::JoinArray.attach_back_to_root(self, args[0], args[1])
|
67
|
+
|
68
|
+
if (key = args[0]).is_a?(Symbol) && ((value = args[1]).is_a?(::Brick::JoinHash) || value.is_a?(Symbol) || value.nil?)
|
69
|
+
# %%% This is for the first symbol added to a JoinArray, cleaning out the leftover {} that is temporarily built out
|
70
|
+
# when doing my_join_array[:value1][:value2] = nil.
|
71
|
+
idx = -1
|
72
|
+
delete_at(idx) if value.nil? && any? { |x| idx += 1; x.is_a?(::Brick::JoinHash) && x.empty? }
|
73
|
+
|
74
|
+
set_matching(key, value)
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.attach_back_to_root(collection, key = nil, value = nil)
|
81
|
+
# Create a list of layers which start at the root
|
82
|
+
layers = []
|
83
|
+
layer = collection
|
84
|
+
while layer.parent
|
85
|
+
layers << layer
|
86
|
+
layer = layer.parent
|
87
|
+
end
|
88
|
+
# Go through the layers from root down to child, attaching everything
|
89
|
+
layers.each do |layer|
|
90
|
+
if (prnt = layer.remove_instance_variable(:@parent))
|
91
|
+
layer.instance_variable_set(:@orig_parent, prnt)
|
92
|
+
end
|
93
|
+
case prnt
|
94
|
+
when ::Brick::JoinHash
|
95
|
+
value = if prnt.key?(layer.parent_key)
|
96
|
+
if layer.is_a?(Hash)
|
97
|
+
layer
|
98
|
+
else
|
99
|
+
::Brick::JoinArray.new.replace([prnt.fetch(layer.parent_key, nil), layer])
|
100
|
+
end
|
101
|
+
else
|
102
|
+
layer
|
103
|
+
end
|
104
|
+
# This is as if we did: prnt[layer.parent_key] = value
|
105
|
+
# but calling it that way would attempt to infinitely recurse back onto this overridden version of the []= method,
|
106
|
+
# so we go directly to ._brick_store() instead.
|
107
|
+
prnt._brick_store(layer.parent_key, value)
|
108
|
+
when ::Brick::JoinArray
|
109
|
+
if (key)
|
110
|
+
puts "X1"
|
111
|
+
prnt[layer.parent_key][key] = value
|
112
|
+
else
|
113
|
+
prnt[layer.parent_key] = layer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_matching(key, value)
|
120
|
+
idx = -1
|
121
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
122
|
+
case matching
|
123
|
+
when ::Brick::JoinHash
|
124
|
+
matching[key] = value
|
125
|
+
when Symbol
|
126
|
+
if value.nil? # If it already exists then no worries
|
127
|
+
matching
|
128
|
+
else
|
129
|
+
# Not yet there, so we will "graduate" this single value into being a key / value pair found in a JoinHash. The
|
130
|
+
# destination hash to be used will be either an existing one if there is a neighbouring JoinHash available, or a
|
131
|
+
# newly-built one placed in the "new_hash" variable if none yet exists.
|
132
|
+
hash = find { |x| x.is_a?(::Brick::JoinHash) } || (new_hash = ::Brick::JoinHash.new)
|
133
|
+
hash._brick_store(key, ::Brick::JoinArray.new.tap { |val_array| val_array.replace([value]) })
|
134
|
+
# hash.instance_variable_set(:@parent, matching.parent) if matching.parent
|
135
|
+
# hash.instance_variable_set(:@parent_key, matching.parent_key) if matching.parent_key
|
136
|
+
|
137
|
+
# When a new JoinHash was created, we place it at the same index where the original lone symbol value was pulled from.
|
138
|
+
# If instead we used an existing JoinHash then since that symbol has now been graduated into a new key / value pair in
|
139
|
+
# the existing JoinHash then we delete the original symbol by its index.
|
140
|
+
new_hash ? _brick_set(idx, new_hash) : delete_at(idx)
|
141
|
+
end
|
142
|
+
when ::Brick::JoinArray # Replace this single thing (usually a Symbol found as a value in a JoinHash)
|
143
|
+
(hash = ::Brick::JoinHash.new)._brick_store(key, value)
|
144
|
+
if matching.parent
|
145
|
+
hash.instance_variable_set(:@parent, matching.parent)
|
146
|
+
hash.instance_variable_set(:@parent_key, matching.parent_key)
|
147
|
+
end
|
148
|
+
_brick_set(idx, hash)
|
149
|
+
else # Doesn't already exist anywhere, so add it to the end of this JoinArray and return the new member
|
150
|
+
if value
|
151
|
+
::Brick::JoinHash.new.tap do |hash|
|
152
|
+
val_collection = if value.is_a?(::Brick::JoinHash)
|
153
|
+
value
|
154
|
+
else
|
155
|
+
::Brick::JoinArray.new.tap { |array| array.replace([value]) }
|
156
|
+
end
|
157
|
+
val_collection.instance_variable_set(:@parent, hash)
|
158
|
+
val_collection.instance_variable_set(:@parent_key, key)
|
159
|
+
hash._brick_store(key, val_collection)
|
160
|
+
hash.instance_variable_set(:@parent, self)
|
161
|
+
hash.instance_variable_set(:@parent_key, length)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
key
|
165
|
+
end.tap { |member| push(member) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class JoinHash < Hash
|
171
|
+
attr_reader :parent, :orig_parent, :parent_key
|
172
|
+
alias _brick_store []=
|
173
|
+
|
174
|
+
def [](*args)
|
175
|
+
if (current = super)
|
176
|
+
current
|
177
|
+
elsif (key = args[0]).is_a?(Symbol)
|
178
|
+
::Brick::JoinHash.new.tap do |child|
|
179
|
+
child.instance_variable_set(:@parent, self)
|
180
|
+
child.instance_variable_set(:@parent_key, key)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def []=(*args)
|
186
|
+
::Brick::JoinArray.attach_back_to_root(self)
|
187
|
+
|
188
|
+
if !(key = args[0]).is_a?(Symbol) || (!(value = args[1]).is_a?(Symbol) && !value.nil?)
|
189
|
+
super # Revert to normal hash behaviour when we're not passed symbols
|
190
|
+
else
|
191
|
+
case (current = fetch(key, nil))
|
192
|
+
when value
|
193
|
+
if value.nil? # Setting a single value where nothing yet exists
|
194
|
+
case orig_parent
|
195
|
+
when ::Brick::JoinHash
|
196
|
+
if self.empty? # Convert this empty hash into a JoinArray
|
197
|
+
orig_parent._brick_store(parent_key, ::Brick::JoinArray.new.replace([key]))
|
198
|
+
else # Call back into []= to use our own logic, this time setting this value from the context of the parent
|
199
|
+
orig_parent[parent_key] = key
|
200
|
+
end
|
201
|
+
when ::Brick::JoinArray
|
202
|
+
orig_parent[parent_key][key] = nil
|
203
|
+
else # No knowledge of any parent, so all we can do is add this single value right here as { key => nil }
|
204
|
+
super
|
205
|
+
end
|
206
|
+
key
|
207
|
+
else # Setting a key / value pair where nothing yet exists
|
208
|
+
puts "X2"
|
209
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
210
|
+
value
|
211
|
+
end
|
212
|
+
when Symbol # Upgrade an existing symbol to be a part of our special JoinArray
|
213
|
+
puts "X3"
|
214
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
215
|
+
when ::Brick::JoinArray # Concatenate new stuff onto any existing JoinArray
|
216
|
+
current.set_matching(value, nil)
|
217
|
+
when ::Brick::JoinHash # Graduate an existing hash into being in an array if things are dissimilar
|
218
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
219
|
+
value
|
220
|
+
else # Perhaps this is part of some hybrid thing
|
221
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
222
|
+
value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|