better_page 2.0.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +357 -0
  5. data/Rakefile +3 -0
  6. data/docs/00-README.md +17 -0
  7. data/docs/01-getting-started.md +137 -0
  8. data/docs/02-component-registry.md +192 -0
  9. data/docs/03-base-pages.md +238 -0
  10. data/docs/04-schema-validation.md +180 -0
  11. data/docs/05-turbo-support.md +220 -0
  12. data/docs/06-compliance-analyzer.md +147 -0
  13. data/docs/07-configuration.md +157 -0
  14. data/guide/00-README.md +32 -0
  15. data/guide/01-quick-start.md +148 -0
  16. data/guide/02-building-index-page.md +258 -0
  17. data/guide/03-building-show-page.md +266 -0
  18. data/guide/04-building-form-page.md +309 -0
  19. data/guide/05-custom-pages.md +325 -0
  20. data/guide/06-best-practices.md +311 -0
  21. data/lib/better_page/base_page.rb +161 -0
  22. data/lib/better_page/compliance/analyzer.rb +409 -0
  23. data/lib/better_page/component_registry.rb +393 -0
  24. data/lib/better_page/config.rb +165 -0
  25. data/lib/better_page/configuration.rb +153 -0
  26. data/lib/better_page/custom_base_page.rb +85 -0
  27. data/lib/better_page/default_components.rb +200 -0
  28. data/lib/better_page/form_base_page.rb +170 -0
  29. data/lib/better_page/index_base_page.rb +69 -0
  30. data/lib/better_page/railtie.rb +34 -0
  31. data/lib/better_page/show_base_page.rb +120 -0
  32. data/lib/better_page/validation_error.rb +7 -0
  33. data/lib/better_page/version.rb +3 -0
  34. data/lib/better_page.rb +80 -0
  35. data/lib/generators/better_page/component_generator.rb +131 -0
  36. data/lib/generators/better_page/install_generator.rb +160 -0
  37. data/lib/generators/better_page/page_generator.rb +101 -0
  38. data/lib/generators/better_page/sync_generator.rb +109 -0
  39. data/lib/generators/better_page/templates/application_page.rb.tt +12 -0
  40. data/lib/generators/better_page/templates/better_page_initializer.rb.tt +53 -0
  41. data/lib/generators/better_page/templates/custom_base_page.rb.tt +83 -0
  42. data/lib/generators/better_page/templates/custom_page.rb.tt +31 -0
  43. data/lib/generators/better_page/templates/edit_page.rb.tt +46 -0
  44. data/lib/generators/better_page/templates/form_base_page.rb.tt +126 -0
  45. data/lib/generators/better_page/templates/index_base_page.rb.tt +65 -0
  46. data/lib/generators/better_page/templates/index_page.rb.tt +56 -0
  47. data/lib/generators/better_page/templates/javascript/controllers/app_nav_controller.js +57 -0
  48. data/lib/generators/better_page/templates/javascript/controllers/drawer_controller.js +99 -0
  49. data/lib/generators/better_page/templates/javascript/controllers/dropdown_controller.js +60 -0
  50. data/lib/generators/better_page/templates/javascript/controllers/index.js +36 -0
  51. data/lib/generators/better_page/templates/javascript/controllers/modal_controller.js +70 -0
  52. data/lib/generators/better_page/templates/javascript/controllers/sidebar_controller.js +152 -0
  53. data/lib/generators/better_page/templates/javascript/controllers/table_controller.js +60 -0
  54. data/lib/generators/better_page/templates/javascript/controllers/tabs_controller.js +89 -0
  55. data/lib/generators/better_page/templates/new_page.rb.tt +46 -0
  56. data/lib/generators/better_page/templates/show_base_page.rb.tt +117 -0
  57. data/lib/generators/better_page/templates/show_page.rb.tt +45 -0
  58. data/lib/generators/better_page/templates/view_components/application_view_component.rb.tt +7 -0
  59. data/lib/generators/better_page/templates/view_components/custom_view_component.html.erb.tt +21 -0
  60. data/lib/generators/better_page/templates/view_components/custom_view_component.rb.tt +21 -0
  61. data/lib/generators/better_page/templates/view_components/form_view_component.html.erb.tt +25 -0
  62. data/lib/generators/better_page/templates/view_components/form_view_component.rb.tt +23 -0
  63. data/lib/generators/better_page/templates/view_components/index_view_component.html.erb.tt +33 -0
  64. data/lib/generators/better_page/templates/view_components/index_view_component.rb.tt +29 -0
  65. data/lib/generators/better_page/templates/view_components/show_view_component.html.erb.tt +29 -0
  66. data/lib/generators/better_page/templates/view_components/show_view_component.rb.tt +25 -0
  67. data/lib/generators/better_page/templates/view_components/ui/alerts_component.html.erb.tt +47 -0
  68. data/lib/generators/better_page/templates/view_components/ui/alerts_component.rb.tt +47 -0
  69. data/lib/generators/better_page/templates/view_components/ui/content_section_component.html.erb.tt +42 -0
  70. data/lib/generators/better_page/templates/view_components/ui/content_section_component.rb.tt +34 -0
  71. data/lib/generators/better_page/templates/view_components/ui/drawer_component.html.erb.tt +73 -0
  72. data/lib/generators/better_page/templates/view_components/ui/drawer_component.rb.tt +78 -0
  73. data/lib/generators/better_page/templates/view_components/ui/errors_component.html.erb.tt +23 -0
  74. data/lib/generators/better_page/templates/view_components/ui/errors_component.rb.tt +18 -0
  75. data/lib/generators/better_page/templates/view_components/ui/field_component.html.erb.tt +65 -0
  76. data/lib/generators/better_page/templates/view_components/ui/field_component.rb.tt +91 -0
  77. data/lib/generators/better_page/templates/view_components/ui/footer_component.html.erb.tt +33 -0
  78. data/lib/generators/better_page/templates/view_components/ui/footer_component.rb.tt +32 -0
  79. data/lib/generators/better_page/templates/view_components/ui/header_component.html.erb.tt +55 -0
  80. data/lib/generators/better_page/templates/view_components/ui/header_component.rb.tt +39 -0
  81. data/lib/generators/better_page/templates/view_components/ui/modal_component.html.erb.tt +70 -0
  82. data/lib/generators/better_page/templates/view_components/ui/modal_component.rb.tt +54 -0
  83. data/lib/generators/better_page/templates/view_components/ui/overview_component.html.erb.tt +22 -0
  84. data/lib/generators/better_page/templates/view_components/ui/overview_component.rb.tt +71 -0
  85. data/lib/generators/better_page/templates/view_components/ui/pagination_component.html.erb.tt +63 -0
  86. data/lib/generators/better_page/templates/view_components/ui/pagination_component.rb.tt +69 -0
  87. data/lib/generators/better_page/templates/view_components/ui/panel_component.html.erb.tt +31 -0
  88. data/lib/generators/better_page/templates/view_components/ui/panel_component.rb.tt +23 -0
  89. data/lib/generators/better_page/templates/view_components/ui/statistics_component.html.erb.tt +33 -0
  90. data/lib/generators/better_page/templates/view_components/ui/statistics_component.rb.tt +51 -0
  91. data/lib/generators/better_page/templates/view_components/ui/table_component.html.erb.tt +112 -0
  92. data/lib/generators/better_page/templates/view_components/ui/table_component.rb.tt +88 -0
  93. data/lib/generators/better_page/templates/view_components/ui/tabs_component.html.erb.tt +52 -0
  94. data/lib/generators/better_page/templates/view_components/ui/tabs_component.rb.tt +76 -0
  95. data/lib/generators/better_page/templates/view_components/ui/widget_component.html.erb.tt +72 -0
  96. data/lib/generators/better_page/templates/view_components/ui/widget_component.rb.tt +34 -0
  97. data/lib/tasks/better_page.rake +70 -0
  98. data/lib/tasks/better_page_tasks.rake +4 -0
  99. metadata +188 -0
@@ -0,0 +1,409 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Compliance
5
+ # Analyzer for page compliance with architecture conventions.
6
+ # Validates that pages follow the presentation-layer pattern:
7
+ # - No database queries
8
+ # - No business logic
9
+ # - No service layer access
10
+ # - Required component methods implemented
11
+ # - Plain Hash objects (no OpenStruct)
12
+ #
13
+ class Analyzer
14
+ attr_reader :results, :total_pages, :compliant_count, :warning_count, :error_count
15
+
16
+ def initialize
17
+ @results = []
18
+ @total_pages = 0
19
+ @compliant_count = 0
20
+ @warning_count = 0
21
+ @error_count = 0
22
+ @verbose = ENV["VERBOSE"] == "true"
23
+ end
24
+
25
+ # Analyze all pages in the app/pages directory
26
+ # @return [void]
27
+ def analyze_all
28
+ page_files = find_all_pages
29
+ @total_pages = page_files.count
30
+
31
+ puts "Found #{@total_pages} page files to analyze..."
32
+ puts
33
+
34
+ page_files.each_with_index do |file_path, index|
35
+ print "\rAnalyzing... #{index + 1}/#{@total_pages}" unless @verbose
36
+
37
+ result = analyze_page(file_path)
38
+ @results << result
39
+
40
+ update_counters(result)
41
+
42
+ if @verbose
43
+ puts format_single_page_report(result)
44
+ puts
45
+ end
46
+ end
47
+
48
+ puts "\r" + (" " * 50) + "\r" unless @verbose
49
+ generate_report
50
+ end
51
+
52
+ # Analyze a single page file
53
+ # @param file_path [String] path to the page file
54
+ # @return [Hash] analysis result
55
+ def analyze_page(file_path)
56
+ content = File.read(file_path)
57
+
58
+ result = {
59
+ file_path: file_path,
60
+ class_name: extract_class_name(content),
61
+ page_type: categorize_page_type(file_path),
62
+ namespace: extract_namespace(file_path),
63
+ issues: [],
64
+ warnings: [],
65
+ compliant: true
66
+ }
67
+
68
+ # Run all compliance checks
69
+ check_page_structure(content, result)
70
+ check_ui_configuration_only(content, result)
71
+ check_template_system_compliance(content, result)
72
+ check_architecture_compliance(content, result)
73
+ check_hash_usage_patterns(content, result)
74
+
75
+ # Determine overall compliance
76
+ result[:compliant] = result[:issues].empty?
77
+ result[:status] = if result[:issues].any?
78
+ :error
79
+ elsif result[:warnings].any?
80
+ :warning
81
+ else
82
+ :compliant
83
+ end
84
+
85
+ result
86
+ rescue StandardError => e
87
+ {
88
+ file_path: file_path,
89
+ class_name: "PARSE_ERROR",
90
+ page_type: :unknown,
91
+ namespace: "UNKNOWN",
92
+ issues: [ "Parse error: #{e.message}" ],
93
+ warnings: [],
94
+ compliant: false,
95
+ status: :error
96
+ }
97
+ end
98
+
99
+ # Format a single page analysis result for display
100
+ # @param result [Hash] analysis result
101
+ # @return [String] formatted report
102
+ def format_single_page_report(result)
103
+ output = []
104
+ status_icon = case result[:status]
105
+ when :compliant then "[OK]"
106
+ when :warning then "[WARN]"
107
+ when :error then "[ERROR]"
108
+ end
109
+
110
+ output << "#{status_icon} #{result[:file_path]}"
111
+ output << " Class: #{result[:class_name]}" if result[:class_name] != "UNKNOWN"
112
+ output << " Type: #{result[:page_type]} | Namespace: #{result[:namespace]}" if result[:page_type] != :unknown
113
+
114
+ if result[:issues].any?
115
+ output << " Issues:"
116
+ result[:issues].each { |issue| output << " - #{issue}" }
117
+ end
118
+
119
+ if result[:warnings].any?
120
+ output << " Warnings:"
121
+ result[:warnings].each { |warning| output << " - #{warning}" }
122
+ end
123
+
124
+ output.join("\n")
125
+ end
126
+
127
+ # Generate and print the final analysis report
128
+ # @return [void]
129
+ def generate_report
130
+ puts "SUMMARY"
131
+ puts "======="
132
+ puts "Total pages analyzed: #{@total_pages}"
133
+ puts "[OK] Fully compliant: #{@compliant_count} (#{percentage(@compliant_count)}%)"
134
+ puts "[WARN] With warnings: #{@warning_count} (#{percentage(@warning_count)}%)"
135
+ puts "[ERROR] With errors: #{@error_count} (#{percentage(@error_count)}%)"
136
+ puts
137
+
138
+ # Show critical issues
139
+ error_results = @results.select { |r| r[:status] == :error }
140
+ if error_results.any?
141
+ puts "CRITICAL ISSUES"
142
+ puts "==============="
143
+ error_results.each do |result|
144
+ puts "- #{result[:file_path]}"
145
+ result[:issues].each { |issue| puts " - #{issue}" }
146
+ end
147
+ puts
148
+ end
149
+
150
+ # Show warnings
151
+ warning_results = @results.select { |r| r[:status] == :warning }
152
+ if warning_results.any?
153
+ puts "WARNINGS"
154
+ puts "========"
155
+ warning_results.each do |result|
156
+ puts "- #{result[:file_path]}"
157
+ result[:warnings].each { |warning| puts " - #{warning}" }
158
+ end
159
+ puts
160
+ end
161
+
162
+ generate_recommendations
163
+ end
164
+
165
+ private
166
+
167
+ def find_all_pages
168
+ Dir.glob("app/pages/**/*_page.rb").sort
169
+ end
170
+
171
+ def extract_class_name(content)
172
+ match = content.match(/class\s+([A-Za-z:]+Page)\s*/)
173
+ match ? match[1] : "UNKNOWN"
174
+ end
175
+
176
+ def extract_namespace(file_path)
177
+ parts = file_path.split("/")
178
+ return "UNKNOWN" unless parts.include?("pages")
179
+
180
+ pages_index = parts.index("pages")
181
+ return "Root" if parts.length <= pages_index + 2
182
+
183
+ parts[pages_index + 1].capitalize
184
+ end
185
+
186
+ def categorize_page_type(file_path)
187
+ case file_path
188
+ when %r{/base_page\.rb$}, %r{/application_page\.rb$}
189
+ :base
190
+ when %r{pages/[^/]+_page\.rb$}
191
+ :root_page
192
+ when %r{pages/\w+/\w+_page\.rb$}
193
+ :namespaced_page
194
+ when %r{/index_page\.rb$}
195
+ :index_page
196
+ when %r{/show_page\.rb$}
197
+ :show_page
198
+ when %r{/new_page\.rb$}
199
+ :form_page
200
+ when %r{/edit_page\.rb$}
201
+ :form_page
202
+ when %r{/custom_page\.rb$}
203
+ :custom_page
204
+ else
205
+ :unknown
206
+ end
207
+ end
208
+
209
+ def check_page_structure(content, result)
210
+ # Check proper class naming
211
+ class_name = result[:class_name]
212
+ result[:issues] << 'Page class must end with "Page"' unless class_name.end_with?("Page")
213
+
214
+ # Check namespace structure for namespaced pages
215
+ if result[:page_type] == :namespaced_page
216
+ expected_module = result[:namespace]
217
+ unless content.match?(/module\s+#{expected_module}/i)
218
+ result[:warnings] << "Expected module #{expected_module} for namespaced page"
219
+ end
220
+ end
221
+
222
+ # Check for initialize method (required for non-base pages)
223
+ return if content.include?("def initialize") || result[:page_type] == :base
224
+
225
+ result[:issues] << "Page must have initialize method"
226
+ end
227
+
228
+ def check_ui_configuration_only(content, result)
229
+ # Check for FORBIDDEN database access
230
+ database_patterns = [
231
+ { pattern: /\.find\(/, message: "Database queries forbidden in Page" },
232
+ { pattern: /\.where\(/, message: "Database queries forbidden in Page" },
233
+ { pattern: /\.all\b/, message: "Database queries forbidden in Page" },
234
+ { pattern: /\.count\b/, message: "Database queries forbidden in Page" },
235
+ { pattern: /\.joins\(/, message: "Database queries forbidden in Page" },
236
+ { pattern: /\.includes\(/, message: "Database queries forbidden in Page" }
237
+ ]
238
+
239
+ database_patterns.each do |check|
240
+ result[:issues] << check[:message] if content.match?(check[:pattern])
241
+ end
242
+
243
+ # Check for business logic (FORBIDDEN)
244
+ business_logic_patterns = [
245
+ { pattern: /def\s+calculate_/, message: "Business calculations forbidden in Page" },
246
+ { pattern: /def\s+process_/, message: "Business processing forbidden in Page" },
247
+ { pattern: /def\s+validate_(?!form_panels)/, message: "Validation logic forbidden in Page" },
248
+ { pattern: /def\s+save_/, message: "Persistence operations forbidden in Page" }
249
+ ]
250
+
251
+ business_logic_patterns.each do |check|
252
+ result[:issues] << check[:message] if content.match?(check[:pattern])
253
+ end
254
+
255
+ # Check for Service layer access (FORBIDDEN)
256
+ if content.match?(/Service\.new|service\s*=.*Service/)
257
+ result[:issues] << "Service layer access forbidden in Page"
258
+ end
259
+
260
+ # Check for external dependencies (FORBIDDEN)
261
+ external_patterns = [
262
+ /Net::HTTP/,
263
+ /HTTParty/,
264
+ /Faraday/,
265
+ /Redis/
266
+ ]
267
+
268
+ external_patterns.each do |pattern|
269
+ if content.match?(pattern)
270
+ result[:issues] << "External dependencies forbidden in Page"
271
+ break
272
+ end
273
+ end
274
+ end
275
+
276
+ def check_template_system_compliance(content, result)
277
+ page_type = result[:page_type]
278
+
279
+ # Check for required component methods based on page type
280
+ # New pattern: simple method names matching registered components
281
+ case page_type
282
+ when :index_page
283
+ required_methods = %w[header table]
284
+ check_required_component_methods(content, result, required_methods)
285
+
286
+ when :show_page
287
+ required_methods = %w[header]
288
+ check_required_component_methods(content, result, required_methods)
289
+
290
+ when :form_page
291
+ required_methods = %w[header panels]
292
+ check_required_component_methods(content, result, required_methods)
293
+
294
+ when :custom_page
295
+ required_methods = %w[content]
296
+ check_required_component_methods(content, result, required_methods)
297
+ end
298
+
299
+ # Check for main page method (index, show, new, edit, form, custom)
300
+ page_methods = %w[index show new edit form custom]
301
+ found_main_method = page_methods.any? { |method| content.include?("def #{method}") }
302
+
303
+ return if found_main_method || result[:page_type] == :base
304
+
305
+ result[:warnings] << "Page should have main method (index, show, form, or custom)"
306
+ end
307
+
308
+ def check_architecture_compliance(content, result)
309
+ # Check for hardcoded paths (DISCOURAGED)
310
+ if content.match?(%r{"/\w+})
311
+ result[:warnings] << "Hardcoded paths detected - prefer Rails path helpers"
312
+ end
313
+
314
+ # Check for HTML generation (should be in components)
315
+ html_patterns = [
316
+ /<\w+/,
317
+ /html_safe/,
318
+ /raw\(/,
319
+ /content_tag/
320
+ ]
321
+
322
+ html_patterns.each do |pattern|
323
+ if content.match?(pattern)
324
+ result[:warnings] << "HTML generation found - should be handled by template system"
325
+ break
326
+ end
327
+ end
328
+ end
329
+
330
+ def check_hash_usage_patterns(content, result)
331
+ # Check for OpenStruct usage (FORBIDDEN)
332
+ if content.include?("OpenStruct") || content.include?("ostruct")
333
+ result[:issues] << "OpenStruct usage forbidden - use plain Hash objects"
334
+ end
335
+
336
+ # Check for Struct usage (DISCOURAGED)
337
+ if content.match?(/Struct\.new/)
338
+ result[:warnings] << "Struct usage discouraged - prefer plain Hash for consistency"
339
+ end
340
+ end
341
+
342
+ def check_required_component_methods(content, result, required_methods)
343
+ required_methods.each do |method|
344
+ unless content.include?("def #{method}")
345
+ result[:issues] << "Missing required component method: #{method}"
346
+ end
347
+ end
348
+ end
349
+
350
+ def update_counters(result)
351
+ case result[:status]
352
+ when :compliant
353
+ @compliant_count += 1
354
+ when :warning
355
+ @warning_count += 1
356
+ when :error
357
+ @error_count += 1
358
+ end
359
+ end
360
+
361
+ def percentage(count)
362
+ return 0 if @total_pages.zero?
363
+
364
+ ((count.to_f / @total_pages) * 100).round(1)
365
+ end
366
+
367
+ def generate_recommendations
368
+ puts "RECOMMENDATIONS"
369
+ puts "==============="
370
+
371
+ issues_by_type = {}
372
+ @results.each do |result|
373
+ (result[:issues] + result[:warnings]).each do |issue|
374
+ issues_by_type[issue] ||= 0
375
+ issues_by_type[issue] += 1
376
+ end
377
+ end
378
+
379
+ if issues_by_type.any?
380
+ puts "Top issues to address:"
381
+ issues_by_type.sort_by { |_, count| -count }.first(5).each_with_index do |(issue, count), index|
382
+ puts "#{index + 1}. #{issue} (#{count} pages affected)"
383
+ end
384
+ else
385
+ puts "All pages are compliant!"
386
+ end
387
+
388
+ puts
389
+ puts "NEXT STEPS:"
390
+
391
+ if @error_count > 0
392
+ puts "1. Remove database queries from Pages"
393
+ puts "2. Remove business logic - keep UI configuration only"
394
+ puts "3. Implement required component methods for template system"
395
+ end
396
+
397
+ if @warning_count > 0
398
+ step = @error_count > 0 ? 4 : 1
399
+ puts "#{step}. Replace OpenStruct with plain Hash objects"
400
+ puts "#{step + 1}. Use Rails path helpers instead of hardcoded paths"
401
+ end
402
+
403
+ if @error_count.zero? && @warning_count.zero?
404
+ puts "1. Consider implementing optional component methods for better UI"
405
+ end
406
+ end
407
+ end
408
+ end
409
+ end