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.
@@ -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, false)
12
- ::Brick.enable_views = app.config.brick.fetch(:enable_views, false)
13
- ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, false)
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? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
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
- ['index', 'show'].include?(args.first) || # Everything has index and show
56
- # Only CRU stuff has create / update / destroy
57
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
58
- ) && instance_variable_set(:@_brick_model, 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
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
- if @_brick_model
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
- if args.first == 'index'
93
- hm_fk_name = if hm_assoc.options[:through]
94
- associative = associatives[hm_assoc.name]
95
- "'#{associative.name}.#{associative.foreign_key}'"
96
- else
97
- hm_assoc.foreign_key
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
- hms_columns << if hm_assoc.macro == :has_many
100
- "<td>
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
- if @_brick_model
113
- schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
114
- # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
115
- # environment or whatever, then get either the controllers or routes list instead
116
- table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
117
- .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
118
- css = "<style>
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: #ffffff;
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
- script = "<script>
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
- inline = case args.first
248
- when 'index'
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>#{"<th></th>" if pk}
257
- <% 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(', ')} }
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 <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
275
+ BT <%= bt[1].bt_link(bt.first) %>
263
276
  <% else %>
264
277
  <%= col %>
265
278
  <% end %>
266
279
  </th>
267
280
  <% end %>
268
- #{hms_headers.map { |h| "<th>#{h.last}</th>\n" }.join}
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(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
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
- <%# Instead of just 'bt_obj we have to put in all of this junk:
280
- # send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))
281
- # Otherwise we get stuff like:
282
- # ActionView::Template::Error (undefined method `vehicle_path' for #<ActionView::Base:0x0000000033a888>) %>
283
- <%= bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
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
- #{hms_columns}
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
- when 'show', 'update'
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
- <% 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(', ')} }
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 <%= \"#\{bt.first\}-\" unless bt_name.underscore == bt.first.to_s %><%= bt_name %>
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(\"#\{bt_obj_class = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
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
- # What happens when keys are UUID?
346
- # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
347
- # If it's not yet enabled then: enable_extension 'uuid-ossp'
348
- # ActiveUUID gem created a new :uuid type %>
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.map do |hm|
361
- next unless (pk = hm.first.klass.primary_key)
362
- "<table id=\"#{hm_name = hm.first.name.to_s}\">
363
- <tr><th>#{hm.last}</th></tr>
364
- <% collection = @#{obj_name}.first.#{hm_name}
365
- collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
366
- if collection.empty? %>
367
- <tr><td>(none)</td></tr>
368
- <% else %>
369
- <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
370
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
371
- <% end %>
372
- <% end %>
373
- </table>" end.join}
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
- end
378
- # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
379
- keys = options.has_key?(:locals) ? options[:locals].keys : []
380
- handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
381
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
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? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
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
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 19
8
+ TINY = 22
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil