rails-architect 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9eaba9e7a51319c2b9c17d2612aa8937ec9858e3c23cdd22f5293f35c7f0b691
4
- data.tar.gz: ec11a0500475321f9941a4e266b77226b692a2eca508b27a9ea47930942ad2dc
3
+ metadata.gz: f5e214be6afcbf72e11d25f85d0a782160457961b4df66fcc108e0a578c4317e
4
+ data.tar.gz: 0f69f574aec7b0a9973aeca1b4a5ebdb2a91a989bc13f9359b53272452d38a42
5
5
  SHA512:
6
- metadata.gz: 7a59adc47f124695972d4fc830cffd2e188bdba523071b8e99d1f51e55c9dd30cec9c96c4d3482b872f886720dcb3a678e6952cb62dc5cf913eec787702bad51
7
- data.tar.gz: 7ad650cf77b1aef49fb1cc7a84784ecf36394f48472231df1f68150a618cfa258f4b73856fe1294468386ee3fb42d23a8393193096eb90cc784f280aef02f912
6
+ metadata.gz: b949a221ba4c2598e562fa284df3c0d6edfbb42d44d8e2d1a5b3a10dc30af8b14d691d17009aa8f334a8cedd73ec8ee22e65e93f24e18042d475942d29ac773a
7
+ data.tar.gz: fd11b11531bbb73e33c4d6611be74ae22415da8c6cff04036904530a43bc383980b692ce00af11ebf73f7bcffd3172db7d1bd84668f402a454aa01f8ec54a5aa
data/README.md CHANGED
@@ -2,15 +2,170 @@
2
2
 
3
3
  A comprehensive architectural enforcement and code quality tool for Rails applications.
4
4
 
