rubymonolith 0.1.5 → 0.1.7

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -4
  3. data/Rakefile +2 -0
  4. data/app/assets/images/monolith/logo.svg +6 -0
  5. data/app/assets/stylesheets/monolith/application.tailwind.css +14 -0
  6. data/app/components/monolith/base.rb +6 -0
  7. data/app/components/monolith/table.rb +37 -0
  8. data/app/controllers/monolith/application_controller.rb +68 -2
  9. data/app/controllers/monolith/emails_controller.rb +61 -0
  10. data/app/controllers/monolith/exceptions_controller.rb +370 -0
  11. data/app/controllers/monolith/gems_controller.rb +230 -0
  12. data/app/controllers/monolith/generators_controller.rb +377 -0
  13. data/app/controllers/monolith/home_controller.rb +10 -0
  14. data/app/controllers/monolith/models_controller.rb +319 -0
  15. data/app/controllers/monolith/routes_controller.rb +157 -0
  16. data/app/controllers/monolith/tables_controller.rb +168 -0
  17. data/app/views/monolith/base.rb +3 -0
  18. data/app/views/monolith/layouts/base.rb +23 -0
  19. data/config/routes.rb +14 -0
  20. data/lib/generators/monolith/content/USAGE +8 -0
  21. data/lib/generators/monolith/content/content_generator.rb +25 -0
  22. data/lib/generators/monolith/generators/base.rb +38 -0
  23. data/lib/generators/monolith/install/install_generator.rb +14 -95
  24. data/lib/generators/monolith/view/USAGE +8 -0
  25. data/lib/generators/monolith/view/view_generator.rb +20 -0
  26. data/lib/monolith/cli/template.rb +78 -5
  27. data/lib/monolith/cli.rb +22 -3
  28. data/lib/monolith/engine.rb +31 -0
  29. data/lib/monolith/version.rb +1 -1
  30. data/lib/tasks/monolith_tasks.rake +13 -4
  31. metadata +66 -10
  32. data/app/assets/stylesheets/monolith/application.css +0 -15
  33. data/app/views/layouts/monolith/application.html.erb +0 -15
@@ -0,0 +1,319 @@
1
+ # app/controllers/monolith/models_controller.rb
2
+ module Monolith
3
+ class ModelsController < Monolith::ApplicationController
4
+ def index
5
+ render Index.new.tap { _1.models = Model.all }
6
+ end
7
+
8
+ def show
9
+ model = Model.find(params[:id].to_s)
10
+ return render plain: "Model not found", status: :not_found unless model
11
+
12
+ render Show.new.tap { |v| v.model = model }
13
+ end
14
+
15
+ # =======================
16
+ # Inline ActiveModel-like object
17
+ # =======================
18
+ class Model
19
+ attr_reader :name, :klass
20
+
21
+ def self.all
22
+ Rails.application.eager_load!
23
+
24
+ models = ActiveRecord::Base.descendants
25
+ .reject(&:abstract_class?)
26
+ .sort_by(&:name)
27
+
28
+ models.map { |klass| new(klass) }
29
+ end
30
+
31
+ def self.find(name)
32
+ Rails.application.eager_load!
33
+
34
+ klass = ActiveRecord::Base.descendants.find { |k| k.name == name }
35
+ klass && new(klass)
36
+ end
37
+
38
+ def initialize(klass)
39
+ @klass = klass
40
+ @name = klass.name
41
+ end
42
+
43
+ def to_param
44
+ name
45
+ end
46
+
47
+ def table_name
48
+ klass.table_name
49
+ end
50
+
51
+ def column_names
52
+ klass.column_names
53
+ rescue
54
+ []
55
+ end
56
+
57
+ def primary_key
58
+ klass.primary_key
59
+ rescue
60
+ nil
61
+ end
62
+
63
+ def validations
64
+ klass.validators.map do |validator|
65
+ {
66
+ type: validator.class.name.demodulize.gsub(/Validator$/, ''),
67
+ attributes: validator.attributes,
68
+ options: validator.options
69
+ }
70
+ end
71
+ rescue
72
+ []
73
+ end
74
+
75
+ def associations
76
+ klass.reflect_on_all_associations.map do |assoc|
77
+ {
78
+ name: assoc.name,
79
+ type: assoc.macro,
80
+ class_name: assoc.class_name,
81
+ foreign_key: assoc.foreign_key,
82
+ options: assoc.options
83
+ }
84
+ end
85
+ rescue
86
+ []
87
+ end
88
+
89
+ def scopes
90
+ # Get custom scopes (not default scopes)
91
+ scope_names = klass.respond_to?(:scopes) ? klass.scopes.keys : []
92
+ scope_names.map(&:to_s).sort
93
+ rescue
94
+ []
95
+ end
96
+
97
+ def callbacks
98
+ [:before_validation, :after_validation, :before_save, :after_save,
99
+ :before_create, :after_create, :before_update, :after_update,
100
+ :before_destroy, :after_destroy].flat_map do |callback_type|
101
+ klass._get_callbacks(callback_type).map do |callback|
102
+ {
103
+ type: callback_type,
104
+ filter: callback.filter
105
+ }
106
+ end
107
+ end.compact
108
+ rescue
109
+ []
110
+ end
111
+
112
+ def source_location
113
+ return nil unless klass.respond_to?(:instance_methods)
114
+
115
+ # Try to find the model file
116
+ file_path = "#{Rails.root}/app/models/#{name.underscore}.rb"
117
+ File.exist?(file_path) ? file_path : nil
118
+ end
119
+
120
+ def source_code
121
+ return nil unless source_location
122
+
123
+ File.read(source_location)
124
+ rescue
125
+ nil
126
+ end
127
+
128
+ def instance_methods
129
+ (klass.instance_methods - ActiveRecord::Base.instance_methods)
130
+ .sort
131
+ .map(&:to_s)
132
+ rescue
133
+ []
134
+ end
135
+
136
+ def class_methods
137
+ (klass.methods - ActiveRecord::Base.methods)
138
+ .sort
139
+ .map(&:to_s)
140
+ rescue
141
+ []
142
+ end
143
+ end
144
+
145
+ # =======================
146
+ # Phlex views
147
+ # =======================
148
+ class Index < View
149
+ attr_writer :models
150
+
151
+ def view_template
152
+ div(class: "p-6 space-y-4") do
153
+ h1(class: "text-2xl font-bold") { "Models" }
154
+ p(class: "text-sm") { "#{@models.size} ActiveRecord models in your application." }
155
+
156
+ Table @models do
157
+ it.row("Model") {
158
+ nav_link it.name, controller: "/monolith/models", action: :show, id: it.to_param
159
+ }
160
+ it.row("Table") { |m|
161
+ code { m.table_name }
162
+ }
163
+ it.row("Columns") {
164
+ it.column_names.size.to_s
165
+ }
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ class Show < View
172
+ attr_writer :model
173
+
174
+ def view_template
175
+ m = @model
176
+
177
+ div(class: "p-6 space-y-6") do
178
+ h1(class: "text-2xl font-bold") { m.name }
179
+ p { code { m.table_name } }
180
+
181
+ # Columns
182
+ section do
183
+ h2(class: "text-xl font-bold mb-2") { "Columns (#{m.column_names.size})" }
184
+ if m.column_names.any?
185
+ ul(class: "list-disc pl-6") do
186
+ m.column_names.each do |col|
187
+ li {
188
+ plain col
189
+ if col == m.primary_key
190
+ span(class: "text-sm") { " (primary key)" }
191
+ end
192
+ }
193
+ end
194
+ end
195
+ else
196
+ p { em { "No columns" } }
197
+ end
198
+ end
199
+
200
+ # Validations
201
+ section do
202
+ h2(class: "text-xl font-bold mb-2") { "Validations (#{m.validations.size})" }
203
+ if m.validations.any?
204
+ ul(class: "list-disc pl-6 space-y-1") do
205
+ m.validations.each do |validation|
206
+ li do
207
+ strong { validation[:type] }
208
+ plain " on "
209
+ code { validation[:attributes].join(", ") }
210
+ if validation[:options].any?
211
+ plain " "
212
+ code { validation[:options].inspect }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ else
218
+ p { em { "No validations" } }
219
+ end
220
+ end
221
+
222
+ # Associations
223
+ section do
224
+ h2(class: "text-xl font-bold mb-2") { "Associations (#{m.associations.size})" }
225
+ if m.associations.any?
226
+ ul(class: "list-disc pl-6 space-y-1") do
227
+ m.associations.each do |assoc|
228
+ li do
229
+ strong { assoc[:type].to_s }
230
+ plain " :"
231
+ code { assoc[:name].to_s }
232
+ plain " → "
233
+ plain assoc[:class_name]
234
+ plain " (FK: "
235
+ code { assoc[:foreign_key].to_s }
236
+ plain ")"
237
+ end
238
+ end
239
+ end
240
+ else
241
+ p { em { "No associations" } }
242
+ end
243
+ end
244
+
245
+ # Scopes
246
+ if m.scopes.any?
247
+ section do
248
+ h2(class: "text-xl font-bold mb-2") { "Scopes (#{m.scopes.size})" }
249
+ ul(class: "list-disc pl-6") do
250
+ m.scopes.each do |scope_name|
251
+ li { code { scope_name } }
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Callbacks
258
+ if m.callbacks.any?
259
+ section do
260
+ h2(class: "text-xl font-bold mb-2") { "Callbacks (#{m.callbacks.size})" }
261
+ ul(class: "list-disc pl-6 space-y-1") do
262
+ m.callbacks.each do |callback|
263
+ li do
264
+ strong { callback[:type].to_s }
265
+ plain " → "
266
+ code { callback[:filter].to_s }
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ # Instance Methods
274
+ if m.instance_methods.any?
275
+ section do
276
+ h2(class: "text-xl font-bold mb-2") { "Instance Methods (#{m.instance_methods.size})" }
277
+ details do
278
+ summary(class: "cursor-pointer") { "Show methods" }
279
+ ul(class: "list-disc pl-6 mt-2 grid grid-cols-2 md:grid-cols-3 gap-1") do
280
+ m.instance_methods.each do |method_name|
281
+ li { code { method_name } }
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ # Class Methods
289
+ if m.class_methods.any?
290
+ section do
291
+ h2(class: "text-xl font-bold mb-2") { "Class Methods (#{m.class_methods.size})" }
292
+ details do
293
+ summary(class: "cursor-pointer") { "Show methods" }
294
+ ul(class: "list-disc pl-6 mt-2 grid grid-cols-2 md:grid-cols-3 gap-1") do
295
+ m.class_methods.each do |method_name|
296
+ li { code { method_name } }
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ # Source Code
304
+ if m.source_code
305
+ section do
306
+ h2(class: "text-xl font-bold mb-2") { "Source Code" }
307
+ p(class: "text-sm mb-2") { code { m.source_location } }
308
+ pre(class: "border p-4 overflow-x-auto text-xs") do
309
+ code { m.source_code }
310
+ end
311
+ end
312
+ end
313
+
314
+ div(class: "pt-4") { nav_link "← All models", controller: "/monolith/models", action: :index }
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,157 @@
1
+ # app/controllers/monolith/routes_controller.rb
2
+ module Monolith
3
+ class RoutesController < Monolith::ApplicationController
4
+ def index
5
+ render Index.new.tap { _1.routes = Route.all }
6
+ end
7
+
8
+ def show
9
+ route = Route.find(params[:id].to_s)
10
+ return render plain: "Route not found", status: :not_found unless route
11
+
12
+ render Show.new.tap { |v| v.route = route }
13
+ end
14
+
15
+ # =======================
16
+ # Inline ActiveModel-like object
17
+ # =======================
18
+ class Route
19
+ attr_reader :name, :verb, :path, :controller, :action, :constraints, :defaults, :required_parts
20
+
21
+ def self.all
22
+ Rails.application.routes.routes.map.with_index { |route, idx| from_route(route, idx) }.compact.sort_by(&:display_name)
23
+ end
24
+
25
+ def self.find(id)
26
+ all.find { |r| r.to_param == id }
27
+ end
28
+
29
+ def self.from_route(route, idx)
30
+ name = route.name.to_s
31
+ verb = route.verb.to_s
32
+ path = route.path.spec.to_s
33
+
34
+ # Extract controller and action
35
+ defaults = route.defaults
36
+ controller = defaults[:controller]
37
+ action = defaults[:action]
38
+
39
+ # Skip routes without controller/action or internal Rails routes
40
+ return nil if controller.nil? || action.nil?
41
+ return nil if controller.to_s.start_with?("rails/")
42
+
43
+ new(
44
+ name: name.empty? ? nil : name,
45
+ verb: verb,
46
+ path: path,
47
+ controller: controller,
48
+ action: action,
49
+ constraints: route.constraints.except(:request_method),
50
+ defaults: defaults.except(:controller, :action),
51
+ required_parts: route.required_parts,
52
+ id: idx
53
+ )
54
+ end
55
+
56
+ def initialize(name:, verb:, path:, controller:, action:, constraints:, defaults:, required_parts:, id:)
57
+ @name = name
58
+ @verb = verb
59
+ @path = path
60
+ @controller = controller
61
+ @action = action
62
+ @constraints = constraints
63
+ @defaults = defaults
64
+ @required_parts = required_parts
65
+ @id = id
66
+ end
67
+
68
+ def to_param
69
+ @id.to_s
70
+ end
71
+
72
+ def display_name
73
+ name || "#{controller}##{action}"
74
+ end
75
+
76
+ def full_controller_action
77
+ "#{controller}##{action}"
78
+ end
79
+ end
80
+
81
+ # =======================
82
+ # Phlex views
83
+ # =======================
84
+ class Index < View
85
+ attr_writer :routes
86
+
87
+ def view_template
88
+ div(class: "p-6 space-y-4") do
89
+ h1(class: "text-2xl font-bold") { "Routes" }
90
+ p(class: "text-sm") { "#{@routes.size} routes in your Rails application." }
91
+
92
+ Table @routes do
93
+ it.row("Name") {
94
+ nav_link it.display_name, controller: "/monolith/routes", action: :show, id: it.to_param
95
+ }
96
+ it.row("Verb") { |r|
97
+ code { r.verb }
98
+ }
99
+ it.row("Path") { |r|
100
+ code { r.path }
101
+ }
102
+ it.row("Controller#Action") {
103
+ it.full_controller_action
104
+ }
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class Show < View
111
+ attr_writer :route
112
+
113
+ def view_template
114
+ r = @route
115
+
116
+ div(class: "p-6 space-y-4") do
117
+ h1(class: "text-2xl font-bold") { r.display_name }
118
+ p { code { "#{r.verb} #{r.path}" } }
119
+
120
+ dl(class: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2") do
121
+ dt(class: "font-semibold") { "Name" }
122
+ dd { r.name || em { "—" } }
123
+
124
+ dt(class: "font-semibold") { "Verb" }
125
+ dd { code { r.verb } }
126
+
127
+ dt(class: "font-semibold") { "Path" }
128
+ dd { code { r.path } }
129
+
130
+ dt(class: "font-semibold") { "Controller" }
131
+ dd { r.controller }
132
+
133
+ dt(class: "font-semibold") { "Action" }
134
+ dd { r.action }
135
+
136
+ if r.required_parts.any?
137
+ dt(class: "font-semibold") { "Required Parts" }
138
+ dd { r.required_parts.join(", ") }
139
+ end
140
+
141
+ if r.defaults.any?
142
+ dt(class: "font-semibold") { "Defaults" }
143
+ dd { code { r.defaults.inspect } }
144
+ end
145
+
146
+ if r.constraints.any?
147
+ dt(class: "font-semibold") { "Constraints" }
148
+ dd { code { r.constraints.inspect } }
149
+ end
150
+ end
151
+
152
+ div(class: "pt-4") { nav_link "← All routes", controller: "/monolith/routes", action: :index }
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,168 @@
1
+ # app/controllers/monolith/tables_controller.rb
2
+ module Monolith
3
+ class TablesController < Monolith::ApplicationController
4
+ before_action :set_pagination
5
+ before_action :set_table, only: :show
6
+
7
+ def index
8
+ render Index.new.tap { _1.tables = Table.all }
9
+ end
10
+
11
+ def show
12
+ not_found! unless @table
13
+ rows = @table.rows(page: @page, per_page: @per_page)
14
+ render Show.new.tap { |v|
15
+ v.table = @table
16
+ v.rows = rows[:data]
17
+ v.total = rows[:total]
18
+ v.page = @page
19
+ v.per_page= @per_page
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def set_pagination
26
+ @page = params[:page].to_i
27
+ @page = 1 if @page < 1
28
+ @per_page = (params[:per_page] || 50).to_i
29
+ @per_page = 50 if @per_page <= 0
30
+ @per_page = 500 if @per_page > 500
31
+ end
32
+
33
+ def set_table
34
+ @table = Table.find(params[:id].to_s) if params[:id].present?
35
+ end
36
+
37
+ def not_found!
38
+ render plain: "Not found", status: :not_found
39
+ end
40
+
41
+ # =======================
42
+ # Inline ActiveModel-like object
43
+ # =======================
44
+ class Table
45
+ attr_reader :name
46
+
47
+ EXCLUDED = %w[schema_migrations ar_internal_metadata].freeze
48
+
49
+ def self.all
50
+ tables.map { |t| new(t) }
51
+ end
52
+
53
+ def self.find(name)
54
+ return nil unless tables.include?(name)
55
+ new(name)
56
+ end
57
+
58
+ # ---- instance ----
59
+ def initialize(name)
60
+ @name = name
61
+ end
62
+
63
+ def to_param = name
64
+
65
+ def columns
66
+ @columns ||= conn.columns(name).map(&:name)
67
+ end
68
+
69
+ def primary_key
70
+ @primary_key ||= conn.primary_key(name)
71
+ end
72
+
73
+ # Returns { data: [Hash], total: Integer }
74
+ def rows(page:, per_page:)
75
+ offset = (page - 1) * per_page
76
+ total = conn.exec_query("SELECT COUNT(*) AS c FROM #{qtn(name)}").first["c"]
77
+
78
+ order_sql = primary_key ? "ORDER BY #{qcn(primary_key)}" : ""
79
+ sql = <<~SQL
80
+ SELECT *
81
+ FROM #{qtn(name)}
82
+ #{order_sql}
83
+ LIMIT #{per_page.to_i} OFFSET #{offset.to_i}
84
+ SQL
85
+
86
+ data = conn.exec_query(sql).to_a
87
+ { data: data, total: total }
88
+ end
89
+
90
+ private
91
+
92
+ def self.conn = ActiveRecord::Base.connection
93
+ def conn = self.class.conn
94
+
95
+ def self.tables
96
+ @tables ||= (conn.tables - EXCLUDED).sort
97
+ end
98
+
99
+ def qtn(t) = conn.quote_table_name(t)
100
+ def qcn(c) = "#{qtn(name)}.#{conn.quote_column_name(c)}"
101
+ end
102
+
103
+ # =======================
104
+ # Phlex views
105
+ # =======================
106
+ class Index < View
107
+ attr_writer :tables
108
+
109
+ def view_template
110
+ div(class: "p-6 space-y-4") do
111
+ h1(class: "text-2xl font-bold") { "Tables" }
112
+ ul(class: "list-disc pl-6 space-y-1") do
113
+ @tables.each do |t|
114
+ li { nav_link t.name, controller: "/monolith/tables", action: :show, id: t.name }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ class Show < View
122
+ attr_writer :table, :rows, :page, :per_page, :total
123
+
124
+ def view_template
125
+ div(class: "p-6 space-y-4") do
126
+ h1(class: "text-2xl font-bold") { @table.name }
127
+ p do
128
+ plain "Rows "
129
+ strong { "#{start_row}-#{end_row}" }
130
+ plain " of #{@total} (per page: #{@per_page})"
131
+ if @table.primary_key
132
+ span(class: "ml-2 text-sm") { "PK: #{@table.primary_key}" }
133
+ end
134
+ end
135
+
136
+ Table @table.columns do
137
+ it.row(col) {
138
+ format_cell(it[col])
139
+ }
140
+ end
141
+
142
+ div(class: "flex items-center gap-3") do
143
+ if @page > 1
144
+ nav_link "← Prev", controller: "/monolith/tables", action: :show, id: @table.name, page: @page - 1, per_page: @per_page
145
+ end
146
+ if end_row < @total
147
+ nav_link "Next →", controller: "/monolith/tables", action: :show, id: @table.name, page: @page + 1, per_page: @per_page
148
+ end
149
+ span(class: "text-sm ml-auto") { "Page #{@page}" }
150
+ nav_link "All tables", controller: "/monolith/tables", action: :index
151
+ end
152
+ end
153
+ end
154
+
155
+ def start_row = ((@page - 1) * @per_page) + 1
156
+ def end_row = [@page * @per_page, @total].min
157
+
158
+ def format_cell(v)
159
+ case v
160
+ when Hash, Array then code { v.ai(plain: true) rescue v.to_json }
161
+ when Time, DateTime, Date then v.iso8601
162
+ else
163
+ v.nil? ? em { "NULL" } : v.to_s
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,3 @@
1
+ class Monolith::Views::Base < Monolith::Components::Base
2
+ include Phlex::Rails::Layout
3
+ end
@@ -0,0 +1,23 @@
1
+ class Monolith::Views::Layouts::Base < Monolith::Views::Base
2
+ def around_template(&)
3
+ html do
4
+ head do
5
+ title { @title }
6
+ meta name: "viewport", content: "width=device-width,initial-scale=1"
7
+ meta charset: "utf-8"
8
+ meta name: "apple-mobile-web-app-capable", content: "yes"
9
+ meta name: "apple-mobile-web-app-status-bar-style", content: "black-translucent"
10
+ csp_meta_tag
11
+ csrf_meta_tags
12
+ stylesheet_link_tag "monolith/tailwind", data_turbo_track: "reload"
13
+ javascript_importmap_tags
14
+ end
15
+
16
+ body(&)
17
+ end
18
+ end
19
+
20
+ def view_template
21
+ yield self if block_given?
22
+ end
23
+ end
data/config/routes.rb CHANGED
@@ -1,2 +1,16 @@
1
1
  Monolith::Engine.routes.draw do
2
+ root to: "home#show"
3
+
4
+ resources :emails, only: [:index, :show]
5
+ resources :tables, only: [:index, :show]
6
+ resources :gems, only: [:index, :show]
7
+ resources :routes, only: [:index, :show]
8
+ resources :models, only: [:index, :show]
9
+ resources :generators, only: [:index, :show] do
10
+ member do
11
+ post :create
12
+ end
13
+ end
14
+
15
+ get "/exceptions/:id", to: "exceptions#show", as: :exception
2
16
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate install Thing
6
+
7
+ This will create:
8
+ what/will/it/create