brick 0.1.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brick
4
+ module Rails
5
+ # See http://guides.rubyonrails.org/engines.html
6
+ class Engine < ::Rails::Engine
7
+ # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
8
+ config.brick = ActiveSupport::OrderedOptions.new
9
+ initializer 'brick.initialisation' do |app|
10
+ Brick.enable_models = app.config.brick.fetch(:enable_models, true)
11
+ Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
12
+
13
+ # ====================================
14
+ # Dynamically create generic templates
15
+ # ====================================
16
+ if (Brick.enable_views = app.config.brick.fetch(:enable_views, true))
17
+ ActionView::LookupContext.class_exec do
18
+ alias :_brick_template_exists? :template_exists?
19
+ def template_exists?(*args, **options)
20
+ unless (is_template_exists = _brick_template_exists?(*args, **options))
21
+ # Need to return true if we can fill in the blanks for a missing one
22
+ # args will be something like: ["index", ["categories"]]
23
+ model = args[1].map(&:camelize).join('::').singularize.constantize
24
+ if (
25
+ is_template_exists = model && (
26
+ ['index', 'show'].include?(args.first) || # Everything has index and show
27
+ # Only CRU stuff has create / update / destroy
28
+ (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
29
+ )
30
+ )
31
+ instance_variable_set(:@_brick_model, model)
32
+ end
33
+ end
34
+ is_template_exists
35
+ end
36
+
37
+ alias :_brick_find_template :find_template
38
+ def find_template(*args, **options)
39
+ if @_brick_model
40
+ model_name = @_brick_model.name
41
+ pk = @_brick_model.primary_key
42
+ obj_name = model_name.underscore
43
+ table_name = model_name.pluralize.underscore
44
+ # This gets has_many as well as has_many :through
45
+ # %%% weed out ones that don't have an available model to reference
46
+ bts, hms = @_brick_model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
47
+ case a.macro
48
+ when :belongs_to
49
+ # Build #brick_descrip if needed
50
+ unless a.klass.instance_methods(false).include?(:brick_descrip)
51
+ descrip_col = (a.klass.columns.map(&:name) - a.klass._brick_get_fks -
52
+ (::Brick.config.metadata_columns || []) -
53
+ [a.klass.primary_key]).first&.to_sym
54
+ if descrip_col
55
+ a.klass.define_method :brick_descrip do
56
+ send(descrip_col)
57
+ end
58
+ end
59
+ end
60
+
61
+ s.first[a.foreign_key] = [a.name, a.klass]
62
+ when :has_many
63
+ s.last[a.name] = a
64
+ end
65
+ s
66
+ end
67
+ # Weed out has_manys that go to an associative table
68
+ hms.select { |k, v| v.options[:through] }.each { |_k, hmt| hms.delete(hmt.options[:through]) }
69
+ show_obj_blurb = "<td><%= link_to \"#\{#{obj_name}.class.name\} ##\{#{obj_name}.id\}\", #{obj_name} %></td>\n"
70
+ hms_headers = hms.each_with_object(+'') { |hm, s| s << "<th>HM #{hm.first}</th>\n" }
71
+ hms_columns = hms.each_with_object(+'') do |hm, s|
72
+ s << "<td>
73
+ <%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm.last.klass.name.underscore.pluralize}_path({ #{hm.last.foreign_key}: #{obj_name}.#{pk} }) %>
74
+ </td>\n"
75
+ end
76
+
77
+ inline = case args.first
78
+ when 'index'
79
+ "<p style=\"color: green\"><%= notice %></p>
80
+
81
+ <h1>#{model_name.pluralize}</h1>
82
+ <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
83
+
84
+ <table id=\"#{table_name}\">
85
+ <tr>
86
+ <% is_first = true; is_need_id_col = nil
87
+ bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last.last.name}, #{v.last.last.primary_key.inspect}]"}.join(', ')} }
88
+ @#{table_name}.columns.map(&:name).each do |col| %>
89
+ <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
90
+ <th>
91
+ <% if bt = bts[col]
92
+ if is_first
93
+ is_first = false
94
+ is_need_id_col = true %>
95
+ </th><th>
96
+ <% end %>
97
+ BT <%= bt[1].name %>
98
+ <% else
99
+ is_first = false %>
100
+ <%= col %>
101
+ <% end %>
102
+ </th>
103
+ <% end %>
104
+ <% if is_first # STILL haven't been able to write a first non-key / non-metadata column?
105
+ is_first = false
106
+ is_need_id_col = true %>
107
+ <td></td>
108
+ <% end %>
109
+ #{hms_headers}
110
+ </tr>
111
+
112
+ <% @#{table_name}.each do |#{obj_name}| %>
113
+ <tr>
114
+ <% if is_need_id_col %>
115
+ <td>#{show_obj_blurb}</td>
116
+ <% end %>
117
+ <% is_first = true; #{obj_name}.attributes.each do |k, val| %>
118
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
119
+ <% if (bt = bts[k]) %>
120
+ <td>
121
+ <%= obj = bt[1].find_by(bt.last => val); link_to obj.brick_descrip, obj %>
122
+ <% elsif is_first %>
123
+ <td>
124
+ <%= is_first = false; link_to val, #{obj_name} %>
125
+ <% else %>
126
+ <td>
127
+ <%= val %>
128
+ <% end %>
129
+ </td>
130
+ <% end %>
131
+ #{hms_columns}
132
+ <!-- td>X</td -->
133
+ </tr>
134
+ <% end %>
135
+ </table>
136
+
137
+ <%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>
138
+ "
139
+ # "<%= @#{@_brick_model.name.underscore.pluralize}.inspect %>"
140
+ when 'show'
141
+ "<%= @#{@_brick_model.name.underscore}.inspect %>"
142
+ end
143
+ puts inline
144
+ # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
145
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
146
+ handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
147
+ ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
148
+ else
149
+ _brick_find_template(*args, **options)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ if (::Brick.enable_routes = app.config.brick.fetch(:enable_routes, true))
156
+ ActionDispatch::Routing::RouteSet.class_exec do
157
+ alias _brick_finalize_routeset! finalize!
158
+ def finalize!(*args, **options)
159
+ unless @finalized
160
+ existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
161
+ ::Rails.application.routes.append do
162
+ # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
163
+ # If auto-controllers and auto-models are both enabled then this makes sense:
164
+ relations = (::Brick.instance_variable_get(:@relations) || {})[ActiveRecord::Base.connection_pool.object_id] || {}
165
+ relations.each do |k, v|
166
+ unless existing_controllers.key?(controller_name = k.underscore.pluralize)
167
+ options = {}
168
+ options[:only] = [:index, :show] if v.key?(:isView)
169
+ send(:resources, controller_name.to_sym, **options)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ _brick_finalize_routeset!(*args, **options)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Do not consider database views when auto-creating models
180
+ ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
181
+
182
+ # Specific database tables and views to omit when auto-creating models
183
+ ::Brick.exclude_tables = app.config.brick.fetch(:exclude_tables, [])
184
+
185
+ # Columns to treat as being metadata for purposes of identifying associative tables for has_many :through
186
+ ::Brick.metadata_columns = app.config.brick.fetch(:metadata_columns, ['created_at', 'updated_at', 'deleted_at'])
187
+
188
+ # Additional references (virtual foreign keys)
189
+ if (ars = (::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)))
190
+ ars = ars.call if ars.is_a?(Proc)
191
+ ars = ars.to_a unless ars.is_a?(Array)
192
+ ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
193
+ ars.each do |fk|
194
+ ::Brick._add_bt_and_hm(fk[0..2])
195
+ end
196
+ end
197
+
198
+ # Find associative tables that can be set up for has_many :through
199
+ ::Brick.relations.each do |_key, tbl|
200
+ tbl_cols = tbl[:cols].keys
201
+ fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = fk.last[:inverse_table] if fk.last[:is_bt]; s }
202
+ # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
203
+ # foreign keys then it can act as an associative table and thus be used with has_many :through.
204
+ if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - tbl[:pkey].values.first).length.zero?
205
+ fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'brick/frameworks/rails/controller'
4
+ require 'brick/frameworks/rails/engine'
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/matchers'
5
+ # require 'brick/frameworks/rspec/helpers'
6
+
7
+ RSpec.configure do |config|
8
+ # config.include ::Brick::RSpec::Helpers::InstanceMethods
9
+ # config.extend ::Brick::RSpec::Helpers::ClassMethods
10
+
11
+ # config.before(:each) do
12
+ # ::Brick.enable_routes = false
13
+ # end
14
+
15
+ # config.before(:each, df_importing: true) do
16
+ # ::Brick.enable_routes = true
17
+ # end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brick
4
+ module Serializers
5
+ # An alternate serializer for, e.g. `versions.object`.
6
+ module JSON
7
+ extend self # makes all instance methods become module methods as well
8
+
9
+ def load(string)
10
+ ActiveSupport::JSON.decode string
11
+ end
12
+
13
+ def dump(object)
14
+ ActiveSupport::JSON.encode object
15
+ end
16
+
17
+ # Returns a SQL LIKE condition to be used to match the given field and
18
+ # value in the serialized object.
19
+ def where_object_condition(arel_field, field, value)
20
+ # Convert to JSON to handle strings and nulls correctly.
21
+ json_value = value.to_json
22
+
23
+ # If the value is a number, we need to ensure that we find the next
24
+ # character too, which is either `,` or `}`, to ensure that searching
25
+ # for the value 12 doesn't yield false positives when the value is
26
+ # 123.
27
+ if value.is_a? Numeric
28
+ arel_field.matches("%\"#{field}\":#{json_value},%")
29
+ .or(arel_field.matches("%\"#{field}\":#{json_value}}%"))
30
+ else
31
+ arel_field.matches("%\"#{field}\":#{json_value}%")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Brick
6
+ module Serializers
7
+ # The default serializer for, e.g. `versions.object`.
8
+ module YAML
9
+ extend self # makes all instance methods become module methods as well
10
+
11
+ def load(string)
12
+ ::YAML.safe_load string
13
+ end
14
+
15
+ def dump(object)
16
+ ::YAML.dump object
17
+ end
18
+
19
+ # Returns a SQL LIKE condition to be used to match the given field and
20
+ # value in the serialized object.
21
+ def where_object_condition(arel_field, field, value)
22
+ arel_field.matches("%\n#{field}: #{value}\n%")
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/brick/util.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module Brick
5
+ module Util
6
+ # ===================================
7
+ # Epic require patch
8
+ def self._patch_require(module_filename, folder_matcher, search_text, replacement_text, autoload_symbol = nil, is_bundler = false)
9
+ mod_name_parts = module_filename.split('.')
10
+ extension = case mod_name_parts.last
11
+ when 'rb', 'so', 'o'
12
+ module_filename = mod_name_parts[0..-2].join('.')
13
+ ".#{mod_name_parts.last}"
14
+ else
15
+ '.rb'
16
+ end
17
+
18
+ if autoload_symbol
19
+ unless Object.const_defined?('ActiveSupport::Dependencies')
20
+ require 'active_support'
21
+ require 'active_support/dependencies'
22
+ end
23
+ alp = ActiveSupport::Dependencies.autoload_paths
24
+ custom_require_dir = ::Brick::Util._custom_require_dir
25
+ # Create any missing folder structure leading up to this file
26
+ module_filename.split('/')[0..-2].inject(custom_require_dir) do |s, part|
27
+ new_part = File.join(s, part)
28
+ Dir.mkdir(new_part) unless Dir.exist?(new_part)
29
+ new_part
30
+ end
31
+ if ::Brick::Util._write_patched(folder_matcher, module_filename, extension, custom_require_dir, nil, search_text, replacement_text) &&
32
+ !alp.include?(custom_require_dir)
33
+ alp.unshift(custom_require_dir)
34
+ end
35
+ elsif is_bundler
36
+ puts "Bundler hack"
37
+ require 'pry-byebug'
38
+ binding.pry
39
+ x = 5
40
+ # bin_path
41
+ # puts Bundler.require.inspect
42
+ else
43
+ unless (require_overrides = ::Brick::Util.instance_variable_get(:@_require_overrides))
44
+ ::Brick::Util.instance_variable_set(:@_require_overrides, (require_overrides = {}))
45
+
46
+ # Patch "require" itself so that when it specifically sees "active_support/values/time_zone" then
47
+ # a copy is taken of the original, an attempt is made to find the line with a circular error, that
48
+ # single line is patched, and then an updated version is written to a temporary folder which is
49
+ # then required in place of the original.
50
+
51
+ Kernel.module_exec do
52
+ # class << self
53
+ alias_method :orig_require, :require
54
+ # end
55
+ # To be most faithful to Ruby's normal behaviour, this should look like a public singleton
56
+ define_method(:require) do |name|
57
+ puts name if name.to_s.include?('cucu')
58
+ if (require_override = ::Brick::Util.instance_variable_get(:@_require_overrides)[name])
59
+ extension, folder_matcher, search_text, replacement_text, autoload_symbol = require_override
60
+ patched_filename = "/patched_#{name.tr('/', '_')}#{extension}"
61
+ if $LOADED_FEATURES.find { |f| f.end_with?(patched_filename) }
62
+ false
63
+ else
64
+ is_replaced = false
65
+ if (replacement_path = ::Brick::Util._write_patched(folder_matcher, name, extension, ::Brick::Util._custom_require_dir, patched_filename, search_text, replacement_text))
66
+ is_replaced = Kernel.send(:orig_require, replacement_path)
67
+ elsif replacement_path.nil?
68
+ puts "Couldn't find #{name} to require it!"
69
+ end
70
+ is_replaced
71
+ end
72
+ else
73
+ Kernel.send(:orig_require, name)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ require_overrides[module_filename] = [extension, folder_matcher, search_text, replacement_text, autoload_symbol]
79
+ end
80
+ end
81
+
82
+ def self._custom_require_dir
83
+ unless (custom_require_dir = ::Brick::Util.instance_variable_get(:@_custom_require_dir))
84
+ ::Brick::Util.instance_variable_set(:@_custom_require_dir, (custom_require_dir = Dir.mktmpdir))
85
+ # So normal Ruby require will now pick this one up
86
+ $LOAD_PATH.unshift(custom_require_dir)
87
+ # When Ruby is exiting, remove this temporary directory
88
+ at_exit do
89
+ FileUtils.rm_rf(::Brick::Util.instance_variable_get(:@_custom_require_dir))
90
+ end
91
+ end
92
+ custom_require_dir
93
+ end
94
+
95
+ # Returns the full path to the replaced filename, or
96
+ # false if the file already exists, and nil if it was unable to write anything.
97
+ def self._write_patched(folder_matcher, name, extension, dir, patched_filename, search_text, replacement_text)
98
+ # See if our replacement file might already exist for some reason
99
+ name = +"/#{name}" unless name.start_with?('/')
100
+ name << extension unless name.end_with?(extension)
101
+ puts (replacement_path = "#{dir}#{patched_filename || name}")
102
+ return false if File.exist?(replacement_path)
103
+
104
+ # Dredge up the original .rb file, doctor it, and then require it instead
105
+ num_written = nil
106
+ orig_path = nil
107
+ orig_as = nil
108
+ # Using Ruby's approach to find files to require
109
+ $LOAD_PATH.each do |path|
110
+ orig_path = "#{path}#{name}"
111
+ break if path.include?(folder_matcher) && (orig_as = File.open(orig_path))
112
+ end
113
+ puts [folder_matcher, name].inspect
114
+ if (orig_text = orig_as&.read)
115
+ File.open(replacement_path, 'w') do |replacement|
116
+ num_written = replacement.write(orig_text.gsub(search_text, replacement_text))
117
+ end
118
+ orig_as.close
119
+ end
120
+ (num_written&.> 0) ? replacement_path : nil
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module Brick
5
+ module VERSION
6
+ MAJOR = 1
7
+ MINOR = 0
8
+ TINY = 2
9
+
10
+ # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
+ PRE = nil
12
+
13
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.').freeze
14
+
15
+ def self.to_s
16
+ STRING
17
+ end
18
+ end
19
+ end