simple_ruby_service 1.0.2 → 1.0.6

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: 665fbb0eacfb11a6e7d863e72a0e091ad96831c0ce72644fb5467b592c567524
4
- data.tar.gz: 98c5caa24124671cd21f56a0f625f8c340e198c3847d05454b9cb41c633ff64b
3
+ metadata.gz: 93055c8359bf7677e53e99e5cae18f128e17f643770b51e207cdfd4892bf1ac2
4
+ data.tar.gz: 916d16d46e8c43ca5815e40b940df3d768e9d22fec56990fa29fb4c0610235a7
5
5
  SHA512:
6
- metadata.gz: f11211b2e212d4324a9c55a09f2a366c152f5e3d53c5069c35d7b77876844cbc6db397e5bb888ceba36d1165ad27cc3dd09c8751cbd3bb838123f68c5695c4b5
7
- data.tar.gz: c1b45fe0900d696f739540cd9cd8f90d9c31133d8df14ebb2a60ad4743b90360241334c102dc70f1d26d4a8ee9415eb564d9631c401a3f633d7502b9b6b2c6be
6
+ metadata.gz: d68822b1a43f2a664c9e4b6f48a7b0a81de97686d346147e5b1d9bc6163e69d3df83a199908a82da6f2f694b704821aab54c019e35f35f3bfefed47c71499f42
7
+ data.tar.gz: cc1b27bbb4d6fd18dfb161d504787a4828a27a0babe4550f0864f1f9f9d740749a0e7983792b8e92d9f030ad7b1afa23be0220ee239939e4cf7cda3b11bb7d96
data/CHANGELOG.md CHANGED
@@ -1,8 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.5 (22-Dec-21)
4
+
5
+ * Added setting to control automatically setting self.value with result of service methods
6
+
7
+ ## 1.0.4 (02-Jul-21)
8
+
9
+ * Polished README (again)
10
+
3
11
  ## 1.0.3 (01-Jul-21)
4
12
 
5
- * Finished Travis CI and Codecov integration
13
+ * Polished README
6
14
 
7
15
  ## 1.0.2 (01-Jul-21)
8
16
 
