simple_ruby_service 1.0.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 +7 -0
- data/.gitignore +63 -0
- data/.rspec +4 -0
- data/.rubocop.yml +86 -0
- data/.simplecov +4 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +410 -0
- data/Rakefile +23 -0
- data/lib/simple_ruby_service.rb +15 -0
- data/lib/simple_ruby_service/error.rb +13 -0
- data/lib/simple_ruby_service/errors.rb +83 -0
- data/lib/simple_ruby_service/failure.rb +5 -0
- data/lib/simple_ruby_service/invalid.rb +5 -0
- data/lib/simple_ruby_service/service.rb +134 -0
- data/lib/simple_ruby_service/service_object.rb +52 -0
- data/lib/simple_ruby_service/version.rb +5 -0
- data/simple_ruby_service.gemspec +61 -0
- metadata +348 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1931553dbf6a835d17206efc865d38d6ea03a627936e5985e851a8c0a26cb79e
|
4
|
+
data.tar.gz: 64ce8e7423db632c6fabc68452428cef5b20c62396ffefa86d8e28b318da267f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e388e6b6889c295f9aff7452692704833aeb85d9b8a1c9c3710e01e27c1ec87caaed477e17c12ef51a7cc8b80e2ed0ccb74680be771030303d4056d4c3c98f9d
|
7
|
+
data.tar.gz: e48303d314abe2837a02b862860f1b09cdc1cd63de85d0331f5143e946d179c7942e5aa31286a2c20e9a25c8df162aa2984a96f410b4bb7897ba61571a807077
|
data/.gitignore
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
*~
|
13
|
+
log/*
|
14
|
+
measurement/*
|
15
|
+
|
16
|
+
# Used by dotenv library to load environment variables.
|
17
|
+
.env
|
18
|
+
|
19
|
+
|
20
|
+
# Ignore Byebug command history file.
|
21
|
+
.byebug_history
|
22
|
+
|
23
|
+
## Specific to RubyMotion:
|
24
|
+
.dat*
|
25
|
+
.repl_history
|
26
|
+
build/
|
27
|
+
*.bridgesupport
|
28
|
+
build-iPhoneOS/
|
29
|
+
build-iPhoneSimulator/
|
30
|
+
|
31
|
+
## Specific to RubyMotion (use of CocoaPods):
|
32
|
+
#
|
33
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
34
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
35
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
36
|
+
#
|
37
|
+
# vendor/Pods/
|
38
|
+
|
39
|
+
## Documentation cache and generated files:
|
40
|
+
/.yardoc/
|
41
|
+
/_yardoc/
|
42
|
+
/doc/
|
43
|
+
/rdoc/
|
44
|
+
|
45
|
+
## Environment normalization:
|
46
|
+
/.bundle/
|
47
|
+
/vendor/bundle
|
48
|
+
/lib/bundler/man/
|
49
|
+
|
50
|
+
# for a library or gem, you might want to ignore these files since the code is
|
51
|
+
# intended to run in multiple environments; otherwise, check them in:
|
52
|
+
Gemfile.lock
|
53
|
+
|
54
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
55
|
+
.rvmrc
|
56
|
+
.ruby-version
|
57
|
+
.ruby-gemset
|
58
|
+
|
59
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
60
|
+
# .rubocop-https?--*
|
61
|
+
|
62
|
+
# rspec failure tracking
|
63
|
+
.rspec_status
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# I can't get the requires to work with sublime:
|
2
|
+
# SEE: https://github.com/pderichs/sublime_rubocop/issues/66
|
3
|
+
# -- https://gist.github.com/topher6345/4f921ca3e7938132563e
|
4
|
+
# -- https://forum.sublimetext.com/t/run-command-after-saving-file/44016
|
5
|
+
# -- https://medium.com/devnetwork/running-rubocop-only-on-modified-files-a21aed86e06d
|
6
|
+
# -- https://code.tutsplus.com/tutorials/how-to-create-a-sublime-text-2-plugin--net-22685
|
7
|
+
|
8
|
+
# -- https://github.com/SublimeLinter/SublimeLinter-rubocop/issues/19
|
9
|
+
# -- https://github.com/fnando/sublime-codefmt
|
10
|
+
# -- https://github.com/fnando/sublime-switch-case
|
11
|
+
# -- https://github.com/fnando/sublime-rubocop-completion/issues/1
|
12
|
+
# required these fixes:
|
13
|
+
# -- install PackageDev https://forum.sublimetext.com/t/pathlib-not-found-in-purchased-version/49978
|
14
|
+
# -- download all 3 packages via git clone rather than package manager
|
15
|
+
# -- renamed several files and directories to match
|
16
|
+
# -- brew update
|
17
|
+
# -- brew doctor
|
18
|
+
# -- brew install prettier yapf
|
19
|
+
|
20
|
+
require:
|
21
|
+
- rubocop-performance
|
22
|
+
- rubocop-rspec
|
23
|
+
|
24
|
+
AllCops:
|
25
|
+
TargetRubyVersion: 2.5
|
26
|
+
NewCops: enable
|
27
|
+
|
28
|
+
Style/Documentation:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Naming/VariableNumber:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Metrics/BlockLength:
|
35
|
+
Exclude:
|
36
|
+
- "**/*.rake"
|
37
|
+
- "config/**/*"
|
38
|
+
- "Rakefile"
|
39
|
+
- "spec/**/*.rb"
|
40
|
+
- "*.gemspec"
|
41
|
+
|
42
|
+
Layout/LineLength:
|
43
|
+
Max: 120
|
44
|
+
|
45
|
+
Style/BlockComments:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Style/ClassAndModuleChildren:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Style/RedundantBegin:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Style/StringLiterals:
|
55
|
+
EnforcedStyle: single_quotes
|
56
|
+
|
57
|
+
Style/StringLiteralsInInterpolation:
|
58
|
+
Enabled: true
|
59
|
+
|
60
|
+
Style/WordArray:
|
61
|
+
EnforcedStyle: brackets
|
62
|
+
|
63
|
+
Metrics/MethodLength:
|
64
|
+
Max: 15
|
65
|
+
|
66
|
+
Style/HashSyntax:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/StringHashKeys:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Metrics/AbcSize:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Layout/DotPosition:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Lint/AmbiguousBlockAssociation:
|
79
|
+
Exclude:
|
80
|
+
- "spec/**/*"
|
81
|
+
|
82
|
+
RSpec/NamedSubject:
|
83
|
+
Enabled: false
|
84
|
+
# Rails:
|
85
|
+
# Enabled: true
|
86
|
+
|
data/.simplecov
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Jay Crouch
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
# Simple Ruby service
|
2
|
+
|
3
|
+
Simple Ruby service is a lightweight framework for Ruby that makes it easy to create Services and Service Objects (SOs).
|
4
|
+
|
5
|
+
The framework provides a simple DSL that:
|
6
|
+
|
7
|
+
1. Adds ActiveModel validations and error handling
|
8
|
+
2. Encourages a succinct, idiomatic coding style
|
9
|
+
3. Standardizes the SO interface
|
10
|
+
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
* Ruby 1.9.2+
|
14
|
+
|
15
|
+
_Simple Ruby service includes helpers for Rails 3.0+, but does not require Rails._
|
16
|
+
|
17
|
+
## Download and installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'simple_ruby_service'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install simple_ruby_service
|
32
|
+
|
33
|
+
Source code can be downloaded on GitHub
|
34
|
+
[github.com/amazing-jay/simple_ruby_service/tree/master](https://github.com/amazing-jay/simple_ruby_service/tree/master)
|
35
|
+
|
36
|
+
|
37
|
+
### The following examples illustrate how Simple Ruby service can help you refactor complex business logic
|
38
|
+
|
39
|
+
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.
|
40
|
+
|
41
|
+
#### ::Before:: Vanilla Rails with a fat controller (a contrived example)
|
42
|
+
```ruby
|
43
|
+
class SomeController < ApplicationController
|
44
|
+
def show
|
45
|
+
raise unless params[:id].present?
|
46
|
+
resource = SomeModel.find(id)
|
47
|
+
authorize! resource
|
48
|
+
resource.do_something
|
49
|
+
value = resource.do_something_related
|
50
|
+
render value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
#### ::After:: Refactored using an SO
|
56
|
+
```ruby
|
57
|
+
class SomeController < ApplicationController
|
58
|
+
def show
|
59
|
+
# NOTE: Simple Ruby service Objects ducktype as Procs and do not need to be instantiated
|
60
|
+
render DoSomething.call(params).value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class DoSomething
|
65
|
+
extend SimpleRubyservice::ServiceObject
|
66
|
+
|
67
|
+
attribute :id
|
68
|
+
attr_accessor :resource
|
69
|
+
|
70
|
+
# NOTE: Validations are executed prior to the business logic encapsulated in `perform`
|
71
|
+
validate do
|
72
|
+
@resource ||= SomeModel.find(id)
|
73
|
+
authorize! resource
|
74
|
+
end
|
75
|
+
|
76
|
+
# NOTE: The return value of `perform` is automatically stored as the SO's `value`
|
77
|
+
def perform
|
78
|
+
resource.do_something
|
79
|
+
resource.do_something_related
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
#### ::Alternate Form:: Refactored using a Service
|
85
|
+
```ruby
|
86
|
+
class SomeController < ApplicationController
|
87
|
+
def show
|
88
|
+
# NOTE: Simple Ruby service methods can be chained together
|
89
|
+
render SomeService.new(params)
|
90
|
+
.do_something
|
91
|
+
.do_something_related
|
92
|
+
.value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class SomeService
|
97
|
+
extend SimpleRubyservice::Service
|
98
|
+
|
99
|
+
attribute :id
|
100
|
+
attr_accessor :resource
|
101
|
+
|
102
|
+
# NOTE: Validations are executed prior to the first service method called
|
103
|
+
validate do
|
104
|
+
@resource ||= SomeModel.find(id)
|
105
|
+
authorize! @resource
|
106
|
+
end
|
107
|
+
|
108
|
+
service_methods do
|
109
|
+
def do_something
|
110
|
+
resource.do_something_related
|
111
|
+
end
|
112
|
+
|
113
|
+
# NOTE: Unlike SOs, `value` must be explicitely set
|
114
|
+
def do_something_related
|
115
|
+
self.value ||= resource.tap &:do_something_related
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Usage
|
122
|
+
|
123
|
+
### Service Objects
|
124
|
+
|
125
|
+
Service Object names should begin with a verb and should not include the words `service` or `object`:
|
126
|
+
- GOOD = `CreateUser`
|
127
|
+
- BAD = `UserCreator`, `CreateUserServiceObject`, etc.
|
128
|
+
|
129
|
+
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).
|
130
|
+
|
131
|
+
#### Short form (_recommended_)
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
result = DoSomething.call!(foo: 'bar')
|
135
|
+
```
|
136
|
+
|
137
|
+
#### Instance form
|
138
|
+
```ruby
|
139
|
+
result = DoSomething.new(foo: 'bar').call!
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Rescue form
|
143
|
+
```ruby
|
144
|
+
result = begin
|
145
|
+
DoSomething.call!(foo: 'bar')
|
146
|
+
rescue SimpleRubyservice::Invalid => e
|
147
|
+
# do something with e.target.attributes
|
148
|
+
rescue SimpleRubyservice::Failure
|
149
|
+
# do something with e.target.value
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
#### Conditional form
|
154
|
+
|
155
|
+
_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`._
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
result = DoSomething.call(foo: 'bar')
|
159
|
+
if result.invalid?
|
160
|
+
# do something with result.attributes
|
161
|
+
elsif result.failure?
|
162
|
+
# do something with result.value
|
163
|
+
else
|
164
|
+
# do something with result.errors
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Block form
|
169
|
+
_note: blocks, if present, are envoked prior to failure check._
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
result = DoSomething.call!(foo: 'bar') do |obj|
|
173
|
+
obj.errors.clear # clear errors
|
174
|
+
'new value' # set result = 'new value'
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
#### Dependency injection form
|
179
|
+
```ruby
|
180
|
+
DoSomething.call!(with: DoSomethingFirst.call!)
|
181
|
+
```
|
182
|
+
|
183
|
+
### Services
|
184
|
+
|
185
|
+
Unlike Service Objects, Service class names should begin with a noun (and may include the words `service` or `object`):
|
186
|
+
- GOOD = `UserCreator`
|
187
|
+
- BAD = `CreateUser`, `UserCreatorService`, etc.
|
188
|
+
|
189
|
+
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.
|
190
|
+
|
191
|
+
#### Short form
|
192
|
+
|
193
|
+
_not available for Services_
|
194
|
+
|
195
|
+
#### Instance form
|
196
|
+
```ruby
|
197
|
+
result = SomeService.new(foo: 'bar').do_something!
|
198
|
+
```
|
199
|
+
|
200
|
+
#### Chained form
|
201
|
+
|
202
|
+
_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 `.do_something!` vs. `.do_something`._
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
result = SomeService.new(foo: 'bar')
|
206
|
+
.do_something
|
207
|
+
.do_something_else
|
208
|
+
.value
|
209
|
+
```
|
210
|
+
|
211
|
+
#### Rescue form
|
212
|
+
```ruby
|
213
|
+
result = begin
|
214
|
+
SomeService.new(foo: 'bar').do_something!
|
215
|
+
rescue SimpleRubyservice::Invalid => e
|
216
|
+
# do something with e.target.attributes
|
217
|
+
rescue SimpleRubyservice::Failure
|
218
|
+
# do something with e.target.value
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
#### Conditional form
|
223
|
+
```ruby
|
224
|
+
result = SomeService.new(foo: 'bar').do_something
|
225
|
+
if result.invalid?
|
226
|
+
# do something with result.attributes
|
227
|
+
elsif result.failure?
|
228
|
+
# do something with result.value
|
229
|
+
else
|
230
|
+
# do something with result.errors
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
#### Block form
|
235
|
+
_note: blocks, if present, are envoked prior to failure check._
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
result = SomeService.new(foo: 'bar').do_something! do |obj|
|
239
|
+
obj.errors.clear # clear errors
|
240
|
+
'new value' # set result = 'new value'
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
## Creating Simple Ruby services
|
245
|
+
|
246
|
+
### Service Objects
|
247
|
+
To implement an Simple Ruby service Object:
|
248
|
+
|
249
|
+
1. extend `SimpleRubyservice::ServiceObject`
|
250
|
+
2. declare attributes with the `attribute` keyword (class level DSL)
|
251
|
+
3. declare validations see [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html)
|
252
|
+
4. implement the special `perform` method (automatically invoked by `call` wrapper method)
|
253
|
+
5. add errors to indicate the operation failed
|
254
|
+
|
255
|
+
_note: `perform` may optionally accept a block param, but no other args._
|
256
|
+
|
257
|
+
Example::
|
258
|
+
```ruby
|
259
|
+
class DoSomething
|
260
|
+
extend SimpleRubyservice::ServiceObject
|
261
|
+
|
262
|
+
attribute :attr1, :attr2 # should include all params required to execute, similar to ActiveRecord
|
263
|
+
validates_presence_of :attr1 # validate params
|
264
|
+
|
265
|
+
def perform(&block)
|
266
|
+
errors.add(:some critical service, message: 'down') and return unless some_critical_service.up?
|
267
|
+
yield if block_given?
|
268
|
+
|
269
|
+
'hello world' # set `value` to the returned value of the operation
|
270
|
+
end
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
### Services
|
275
|
+
To implement an Simple Ruby service:
|
276
|
+
|
277
|
+
1. extend `SimpleRubyservice::Service`
|
278
|
+
2. declare attributes with the `attribute` keyword (class level DSL)
|
279
|
+
3. declare validations see [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html)
|
280
|
+
4. define operations within a `service_methods` block (each method defined will be wrapped)
|
281
|
+
5. set (or modify) `self.value` (if your service method creates artifacts
|
282
|
+
6. add errors to indicate the operation failed
|
283
|
+
|
284
|
+
_note: service methods may accept any arguments, required or otherwise._
|
285
|
+
|
286
|
+
Example::
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
class SomeService
|
290
|
+
extend SimpleRubyservice::Service
|
291
|
+
|
292
|
+
attribute :attr1, :attr2 # should include all params required to execute, similar to ActiveRecord
|
293
|
+
validates_presence_of :attr1 # validate params
|
294
|
+
|
295
|
+
service_methods do
|
296
|
+
def hello
|
297
|
+
self.value = 'hello world' # set value
|
298
|
+
end
|
299
|
+
|
300
|
+
def oops
|
301
|
+
errors.add(:foo, :bar) # indicate failure
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
def goodbye(arg1, arg2 = nil, arg3: arg4: nil, &block)
|
306
|
+
self.value += '...goodnight sweet world' # modify value
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
```
|
311
|
+
|
312
|
+
## FAQ
|
313
|
+
|
314
|
+
### Why should I use Services & SOs?
|
315
|
+
|
316
|
+
[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.
|
317
|
+
|
318
|
+
**TLDR; fat models and fat controllers are bad! Services and Service Objects help you DRY things up.**
|
319
|
+
|
320
|
+
### How is a Service different from an SO?
|
321
|
+
|
322
|
+
An SO is just a Service that encapsulates a single operation (i.e. **one, and only one, responsibility**).
|
323
|
+
|
324
|
+
### When should I choose a Service over an SO, and vice-versa?
|
325
|
+
|
326
|
+
Use a `Service` when encapsulating related operations that share dependencies & validations.
|
327
|
+
|
328
|
+
i.e.:
|
329
|
+
|
330
|
+
* Create a Service with two service methods when operation `A` and operation `B` both act on a `User` (and are related in some way).
|
331
|
+
* Create two Service Objects when operation `A` and operation `B` are related, but `A` acts on a `User` while `B` acts on a `Company`.
|
332
|
+
|
333
|
+
_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`._
|
334
|
+
|
335
|
+
### Atomicity
|
336
|
+
The framework does not include transaction support by default. You are responsible for wrapping with a transaction if atomicity is desired.
|
337
|
+
|
338
|
+
### Control Flow
|
339
|
+
Rescue exceptions that represent internal control flow and propogate the rest.
|
340
|
+
|
341
|
+
For example, if an internal call to User.create! is expected to always succeed, allow `ActiveRecord::RecordInvalid` to propogate to the caller. If, on the otherhand, an internal call to User.create! is anticipated to conditionally fail on a uniqueness constraint, rescue `ActiveRecord::RecordInvalid` and rely on the framework to raise `SimpleRubyservice::Failure`.
|
342
|
+
|
343
|
+
Example::
|
344
|
+
```ruby
|
345
|
+
class DoSomethingDangerous < SimpleRubyservice::ObjectBase
|
346
|
+
attribute :attr1, :attr2 # should include all params required to execute
|
347
|
+
validates_presence_of :attr1 # validate params to call
|
348
|
+
|
349
|
+
def perform
|
350
|
+
ActiveRecord::Base.transaction do # optional
|
351
|
+
self.value = # ... do work ... # save results for the caller
|
352
|
+
end
|
353
|
+
rescue SomeDependency::Failed
|
354
|
+
errors[:base] << e.message # notify the caller of error
|
355
|
+
rescue ActiveRecord::RecordInvalid
|
356
|
+
# ... fix things and retry ...
|
357
|
+
end
|
358
|
+
end
|
359
|
+
```
|
360
|
+
|
361
|
+
## Workflows
|
362
|
+
SOs often need to call other SOs in order to implement various workflows:
|
363
|
+
```ruby
|
364
|
+
class PerformSomeWorkflow < SimpleRubyservice::ObjectBase
|
365
|
+
def perform
|
366
|
+
dependency = SimpleRubyservice1.call!
|
367
|
+
result = SimpleRubyservice2.call(dependency)
|
368
|
+
raise unless result.success?
|
369
|
+
SimpleRubyservice3(dependency, result.value).call!
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
## MISC
|
375
|
+
|
376
|
+
### Attributes
|
377
|
+
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.
|
378
|
+
|
379
|
+
### To bang!, or not to bang
|
380
|
+
|
381
|
+
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.
|
382
|
+
|
383
|
+
Similar in pattern to `ActiveRecord#save!`, the bang version of each operation:
|
384
|
+
* raises `SimpleRubyservice::Invalid` if `valid?` is falsey
|
385
|
+
* raises `SimpleRubyservice::Failure` if the block provided returns a falsey value
|
386
|
+
* returns `@value`
|
387
|
+
|
388
|
+
Whereas, similar in pattern to `ActiveRecord#save`, the regular version of each operation:
|
389
|
+
* doesn't raise any exceptions
|
390
|
+
* passes the return value of the block provided to `#success?`
|
391
|
+
* returns self << _note: this is unlike `ActiveRecord#save`_
|
392
|
+
|
393
|
+
## Development
|
394
|
+
|
395
|
+
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.
|
396
|
+
|
397
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
398
|
+
|
399
|
+
## Contributing
|
400
|
+
|
401
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amazing-jay/simple_ruby_service. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
402
|
+
|
403
|
+
## License
|
404
|
+
|
405
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
406
|
+
|
407
|
+
## DEVELOPMENT ROADMAP
|
408
|
+
|
409
|
+
1. Isolate validation errors from execution errors (so that invalid? is not always true when failed? is true)
|
410
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
def initialize_rake_environment
|
4
|
+
return unless require_relative "config/environment"
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
Bundler::GemHelper.install_tasks
|
10
|
+
|
11
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
12
|
+
load 'rails/tasks/engine.rake'
|
13
|
+
|
14
|
+
task default: :spec
|
15
|
+
|
16
|
+
require "rspec/core/rake_task"
|
17
|
+
RSpec::Core::RakeTask.new(:spec)
|
18
|
+
|
19
|
+
require "rubocop/rake_task"
|
20
|
+
RuboCop::RakeTask.new do |task|
|
21
|
+
task.requires << "rubocop-performance"
|
22
|
+
task.requires << "rubocop-rspec"
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# requre the runtime dependencies that aren't automatically loaded by bundler
|
4
|
+
require 'active_model'
|
5
|
+
|
6
|
+
# namespace
|
7
|
+
module SimpleRubyservice; end
|
8
|
+
|
9
|
+
require 'simple_ruby_service/version'
|
10
|
+
require 'simple_ruby_service/error'
|
11
|
+
require 'simple_ruby_service/errors'
|
12
|
+
require 'simple_ruby_service/failure'
|
13
|
+
require 'simple_ruby_service/invalid'
|
14
|
+
require 'simple_ruby_service/service'
|
15
|
+
require 'simple_ruby_service/service_object'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleRubyservice
|
4
|
+
# Extnding ActiveModel::Errors with additional features:
|
5
|
+
# 1. Adds ability to return array of errors
|
6
|
+
# 2. Adds ability to decorate errors
|
7
|
+
# 2. Adds ability to internationalize errors
|
8
|
+
class Errors
|
9
|
+
delegate_missing_to :@active_model_errors
|
10
|
+
|
11
|
+
attr_reader :original_errors
|
12
|
+
attr_reader :active_model_errors
|
13
|
+
|
14
|
+
def initialize(base)
|
15
|
+
@base = base
|
16
|
+
@original_errors = []
|
17
|
+
@active_model_errors = ActiveModel::Errors.new(base)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(attribute, message = :invalid, options = {})
|
21
|
+
# store original arguments, since Rails 5 errors don't store `message`, only it's translation.
|
22
|
+
# can be avoided with Rails 6.
|
23
|
+
original_errors << OpenStruct.new(
|
24
|
+
attribute: attribute,
|
25
|
+
message: message,
|
26
|
+
options: options
|
27
|
+
)
|
28
|
+
active_model_errors.add(attribute, message, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def api_errors(&decorate_error)
|
32
|
+
original_errors.map do |e|
|
33
|
+
type = e.message.is_a?(Symbol) ? e.message : nil
|
34
|
+
message = type ? active_model_errors.generate_message(e.attribute, e.message, e.options) : e.message
|
35
|
+
full_message = full_message(e.attribute, message)
|
36
|
+
|
37
|
+
error = {
|
38
|
+
full_message: full_message,
|
39
|
+
message: message,
|
40
|
+
type: type,
|
41
|
+
attribute: e.attribute,
|
42
|
+
options: e.options
|
43
|
+
}
|
44
|
+
|
45
|
+
error = decorate_error.call(error) if decorate_error
|
46
|
+
error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Support ovveriding format for each error (not avaliable in rails 5)
|
51
|
+
def full_message(attribute, message)
|
52
|
+
return message if attribute == :base
|
53
|
+
|
54
|
+
attr_name = attribute.to_s.tr('.', '_').humanize
|
55
|
+
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
|
56
|
+
|
57
|
+
defaults = i18n_keys(attribute, '_format')
|
58
|
+
defaults << :"errors.format"
|
59
|
+
defaults << '%{attribute} %{message}'
|
60
|
+
|
61
|
+
I18n.t(defaults.shift,
|
62
|
+
default: defaults,
|
63
|
+
attribute: attr_name,
|
64
|
+
message: message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def i18n_keys(attribute, key)
|
68
|
+
if @base.class.respond_to?(:i18n_scope)
|
69
|
+
i18n_scope = @base.class.i18n_scope.to_s
|
70
|
+
@base.class.lookup_ancestors.flat_map do |klass|
|
71
|
+
[:"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{key}"]
|
72
|
+
end
|
73
|
+
else
|
74
|
+
[]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def clear
|
79
|
+
original_errors.clear
|
80
|
+
active_model_errors.clear
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# See /README.md
|
4
|
+
module SimpleRubyservice
|
5
|
+
module Service
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::AttributeAssignment
|
8
|
+
include ActiveModel::Validations
|
9
|
+
|
10
|
+
included do
|
11
|
+
attr_accessor :value
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def attributes
|
16
|
+
@attributes ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Class level DSL that registers attributes to inform #attributes getter.
|
20
|
+
def attribute(*attrs)
|
21
|
+
Module.new.tap do |m| # Using anonymous modules so that super can be used to extend accessor methods
|
22
|
+
include m
|
23
|
+
|
24
|
+
attrs = attrs.map(&:to_sym)
|
25
|
+
(attrs - attributes).each do |attr_name|
|
26
|
+
attributes << attr_name.to_sym
|
27
|
+
m.attr_accessor attr_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Class level DSL that wraps the methods defined in inherited classes.
|
33
|
+
def service_methods(&blk)
|
34
|
+
Module.new.tap do |m| # Using anonymous modules so that super can be used to extend service methods
|
35
|
+
m.module_eval &blk
|
36
|
+
include m
|
37
|
+
|
38
|
+
m.instance_methods.each do |service_method|
|
39
|
+
m.alias_method "perform_#{service_method}", service_method
|
40
|
+
|
41
|
+
# Returns self (for chainability).
|
42
|
+
# Evaluates validity prior to executing the block provided.
|
43
|
+
define_method service_method do |*args, **kwargs, &callback|
|
44
|
+
perform(service_method, *args, **kwargs, &callback) if valid?
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns #value (i.e. the result of block provided).
|
50
|
+
# Raises Invalid if validation reports any errors.
|
51
|
+
# Raises Failure if the blk provided reports any errors.
|
52
|
+
define_method "#{service_method}!" do |*args, **kwargs, &callback|
|
53
|
+
send(service_method, *args, **kwargs, &callback)
|
54
|
+
raise Invalid.new self, errors.full_messages unless valid?
|
55
|
+
raise Failure.new self, errors.full_messages unless success?
|
56
|
+
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Always returns self (for chainability).
|
65
|
+
def reset!
|
66
|
+
errors.clear
|
67
|
+
@valid = nil
|
68
|
+
self.value = nil
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns true unless validations or any actions added errors [even if no action(s) has been executed]
|
74
|
+
def success?
|
75
|
+
valid? && errors.empty? # valid? ensures validations have run, errors.empty? catchs errors added during execution
|
76
|
+
end
|
77
|
+
|
78
|
+
def failure?
|
79
|
+
!success?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Ensures all validations have run, then returns true if no errors were found, false otherwise.
|
83
|
+
# NOTE: Overriding ActiveModel::Validations#valid?, so as to not re-validate (unless reset! is called).
|
84
|
+
# SEE: https://www.rubydoc.info/gems/activemodel/ActiveModel/Validations#valid%3F-instance_method
|
85
|
+
def valid?(context = nil)
|
86
|
+
return @valid unless @valid.nil?
|
87
|
+
|
88
|
+
@valid = super # memoize result to indicate validations have run
|
89
|
+
end
|
90
|
+
|
91
|
+
def attributes
|
92
|
+
self.class.attributes.each_with_object({}) do |attr_name, h|
|
93
|
+
h[attr_name] = send(attr_name)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Hook override to enable validation of non-attributes and error messages for random keys
|
98
|
+
# e.g. validates :random, presence: true
|
99
|
+
# e.g. errors.add :random, :message => 'evil is near'
|
100
|
+
# SEE: https://www.rubydoc.info/docs/rails/ActiveModel%2FValidations:read_attribute_for_validation
|
101
|
+
def read_attribute_for_validation(attribute)
|
102
|
+
send(attribute) if respond_to?(attribute)
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
|
107
|
+
def initialize(attributes = {})
|
108
|
+
assign_attributes attributes
|
109
|
+
end
|
110
|
+
|
111
|
+
def perform(service_method, *args, **kwargs, &callback)
|
112
|
+
if kwargs.empty?
|
113
|
+
send("perform_#{service_method}", *args, &callback)
|
114
|
+
else
|
115
|
+
send("perform_#{service_method}", *args, **kwargs, &callback)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_errors_from_object(obj, key: nil, full_messages: false)
|
120
|
+
if full_messages
|
121
|
+
key ||= obj.class.name.underscore.to_sym
|
122
|
+
obj.errors.full_messages.each do |msg|
|
123
|
+
errors.add(key, msg)
|
124
|
+
end
|
125
|
+
else
|
126
|
+
obj.errors.messages.each do |obj_key, msgs|
|
127
|
+
msgs.each { |msg| errors.add(key || obj_key, msg) }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# See /README.md
|
4
|
+
module SimpleRubyservice
|
5
|
+
module ServiceObject
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include Service
|
8
|
+
|
9
|
+
included do
|
10
|
+
class << self
|
11
|
+
undef_method :service_methods
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Class level DSL for convenience.
|
16
|
+
class_methods do
|
17
|
+
def call(attributes = {}, &callback)
|
18
|
+
new(attributes).call(&callback)
|
19
|
+
end
|
20
|
+
|
21
|
+
def call!(attributes = {}, &callback)
|
22
|
+
new(attributes).call!(&callback)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns self (for chainability).
|
27
|
+
# Memoizes to `value` the result of the blk provided.
|
28
|
+
# Evaluates validity prior to executing the block provided.
|
29
|
+
def call(&callback)
|
30
|
+
self.value = perform(&callback) if valid?
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns #value (i.e. the result of block provided).
|
36
|
+
# Raises Invalid if validation reports any errors.
|
37
|
+
# Raises Failure if the blk provided reports any errors.
|
38
|
+
def call!(&callback)
|
39
|
+
call(&callback)
|
40
|
+
raise Invalid.new self, errors.full_messages unless valid?
|
41
|
+
raise Failure.new self, errors.full_messages unless success?
|
42
|
+
|
43
|
+
value
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
# Abstract method
|
48
|
+
def perform
|
49
|
+
raise NoMethodError, "#perform must be implemented."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "simple_ruby_service/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "simple_ruby_service"
|
9
|
+
spec.version = SimpleRubyservice::VERSION
|
10
|
+
spec.authors = ["Jay Crouch"]
|
11
|
+
spec.email = ["i.jaycrouch@gmail.com"]
|
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: adds ActiveModel validations and error handling; encourages a succinct, idiomatic coding style; and standardizes the SO interface.'
|
15
|
+
spec.homepage = 'https://github.com/amazing-jay/simple_ruby_service'
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
19
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
20
|
+
if spec.respond_to?(:metadata)
|
21
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
22
|
+
|
23
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
24
|
+
spec.metadata["source_code_uri"] = 'https://github.com/amazing-jay/simple_ruby_service'
|
25
|
+
spec.metadata["changelog_uri"] = "https://github.com/amazing-jay/simple_ruby_service/blob/master/CHANGELOG.md"
|
26
|
+
else
|
27
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
28
|
+
end
|
29
|
+
|
30
|
+
# Specify which files should be added to the gem when it is released.
|
31
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
32
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|bin|config)/}) }
|
34
|
+
end
|
35
|
+
spec.bindir = "exe"
|
36
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
37
|
+
spec.require_paths = ["lib"]
|
38
|
+
|
39
|
+
spec.add_dependency 'activemodel'
|
40
|
+
spec.add_dependency 'activesupport'
|
41
|
+
|
42
|
+
# spec.add_development_dependency 'actionpack'
|
43
|
+
spec.add_development_dependency "awesome_print", "~> 1.9.2"
|
44
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
45
|
+
spec.add_development_dependency "database_cleaner", "~> 2.0.1"
|
46
|
+
spec.add_development_dependency "dotenv", "~> 2.5"
|
47
|
+
spec.add_development_dependency "factory_bot", "~> 6.2.0"
|
48
|
+
spec.add_development_dependency "faker", "~> 2.18"
|
49
|
+
spec.add_development_dependency "listen", "~> 3.5.1"
|
50
|
+
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
51
|
+
spec.add_development_dependency "rails", "~> 6.1.3.2"
|
52
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
53
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
54
|
+
spec.add_development_dependency "rspec-rails", "~> 5.0.1"
|
55
|
+
spec.add_development_dependency "rubocop", "~> 0.60"
|
56
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.5"
|
57
|
+
spec.add_development_dependency "rubocop-rspec", "~> 1.37"
|
58
|
+
spec.add_development_dependency "simplecov", "~> 0.16"
|
59
|
+
spec.add_development_dependency "sqlite3", "~> 1.4.2"
|
60
|
+
spec.add_development_dependency "webmock", "~> 3.13"
|
61
|
+
end
|
metadata
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_ruby_service
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jay Crouch
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-07-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: awesome_print
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.9.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.9.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.17'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.17'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.0.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.0.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dotenv
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: factory_bot
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 6.2.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 6.2.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: faker
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.18'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.18'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: listen
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.5.1
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 3.5.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry-byebug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.9'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.9'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rails
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 6.1.3.2
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 6.1.3.2
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rake
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '10.0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '10.0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rspec
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '3.0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '3.0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rspec-rails
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 5.0.1
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 5.0.1
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: rubocop
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0.60'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0.60'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: rubocop-performance
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '1.5'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - "~>"
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '1.5'
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: rubocop-rspec
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - "~>"
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '1.37'
|
244
|
+
type: :development
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - "~>"
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: '1.37'
|
251
|
+
- !ruby/object:Gem::Dependency
|
252
|
+
name: simplecov
|
253
|
+
requirement: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - "~>"
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: '0.16'
|
258
|
+
type: :development
|
259
|
+
prerelease: false
|
260
|
+
version_requirements: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - "~>"
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: '0.16'
|
265
|
+
- !ruby/object:Gem::Dependency
|
266
|
+
name: sqlite3
|
267
|
+
requirement: !ruby/object:Gem::Requirement
|
268
|
+
requirements:
|
269
|
+
- - "~>"
|
270
|
+
- !ruby/object:Gem::Version
|
271
|
+
version: 1.4.2
|
272
|
+
type: :development
|
273
|
+
prerelease: false
|
274
|
+
version_requirements: !ruby/object:Gem::Requirement
|
275
|
+
requirements:
|
276
|
+
- - "~>"
|
277
|
+
- !ruby/object:Gem::Version
|
278
|
+
version: 1.4.2
|
279
|
+
- !ruby/object:Gem::Dependency
|
280
|
+
name: webmock
|
281
|
+
requirement: !ruby/object:Gem::Requirement
|
282
|
+
requirements:
|
283
|
+
- - "~>"
|
284
|
+
- !ruby/object:Gem::Version
|
285
|
+
version: '3.13'
|
286
|
+
type: :development
|
287
|
+
prerelease: false
|
288
|
+
version_requirements: !ruby/object:Gem::Requirement
|
289
|
+
requirements:
|
290
|
+
- - "~>"
|
291
|
+
- !ruby/object:Gem::Version
|
292
|
+
version: '3.13'
|
293
|
+
description: 'Simple Ruby Service is a lightweight framework for Ruby that makes it
|
294
|
+
easy to create Services and Service Objects (SOs). The framework provides a simple
|
295
|
+
DSL that: adds ActiveModel validations and error handling; encourages a succinct,
|
296
|
+
idiomatic coding style; and standardizes the SO interface.'
|
297
|
+
email:
|
298
|
+
- i.jaycrouch@gmail.com
|
299
|
+
executables: []
|
300
|
+
extensions: []
|
301
|
+
extra_rdoc_files: []
|
302
|
+
files:
|
303
|
+
- ".gitignore"
|
304
|
+
- ".rspec"
|
305
|
+
- ".rubocop.yml"
|
306
|
+
- ".simplecov"
|
307
|
+
- CHANGELOG.md
|
308
|
+
- Gemfile
|
309
|
+
- LICENSE.txt
|
310
|
+
- README.md
|
311
|
+
- Rakefile
|
312
|
+
- lib/simple_ruby_service.rb
|
313
|
+
- lib/simple_ruby_service/error.rb
|
314
|
+
- lib/simple_ruby_service/errors.rb
|
315
|
+
- lib/simple_ruby_service/failure.rb
|
316
|
+
- lib/simple_ruby_service/invalid.rb
|
317
|
+
- lib/simple_ruby_service/service.rb
|
318
|
+
- lib/simple_ruby_service/service_object.rb
|
319
|
+
- lib/simple_ruby_service/version.rb
|
320
|
+
- simple_ruby_service.gemspec
|
321
|
+
homepage: https://github.com/amazing-jay/simple_ruby_service
|
322
|
+
licenses:
|
323
|
+
- MIT
|
324
|
+
metadata:
|
325
|
+
homepage_uri: https://github.com/amazing-jay/simple_ruby_service
|
326
|
+
source_code_uri: https://github.com/amazing-jay/simple_ruby_service
|
327
|
+
changelog_uri: https://github.com/amazing-jay/simple_ruby_service/blob/master/CHANGELOG.md
|
328
|
+
post_install_message:
|
329
|
+
rdoc_options: []
|
330
|
+
require_paths:
|
331
|
+
- lib
|
332
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
333
|
+
requirements:
|
334
|
+
- - ">="
|
335
|
+
- !ruby/object:Gem::Version
|
336
|
+
version: '0'
|
337
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
338
|
+
requirements:
|
339
|
+
- - ">="
|
340
|
+
- !ruby/object:Gem::Version
|
341
|
+
version: '0'
|
342
|
+
requirements: []
|
343
|
+
rubygems_version: 3.0.9
|
344
|
+
signing_key:
|
345
|
+
specification_version: 4
|
346
|
+
summary: Simple Ruby Service is a lightweight framework for Ruby that makes it easy
|
347
|
+
to create Services and Service Objects (SOs).
|
348
|
+
test_files: []
|