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.
@@ -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)
@@ -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
- is_template_exists = model && (
56
- ['index', 'show'].include?(args.first) || # Everything has index and show
57
- # Only CRU stuff has create / update / destroy
58
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
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
- if @_brick_model
70
- model_name = @_brick_model.name
71
- pk = @_brick_model.primary_key
72
- obj_name = model_name.underscore
73
- table_name = model_name.pluralize.underscore
74
- # This gets has_many as well as has_many :through
75
- # %%% weed out ones that don't have an available model to reference
76
- bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
77
- # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
78
- # as well as any possible polymorphic associations
79
- exclude_hms = {}
80
- associatives = hms.each_with_object({}) do |hmt, s|
81
- if (through = hmt.last.options[:through])
82
- exclude_hms[through] = nil
83
- s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
84
- elsif hmt.last.inverse_of.nil?
85
- puts "SKIPPING #{hmt.last.name.inspect}"
86
- # %%% If we don't do this then below associative.name will find that associative is nil
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
- <%= 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? %>
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
- else # has_one
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
- end
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
- css = "<style>
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
- script = "<script>
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
- var brickSchema = changeout(location.href, \"_brick_schema\");
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
- inline = case args.first
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>#{"<th></th>" if pk }
227
- <% 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(', ')} }
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
- <%# Instead of just 'bt_obj we have to put in all of this junk:
250
- # send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))
251
- # Otherwise we get stuff like:
252
- # ActionView::Template::Error (undefined method `vehicle_path' for #<ActionView::Base:0x0000000033a888>) %>
253
- <%= 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 %>
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
- when 'show'
269
- "#{css}
270
- <p style=\"color: green\"><%= notice %></p>#{"
271
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
272
- <select id=\"tbl\">#{table_options}</select>
273
- <h1>#{model_name}: <%= (obj = @#{obj_name}.first).brick_descrip %></h1>
274
- <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
275
- <%= form_for obj do |f| %>
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
- <% 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(', ')} }
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
- bt << bt[1].order(:#{pk}).map { |obj| [obj.brick_descrip, obj.#{pk}] } if bt.length < 4 %>
286
- BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
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
- <%= f.select k.to_sym, bt[3], {}, prompt: 'Select #{model_name}' %>
294
- <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
295
- <% elsif is_bcrypt?(val) %>
296
- <%= hide_bcrypt(val) %>
297
- <% else %>
298
- <%= f.text_field k.to_sym %>
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
- <% end %>
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
- end
323
- # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
324
- keys = options.has_key?(:locals) ? options[:locals].keys : []
325
- handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
326
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
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? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
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
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 17
8
+ TINY = 20
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil