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.
- checksums.yaml +7 -0
- data/lib/brick/config.rb +92 -0
- data/lib/brick/extensions.rb +432 -0
- data/lib/brick/frameworks/cucumber.rb +39 -0
- data/lib/brick/frameworks/rails/controller.rb +41 -0
- data/lib/brick/frameworks/rails/engine.rb +211 -0
- data/lib/brick/frameworks/rails.rb +4 -0
- data/lib/brick/frameworks/rspec.rb +18 -0
- data/lib/brick/serializers/json.rb +36 -0
- data/lib/brick/serializers/yaml.rb +26 -0
- data/lib/brick/util.rb +123 -0
- data/lib/brick/version_number.rb +19 -0
- data/lib/brick.rb +528 -0
- data/lib/generators/brick/USAGE +2 -0
- data/lib/generators/brick/install_generator.rb +142 -0
- data/lib/generators/brick/model_generator.rb +117 -0
- data/lib/generators/brick/templates/add_object_changes_to_versions.rb.erb +12 -0
- data/lib/generators/brick/templates/create_versions.rb.erb +36 -0
- metadata +195 -48
- data/bin/brick +0 -15
@@ -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,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
|