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