brick 1.0.19 → 1.0.22
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 +14 -0
- data/lib/brick/extensions.rb +329 -66
- data/lib/brick/frameworks/rails/engine.rb +123 -109
- data/lib/brick/join_array.rb +227 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +29 -5
- data/lib/generators/brick/install_generator.rb +14 -9
- 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)
|
@@ -52,70 +54,67 @@ module Brick
|
|
52
54
|
# args will be something like: ["index", ["categories"]]
|
53
55
|
model = args[1].map(&:camelize).join('::').singularize.constantize
|
54
56
|
if is_template_exists = model && (
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
59
62
|
end
|
60
63
|
end
|
61
64
|
is_template_exists
|
62
65
|
end
|
63
66
|
|
67
|
+
def path_keys(fk_name, obj_name, pk)
|
68
|
+
if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
69
|
+
fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
|
70
|
+
else
|
71
|
+
[[fk_name, "#{obj_name}.#{pk}"]]
|
72
|
+
end.map { |x| "#{x.first}: #{x.last}"}.join(', ')
|
73
|
+
end
|
74
|
+
|
64
75
|
alias :_brick_find_template :find_template
|
65
76
|
def find_template(*args, **options)
|
66
|
-
|
67
|
-
model_name = @_brick_model.name
|
68
|
-
pk = @_brick_model.primary_key
|
69
|
-
obj_name = model_name.underscore
|
70
|
-
table_name = model_name.pluralize.underscore
|
71
|
-
# This gets has_many as well as has_many :through
|
72
|
-
# %%% weed out ones that don't have an available model to reference
|
73
|
-
bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
|
74
|
-
# Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
|
75
|
-
# as well as any possible polymorphic associations
|
76
|
-
exclude_hms = {}
|
77
|
-
associatives = hms.each_with_object({}) do |hmt, s|
|
78
|
-
if (through = hmt.last.options[:through])
|
79
|
-
exclude_hms[through] = nil
|
80
|
-
s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
|
81
|
-
elsif hmt.last.inverse_of.nil?
|
82
|
-
puts "SKIPPING #{hmt.last.name.inspect}"
|
83
|
-
# %%% If we don't do this then below associative.name will find that associative is nil
|
84
|
-
exclude_hms[hmt.last.name] = nil
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
hms_columns = +'' # Used for 'index'
|
89
|
-
hms_headers = hms.each_with_object([]) do |hm, s|
|
90
|
-
next if exclude_hms.key?((hm_assoc = hm.last).name)
|
77
|
+
return _brick_find_template(*args, **options) unless @_brick_model
|
91
78
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
79
|
+
model_name = @_brick_model.name
|
80
|
+
pk = @_brick_model.primary_key
|
81
|
+
obj_name = model_name.underscore
|
82
|
+
table_name = model_name.pluralize.underscore
|
83
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
84
|
+
hms_columns = [] # Used for 'index'
|
85
|
+
skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
|
86
|
+
hms_headers = hms.each_with_object([]) do |hm, s|
|
87
|
+
hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
|
88
|
+
hm_fk_name = if hm_assoc.options[:through]
|
89
|
+
associative = associatives[hm_assoc.name]
|
90
|
+
"'#{associative.name}.#{associative.foreign_key}'"
|
91
|
+
else
|
92
|
+
hm_assoc.foreign_key
|
93
|
+
end
|
94
|
+
if args.first == 'index'
|
95
|
+
hms_columns << if hm_assoc.macro == :has_many
|
96
|
+
set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
|
97
|
+
'nil'
|
98
|
+
else
|
99
|
+
"#{obj_name}._br_#{assoc_name}_ct || 0"
|
100
|
+
end
|
101
|
+
"<%= ct = #{set_ct}
|
102
|
+
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
103
|
+
else # has_one
|
104
|
+
"<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
|
98
105
|
end
|
99
|
-
|
100
|
-
"
|
101
|
-
<%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless #{obj_name}.#{hm.first}.count.zero? %>
|
102
|
-
</td>\n"
|
103
|
-
else # has_one
|
104
|
-
"<td>
|
105
|
-
<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
|
106
|
-
</td>\n"
|
107
|
-
end
|
108
|
-
end
|
109
|
-
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
106
|
+
elsif args.first == 'show'
|
107
|
+
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
110
108
|
end
|
109
|
+
s << hm_stuff
|
111
110
|
end
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
111
|
+
|
112
|
+
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
113
|
+
# %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
|
114
|
+
# environment or whatever, then get either the controllers or routes list instead
|
115
|
+
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
116
|
+
.each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
|
117
|
+
css = +"<style>
|
119
118
|
table {
|
120
119
|
border-collapse: collapse;
|
121
120
|
margin: 25px 0;
|
@@ -127,9 +126,12 @@ table {
|
|
127
126
|
|
128
127
|
table thead tr th, table tr th {
|
129
128
|
background-color: #009879;
|
130
|
-
color: #
|
129
|
+
color: #fff;
|
131
130
|
text-align: left;
|
132
131
|
}
|
132
|
+
table thead tr th a, table tr th a {
|
133
|
+
color: #80FFB8;
|
134
|
+
}
|
133
135
|
|
134
136
|
table th, table td {
|
135
137
|
padding: 0.2em 0.5em;
|
@@ -138,6 +140,9 @@ table th, table td {
|
|
138
140
|
.show-field {
|
139
141
|
background-color: #004998;
|
140
142
|
}
|
143
|
+
.show-field a {
|
144
|
+
color: #80B8D2;
|
145
|
+
}
|
141
146
|
|
142
147
|
table tbody tr {
|
143
148
|
border-bottom: thin solid #dddddd;
|
@@ -189,7 +194,12 @@ def hide_bcrypt(val)
|
|
189
194
|
is_bcrypt?(val) ? '(hidden)' : val
|
190
195
|
end %>"
|
191
196
|
|
192
|
-
|
197
|
+
if ['index', 'show', 'update'].include?(args.first)
|
198
|
+
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(', ')} } %>"
|
199
|
+
end
|
200
|
+
|
201
|
+
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
202
|
+
script = "<script>
|
193
203
|
var schemaSelect = document.getElementById(\"schema\");
|
194
204
|
var brickSchema;
|
195
205
|
if (schemaSelect) {
|
@@ -243,9 +253,13 @@ function changeout(href, param, value) {
|
|
243
253
|
return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
|
244
254
|
}
|
245
255
|
</script>"
|
246
|
-
|
247
|
-
|
248
|
-
|
256
|
+
inline = case args.first
|
257
|
+
when 'index'
|
258
|
+
obj_pk = if pk&.is_a?(Array) # Composite primary key?
|
259
|
+
"[#{pk.map { |pk_part| "#{obj_name}.#{pk_part}" }.join(', ')}]"
|
260
|
+
elsif pk
|
261
|
+
"#{obj_name}.#{pk}"
|
262
|
+
end
|
249
263
|
"#{css}
|
250
264
|
<p style=\"color: green\"><%= notice %></p>#{"
|
251
265
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -253,40 +267,40 @@ function changeout(href, param, value) {
|
|
253
267
|
<h1>#{model_name.pluralize}</h1>
|
254
268
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
255
269
|
<table id=\"#{table_name}\">
|
256
|
-
<thead><tr>#{
|
257
|
-
<%
|
258
|
-
@#{table_name}.columns.map(&:name).each do |col| %>
|
270
|
+
<thead><tr>#{'<th></th>' if pk}
|
271
|
+
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
259
272
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
260
273
|
<th>
|
261
274
|
<% if (bt = bts[col]) %>
|
262
|
-
BT <%=
|
275
|
+
BT <%= bt[1].bt_link(bt.first) %>
|
263
276
|
<% else %>
|
264
277
|
<%= col %>
|
265
278
|
<% end %>
|
266
279
|
</th>
|
267
280
|
<% end %>
|
268
|
-
|
281
|
+
<%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
|
282
|
+
#{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.pluralize}_path) %></th>\n" }.join}
|
269
283
|
</tr></thead>
|
270
284
|
|
271
285
|
<tbody>
|
272
286
|
<% @#{table_name}.each do |#{obj_name}| %>
|
273
287
|
<tr>#{"
|
274
|
-
<td><%= link_to '⇛', #{obj_name}_path(#{
|
288
|
+
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if pk}
|
275
289
|
<% #{obj_name}.attributes.each do |k, val| %>
|
276
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
290
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
277
291
|
<td>
|
278
292
|
<% if (bt = bts[k]) %>
|
279
|
-
<%#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
293
|
+
<%# binding.pry # Postgres column names are limited to 63 characters!!! %>
|
294
|
+
<% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last[0..62]) }, @_brick_bt_descrip[bt.first][2]) %>
|
295
|
+
<% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
296
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
297
|
+
<%#= 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 %>
|
284
298
|
<% else %>
|
285
299
|
<%= hide_bcrypt(val) %>
|
286
300
|
<% end %>
|
287
301
|
</td>
|
288
302
|
<% end %>
|
289
|
-
|
303
|
+
<td>#{hms_columns.join('</td><td>')}</td>
|
290
304
|
<!-- td>X</td -->
|
291
305
|
</tr>
|
292
306
|
</tbody>
|
@@ -295,7 +309,7 @@ function changeout(href, param, value) {
|
|
295
309
|
|
296
310
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
|
297
311
|
#{script}"
|
298
|
-
|
312
|
+
when 'show', 'update'
|
299
313
|
"#{css}
|
300
314
|
<p style=\"color: green\"><%= notice %></p>#{"
|
301
315
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -304,12 +318,11 @@ function changeout(href, param, value) {
|
|
304
318
|
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
305
319
|
<% if obj %>
|
306
320
|
<%= # path_options = [obj.#{pk}]
|
307
|
-
# path_options << { '_brick_schema': } if
|
321
|
+
# path_options << { '_brick_schema': } if
|
308
322
|
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
309
|
-
form_for(obj) do |f| %>
|
323
|
+
form_for(obj.becomes(#{model_name})) do |f| %>
|
310
324
|
<table>
|
311
|
-
<%
|
312
|
-
@#{obj_name}.first.attributes.each do |k, val| %>
|
325
|
+
<% @#{obj_name}.first.attributes.each do |k, val| %>
|
313
326
|
<tr>
|
314
327
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
315
328
|
<th class=\"show-field\">
|
@@ -321,7 +334,7 @@ function changeout(href, param, value) {
|
|
321
334
|
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
322
335
|
bt[1].order(:#{pk}).each { |obj| option_detail << [obj.brick_descrip, obj.#{pk}] }
|
323
336
|
end %>
|
324
|
-
BT <%=
|
337
|
+
BT <%= bt[1].bt_link(bt.first) %>
|
325
338
|
<% else %>
|
326
339
|
<%= k %>
|
327
340
|
<% end %>
|
@@ -331,7 +344,7 @@ function changeout(href, param, value) {
|
|
331
344
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
332
345
|
html_options[:class] = 'dimmed' unless val %>
|
333
346
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
334
|
-
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{
|
347
|
+
<%= 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 %>
|
335
348
|
<% else case #{model_name}.column_for_attribute(k).type
|
336
349
|
when :string, :text %>
|
337
350
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -342,10 +355,10 @@ function changeout(href, param, value) {
|
|
342
355
|
<% when :boolean %>
|
343
356
|
<%= f.check_box k.to_sym %>
|
344
357
|
<% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
358
|
+
# What happens when keys are UUID?
|
359
|
+
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
360
|
+
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
361
|
+
# ActiveUUID gem created a new :uuid type %>
|
349
362
|
<%= val %>
|
350
363
|
<% when :binary, :primary_key %>
|
351
364
|
<% end %>
|
@@ -357,36 +370,37 @@ function changeout(href, param, value) {
|
|
357
370
|
</table>
|
358
371
|
<% end %>
|
359
372
|
|
360
|
-
#{hms_headers.
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
373
|
+
#{hms_headers.each_with_object(+'') do |hm, s|
|
374
|
+
if (pk = hm.first.klass.primary_key)
|
375
|
+
s << "<table id=\"#{hm_name = hm.first.name.to_s}\">
|
376
|
+
<tr><th>#{hm[3]}</th></tr>
|
377
|
+
<% collection = @#{obj_name}.first.#{hm_name}
|
378
|
+
collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
|
379
|
+
if collection.empty? %>
|
380
|
+
<tr><td>(none)</td></tr>
|
381
|
+
<% else %>
|
382
|
+
<% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
|
383
|
+
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
|
384
|
+
<% end %>
|
385
|
+
<% end %>
|
386
|
+
</table>"
|
387
|
+
else
|
388
|
+
s
|
389
|
+
end
|
390
|
+
end}
|
374
391
|
<% end %>
|
375
392
|
#{script}"
|
376
393
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
else
|
383
|
-
_brick_find_template(*args, **options)
|
384
|
-
end
|
394
|
+
end
|
395
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
396
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
397
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
398
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
385
399
|
end
|
386
400
|
end
|
387
401
|
end
|
388
402
|
|
389
|
-
if ::Brick.enable_routes?
|
403
|
+
if ::Brick.enable_routes?
|
390
404
|
ActionDispatch::Routing::RouteSet.class_exec do
|
391
405
|
# In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
|
392
406
|
prepend ::Brick::RouteSet
|
@@ -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
|