light-services 2.0.0.rc7 → 2.0.0.rc8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56f8e05e0898a95e0012bd2bedf572743cbc6869b560b4a7c53aaf826a148851
4
- data.tar.gz: 31349929949615aa616c6c5b58737c4248cf199df340cfa52672b446ef7dd0b3
3
+ metadata.gz: 033745e056484b3cb06dd7047bde7aa8f57d3e7a831f5f9eb2495aec0f9f96e9
4
+ data.tar.gz: 6927b650f82197c0ee0403487f18f71015096d244785ae947d0b8ea4dd56373f
5
5
  SHA512:
6
- metadata.gz: b9fb14ebb26407e8eda911e54bd3bc40cf57977ddda0b7626d2712624e446b9c55e1727a43308771bc85792be3df9701da3441db7127e40bf77a35acb89bf4db
7
- data.tar.gz: 48fade5a78989a8a71c07c055f92d666585367a1140dc26b2799162738efa072815d8511e6c1d7c68d20c6163393ae9acdc7868720462d972b67810c6335158d
6
+ metadata.gz: c87c5af17cccc1cce0514a65dd8f87b84d21599fcafec91ea92fa9f5d068842bfb01f0055b98c62340809a47e4eb3721a974b5958c7cd6b6410249251cf4cdca
7
+ data.tar.gz: a48e79125c81939dd5967ea08e6fcafcb6474f9a13edbc07f1e1d4ec69fc83b23e2bc43a660bf978d999d4b97a57c4f543f5f14b65219c555f2266ce3d06b51f
data/Gemfile CHANGED
@@ -9,12 +9,12 @@ group :test do
9
9
  gem "database_cleaner-active_record", "~> 1.8"
10
10
  gem "sqlite3", "~> 1.4"
11
11
 
12
- gem "codecov", "~> 0.1.17"
12
+ gem "codecov", "~> 0.6.0"
13
13
  gem "rake", "~> 13.0"
14
- gem "rspec", "~> 3.9"
15
- gem "simplecov", "~> 0.18"
14
+ gem "rspec", "~> 3.11"
15
+ gem "simplecov", "~> 0.21"
16
16
 
17
- gem "rubocop", "~> 0.86", require: false
18
- gem "rubocop-performance", "~> 1.6", require: false
19
- gem "rubocop-rspec", "~> 1.40", require: false
17
+ gem "rubocop", "~> 1.27", require: false
18
+ gem "rubocop-performance", "~> 1.13", require: false
19
+ gem "rubocop-rspec", "~> 2.9", require: false
20
20
  end
data/Gemfile.lock CHANGED
@@ -1,101 +1,98 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- light-services (2.0.0.rc7)
4
+ light-services (2.0.0.rc8)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- activemodel (6.1.4)
10
- activesupport (= 6.1.4)
11
- activerecord (6.1.4)
12
- activemodel (= 6.1.4)
13
- activesupport (= 6.1.4)
14
- activesupport (6.1.4)
9
+ activemodel (6.1.5)
10
+ activesupport (= 6.1.5)
11
+ activerecord (6.1.5)
12
+ activemodel (= 6.1.5)
13
+ activesupport (= 6.1.5)
14
+ activesupport (6.1.5)
15
15
  concurrent-ruby (~> 1.0, >= 1.0.2)
16
16
  i18n (>= 1.6, < 2)
17
17
  minitest (>= 5.1)
18
18
  tzinfo (~> 2.0)
19
19
  zeitwerk (~> 2.3)
20
20
  ast (2.4.2)
21
- codecov (0.1.21)
22
- json
23
- simplecov
24
- concurrent-ruby (1.1.9)
21
+ codecov (0.6.0)
22
+ simplecov (>= 0.15, < 0.22)
23
+ concurrent-ruby (1.1.10)
25
24
  database_cleaner (1.99.0)
26
25
  database_cleaner-active_record (1.99.0)
27
26
  activerecord
28
27
  database_cleaner (~> 1.99.0)
29
- diff-lcs (1.4.4)
28
+ diff-lcs (1.5.0)
30
29
  docile (1.4.0)
31
- i18n (1.8.10)
30
+ i18n (1.10.0)
32
31
  concurrent-ruby (~> 1.0)
33
- json (2.5.1)
34
- minitest (5.14.4)
35
- parallel (1.20.1)
36
- parser (3.0.2.0)
32
+ minitest (5.15.0)
33
+ parallel (1.22.1)
34
+ parser (3.1.2.0)
37
35
  ast (~> 2.4.1)
38
- rainbow (3.0.0)
36
+ rainbow (3.1.1)
39
37
  rake (13.0.6)