data/README.md CHANGED
@@ -3,13 +3,31 @@
3
3
  [![Build Status](https://travis-ci.com/amazing-jay/simple_ruby_service.svg?branch=master)](https://travis-ci.com/amazing-jay/simple_ruby_service)
4
4
  [![Test Coverage](https://codecov.io/gh/amazing-jay/simple_ruby_service/graph/badge.svg)](https://codecov.io/gh/amazing-jay/simple_ruby_service)
5
5
 
6
- Simple Ruby Service is a lightweight framework for Ruby that makes it easy to create Services and Service Objects (SOs).
6
+ Simple Ruby Service is a lightweight framework for creating Services and Service Objects (SOs) in Ruby.
7
7
 
8
- The framework provides a simple DSL that:
8
+ The framework makes Services and SOs look and feel like ActiveModels, complete with:
9
9
 
10
- 1. Incorporates ActiveModel validations and error handling
11
- 2. Encourages a succinct, idiomatic coding style
12
- 3. Ducktypes Service Objects as Procs
10
+ 1. Validations and robust error handling
11
+ 2. Workflows and method chaining
12
+ 3. Consistent interfaces
13
+
14
+ Additionally, Simple Ruby Service Objects can stand in for Procs, wherever Procs are expected (via ducktyping).
15
+
16
+ #### What problem does Simple Ruby Service solve?
17
+
18
+ Currently, most ruby developers roll their own services from scratch. As a result, most services are hastely built (in isolation), and this leads to inconsistant interfaces that are difficult to read. Also, error handling tends to vary wildly within an application, and support code tends to be implemented over and over again.
19
+
20
+ Simple Ruby Service addresses these problems and encourages succinct, idiomatic coding styles.
21
+
22
+ #### Should I be using Services & SOs in Ruby / Rails?
23
+
24
+ [LMGTFY](https://www.google.com/search?q=service+object+pattern+rails&rlz=1C5CHFA_enUS893US893&oq=service+object+pattern+rails) to learn more about Services & SOs.
25
+
26
+ **TLDR** - Fat models and fat controllers are bad! Services and Service Objects help you DRY things up.
27
+
28
+ #### How is a Service different from an SO?
29
+
30
+ An SO is just a Service that encapsulates a single operation (i.e. **one, and only one, responsibility**).
13
31
 
14
32
  ## Requirements
15
33
 
@@ -37,10 +55,12 @@ Source code can be downloaded on GitHub
37
55
  [github.com/amazing-jay/simple_ruby_service/tree/master](https://github.com/amazing-jay/simple_ruby_service/tree/master)
38
56
 
39
57
 
40
- ### The following examples illustrate how to refactor complex business logic with Simple Ruby Service
58
+ ## Quick Start
41
59
 
42
60
  See [Usage](https://github.com/amazing-jay/simple_ruby_service#usage) & [Creating Simple Ruby Services](https://github.com/amazing-jay/simple_ruby_service#creating-simple-ruby-services) for more information.
43
61
 
62
+ ### How to refactor complex business logic with Simple Ruby Service
63
+
44
64
  #### ::Before:: Vanilla Rails with a fat controller (a contrived example)
45
65
  ```ruby
46
66
  # in app/controllers/some_controller.rb
@@ -51,56 +71,85 @@ class SomeController < ApplicationController
51
71
  authorize! resource
52
72
  resource.do_something
53
73
  value = resource.do_something_related
74
+ raise unless resource.errors
54
75
  render value
55
76
  end
56
77
  end
57
78
  ```
58
79
 
59
- #### ::After:: Refactored using an SO
80
+ #### ::After:: Refactored using an Simple Ruby Service Object
81
+ ```ruby
82
+ # in app/controllers/some_controller.rb
83
+ class SomeController < ApplicationController
84
+ def show
85
+ # NOTE: That's right... just one, readable line of code
86
+ render DoSomething.call!(params)
87
+ end
88
+ end
89
+ ```
90
+
91
+ #### ::Alternate After:: Refactored using a Simple Ruby Service
60
92
  ```ruby
61
93
  # in app/controllers/some_controller.rb
62
94
  class SomeController < ApplicationController
63
95
  def show
64
- # NOTE: Simple Ruby Service Objects ducktype as Procs and do not need to be instantiated
65
- render DoSomething.call(params).value
96
+ # NOTE: Simple Ruby Service methods can be chained together
97
+ render SomeService.new(params)
98
+ .do_something
99
+ .do_something_related
100
+ .value
66
101
  end
67
102
  end
103
+ ```
104
+
105
+ ### Taking a peek under the hood
106
+
107
+ `DoSomething.call!(params)` is deliberately designed to look and feel like `ActiveRecord::Base#save!`.
108
+
109
+ The following (simplified) implementation illustrates what happens under the hood:
68
110
 
111
+ ```ruby
112
+ module SimpleRubyService::Object
113
+ def self.call!(params)
114
+ instance = new(params)
115
+ raise Invalid unless instance.valid?
116
+ self.value = instance.call
117
+ raise Invalid unless instance.failed?
118
+ value
119
+ end
120
+ end
121
+ ```
69
122
 
123
+ ### Anatomy of a Simple Ruby Service Object
124
+ ```ruby
70
125
  # in app/service_objects/do_something.rb
71
126
  class DoSomething
72
127
  include SimpleRubyService::ServiceObject
73
128
 
129
+ # `attribute` behaves similar to ActiveRecord::Base#attribute, but is not typed, or bound to persistant storage
74
130
  attribute :id
75
131
  attr_accessor :resource
76
132
 
77
- # NOTE: Validations are executed prior to the business logic encapsulated in `perform`
133
+ # Validations are executed prior to the business logic encapsulated in `perform`
78
134
  validate do
79
135
  @resource ||= SomeModel.find(id)
80
136
  authorize! resource
81
137
  end
82
138
 
83
- # NOTE: The return value of `perform` is automatically stored as the SO's `value`
139
+ # The result of `perform` is automatically stored as the SO's `value`
84
140
  def perform
85
- resource.do_something
86
- resource.do_something_related
141
+ resource.do_something
142
+ result = resource.do_something_related
143
+
144
+ # Adding any kind of error indicates failure
145
+ add_errors_from_object resource
146
+ result
87
147
  end
88
148
  end
89
149
  ```
90
150
 
91
- #### ::Alternate Form:: Refactored using a Service
151
+ ### Anatomy of a Simple Ruby Service
92
152
  ```ruby
93
- # in app/controllers/some_controller.rb
94
- class SomeController < ApplicationController
95
- def show
96
- # NOTE: Simple Ruby Service methods can be chained together
97
- render SomeService.new(params)
98
- .do_something
99
- .do_something_related
100
- .value
101
- end
102
- end
103
-
104
153
  # in app/services/do_something.rb
105
154
  class SomeService
106
155
  include SimpleRubyService::Service
@@ -108,25 +157,38 @@ class SomeService
108
157
  attribute :id
109
158
  attr_accessor :resource
110
159
 
111
- # NOTE: Validations are executed prior to the first service method called
160
+ # Similar to SOs, validations are executed prior to the first service method called
112
161
  validate do
113
162
  @resource ||= SomeModel.find(id)
114
163
  authorize! @resource
115
164
  end
116
165
 
166
+ # Unlike SOs, Services can define an arbitrary number of service methods with arbitrary names
117
167
  service_methods do
118
168
  def do_something
119
- resource.do_something_related
169
+ resource.do_something
120
170
  end
121
171
 
122
- # NOTE: Unlike SOs, `value` must be explicitely set for Service methods
172
+ # Unlike SOs, `value` must be explicitely set for Service methods
123
173
  def do_something_related
124
174
  self.value ||= resource.tap &:do_something_related
175
+ add_errors_from_object resource
125
176
  end
126
177
  end
127
178
  end
128
179
  ```
129
180
 
181
+ ## A special note about Simple Ruby Service Objects, Procs, and Ducktyping
182
+
183
+ Simple Ruby Service Objects respond to (`#call`) so they can stand in for Procs, i.e.:
184
+ ```ruby
185
+ # in app/models/some_model.rb
186
+ class SomeModel < ApplicationRecord
187
+ validates :some_attribute, if: SomeServiceObject
188
+ [...]
189
+ ```
190
+ _See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.call!` vs. `.call`._
191
+
130
192
  ## Usage
131
193
 
132
194
  ### Service Objects
@@ -137,8 +199,6 @@ Service Object names should begin with a verb and should not include the words `
137
199
 
138
200
  Also, only one operation should be made public, it should always be named `call`, and it should not accept arguments (except for an optional block).
139
201
 
140
- _See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.call!` vs. `.call`._
141
-
142
202
  #### Short form (_recommended_)
143
203
 
144
204
  ```ruby
@@ -197,9 +257,6 @@ Unlike Service Objects, Service class names should begin with a noun (and may in
197
257
 
198
258
  Also, any number of operations may be made public, any of these operations may be named `call`, and any of these operations may accept arguments.
199
259
 
200
- _See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.service_method_name!` vs. `.service_method_name`._
201
-
202
-
203
260
  #### Short form
204
261
 
205
262
  _not available for Services_
@@ -254,7 +311,7 @@ end
254
311
  ## Creating Simple Ruby Services
255
312
 
256
313
  ### Service Objects
257
- To implement an Simple Ruby Service Object:
314
+ To implement a Simple Ruby Service Object:
258
315
 
259
316
  1. include `SimpleRubyService::ServiceObject`
260
317
  2. declare attributes with the `attribute` keyword (class level DSL)
@@ -282,7 +339,7 @@ end
282
339
  ```
283
340
 
284
341
  ### Services
285
- To implement an Simple Ruby Service:
342
+ To implement a Simple Ruby Service:
286
343
 
287
344
  1. include `SimpleRubyService::Service`
288
345
  2. declare attributes with the `attribute` keyword (class level DSL)
@@ -319,19 +376,46 @@ class SomeService
319
376
  end
320
377
  ```
321
378
 
322
- ## FAQ
379
+ ### Workflows
380
+ Simple Ruby Services are inherently a good fit for workflows because they support chaining, i.e.:
323
381
 
324
- ### Why should I use Services & SOs?
382
+ ```ruby
383
+ SomeService.new(params)
384
+ .do_something
385
+ .do_something_related
386
+ .value
387
+ ```
325
388
 
326
- [Click here](https://www.google.com/search?q=service+object+pattern+rails&rlz=1C5CHFA_enUS893US893&oq=service+object+pattern+rails) to learn more about the Services & SO design pattern.
389
+ But SOs can also implement various workflows with dependency injection:
327
390
 
328
- **TLDR; fat models and fat controllers are bad! Services and Service Objects help you DRY things up.**
391
+ ```ruby
392
+ class PerformSomeWorkflow < SimpleRubyService::ServiceObject
393
+ def perform
394
+ dependency = SimpleRubyService1.call!
395
+ result = SimpleRubyService2.call(dependency)
396
+ raise unless result.success?
397
+ SimpleRubyService3(dependency, result.value).call!
398
+ end
399
+ end
400
+ ```
329
401
 
330
- ### How is a Service different from an SO?
402
+ ## MISC
331
403
 
332
- An SO is just a Service that encapsulates a single operation (i.e. **one, and only one, responsibility**).
404
+ ### To bang!, or not to bang
405
+
406
+ Use the bang! version of an operation whenever you expect the operation to succeed more often than fail, and you don't need to chain operations together.
407
+
408
+ Similar in pattern to `ActiveRecord#save!`, the bang version of each operation:
409
+ * raises `SimpleRubyService::Invalid` if `valid?` is falsey
410
+ * raises `SimpleRubyService::Failure` if the block provided returns a falsey value
411
+ * returns `@value`
333
412
 
334
- ### When should I choose a Service over an SO, and vice-versa?
413
+ Whereas, similar in pattern to `ActiveRecord#save`, the regular version of each operation:
414
+ * doesn't raise any exceptions
415
+ * passes the return value of the block provided to `#success?`
416
+ * returns self << _note: this is unlike `ActiveRecord#save`_
417
+
418
+ ### Service or SO?
335
419
 
336
420
  Use a `Service` when encapsulating related operations that share dependencies & validations.
337
421
 
@@ -342,9 +426,6 @@ i.e.:
342
426
 
343
427
  _note: Things get fuzzy when operations share some, but not all, dependencies & validations. Use your best judgement when operation `A` and operation `B` are related but `A` acts on a `User` while `B` acts on both a `User` & a `Company`._
344
428
 
345
- ### Atomicity
346
- The framework does not include transaction support by default. You are responsible for wrapping with a transaction if atomicity is desired.
347
-
348
429
  ### Control Flow
349
430
  Rescue exceptions that represent internal control flow and propogate the rest.
350
431
 
@@ -352,7 +433,7 @@ For example, if an internal call to User.create! is expected to always succeed,
352
433
 
353
434
  Example::
354
435
  ```ruby
355
- class DoSomethingDangerous < SimpleRubyService::ObjectBase
436
+ class DoSomethingDangerous < SimpleRubyService::ServiceObject
356
437
  attribute :attr1, :attr2 # should include all params required to execute
357
438
  validates_presence_of :attr1 # validate params to call
358
439
 
@@ -368,38 +449,6 @@ class DoSomethingDangerous < SimpleRubyService::ObjectBase
368
449
  end
369
450
  ```
370
451
 
371
- ## Workflows
372
- SOs often need to call other SOs in order to implement various workflows:
373
- ```ruby
374
- class PerformSomeWorkflow < SimpleRubyService::ObjectBase
375
- def perform
376
- dependency = SimpleRubyService1.call!
377
- result = SimpleRubyService2.call(dependency)
378
- raise unless result.success?
379
- SimpleRubyService3(dependency, result.value).call!
380
- end
381
- end
382
- ```
383
-
384
- ## MISC
385
-
386
- ### Attributes
387
- The `attribute` and `attributes` keywords behaves similar to [ActiveRecord::Base.attribute](https://api.rubyonrails.org/v6.1.3.1/classes/ActiveRecord/Attributes/ClassMethods.html), but they are not typed or bound to persistant storage.
388
-
389
- ### To bang!, or not to bang
390
-
391
- Use the bang! version of an operation whenever you expect the operation to succeed more often than fail, and you don't need to chain operations together.
392
-
393
- Similar in pattern to `ActiveRecord#save!`, the bang version of each operation:
394
- * raises `SimpleRubyService::Invalid` if `valid?` is falsey
395
- * raises `SimpleRubyService::Failure` if the block provided returns a falsey value
396
- * returns `@value`
397
-
398
- Whereas, similar in pattern to `ActiveRecord#save`, the regular version of each operation:
399
- * doesn't raise any exceptions
400
- * passes the return value of the block provided to `#success?`
401
- * returns self << _note: this is unlike `ActiveRecord#save`_
402
-
403
452
  ## Development
404
453
 
405
454
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -416,6 +465,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
416
465
 
417
466
  ## DEVELOPMENT ROADMAP
418
467
 
419
- 1. Create a helper to dynamically generate default SOs for ActiveRecord models (`create`, `update`, and `destroy`) _(when used in a project that includes [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord))_.
420
- 2. Consider isolating validation errors from execution errors (so that invalid? is not always true when failed? is true)
468
+ 1. Create a class level DSL to stop before each Service method unless errors.empty?
469
+ 2. Create a helper to dynamically generate default SOs for ActiveRecord models (`create`, `update`, and `destroy`) _(when used in a project that includes [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord))_.
470
+ 3. Consider isolating validation errors from execution errors (so that invalid? is not always true when failed? is true)
421
471
 
@@ -6,9 +6,13 @@ module SimpleRubyService
6
6
  extend ActiveSupport::Concern
7
7
  include ActiveModel::AttributeAssignment
8
8
  include ActiveModel::Validations
9
+ include ActiveModel::Validations::Callbacks
9
10
 
10
11
  included do
11
12
  attr_accessor :value
13
+
14
+ class_attribute :set_value_when_service_methods_return
15
+ self.set_value_when_service_methods_return = true
12
16
  end
13
17
 
14
18
  class_methods do
@@ -32,7 +36,7 @@ module SimpleRubyService
32
36
  # Class level DSL that wraps the methods defined in inherited classes.
33
37
  def service_methods(&blk)
34
38
  Module.new.tap do |m| # Using anonymous modules so that super can be used to extend service methods
35
- m.module_eval &blk
39
+ m.module_eval(&blk)
36
40
  include m
37
41
 
38
42
  m.instance_methods.each do |service_method|
@@ -41,7 +45,8 @@ module SimpleRubyService
41
45
  # Returns self (for chainability).
42
46
  # Evaluates validity prior to executing the block provided.
43
47
  define_method service_method do |*args, **kwargs, &callback|
44
- perform(service_method, *args, **kwargs, &callback) if valid?
48
+ result = perform(service_method, *args, **kwargs, &callback) if valid?
49
+ self.value = result if set_value_when_service_methods_return
45
50
 
46
51
  self
47
52
  end
@@ -130,5 +135,3 @@ module SimpleRubyService
130
135
  end
131
136
  end
132
137
  end
133
-
134
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleRubyService
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.6'
5
5
  end
@@ -10,8 +10,9 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = ["Jay Crouch"]
11
11
  spec.email = ["i.jaycrouch@gmail.com"]
12
12
 
13
- spec.summary = 'Simple Ruby Service is a lightweight framework for Ruby that makes it easy to create Services and Service Objects (SOs).'
14
- spec.description = 'Simple Ruby Service is a lightweight framework for Ruby that makes it easy to create Services and Service Objects (SOs). The framework provides a simple DSL that: incorporates ActiveModel validations and error handling; encourages a succinct, idiomatic coding style; Ducktypes Service Objects as Procs.'
13
+ spec.summary = 'Simple Ruby Service is a lightweight framework for creating Services and Service Objects (SOs) in Ruby.'
14
+ spec.description = 'Simple Ruby Service is a lightweight framework for creating Services and Service Objects (SOs) in Ruby. ' \
15
+ 'The framework makes Services and SOs look and feel like ActiveModels, complete with: 1. validations and robust error handling; 2. workflows and method chaining; and 3. consistent interfaces. Additionally, Simple Ruby Service Objects can stand in for Procs, wherever Procs are expected (via ducktyping).'
15
16
  spec.homepage = 'https://github.com/amazing-jay/simple_ruby_service'
16
17
  spec.license = "MIT"
17
18
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_ruby_service
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Crouch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-02 00:00:00.000000000 Z
11
+ date: 2021-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -304,10 +304,11 @@ dependencies:
304
304
  - - "~>"
305
305
  - !ruby/object:Gem::Version
306
306
  version: '3.13'
307
- description: 'Simple Ruby Service is a lightweight framework for Ruby that makes it
308
- easy to create Services and Service Objects (SOs). The framework provides a simple
309
- DSL that: incorporates ActiveModel validations and error handling; encourages a
310
- succinct, idiomatic coding style; Ducktypes Service Objects as Procs.'
307
+ description: 'Simple Ruby Service is a lightweight framework for creating Services
308
+ and Service Objects (SOs) in Ruby. The framework makes Services and SOs look and
309
+ feel like ActiveModels, complete with: 1. validations and robust error handling;
310
+ 2. workflows and method chaining; and 3. consistent interfaces. Additionally, Simple
311
+ Ruby Service Objects can stand in for Procs, wherever Procs are expected (via ducktyping).'
311
312
  email:
312
313
  - i.jaycrouch@gmail.com
313
314
  executables: []
@@ -358,6 +359,6 @@ requirements: []
358
359
  rubygems_version: 3.0.9
359
360
  signing_key:
360
361
  specification_version: 4
361
- summary: Simple Ruby Service is a lightweight framework for Ruby that makes it easy
362
- to create Services and Service Objects (SOs).
362
+ summary: Simple Ruby Service is a lightweight framework for creating Services and
363
+ Service Objects (SOs) in Ruby.
363
364
  test_files: []