5
- Rails Architect helps maintain clean Rails architecture by automatically detecting and preventing common anti-patterns, monitoring code complexity, enforcing test coverage, and providing architectural visualizations.
5
+ Rails Architect helps maintain clean Rails architecture by automatically enforcing software design principles and detecting common anti-patterns.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Architecture Pattern Enforcement**: Detects fat models, complex controllers, and suggests service objects
10
- - **Code Complexity Monitoring**: Tracks method complexity and suggests refactoring
11
- - **XP/Agile Integration**: Built-in checks for test coverage requirements and TDD patterns
12
- - **Performance Alerts**: Detects N+1 queries and inefficient database operations
13
- - **Architecture Visualization**: Generates dependency diagrams and service flow representations
9
+ - **SOLID Principles**: Enforces Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles
10
+ - **KISS Principle**: Detects overly complex code and suggests simplification
11
+ - **DRY Principle**: Identifies code duplication and repeated patterns
12
+ - **Automated Checks**: Run as rake tasks or integrate into CI/CD pipelines
13
+
14
+ ## SOLID Principles Guide
15
+
16
+ Here's a quick, Rails-flavored guide to the **SOLID** principles—with tiny examples and common pitfalls.
17
+
18
+ ### S — Single Responsibility
19
+
20
+ **One class = one reason to change.**
21
+ Rails gotchas: "Fat models," callback soup, controllers doing business logic.
22
+
23
+ **Smell**
24
+
25
+ ```ruby
26
+ class Order < ApplicationRecord
27
+ after_create :send_receipt_email
28
+ def total_cents; ... complex tax/discount logic ... end
29
+ end
30
+ ```
31
+
32
+ **Refactor**
33
+
34
+ ```ruby
35
+ # app/services/orders/calculate_total.rb
36
+ class Orders::CalculateTotal
37
+ def self.call(order) = ... # pure calc
38
+ end
39
+
40
+ # app/jobs/order_receipt_job.rb
41
+ class OrderReceiptJob < ApplicationJob
42
+ def perform(order_id) = OrderMailer.receipt(Order.find(order_id)).deliver_now
43
+ end
44
+
45
+ # app/models/order.rb
46
+ class Order < ApplicationRecord
47
+ def total_cents = Orders::CalculateTotal.call(self)
48
+ end
49
+ ```
50
+
51
+ Tip: prefer **service objects, form objects, query objects, mailer jobs** over callbacks.
52
+
53
+ ### O — Open/Closed
54
+
55
+ **Open for extension, closed for modification.**
56
+ Use composition & small objects; avoid editing core classes for each new case.
57
+
58
+ **Example: pluggable pricing rules**
59
+
60
+ ```ruby
61
+ class PriceCalculator
62
+ def initialize(rules = [LoyaltyRule.new, CouponRule.new])
63
+ @rules = rules
64
+ end
65
+
66
+ def call(order)
67
+ @rules.reduce(order.subtotal) { |total, rule| rule.apply(order, total) }
68
+ end
69
+ end
70
+ ```
71
+
72
+ Add a new rule class, don't edit `PriceCalculator`.
73
+
74
+ ### L — Liskov Substitution
75
+
76
+ **Subtypes must be usable wherever their base is.**
77
+ Rails gotcha: STI models or polymorphic interfaces that don't honor contracts.
78
+
79
+ **Bad**
80
+
81
+ ```ruby
82
+ class Payment < ApplicationRecord; end
83
+ class CreditCardPayment < Payment; def refund!; ...; end
84
+ class GiftCardPayment < Payment; end # no refund!
85
+ ```
86
+
87
+ **Better (explicit interface)**
88
+
89
+ ```ruby
90
+ module Refundable
91
+ def refund! = raise NotImplementedError
92
+ end
93
+
94
+ class CreditCardPayment < ApplicationRecord
95
+ include Refundable
96
+ def refund!; ... end
97
+ end
98
+
99
+ class GiftCardPayment < ApplicationRecord
100
+ include Refundable
101
+ def refund!; ... end
102
+ end
103
+ ```
104
+
105
+ Or avoid STI; use separate models + shared interface modules and tests.
106
+
107
+ ### I — Interface Segregation
108
+
109
+ **Clients shouldn't depend on methods they don't use.**
110
+ Rails tip: keep "narrow" objects per use-case: **FormObject**, **Presenter**, **Query**—not one god-service.
111
+
112
+ **Example**
113
+
114
+ ```ruby
115
+ # Too broad:
116
+ class AccountService; def create; def suspend; def invite; def export_csv; end; end
117
+
118
+ # Split:
119
+ class Accounts::Creator; end
120
+ class Accounts::Suspender; end
121
+ class Accounts::Inviter; end
122
+ ```
123
+
124
+ For APIs, expose slim endpoints; for views, use **ViewComponents/Presenters** instead of fat helpers.
125
+
126
+ ### D — Dependency Inversion
127
+
128
+ **Depend on abstractions, not concretions.**
129
+ Rails tip: inject collaborators; don't hard-wire globals (e.g., a specific HTTP client).
130
+
131
+ **Example**
132
+
133
+ ```ruby
134
+ # app/services/notifications/send_sms.rb
135
+ class Notifications::SendSms
136
+ def initialize(client: Sms::TwilioClient.new) = @client = client
137
+ def call(to:, body:) = @client.deliver(to:, body:)
138
+ end
139
+
140
+ # test
141
+ fake = Class.new { def deliver(...) = true }.new
142
+ expect(Notifications::SendSms.new(client: fake).call(to: "...", body: "...")).to be true
143
+ ```
144
+
145
+ ### Common Rails patterns that support SOLID
146
+
147
+ * **Service objects** (`app/services/...`) for business actions.
148
+ * **Form objects** (e.g., `Reform`, or POROs with `ActiveModel::Model`) to keep controllers skinny.
149
+ * **Query objects** for complex scopes (compose instead of stuffing models).
150
+ * **Policies** (`Pundit`) for authorization logic (SRP).
151
+ * **ViewComponent** (or presenters) to isolate view behavior (ISP/SRP).
152
+ * **Background jobs** for side effects (mail, webhooks) instead of callbacks.
153
+ * **Adapters/ports** for external services (DIP).
154
+
155
+ ### Anti-patterns to watch
156
+
157
+ * Heavy **`ActiveSupport::Concern`** mixing multiple responsibilities.
158
+ * Model callbacks triggering network calls (violates SRP & DIP).
159
+ * STI where subclasses violate the base contract (breaks LSP).
160
+ * "Helper modules" with dozens of unrelated methods (violates SRP/ISP).
161
+
162
+ ### Tiny checklist (paste in your PR template)
163
+
164
+ * Does each class have **one reason to change**?
165
+ * If a new variant appears, can I **add a class** (not edit many)?
166
+ * Do polymorphic things **share a clear interface** and tests?
167
+ * Are public APIs **small and focused**?
168
+ * Can I **swap dependencies** in tests via initializer args?
14
169
 