40
- regexp_parser (2.1.1)
38
+ regexp_parser (2.3.0)
41
39
  rexml (3.2.5)
42
- rspec (3.10.0)
43
- rspec-core (~> 3.10.0)
44
- rspec-expectations (~> 3.10.0)
45
- rspec-mocks (~> 3.10.0)
46
- rspec-core (3.10.1)
47
- rspec-support (~> 3.10.0)
48
- rspec-expectations (3.10.1)
40
+ rspec (3.11.0)
41
+ rspec-core (~> 3.11.0)
42
+ rspec-expectations (~> 3.11.0)
43
+ rspec-mocks (~> 3.11.0)
44
+ rspec-core (3.11.0)
45
+ rspec-support (~> 3.11.0)
46
+ rspec-expectations (3.11.0)
49
47
  diff-lcs (>= 1.2.0, < 2.0)
50
- rspec-support (~> 3.10.0)
51
- rspec-mocks (3.10.2)
48
+ rspec-support (~> 3.11.0)
49
+ rspec-mocks (3.11.1)
52
50
  diff-lcs (>= 1.2.0, < 2.0)
53
- rspec-support (~> 3.10.0)
54
- rspec-support (3.10.2)
55
- rubocop (0.93.1)
51
+ rspec-support (~> 3.11.0)
52
+ rspec-support (3.11.0)
53
+ rubocop (1.27.0)
56
54
  parallel (~> 1.10)
57
- parser (>= 2.7.1.5)
55
+ parser (>= 3.1.0.0)
58
56
  rainbow (>= 2.2.2, < 4.0)
59
- regexp_parser (>= 1.8)
57
+ regexp_parser (>= 1.8, < 3.0)
60
58
  rexml
61
- rubocop-ast (>= 0.6.0)
59
+ rubocop-ast (>= 1.16.0, < 2.0)
62
60
  ruby-progressbar (~> 1.7)
63
- unicode-display_width (>= 1.4.0, < 2.0)
64
- rubocop-ast (1.8.0)
65
- parser (>= 3.0.1.1)
66
- rubocop-performance (1.10.2)
67
- rubocop (>= 0.90.0, < 2.0)
61
+ unicode-display_width (>= 1.4.0, < 3.0)
62
+ rubocop-ast (1.17.0)
63
+ parser (>= 3.1.1.0)
64
+ rubocop-performance (1.13.3)
65
+ rubocop (>= 1.7.0, < 2.0)
68
66
  rubocop-ast (>= 0.4.0)
69
- rubocop-rspec (1.44.1)
70
- rubocop (~> 0.87)
71
- rubocop-ast (>= 0.7.1)
67
+ rubocop-rspec (2.9.0)
68
+ rubocop (~> 1.19)
72
69
  ruby-progressbar (1.11.0)
73
70
  simplecov (0.21.2)
74
71
  docile (~> 1.1)
75
72
  simplecov-html (~> 0.11)
76
73
  simplecov_json_formatter (~> 0.1)
77
74
  simplecov-html (0.12.3)
78
- simplecov_json_formatter (0.1.3)
75
+ simplecov_json_formatter (0.1.4)
79
76
  sqlite3 (1.4.2)
80
77
  tzinfo (2.0.4)
81
78
  concurrent-ruby (~> 1.0)
82
- unicode-display_width (1.7.0)
83
- zeitwerk (2.4.2)
79
+ unicode-display_width (2.1.0)
80
+ zeitwerk (2.5.4)
84
81
 
85
82
  PLATFORMS
86
83
  ruby
87
84
 
88
85
  DEPENDENCIES
89
86
  activerecord (~> 6.0)
90
- codecov (~> 0.1.17)
87
+ codecov (~> 0.6.0)
91
88
  database_cleaner-active_record (~> 1.8)
92
89
  light-services!
93
90
  rake (~> 13.0)
94
- rspec (~> 3.9)
95
- rubocop (~> 0.86)
96
- rubocop-performance (~> 1.6)
97
- rubocop-rspec (~> 1.40)
98
- simplecov (~> 0.18)
91
+ rspec (~> 3.11)
92
+ rubocop (~> 1.27)
93
+ rubocop-performance (~> 1.13)
94
+ rubocop-rspec (~> 2.9)
95
+ simplecov (~> 0.21)
99
96
  sqlite3 (~> 1.4)
100
97
 
101
98
  BUNDLED WITH
data/README.md CHANGED
@@ -1,4 +1,328 @@
1
- # Light Services
2
- ![CI](https://github.com/light-ruby/light-services/workflows/CI/badge.svg)
3
- [![Codecov](https://codecov.io/gh/light-ruby/light-services/branch/master/graph/badge.svg)](https://codecov.io/gh/light-ruby/light-services)
1
+ # 🚀 Light Services <sup>BETA</sup>
4
2
 
3
+ Implementation of Service Object pattern for Ruby/Rails applications.
4
+
5
+ ## 👀 Table of Contents
6
+ 1. [Simple Example](#simple-example)
7
+ 2. [Usage](#usage)
8
+ 1. [Arguments](#arguments)
9
+ 2. [Steps](#steps)
10
+ 3. [Outputs](#outputs)
11
+ 4. [Context](#context)
12
+ 3. [Complex Example](#complex-example)
13
+ 4. [More Examples](#more-examples)
14
+
15
+ ## 💪 Features
16
+
17
+ 1. Ability to define `arguments`, `steps` and `outputs`
18
+ 2. Isolated behaviour of each service object
19
+ 3. Raising of errors to stop processing next steps
20
+ 4. Wrapping actions into database transactions
21
+ 5. Ability to pass context to child service object
22
+ 6. Framework agnostic
23
+ 7. 100% test coverage
24
+
25
+ ## ❌ Problems
26
+
27
+ As this gem was just for internal usage, it has some problems:
28
+
29
+ 1. Gem isn't documented well
30
+ 2. Code doesn't have any comments
31
+ 3. Repo doesn't have any CI/CD
32
+
33
+ ## Installation
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem 'light-services', '~> 2.0.0.rc7'
39
+ ```
40
+
41
+ ## Simple Example
42
+
43
+ ### Send notification
44
+
45
+ Let's create an elementary service object that sends a notification to the user.
46
+
47
+ ```ruby
48
+ class User::SendNotification < ApplicationService
49
+ # Arguments
50
+ arg :user, type: User
51
+ arg :text, type: :string
52
+
53
+ # Steps
54
+ step :validate_user
55
+ step :validate_text
56
+ step :send_notification
57
+
58
+ # Outputs
59
+ output :response
60
+
61
+ private
62
+
63
+ def validate_user
64
+ return if user.active?
65
+
66
+ errors.add(:user, "isn't active")
67
+ end
68
+
69
+ def validate_text
70
+ return if text.present?
71
+
72
+ errors.add(:text, "must be present")
73
+ end
74
+
75
+ def send_notification
76
+ self.response = ExternalAPI.send_message(...)
77
+ rescue ExternalAPI::Error
78
+ errors.add(:base, "External API doesn't work")
79
+ end
80
+ end
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ ### Arguments
86
+
87
+ You may send some arguments into the service object.
88
+
89
+ **How to define arguments:**
90
+ ```ruby
91
+ class User::SendNotification < ApplicationService
92
+ # Required argument
93
+ arg :user, type: User
94
+
95
+ # Optional argument
96
+ arg :device, type: Device, optional: true
97
+
98
+ # Argument with default value
99
+ arg :text, type: :string, default: "Hello, how are you?"
100
+
101
+ # Argument with multiple allowed types
102
+ arg :retry, type: [TrueClass, FalseClass], default: false
103
+
104
+ # Argument which will be automatically passed into child components
105
+ arg :provider, type: Provider, context: true
106
+ end
107
+ ```
108
+
109
+ **How to pass arguments in controller:**
110
+ ```ruby
111
+ class UsersController
112
+ def send_notification
113
+ service = User::SendNotification.run(user: User.first, provider: Provider.first)
114
+ # ...
115
+ end
116
+ end
117
+ ```
118
+
119
+ **How to pass arguments and context from parent to child service object:**
120
+ ```ruby
121
+ class User::Update
122
+ # Arguments
123
+ arg :user, type: User, context: true
124
+
125
+ # Steps
126
+ # ...
127
+ step :send_notification
128
+
129
+ private
130
+
131
+ # ...
132
+
133
+ def send_notification
134
+ User::SendNotification
135
+ .with(self) # This line specifies the current service object as a parent and passes all context arguments into a child service object
136
+ .run(text: "Your profile was updated") # We don't need to pass `user` here as it's a context argument
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Steps
142
+
143
+ Steps are a bit more powerful than you think.
144
+
145
+ ```ruby
146
+ class User::Charge
147
+ # Run step only when condition meets
148
+ step :create_payment_account, unless: :payment_account?
149
+
150
+ # Run step only when condition meets
151
+ step :charge_credit_card, if: :pay_with_credit_card?
152
+
153
+ # Run step after other step
154
+ step :update_payment_account, after: :create_payment_account
155
+
156
+ # Or before
157
+ step :save_information, before: :log_action
158
+ end
159
+ ```
160
+
161
+ ### Outputs
162
+
163
+ Outputs are pretty straightforward.
164
+
165
+ ```ruby
166
+ class User::Charge
167
+ # Simple output
168
+ output :payment
169
+
170
+ # Output with initial value
171
+ output :items, default: []
172
+ end
173
+ ```
174
+
175
+ ### Context
176
+
177
+ The context specifies the relationship between parent and child service objects.
178
+
179
+ What context does:
180
+ 1. It tells the parent service object to pass context arguments into a child service object
181
+ 2. When the child service object fails, it tells the parent service object to fail too (customizable)
182
+
183
+ ```ruby
184
+ class User::Charge
185
+ # Arguments
186
+ arg :user, type: User, context: true
187
+ arg :cents, type: Integer
188
+
189
+ # ...
190
+
191
+ private
192
+
193
+ # ...
194
+
195
+ def send_notification
196
+ # Run service object w/o any context
197
+ User::SendNotification
198
+ .run(user: user, text: "...")
199
+
200
+ # Run service object and specify current one as a parent
201
+ User::SendNotification
202
+ .with(self)
203
+ .run(text: "...")
204
+
205
+ # Run service object with context but don't load errors from the child service object
206
+ service = User::SendNotification
207
+ .with(self, load_errors: false)
208
+ .run(text: "...")
209
+
210
+ if service.failed?
211
+ # That's ok. Process it somehow...
212
+ end
213
+ end
214
+ end
215
+ ```
216
+
217
+ ## Complex Example
218
+
219
+ ### Creation of records
220
+
221
+ Let's investigate a more exciting example where we create a wrapper to create database records.
222
+
223
+ **Here is an example of controller (pretty thin, yeah? but we can make it even thinner):**
224
+ ```ruby
225
+ class ContactsController < ApplicationController
226
+ # ...
227
+
228
+ def create
229
+ service = Contact::Create.run(service_args)
230
+
231
+ if service.success?
232
+ render locals: { contact: service.contact }, status: :ok
233
+ else
234
+ render "shared/errors", locals: { service: service }, status: :bad_request
235
+ end
236
+ end
237
+
238
+ # ...
239
+ end
240
+ ```
241
+
242
+ **Then, let's create a service object (no way, it couldn't be so simple):**
243
+ ```ruby
244
+ class Contact::Create < CreateService
245
+ # We create alias just for a better readability
246
+ # so that we can call `service.contact` instead of `service.record`
247
+ alias contact record
248
+
249
+ private
250
+
251
+ def filtered_params
252
+ params.require(:contact).permit(:name, :phone)
253
+ end
254
+ end
255
+ ```
256
+
257
+ **Let's check what logic we put into `CreateService`:**
258
+ ```ruby
259
+ class CreateService < ApplicationService
260
+ # Arguments
261
+ arg :attributes, type: Hash, optional: true
262
+
263
+ # Outputs
264
+ output :record
265
+
266
+ # Steps
267
+ step :initialize_record
268
+ step :assign_attributes
269
+ step :authorize
270
+ step :validate
271
+ step :save_record
272
+
273
+ private
274
+
275
+ def initialize_record
276
+ self.record = self.class.module_parent.new
277
+ end
278
+
279
+ def assign_attributes
280
+ record.assign_attributes(filtered_params)
281
+ end
282
+
283
+ def authorize
284
+ return if force || attributes
285
+
286
+ # Here is some Pundit logic 👇
287
+ authorize!(record, with_action: :create?)
288
+ end
289
+
290
+ def validate
291
+ return if record.valid?
292
+
293
+ errors.copy_from(record)
294
+ end
295
+
296
+ def save_record
297
+ record.save_with!(self)
298
+ end
299
+
300
+ def filtered_params
301
+ raise NotImplementedError
302
+ end
303
+ end
304
+ ```
305
+
306
+ **Now we can easily reuse all this code and create as many services as we want:**
307
+ ```ruby
308
+ class Team::Create < CreateService
309
+ alias team record
310
+
311
+ private
312
+
313
+ def filtered_params
314
+ params.require(:team).permit(:name)
315
+ end
316
+ end
317
+ ```
318
+
319
+ ## More examples
320
+
321
+ You can find more examples here:
322
+ [https://github.com/light-ruby/light-services/tree/v2/spec/data/services](https://github.com/light-ruby/light-services/tree/v2/spec/data/services)
323
+
324
+ # Happy coding!
325
+
326
+ ## License
327
+
328
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -102,7 +102,7 @@ module Light
102
102
 
103
103
  def with(service_or_config = {}, config = {})
104
104
  service = service_or_config.is_a?(Hash) ? nil : service_or_config
105
- config = service ? config : service_or_config
105
+ config = service_or_config unless service
106
106
 
107
107
  BaseWithContext.new(self, service, config)
108
108
  end
@@ -180,9 +180,9 @@ module Light
180
180
  log "🏎 Run service #{self.class}"
181
181
  end
182
182
 
183
- def within_transaction
183
+ def within_transaction(&block)
184
184
  if @config[:use_transactions] && defined?(ActiveRecord::Base)
185
- ActiveRecord::Base.transaction(requires_new: true) { yield }
185
+ ActiveRecord::Base.transaction(requires_new: true, &block)
186
186
  else
187
187
  yield
188
188
  end
@@ -40,7 +40,11 @@ module Light
40
40
  settings_collection.each do |name, settings|
41
41
  next if !settings.default_exists || key?(name)
42
42
 
43
- set(name, deep_dup(settings.default))
43
+ if settings.default.is_a?(Proc)
44
+ set(name, settings.default.call)
45
+ else
46
+ set(name, deep_dup(settings.default))
47
+ end
44
48
  end
45
49
  end
46
50
 
@@ -10,12 +10,12 @@ module Light
10
10
  @messages = {}
11
11
  end
12
12
 
13
- def add(key, text, opts = {})
14
- raise Light::Services::Error, "Error text can't be blank" if !text || text.blank?
13
+ def add(key, texts, opts = {})
14
+ raise Light::Services::Error, "Error text can't be blank" if !texts || texts.blank?
15
15
 
16
16
  message = nil
17
17
 
18
- [*text].each do |text|
18
+ [*texts].each do |text|
19
19
  message = text.is_a?(Message) ? text : Message.new(key, text, opts)
20
20
 
21
21
  @messages[key] ||= []
@@ -48,7 +48,7 @@ module Light
48
48
  end
49
49
 
50
50
  def copy_to(entity)
51
- if defined?(ActiveRecord::Base) && entity.is_a?(ActiveRecord::Base) || entity.is_a?(Light::Services::Base)
51
+ if (defined?(ActiveRecord::Base) && entity.is_a?(ActiveRecord::Base)) || entity.is_a?(Light::Services::Base)
52
52
  each do |key, messages|
53
53
  messages.each do |message|
54
54
  entity.errors.add(key, message.to_s)
@@ -67,7 +67,7 @@ module Light
67
67
  end
68
68
 
69
69
  def to_h
70
- @messages.to_h.map { |key, value| [key, value.map(&:to_s)] }.to_h
70
+ @messages.to_h.transform_values { |value| value.map(&:to_s) }
71
71
  end
72
72
 
73
73
  def method_missing(method, *args, &block)
@@ -25,9 +25,10 @@ module Light
25
25
 
26
26
  def valid_type?(value)
27
27
  return if !@type || [*@type].any? do |type|
28
- if type == :boolean
28
+ case type
29
+ when :boolean
29
30
  value.is_a?(TrueClass) || value.is_a?(FalseClass)
30
- elsif type.is_a?(Symbol)
31
+ when Symbol
31
32
  arg_type(value) == type
32
33
  else
33
34
  value.is_a?(type)
@@ -45,9 +46,9 @@ module Light
45
46
 
46
47
  @arg_types_cache[klass] ||= klass
47
48
  .name
48
- .gsub(/::/, '/')
49
- .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
50
- .gsub(/([a-z\d])([A-Z])/,'\1_\2')
49
+ .gsub(/::/, "/")
50
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
51
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
51
52
  .tr("-", "_")
52
53
  .downcase
53
54
  .to_sym
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Light
4
4
  module Services
5
- VERSION = "2.0.0.rc7"
5
+ VERSION = "2.0.0.rc8"
6
6
  end
7
7
  end
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.bindir = "exe"
30
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
+ spec.metadata["rubygems_mfa_required"] = "true"
32
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.rc7
4
+ version: 2.0.0.rc8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Emelianenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-07 00:00:00.000000000 Z
11
+ date: 2022-09-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Powerful implementation of Service Object pattern for Ruby and Rails
14
14
  email:
@@ -56,6 +56,7 @@ metadata:
56
56
  homepage_uri: https://github.com/light-ruby/light-services
57
57
  source_code_uri: https://github.com/light-ruby/light-services
58
58
  changelog_uri: https://github.com/light-ruby/light-services/blob/master/CHANGELOG.md
59
+ rubygems_mfa_required: 'true'
59
60
  post_install_message:
60
61
  rdoc_options: []
61
62
  require_paths: