light-services 2.0.0.rc7 → 2.0.0.rc8

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: 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: