rubocop-cuseum 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad683ca7ea03f7bec7d45905e18daa39d264479bf4773c808cddab3d789d82a5
4
+ data.tar.gz: 259e2e8242b0f5c159954b7418e2bfeaefe03a6b3a3e3ec44b017861119479c1
5
+ SHA512:
6
+ metadata.gz: fc1724b3648dd94d8f284fa31b43a75d57898b0e6155ab824ec7e3e7f65b174636177c5f53e08c9f63b1d72a545f1c5d80eb56d46df61cf4a18635e761330326
7
+ data.tar.gz: 2538c7425f8185d9696791076042be51f49305029650fb9805e5f5330382acea94e807cade06145a2e5c715c782d92b25647b37fd109a8aa2c43f779cb81dede
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # RuboCop Cuseum
2
+
3
+ Custom RuboCop cops for enforcing Cuseum service class conventions.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rubocop-cuseum', github: 'cuseum/rubocop-cuseum'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install rubocop-cuseum
20
+
21
+ ## Usage
22
+
23
+ Put this into your `.rubocop.yml`:
24
+
25
+ ```yaml
26
+ require:
27
+ - rubocop-cuseum
28
+ ```
29
+
30
+ ## Cops
31
+
32
+ ### Cuseum/SinglePublicMethodService
33
+
34
+ Ensures that service classes have at most one public method.
35
+
36
+ ```ruby
37
+ # good - zero public methods
38
+ class UserService < BaseService
39
+ private
40
+
41
+ def process_user
42
+ # ...
43
+ end
44
+ end
45
+
46
+ # good - one public method
47
+ class UserService < BaseService
48
+ def call
49
+ # ...
50
+ end
51
+
52
+ private
53
+
54
+ def process_user
55
+ # ...
56
+ end
57
+ end
58
+
59
+ # bad - multiple public methods
60
+ class UserService < BaseService
61
+ def call
62
+ # ...
63
+ end
64
+
65
+ def execute # ← should be private
66
+ # ...
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### Cuseum/InheritFromBaseService
72
+
73
+ Ensures that service classes inherit from BaseService directly or indirectly.
74
+
75
+ ```ruby
76
+ # good - direct inheritance
77
+ class UserService < BaseService
78
+ def call
79
+ # ...
80
+ end
81
+ end
82
+
83
+ # good - indirect inheritance through ApplicationService
84
+ class UserService < ApplicationService
85
+ def call
86
+ # ...
87
+ end
88
+ end
89
+
90
+ # bad - no inheritance from BaseService
91
+ class UserService
92
+ def call
93
+ # ...
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Cuseum/PublicMethodNamedCall
99
+
100
+ Ensures that any public method in service classes is named "call".
101
+
102
+ ```ruby
103
+ # good - public method named "call"
104
+ class UserService < BaseService
105
+ def call
106
+ # ...
107
+ end
108
+
109
+ private
110
+
111
+ def process_user
112
+ # ...
113
+ end
114
+ end
115
+
116
+ # bad - public method with wrong name
117
+ class UserService < BaseService
118
+ def execute # ← should be "call"
119
+ # ...
120
+ end
121
+ end
122
+ ```
123
+
124
+ ## Development
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
127
+
128
+ ## Contributing
129
+
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cuseum/rubocop-cuseum.
131
+
132
+ ## License
133
+
134
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,17 @@
1
+ Cuseum/SinglePublicMethodService:
2
+ Description: Service classes should have at most one public method.
3
+ Enabled: true
4
+ Include:
5
+ - app/services/**/*.rb
6
+
7
+ Cuseum/InheritFromBaseService:
8
+ Description: Service classes must inherit from BaseService directly or indirectly.
9
+ Enabled: true
10
+ Include:
11
+ - app/services/**/*.rb
12
+
13
+ Cuseum/PublicMethodNamedCall:
14
+ Description: Public methods in service classes should be named "call".
15
+ Enabled: true
16
+ Include:
17
+ - app/services/**/*.rb
@@ -0,0 +1,83 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Cuseum
7
+ # Ensures that service classes in app/services inherit from BaseService directly or indirectly.
8
+ #
9
+ # @example
10
+ # # good - direct inheritance
11
+ # class UserService < BaseService
12
+ # def call
13
+ # # ...
14
+ # end
15
+ # end
16
+ #
17
+ # # good - indirect inheritance
18
+ # class ApplicationService < BaseService
19
+ # end
20
+ #
21
+ # class UserService < ApplicationService
22
+ # def call
23
+ # # ...
24
+ # end
25
+ # end
26
+ #
27
+ # # bad - no inheritance from BaseService
28
+ # class UserService
29
+ # def call
30
+ # # ...
31
+ # end
32
+ # end
33
+ #
34
+ # # bad - inherits from something else
35
+ # class UserService < SomeOtherClass
36
+ # def call
37
+ # # ...
38
+ # end
39
+ # end
40
+ #
41
+ class InheritFromBaseService < Base
42
+ MSG = "Service classes must inherit from BaseService directly or indirectly."
43
+
44
+ def on_class(node)
45
+ return if inherits_from_base_service?(node)
46
+
47
+ add_offense(node, message: MSG)
48
+ end
49
+
50
+ private
51
+
52
+ def inherits_from_base_service?(node)
53
+ parent_class = node.parent_class
54
+ return false unless parent_class
55
+
56
+ check_inheritance_chain(parent_class)
57
+ end
58
+
59
+ def check_inheritance_chain(parent_class_node)
60
+ return false unless parent_class_node
61
+
62
+ class_name = extract_class_name(parent_class_node)
63
+ return true if class_name == "BaseService"
64
+
65
+ # For more complex inheritance checking, we'd need to resolve the actual class
66
+ # and check its ancestors, but for most cases, direct parent checking should suffice
67
+ # This cop assumes BaseService inheritance is usually direct or one level deep
68
+ false
69
+ end
70
+
71
+ def extract_class_name(class_node)
72
+ case class_node.type
73
+ when :const
74
+ class_node.const_name
75
+ when :send
76
+ # Handle namespaced classes like Module::BaseService
77
+ class_node.method_name.to_s if class_node.receiver.nil?
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,124 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Cuseum
7
+ # Ensures that any public method in service classes is named "call".
8
+ #
9
+ # @example
10
+ # # good - no public methods
11
+ # class UserService < BaseService
12
+ # private
13
+ #
14
+ # def process_user
15
+ # # ...
16
+ # end
17
+ # end
18
+ #
19
+ # # good - public method named "call"
20
+ # class UserService < BaseService
21
+ # def call
22
+ # # ...
23
+ # end
24
+ #
25
+ # private
26
+ #
27
+ # def process_user
28
+ # # ...
29
+ # end
30
+ # end
31
+ #
32
+ # # bad - public method with wrong name
33
+ # class UserService < BaseService
34
+ # def execute # ← should be "call"
35
+ # # ...
36
+ # end
37
+ # end
38
+ #
39
+ # # bad - multiple public methods, some with wrong names
40
+ # class UserService < BaseService
41
+ # def call
42
+ # # ...
43
+ # end
44
+ #
45
+ # def execute # ← should be "call" or private
46
+ # # ...
47
+ # end
48
+ # end
49
+ #
50
+ class PublicMethodNamedCall < Base
51
+ MSG = 'Public methods in service classes should be named "call".'
52
+
53
+ def on_class(node)
54
+ public_methods = extract_public_methods(node)
55
+
56
+ public_methods.each do |method_node|
57
+ method_name = method_node.method_name.to_s
58
+ unless method_name == "call"
59
+ add_offense(method_node, message: MSG)
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def extract_public_methods(class_node)
67
+ public_methods = []
68
+ current_visibility = :public
69
+
70
+ if class_node.body
71
+ if class_node.body.begin_type?
72
+ # Multiple statements in class body
73
+ class_node.body.each_child_node do |child|
74
+ case child.type
75
+ when :send
76
+ if visibility_modifier?(child)
77
+ current_visibility = child.method_name
78
+ end
79
+ when :def
80
+ if current_visibility == :public && regular_method?(child)
81
+ public_methods << child
82
+ end
83
+ end
84
+ end
85
+ else
86
+ # Single statement in class body (likely a single method)
87
+ child = class_node.body
88
+ case child.type
89
+ when :send
90
+ if visibility_modifier?(child)
91
+ current_visibility = child.method_name
92
+ end
93
+ when :def
94
+ if current_visibility == :public && regular_method?(child)
95
+ public_methods << child
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ public_methods
102
+ end
103
+
104
+ def visibility_modifier?(node)
105
+ return false unless node.send_type?
106
+
107
+ [:private, :protected, :public].include?(node.method_name) &&
108
+ node.receiver.nil? &&
109
+ node.arguments.empty?
110
+ end
111
+
112
+ def regular_method?(method_node)
113
+ return false unless method_node.def_type?
114
+
115
+ method_name = method_node.method_name.to_s
116
+ # Exclude initialize and special methods
117
+ method_name != "initialize" &&
118
+ !method_name.start_with?("initialize") &&
119
+ !method_name.match?(/^[^a-zA-Z]/)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,119 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Cuseum
7
+ # Ensures that service classes in app/services have at most one public method.
8
+ #
9
+ # @example
10
+ # # good - zero public methods
11
+ # class UserService < BaseService
12
+ # private
13
+ #
14
+ # def process_user
15
+ # # ...
16
+ # end
17
+ # end
18
+ #
19
+ # # good - one public method
20
+ # class UserService < BaseService
21
+ # def call
22
+ # # ...
23
+ # end
24
+ #
25
+ # private
26
+ #
27
+ # def process_user
28
+ # # ...
29
+ # end
30
+ # end
31
+ #
32
+ # # bad - multiple public methods
33
+ # class UserService < BaseService
34
+ # def call
35
+ # # ...
36
+ # end
37
+ #
38
+ # def execute # ← should be private
39
+ # # ...
40
+ # end
41
+ # end
42
+ #
43
+ class SinglePublicMethodService < Base
44
+ MSG = "Service classes should have at most one public method."
45
+
46
+ def on_class(node)
47
+ public_methods = extract_public_methods(node)
48
+
49
+ # Allow 0 or 1 public methods, flag 2 or more
50
+ return if public_methods.size <= 1
51
+
52
+ public_methods.each_with_index do |method_node, index|
53
+ next if index.zero? # Allow the first public method
54
+
55
+ add_offense(method_node, message: MSG)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def extract_public_methods(class_node)
62
+ public_methods = []
63
+ current_visibility = :public
64
+
65
+ if class_node.body
66
+ if class_node.body.begin_type?
67
+ # Multiple statements in class body
68
+ class_node.body.each_child_node do |child|
69
+ case child.type
70
+ when :send
71
+ if visibility_modifier?(child)
72
+ current_visibility = child.method_name
73
+ end
74
+ when :def
75
+ if current_visibility == :public && regular_method?(child)
76
+ public_methods << child
77
+ end
78
+ end
79
+ end
80
+ else
81
+ # Single statement in class body (likely a single method)
82
+ child = class_node.body
83
+ case child.type
84
+ when :send
85
+ if visibility_modifier?(child)
86
+ current_visibility = child.method_name
87
+ end
88
+ when :def
89
+ if current_visibility == :public && regular_method?(child)
90
+ public_methods << child
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ public_methods
97
+ end
98
+
99
+ def visibility_modifier?(node)
100
+ return false unless node.send_type?
101
+
102
+ [:private, :protected, :public].include?(node.method_name) &&
103
+ node.receiver.nil? &&
104
+ node.arguments.empty?
105
+ end
106
+
107
+ def regular_method?(method_node)
108
+ return false unless method_node.def_type?
109
+
110
+ method_name = method_node.method_name.to_s
111
+ # Exclude initialize and special methods
112
+ method_name != "initialize" &&
113
+ !method_name.start_with?("initialize") &&
114
+ !method_name.match?(/^[^a-zA-Z]/)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,6 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "cuseum/single_public_method_service"
5
+ require_relative "cuseum/inherit_from_base_service"
6
+ require_relative "cuseum/public_method_named_call"
@@ -0,0 +1,6 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "rubocop"
5
+
6
+ require_relative "rubocop/cop/cuseum_cops"
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-cuseum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cuseum Team
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rubocop
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '12.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '12.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ description: Custom RuboCop cops to enforce Cuseum service class patterns and best
69
+ practices
70
+ email:
71
+ - dev@cuseum.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - config/default.yml
78
+ - lib/rubocop-cuseum.rb
79
+ - lib/rubocop/cop/cuseum/inherit_from_base_service.rb
80
+ - lib/rubocop/cop/cuseum/public_method_named_call.rb
81
+ - lib/rubocop/cop/cuseum/single_public_method_service.rb
82
+ - lib/rubocop/cop/cuseum_cops.rb
83
+ homepage: https://github.com/cuseum/rubocop-cuseum
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ homepage_uri: https://github.com/cuseum/rubocop-cuseum
88
+ source_code_uri: https://github.com/cuseum/rubocop-cuseum
89
+ changelog_uri: https://github.com/cuseum/rubocop-cuseum/blob/main/CHANGELOG.md
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: 3.0.0
98
+ - - "<="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.3.10
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.6.9
108
+ specification_version: 4
109
+ summary: RuboCop extension for Cuseum service class conventions
110
+ test_files: []