service_core 0.1.3 → 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: 01d926ff29ac3ea5c0ecb4f7a754ea6531732cfe8bfe33591f17a0827132fd21
4
- data.tar.gz: b79448edd6a9a4900430f246b0410362782d197a0b9318add7e6f7d63d71df1b
3
+ metadata.gz: f67c3782ff8c7f937074ff46819d3be8e1a292c34626d78679b3c76678b7691b
4
+ data.tar.gz: c7d291d06cfa2c133ced540e3a7d93c587af49237ec9623a4faaeb2aa04aff11
5
5
  SHA512:
6
- metadata.gz: b192d8ca5f4439ca1d178697c70ed5ed1e288108c23c72b1aafa8ac1e7a17124f97c6b8f95978615f62e0f58e536c1a1f48cddc6b3e6ab39e5402f314ad3fa36
7
- data.tar.gz: b855792ae11ac1a308004437e01374daac234ae4588416dc936eb2d5ac9cf973b03c2435a48d515e3a577cc29016dcbcdd88939d4c4b43d6d98b7b2f0cdd1109
6
+ metadata.gz: c0b1d2784eeeb0c9c1bb640127a4a492f702b7fc092b584f5f6f019afb13999e3cfda5a101ef1561971c543f3eb56f5e1fd677fbd832c2f0739daa2898474d3f
7
+ data.tar.gz: 5f069a26c2734b989cd870671d1f66e99c9a1fe3559be78ab5f1ad4a133a7677cd00c614278f138f1a656ab5f7737850f7df286d5f787f8ab5e15258acad2a3d
data/.rubocop.yml CHANGED
@@ -1,5 +1,9 @@
1
+ require:
2
+ - rubocop-rspec
3
+
1
4
  AllCops:
2
- TargetRubyVersion: 2.6
5
+ TargetRubyVersion: 2.7
6
+ NewCops: enable
3
7
 
4
8
  Style/StringLiterals:
5
9
  Enabled: true
@@ -16,4 +20,16 @@ Style/Documentation:
16
20
  Enabled: false
17
21
 
18
22
  Metrics/BlockLength:
23
+ Enabled: false
24
+
25
+ RSpec/MultipleExpectations:
26
+ Enabled: false
27
+
28
+ RSpec/ExampleLength:
29
+ Enabled: false
30
+
31
+ RSpec/MessageSpies:
32
+ Enabled: false
33
+
34
+ RSpec/VerifiedDoubleReference:
19
35
  Enabled: false
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ServiceCore
2
2
 
3
- ServiceCore provides a standardized way to define and use service objects in your Ruby and Rails applications. It includes support for defining fields, validations, responses, and logging.
3
+ ServiceCore provides a standardized way to define and use service objects in Ruby and Rails applications. It includes support for specifying fields, validations, responses, and error logging. This approach is inspired by the DRY (Don't Repeat Yourself) principle and Rails' convention over configuration philosophy.
4
4
 
5
5
  ## Installation
6
6
  Install the gem and add to the application's Gemfile by executing:
@@ -8,19 +8,32 @@ Install the gem and add to the application's Gemfile by executing:
8
8
  ```sh
9
9
  bundle add service_core
10
10
  ```
11
- If bundler is not being used to manage dependencies, install the gem by executing:
11
+ Or in your Gemfile:
12
+
13
+ ```ruby
14
+ gem "service_core"
15
+ ```
16
+
17
+ If the bundler is not being used to manage dependencies, install the gem by executing:
12
18
 
13
19
  ```sh
14
20
  gem install service_core
15
21
  ```
22
+
23
+ ### Service Response Structure
24
+ The idea is to define a convention that the response from a service can have only four keys:
25
+ - status
26
+ - data
27
+ - message
28
+ - errors
29
+
30
+ The data type of any of the above keys is not enforced, giving the developer flexibility to return based on the use case, but should follow this response structure.
16
31
  ## Usage
17
32
 
18
33
  ### Defining a Service
19
34
 
20
- To define a new service, include the `ServiceCore` module in your service class and define your fields and the perform method.
35
+ To define a new service, include the `ServiceCore` module in your service class and define your fields and the `performmethod.
21
36
  ```ruby
22
- # app/services/my_service.rb
23
-
24
37
  class MyService
25
38
  include ServiceCore
26
39
 
@@ -60,8 +73,181 @@ puts service.output
60
73
  # }
61
74
  ```
62
75
 
76
+ The `call` method can be invoked on the service class and it too will return the object of the service.
77
+ ```ruby
78
+ obj = MyService.call(first_name: "John", last_name: "Doe")
79
+ puts obj.output
80
+ # Output:
81
+ # {
82
+ # status: "success",
83
+ # message: "Hello, World",
84
+ #. data: "John Doe"
85
+ # }
86
+ ```
87
+
88
+ ### `field` method
89
+ The `field` method can define primitive types and objects, like hash/array or any object. For objects, there is no need to declare the datatype.
90
+ ```ruby
91
+ class MyService
92
+ include ServiceCore
93
+
94
+ field :first_name, :string
95
+ field :last_name, :string
96
+ field :payload # can be object/hash/array
97
+
98
+ def perform
99
+ success_response(message: "Hello, World", data: name)
100
+ end
101
+
102
+ def name
103
+ "#{first_name} #{last_name}"
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### `set_output` method
109
+
110
+ The `set_output` method provides a way to set output of a specific key. It is the method used by the response_setters to set specific output value
111
+ ```ruby
112
+ class MyService
113
+ include ServiceCore
114
+
115
+ field :first_name, :string
116
+ field :last_name, :string
117
+ field :payload # can be object/hash/array
118
+
119
+ def perform
120
+ set_output :message, "Hello, World"
121
+ set_output :data, name
122
+ end
123
+
124
+ def name
125
+ "#{first_name} #{last_name}"
126
+ end
127
+ end
128
+
129
+ obj = MyService.call(first_name: "John", last_name: "Doe")
130
+ puts obj.output
131
+ # Output:
132
+ # {
133
+ # status: "success",
134
+ # message: "Hello, World",
135
+ #. data: "John Doe"
136
+ # }
137
+ ```
138
+ *NOTE:* If `:status` is not explicitly set in the perform method, the `success` status is returned if `errors` are blank else the `error` status is returned.
139
+
140
+ ### Response Setters
141
+
142
+ #### `success_response`
143
+ Use the `success_response` method to return the `success` status, `data` and `message`
144
+ ```ruby
145
+
146
+ class MyService
147
+ include ServiceCore
148
+
149
+ field :first_name, :string
150
+ field :last_name, :string
151
+ field :active, :boolean, default: true
152
+
153
+ def perform
154
+ success_response(message: "Hello, World", data: name)
155
+ end
156
+
157
+ def name
158
+ "#{first_name} #{last_name}"
159
+ end
160
+ end
161
+
162
+ service = MyService.new(first_name: "John", last_name: "Doe")
163
+ result = service.call
164
+ puts result
165
+ # Output:
166
+ # {
167
+ # status: "success",
168
+ # message: "Hello, World",
169
+ #. data: "John Doe"
170
+ # }
171
+ ```
172
+
173
+ `success_response` accepts following arguments:
174
+ - message
175
+ - data
176
+
177
+ #### `error_response`
178
+ Use the `error_response` method to return the `error` status, `errors` and `message`
179
+ ```ruby
180
+
181
+ class MyService
182
+ include ServiceCore
183
+
184
+ field :first_name, :string
185
+ field :last_name, :string
186
+ field :active, :boolean, default: true
187
+
188
+ def perform
189
+ error_response(message: "validation failure", errors: "last_name can't be blank")
190
+ end
191
+
192
+ def name
193
+ "#{first_name} #{last_name}"
194
+ end
195
+ end
196
+
197
+ service = MyService.new(first_name: "John")
198
+ result = service.call
199
+ puts result
200
+ # Output:
201
+ # {
202
+ # status: "error",
203
+ # message: "validation failure",
204
+ #. errors: "last_name can't be blank"
205
+ # }
206
+ ```
207
+
208
+ `error_response` accepts following arguments:
209
+ - message
210
+ - errors
211
+
212
+ #### `formatted_response`
213
+ Use the `formatted_response` method to return any status other than `success` or `error`.
214
+ ```ruby
215
+
216
+ class MyService
217
+ include ServiceCore
218
+
219
+ field :first_name, :string
220
+ field :last_name, :string
221
+ field :active, :boolean, default: true
222
+
223
+ def perform
224
+ formatted_response(status: 'processed', message: "Hello, World", data: name)
225
+ end
226
+
227
+ def name
228
+ "#{first_name} #{last_name}"
229
+ end
230
+ end
231
+
232
+ service = MyService.new(first_name: "John", last_name: "Doe")
233
+ result = service.call
234
+ puts result
235
+ # Output:
236
+ # {
237
+ # status: "processed",
238
+ # message: "Hello, World",
239
+ #. data: "John Doe"
240
+ # }
241
+ ```
242
+
243
+ `formatted_response` accepts following arguments:
244
+ - status
245
+ - message
246
+ - data
247
+ - errors
248
+
63
249
  ### Validations
64
- You can define validation on the service and those will be invoked before service logic is invoked
250
+ Define validation on the service and those will be invoked before service logic is invoked.
65
251
  ```ruby
66
252
  class MyService
67
253
  include ServiceCore
@@ -86,7 +272,7 @@ puts result
86
272
  ```
87
273
 
88
274
  ### Step Validation
89
- You can perform validation at each step of service logic. This is helpful when result of previous step decides next logic.
275
+ Perform validation at each step of service logic. This is helpful when the result of the previous step decides the next logic.
90
276
  ```ruby
91
277
  class MyService
92
278
  include ServiceCore
@@ -108,7 +294,8 @@ class MyService
108
294
  end
109
295
  end
110
296
 
111
- MyService.call(first_name: 'abc')
297
+ obj = MyService.call(first_name: 'abc')
298
+ obj.output
112
299
  # output:
113
300
  # {
114
301
  # status: "error",
@@ -118,7 +305,7 @@ MyService.call(first_name: 'abc')
118
305
  ```
119
306
 
120
307
  ### Logging Errors
121
- You can log errors using the `log_error` method.
308
+ Log errors using the `log_error` method.
122
309
  ```ruby
123
310
  class MyService
124
311
  include ServiceCore
@@ -148,50 +335,13 @@ puts result
148
335
  ```
149
336
 
150
337
  ### Configuring the Logger
151
- You can configure the logger for the ServiceCore module.
338
+ Configure the logger for the ServiceCore module.
152
339
  ```ruby
153
340
  ServiceCore.configure do |config|
154
341
  config.logger = Logger.new(STDOUT)
155
342
  end
156
343
  ```
157
344
 
158
- ### Custom Response
159
- use `formatted_response` method to return any other status other than `success` or `error`
160
- ```ruby
161
-
162
- class MyService
163
- include ServiceCore
164
-
165
- field :first_name, :string
166
- field :last_name, :string
167
- field :active, :boolean, default: true
168
-
169
- def perform
170
- formatted_response(status: 'processed', message: "Hello, World", data: name)
171
- end
172
-
173
- def name
174
- "#{first_name} #{last_name}"
175
- end
176
- end
177
-
178
- service = MyService.new(first_name: "John", last_name: "Doe")
179
- result = service.call
180
- puts result
181
- # Output:
182
- # {
183
- # status: "processed",
184
- # message: "Hello, World",
185
- #. data: "John Doe"
186
- # }
187
- ```
188
-
189
- `formatted_response` accepts following arguments:
190
- - status
191
- - message
192
- - data
193
- - errors
194
-
195
345
  ## Contributing
196
346
 
197
347
  Bug reports and pull requests are welcome on GitHub at https://github.com/sehgalmayank001/service-core. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sehgalmayank001/service-core/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -16,6 +16,6 @@ task :release do
16
16
  version = `ruby -r ./lib/service_core/version -e "puts ServiceCore::VERSION"`.strip
17
17
  # sh "git add ."
18
18
  # sh "git commit -m 'Prepare for version #{version} release'"
19
- # sh "git tag v#{version}"
20
- # sh "git push origin main --tags"
21
- end
19
+ sh "git tag v#{version}"
20
+ sh "git push origin main --tags"
21
+ end
@@ -9,13 +9,12 @@ module ServiceCore
9
9
  extend ActiveSupport::Concern
10
10
 
11
11
  included do
12
+ # ServiceCore::Response is included first as it inherited output,
13
+ # which too has initialize method.
14
+ include ServiceCore::Response
12
15
  include ActiveModel::Model
13
16
  include ActiveModel::Attributes
14
17
  include ActiveModel::Validations
15
- include ServiceCore::Response
16
-
17
- # NOTE: output attribute will hold output of the service
18
- attr_reader :output
19
18
 
20
19
  # NOTE: fields attribute will hold the fields defined and their values
21
20
  attr_reader :fields
@@ -50,7 +49,9 @@ module ServiceCore
50
49
  end
51
50
 
52
51
  def call(attributes = {})
53
- new(attributes).call
52
+ obj = new(attributes)
53
+ obj.call
54
+ obj
54
55
  end
55
56
  end
56
57
 
@@ -62,9 +63,6 @@ module ServiceCore
62
63
  self.class.fields_defined.each_key do |name|
63
64
  @fields[name] = send(name)
64
65
  end
65
-
66
- # default value of output
67
- @output = { status: "initialized" }
68
66
  end
69
67
 
70
68
  def call
@@ -74,15 +72,15 @@ module ServiceCore
74
72
  # perform the operation
75
73
  perform
76
74
 
75
+ # auto assign status if output is dirty
76
+ auto_assign_status
77
+
77
78
  # return output
78
79
  output
79
80
  end
80
81
 
81
82
  private
82
83
 
83
- # output writer is protected to be used inside class and sub-classes only
84
- attr_writer :output
85
-
86
84
  def perform
87
85
  raise StandardError, "perform method not implemented"
88
86
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ # require "active_model/errors"
5
+
6
+ module ServiceCore
7
+ module Output
8
+ extend ActiveSupport::Concern
9
+
10
+ ALLOWED_KEYS = %i[status data message errors].freeze
11
+
12
+ # NOTE: output attribute will hold output of the service
13
+ attr_reader :output
14
+
15
+ def initialize(_attributes = {})
16
+ @output_dirty = false
17
+ @status_dirty = false
18
+ @output = { status: "initialized" }
19
+ end
20
+
21
+ private
22
+
23
+ def set_output(key, value)
24
+ return unless key && value
25
+ raise ArgumentError, "Invalid key. Allowed keys are: #{ALLOWED_KEYS.join(", ")}" unless ALLOWED_KEYS.include?(key)
26
+
27
+ @output_dirty ||= true
28
+ @status_dirty = true if key == :status && !@status_dirty
29
+ @output[key] = value
30
+ end
31
+
32
+ def auto_assign_status
33
+ if !@output_dirty
34
+ set_output(:status, "success")
35
+ elsif @output_dirty && !@status_dirty
36
+ status_value = output[:errors].blank? ? "success" : "error"
37
+ set_output(:status, status_value)
38
+ elsif @output_dirty && @status_dirty
39
+ # ignore
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require_relative "output"
4
5
  # require "active_model/errors"
5
6
 
6
7
  module ServiceCore
7
8
  module Response
8
9
  extend ActiveSupport::Concern
9
10
 
11
+ include ServiceCore::Output
12
+
10
13
  protected
11
14
 
12
15
  def success_response(message: nil, data: nil)
@@ -19,11 +22,11 @@ module ServiceCore
19
22
 
20
23
  # set output response
21
24
  def formatted_response(status:, message: nil, data: nil, errors: nil)
22
- @output[:status] = status
23
- @output[:message] = message if message.present?
24
- @output[:data] = data if data.present?
25
- @output[:errors] = error_messages(errors) if errors.present?
26
- @output
25
+ set_output(:status, status)
26
+ set_output(:message, message) if message.present?
27
+ set_output(:data, data) if data.present?
28
+ set_output(:errors, error_messages(errors)) if errors.present?
29
+ output
27
30
  end
28
31
 
29
32
  def error_messages(errors)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceCore
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: service_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mayank sehgal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-17 00:00:00.000000000 Z
11
+ date: 2024-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -68,15 +68,16 @@ files:
68
68
  - lib/service_core.rb
69
69
  - lib/service_core/base.rb
70
70
  - lib/service_core/logger.rb
71
+ - lib/service_core/output.rb
71
72
  - lib/service_core/response.rb
72
73
  - lib/service_core/step_validation.rb
73
74
  - lib/service_core/version.rb
74
- - service_core.gemspec
75
75
  - sig/service_core.rbs
76
76
  homepage: https://github.com/sehgalmayank001/service-core
77
77
  licenses:
78
78
  - MIT
79
- metadata: {}
79
+ metadata:
80
+ rubygems_mfa_required: 'true'
80
81
  post_install_message:
81
82
  rdoc_options: []
82
83
  require_paths:
@@ -85,7 +86,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
86
  requirements:
86
87
  - - ">="
87
88
  - !ruby/object:Gem::Version
88
- version: 2.6.0
89
+ version: 2.7.0
89
90
  required_rubygems_version: !ruby/object:Gem::Requirement
90
91
  requirements:
91
92
  - - ">="
data/service_core.gemspec DELETED
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/service_core/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "service_core"
7
- spec.version = ServiceCore::VERSION
8
- spec.authors = ["mayank sehgal"]
9
- spec.email = ["sehgalmayank001@gmail.com"]
10
-
11
- spec.summary = "A Rails service pattern implementation"
12
- spec.description = "A service pattern implementation for Rails applications."
13
- spec.homepage = "https://github.com/sehgalmayank001/service-core"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.6.0"
16
-
17
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
-
19
- # spec.metadata["homepage_uri"] = spec.homepage
20
- # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
21
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (File.expand_path(f) == __FILE__) ||
28
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
29
- end
30
- end
31
- spec.bindir = "exe"
32
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
34
-
35
- spec.add_runtime_dependency "activemodel", ">= 6.1", "< 8.0"
36
- spec.add_runtime_dependency "activesupport", ">= 6.1", "< 8.0"
37
-
38
-
39
- # Uncomment to register a new dependency of your gem
40
- # spec.add_dependency "example-gem", "~> 1.0"
41
-
42
- # For more information and examples about making a new gem, check out our
43
- # guide at: https://bundler.io/guides/creating_gem.html
44
- end