15
170
  ## Installation
16
171
 
@@ -32,103 +187,190 @@ Or install it yourself as:
32
187
  gem install rails-architect
33
188
  ```
34
189
 
35
- ### Configuration
190
+ ## Usage
36
191
 
37
- Create `config/rails_architect.yml` in your Rails app:
192
+ ### Rake Tasks
38
193
 
39
- ```yaml
40
- defaults: &defaults
41
- model_method_threshold: 10
42
- controller_action_threshold: 5
43
- method_complexity_threshold: 10
44
- test_coverage_minimum: 80
194
+ Run individual principle checks in your Rails application:
45
195
 
46
- development:
47
- <<: *defaults
196
+ ```bash
197
+ bundle exec rake rails_architect:check_solid # Check SOLID principles
198
+ bundle exec rake rails_architect:check_kiss # Check KISS principle
199
+ bundle exec rake rails_architect:check_dry # Check DRY principle
200
+ ```
48
201
 
49
- test:
50
- <<: *defaults
202
+ Or with Docker:
51
203
 
52
- production:
53
- <<: *defaults
204
+ ```bash
205
+ docker exec your_container bundle exec rake rails_architect:check_solid
206
+ docker exec your_container bundle exec rake rails_architect:check_kiss
207
+ docker exec your_container bundle exec rake rails_architect:check_dry
54
208
  ```
55
209
 
56
- ### Bullet Integration
210
+ ### Example Test Application
57
211
 
58
- Add to your `Gemfile`:
212
+ The gem includes a test Rails application in `test/rails_app/` that demonstrates intentional violations of all principles:
59
213
 
60
- ```ruby
61
- group :development do
62
- gem 'bullet'
63
- end
64
- ```
214
+ **SOLID Violations:**
215
+ - **SRP**: `User` model has 15+ methods (fat model), `UsersController` has business logic and 9+ actions
216
+ - **OCP**: Deep inheritance chains and case statements that may need modification
217
+ - **LSP**: STI models and polymorphic associations that might violate contracts
218
+ - **ISP**: Heavy concerns and service objects with too many methods
219
+ - **DIP**: Hard-coded external service dependencies and network calls in callbacks
65
220
 
66
- And configure in `config/environments/development.rb`:
221
+ **KISS Violations:**
222
+ - **Complexity**: `ReportGenerator#generate_complex_report` has cyclomatic complexity of 14
223
+ - **Length**: Methods over 64 lines with deep nesting (5+ levels)
224
+ - **Class Size**: Large classes with multiple responsibilities
67
225
 
