simple_ruby_service 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
4
+ --order rand
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
@@ -0,0 +1,4 @@
1
+ SimpleCov.start do
2
+ add_filter "/spec/"
3
+ add_filter "/tmp/"
4
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 (01-Jul-21)
4
+
5
+ * Initial public release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in simple_ruby_service.gemspec
8
+ gemspec
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleRubyservice
4
+ # simple exception class with target and message
5
+ class Error < StandardError
6
+ attr_accessor :target
7
+
8
+ def initialize(target, msg)
9
+ @target = target
10
+ super msg
11
+ end
12
+ end
13
+ end
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleRubyservice
4
+ class Failure < Error; end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleRubyservice
4
+ class Invalid < Error; end
5
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleRubyservice
4
+ VERSION = '1.0.0'
5
+ 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: []