68
- ```ruby
69
- config.after_initialize do
70
- Bullet.enable = true
71
- Bullet.alert = true
72
- Bullet.bullet_logger = true
73
- Bullet.console = true
74
- Bullet.rails_logger = true
75
- end
76
- ```
226
+ **DRY Violations:**
227
+ - **Code Duplication**: Identical method implementations across classes
228
+ - **Repeated Patterns**: Same filtering logic in multiple controller actions
229
+ - **String Literals**: Repeated long strings that should be constants
230
+ - **Method Calls**: Excessive calls to the same methods (e.g., `.count` called 7+ times)
231
+
232
+ **Configuration:**
233
+ See `test/rails_app/README.md` for detailed violation documentation.
234
+
235
+ ## Detected Violations
236
+
237
+ ### SOLID Principles
238
+
239
+ **Single Responsibility Principle (SRP):**
240
+ - Models with 15+ public methods (fat models)
241
+ - Controllers with 8+ actions (fat controllers)
242
+ - Models with 2+ callbacks (callback soup)
243
+ - Controllers doing business logic (calling services, mailers)
244
+ - Models with network calls in callbacks
245
+
246
+ **Open/Closed Principle (OCP):**
247
+ - Classes with inheritance depth > 3
248
+ - Complex case statements with 3+ conditions
249
+
250
+ **Liskov Substitution Principle (LSP):**
251
+ - STI models that may violate base contracts
252
+ - Polymorphic associations without clear interfaces
77
253
 
78
- ### Pre-commit Hook
254
+ **Interface Segregation Principle (ISP):**
255
+ - Controllers including 3+ modules
256
+ - ActiveSupport::Concern modules with 5+ methods
257
+ - Service objects with 10+ methods
79
258
 
80
- Copy the hook to your git hooks:
259
+ **Dependency Inversion Principle (DIP):**
260
+ - Direct instantiation of classes (`.new` calls > 2)
261
+ - Hard-coded external service dependencies
262
+ - Network calls in model callbacks
263
+
264
+ ### KISS Principle
265
+
266
+ - Methods with cyclomatic complexity > 10
267
+ - Classes with > 100 lines
268
+ - Methods with > 64 lines
269
+ - Nesting depth > 5 levels
270
+
271
+ ### DRY Principle
272
+
273
+ - Identical method implementations across classes
274
+ - String literals repeated > 2 times
275
+ - Method calls repeated > 5 times (e.g., `.count`, `.where`)
276
+ - Duplicate code patterns in controllers
277
+
278
+ ## Development
279
+
280
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `ruby -Ilib:test test/*_test.rb` to run the tests.
281
+
282
+ To install this gem onto your local machine, run `bundle exec rake install`.
283
+
284
+ ## Publishing and Releasing the Gem
285
+
286
+ ### Prerequisites
287
+
288
+ 1. **RubyGems Account**: Create an account at [rubygems.org](https://rubygems.org) if you don't have one
289
+ 2. **API Credentials**: Ensure you're logged in locally:
290
+ ```bash
291
+ gem signin
292
+ ```
293
+
294
+ ### Build the Gem
295
+
296
+ Build the gem package from the gemspec:
81
297
 
82
298
  ```bash
83
- cp $(bundle show rails-architect)/hooks/pre-commit .git/hooks/pre-commit
84
- chmod +x .git/hooks/pre-commit
299
+ gem build rails-architect.gemspec
85
300
  ```
86
301
 
87
- ## Usage
302
+ This creates a `.gem` file (e.g., `rails-architect-0.1.0.gem`) in your project root.
88
303
 
89
- ### Command Line
304
+ ### Publishing to RubyGems
90
305
 
91
- Run analysis on your Rails project:
306
+ Push the built gem to RubyGems:
92
307
 
93
308
  ```bash
94
- rails-architect analyze # Run all checks
95
- rails-architect visualize # Generate diagrams
309
+ gem push rails-architect-0.1.0.gem
96
310
  ```
97
311
 
98
- ### Rake Tasks
312
+ Or use the rake task:
99
313
 
100
314
  ```bash
101
- rake rails_architect:analyze # Run all checks (fails on issues)
102
- rake rails_architect:visualize # Generate visualizations
103
- rake rails_architect:check_coverage # Check test coverage
104
- rake rails_architect:check_performance # Run performance checks
315
+ bundle exec rake release
105
316
  ```
106
317
 
107
- ### Programmatic Usage
318
+ This rake task will:
319
+ - Build the gem
320
+ - Create a git tag for the version
321
+ - Push the tag to GitHub
322
+ - Push the gem to RubyGems
108
323
 
109
- ```ruby
110
- require 'rails_architect'
324
+ ### Version Management
111
325
 
112
- # Analyze the codebase
113
- issues = RailsArchitect.analyze
114
- issues.each do |issue|
115
- puts "#{issue[:type]}: #{issue[:message]} in #{issue[:file]}"
116
- end
326
+ 1. Update the version in `lib/rails_architect/version.rb`:
327
+ ```ruby
328
+ module RailsArchitect
329
+ VERSION = "0.2.0"
330
+ end
331
+ ```
117
332
 
118
- # Generate visualizations
119
- RailsArchitect.visualize
120
- ```
333
+ 2. Commit the version change:
334
+ ```bash
335
+ git add lib/rails_architect/version.rb
336
+ git commit -m "Bump version to 0.2.0"
337
+ ```
121
338
 
122
- ## Development
339
+ 3. Create and push a git tag:
340
+ ```bash
341
+ git tag v0.2.0
342
+ git push origin main --tags
343
+ ```
344
+
345
+ 4. Build and push the gem:
346
+ ```bash
347
+ gem build rails-architect.gemspec
348
+ gem push rails-architect-0.2.0.gem
349
+ ```
123
350
 
124
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
351
+ ### Yanking a Release
125
352
 
126
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`.
353
+ If you need to remove a version from RubyGems:
354
+
355
+ ```bash
356
+ gem yank rails-architect -v 0.1.0
357
+ ```
127
358
 
128
359
  ## Contributing
129
360
 
130
361
  Bug reports and pull requests are welcome on GitHub at https://github.com/samydghim/rails-architect.
131
362
 
132
- ## License
363
+ ## Testing
364
+
365
+ The gem includes a comprehensive test suite using Minitest. Run tests with:
366
+
367
+ ```bash
368
+ ruby -Ilib:test test/*_test.rb
369
+ ```
133
370
 
134
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
371
+ Test coverage includes:
372
+ - SOLID principles validation (SRP, OCP, LSP, ISP, DIP)
373
+ - KISS principle enforcement (complexity, size, nesting)
374
+ - DRY principle detection (duplication, patterns, repetition)
375
+ - Integration tests with sample Rails app demonstrating violations
376
+ - Rake task execution and output formatting
@@ -0,0 +1,25 @@
1
+ defaults: &defaults
2
+ # Model complexity: 7 methods is more realistic for most Rails apps
3
+ model_method_threshold: 7
4
+ # Controller complexity: 5 actions keeps controllers focused
5
+ controller_action_threshold: 5
6
+ # Method complexity: 8 is industry standard (vs 10 which is too lenient)
7
+ method_complexity_threshold: 8
8
+ # Test coverage: 85% is a good minimum for quality assurance
9
+ test_coverage_minimum: 85
10
+
11
+ development:
12
+ <<: *defaults
13
+ # Development can be more lenient for rapid prototyping
14
+ model_method_threshold: 10
15
+ method_complexity_threshold: 10
16
+
17
+ test:
18
+ <<: *defaults
19
+ # Strict testing requirements for CI/CD
20
+ test_coverage_minimum: 90
21
+
22
+ production:
23
+ <<: *defaults
24
+ # Production should have highest quality standards
25
+ test_coverage_minimum: 90
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsArchitect
4
+ class DryAnalyzer
5
+ attr_reader :issues
6
+
7
+ def initialize
8
+ @issues = []
9
+ end
10
+
11
+ def analyze
12
+ check_code_duplication
13
+ check_repeated_patterns
14
+ end
15
+
16
+ private
17
+
18
+ def add_issue(file, type, message)
19
+ @issues << { file: file, type: type, message: message }
20
+ end
21
+
22
+ private
23
+
24
+ def check_code_duplication
25
+ files_content = {}
26
+
27
+ # Read all Ruby files
28
+ Dir.glob("app/**/*.rb").each do |file|
29
+ files_content[file] = File.read(file)
30
+ end
31
+
32
+ # Check for duplicate methods
33
+ methods = extract_all_methods(files_content)
34
+
35
+ # Find duplicate method bodies
36
+ method_bodies = methods.values
37
+ duplicates = find_duplicates(method_bodies)
38
+
39
+ duplicates.each do |body, count|
40
+ if count > 1 && body.strip.length > 50 # Only report significant duplicates
41
+ duplicate_methods = methods.select { |_, b| b == body }.keys
42
+ classes = duplicate_methods.map { |m| m.split('#').first }.uniq
43
+
44
+ if classes.count > 1 # Only if duplicated across different classes
45
+ add_issue("multiple_files", "dry_duplication", "Code duplication detected: #{duplicate_methods.join(', ')} have identical implementations")
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def check_repeated_patterns
52
+ Dir.glob("app/**/*.rb").each do |file|
53
+ content = File.read(file)
54
+
55
+ # Check for repeated string literals
56
+ strings = content.scan(/"([^"]{10,})"/).flatten + content.scan(/'([^']{10,})'/).flatten
57
+ string_counts = strings.tally
58
+
59
+ string_counts.each do |str, count|
60
+ if count > 2
61
+ class_name = extract_class_name(file)
62
+ add_issue(file, "dry_strings", "#{class_name} repeats string '#{str[0..30]}...' #{count} times, consider extracting constants")
63
+ end
64
+ end
65
+
66
+ # Check for repeated method calls
67
+ method_calls = content.scan(/\.(\w+)\b/).flatten
68
+ call_counts = method_calls.tally
69
+
70
+ call_counts.each do |method, count|
71
+ if count > 5 && method.length > 3
72
+ class_name = extract_class_name(file)
73
+ add_issue(file, "dry_calls", "#{class_name} calls .#{method} #{count} times, consider extracting helper method")
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def extract_all_methods(files_content)
80
+ methods = {}
81
+
82
+ files_content.each do |file, content|
83
+ class_name = extract_class_name(file)
84
+ file_methods = extract_methods(content)
85
+
86
+ file_methods.each do |method_name, body|
87
+ full_name = "#{class_name}##{method_name}"
88
+ methods[full_name] = body.strip
89
+ end
90
+ end
91
+
92
+ methods
93
+ end
94
+
95
+ def extract_methods(content)
96
+ methods = {}
97
+ current_method = nil
98
+ indent_level = 0
99
+ method_start = false
100
+
101
+ content.each_line do |line|
102
+ if line.match?(/^\s*def \w+/)
103
+ current_method = line.strip.match(/def (\w+)/)[1]
104
+ methods[current_method] = ""
105
+ method_start = true
106
+ indent_level = line.match(/^\s*/).to_s.length
107
+ elsif method_start && line.match(/^\s*end\s*$/) && line.match(/^\s*/).to_s.length == indent_level
108
+ method_start = false
109
+ current_method = nil
110
+ elsif method_start
111
+ methods[current_method] += line
112
+ end
113
+ end
114
+
115
+ methods
116
+ end
117
+
118
+ def find_duplicates(array)
119
+ duplicates = {}
120
+ seen = {}
121
+
122
+ array.each do |item|
123
+ normalized = item.strip
124
+ if seen[normalized]
125
+ duplicates[normalized] = (duplicates[normalized] || 2) + 1
126
+ else
127
+ seen[normalized] = true
128
+ end
129
+ end
130
+
131
+ duplicates
132
+ end
133
+
134
+ def extract_class_name(file)
135
+ relative_path = file.sub('app/', '').sub('.rb', '')
136
+ parts = relative_path.split('/')
137
+ class_parts = parts.map do |part|
138
+ part.split('_').map(&:capitalize).join
139
+ end
140
+ class_parts.join('::')
141
+ end
142
+ end
143
+ end