Skip to content

Commit a84feb3

Browse files
committed
Merge branch 'feature/errors'
2 parents f0ce6e3 + 2e31591 commit a84feb3

15 files changed

Lines changed: 505 additions & 131 deletions

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ DEPENDENCIES
3232
single_action_service!
3333

3434
BUNDLED WITH
35-
1.17.3
35+
2.2.9

README.md

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,70 +22,139 @@ Or install it yourself as:
2222

2323
## Usage
2424

25-
1. Inherit:
25+
Create an inheritor from a `SingleActionService::Base` with a single method named `call`
26+
2627
```ruby
27-
class MySingleAction < SingleActionService::Base
28+
class Summator < SingleActionService::Base
29+
def call
30+
end
2831
end
2932
```
30-
2. Create a constructor with parameters or pass them to call:
33+
34+
Create a constructor with parameters
35+
3136
```ruby
32-
# Usually a data source is passed
33-
def initialize(x, y)
34-
@x = x
35-
@y = y
37+
class Summator < SingleActionService::Base
38+
def initialize(x, y)
39+
@x = x
40+
@y = y
41+
end
3642
end
3743
```
38-
or
44+
45+
or pass them to the call method
46+
3947
```ruby
40-
def call(x, y)
48+
class Summator < SingleActionService::Base
49+
def call(x, y)
50+
end
4151
end
4252
```
43-
3. Perform the action:
44-
```ruby
45-
sum = x + y
46-
```
47-
4. Return the result:
48-
```ruby
49-
success(sum)
50-
```
51-
or without data
53+
54+
Perform the action and return the result by calling `success(data = nil)`
55+
5256
```ruby
53-
success
57+
class Summator < SingleActionService::Base
58+
def call(x, y)
59+
sum = x + y
60+
success(sum)
61+
end
62+
end
5463
```
55-
5. Or return an error:
64+
65+
or return the error based on the validations by calling `error(data: nil, code: nil)`
66+
5667
```ruby
57-
return error(data: sum, code: 1) unless sum.positive?
68+
class Summator < SingleActionService::Base
69+
NIL_NUMBERS_ERROR = :nil_numbers_error
70+
71+
def call(x, y)
72+
if x.nil? || y.nil?
73+
return error(code: NIL_NUMBERS_ERROR)
74+
end
75+
end
76+
end
5877
```
59-
The 'data' parameter is any data (optional).
6078

61-
The 'code' parameter is used to identify the error (optional).
79+
Call the service and process a result
6280

63-
6. You can process the result received from the service as follows:
6481
```ruby
65-
action = MySingleAction.new
66-
result = action.call(1,2)
67-
# or uses result.error?
82+
summator = Summator.new
83+
result = summator.call(1, 2)
84+
6885
if result.success?
6986
result.data
7087
else
71-
{
72-
error_code: result.error_code,
73-
data: result.data
74-
}
88+
result.error_code
7589
end
7690
```
77-
or uses data! if you want to throw an exception
91+
92+
Or you can use a `data!` method if you want to throw an exception when an error has occurred:
93+
7894
```ruby
7995
begin
80-
result = MySingleAction.new.call(1,2).data!
96+
result = Summator.new.call(1, 2).data!
8197
rescue SingleActionService::InvalidResult => e
82-
{
83-
error_code: e.result.error_code,
84-
data: e.result.data
85-
}
98+
e.result.error_code
8699
end
87100
```
88101

102+
You can define a list of errors of a service by calling a `self.errors(errors_data)` method.<br/>For each error, a method `#{error_name}_error(data = nil)` will be generated to instantiate the result with the appropriate code and optional data:
103+
104+
```ruby
105+
class Summator < SingleActionService::Base
106+
errors [
107+
{ name: :nil_numbers, code: 1 }
108+
]
109+
110+
def call(x, y)
111+
if x.nil? || y.nil?
112+
return nil_numbers_error
113+
end
114+
end
115+
end
116+
```
117+
118+
You can check for the specific error calling an autogenerated checking method of the result:
119+
120+
```ruby
121+
result = Summator.new.call(nil, nil)
122+
result.nil_numbers_error? # true
123+
```
124+
125+
## API Reference
126+
127+
### SingleActionService::Base
128+
129+
A base class for services. Create an inheritor from him to use it. Methods:
130+
131+
Name |Description
132+
---------------------------------|-----------
133+
`success(data = nil)` |Returns a successful `SingleActionService::Result` with<br/>data passed in arguments.
134+
`error(data: nil, code: nil)` |Returns an error `SingleActionService::Result` with<br/> data and the error code passed in arguments.
135+
`#{error_name}_error(data = nil)`|Autogenerated method to create an error result <br/>with the specific error code<br/>without having to pass it in arguments.<br/>Generated for each error passed to the `self.errors` method.<br/>Returns an error `SingleActionService::Result` with<br/> data passed to arguments.
136+
`self.errors(errors_data)` |Call this method to identify possible service errors.<br/>Accepts an array of objects:<br/>`{ name: :error_name, code: :error_code }`
137+
138+
### SingleActionService::Result
139+
140+
A base class for the result that the service returns. Instantiated by service methods such as `success`, `error` and autogenerated error methods. Methods:
141+
142+
Name |Description
143+
----------------------|-----------
144+
`success?` |Call this method to check the result for success.<br/>Returns `true` for successful results created by the<br/>`success` method of a service.
145+
`error?` |Call this method to check the result for error.<br/>Returns `false` for error results created by the<br/>`error` method of a service or by autogenerated<br/> error methods.
146+
`#{error_name}_error?`|Call this method to check the result for the specific error.<br/>Autogenerated for each error passed to the `self.errors`<br/> of a service.
147+
`data` |Returns data passed to the result instantiation method
148+
`data!` |Returns data passed to the result instantiation method.<br/>Throws a `SingleActionService::InvalidResult`<br/>exception if the result contains an error.
149+
150+
### SingleActionService::InvalidResult
151+
152+
An exception thrown by the `data!` method of a result. Methods:
153+
154+
Name |Description
155+
------------|-----------
156+
`result` |Returns a `SingleActionService::Result`
157+
89158
## Contributing
90159

91160
Bug reports and pull requests are welcome on GitHub at https://github.com/sequenia/single_action_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.

lib/single_action_service.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
require "single_action_service/exceptions"
2-
require "single_action_service/base"
3-
require "single_action_service/result"
4-
require "single_action_service/version"
1+
require 'single_action_service/exceptions'
2+
require 'single_action_service/module_helper'
3+
require 'single_action_service/base'
4+
require 'single_action_service/result'
5+
require 'single_action_service/service_error'
6+
require 'single_action_service/version'
57

68
module SingleActionService
79
end

lib/single_action_service/base.rb

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,100 @@
1-
module SingleActionService
2-
class Base
3-
protected
1+
# Parent class for services.
2+
# A service is an object that implements part of the business logic.
3+
# Create an inheritor to use it
4+
# and call 'success' or 'error' methods to return a result object.
5+
class SingleActionService::Base
6+
protected
47

5-
def success(data = nil)
6-
Result.new(true, data: data)
8+
# Helper methods to setup the service
9+
class << self
10+
# Some usefull methods that exists in Rails
11+
# but does not exists in the pure ruby
12+
unless respond_to?(:module_parent)
13+
include SingleActionService::ModuleHelper
714
end
815

9-
def error(data: nil, code: nil)
10-
Result.new(false, data: data, error_code: code)
16+
# Call this method to generate methods in the service to return
17+
# specific errors.
18+
#
19+
# @param errors_data is an array of hashes with information about errors.
20+
# Each hash can contain keys:
21+
# :name => A symbol representing a name of the error.
22+
# :code => A symbol representing an error code of the error.
23+
#
24+
# For each name, a method "#{name}_error" will be generated
25+
# to return a result with the corresponding error code.
26+
# The returned result will have "#{name}_error?" methods
27+
# for checking for a specific error.
28+
#
29+
# For example, if you pass an array:
30+
# [{ name: :already_exists, code: :'errors.already_exists' }],
31+
# the 'already_exists_error' method will be generated to return the
32+
# result with a :'errors.already_exists' code.
33+
# You can check for the error by calling 'already_exists_error?'
34+
# method on the result object.
35+
def errors(errors_data = nil)
36+
return @errors if errors_data.nil?
37+
38+
parse_errors(errors_data)
39+
create_result_class
40+
define_methods_to_create_error_results
41+
end
42+
43+
def parse_errors(errors_data)
44+
@errors = errors_data.map do |error_data|
45+
SingleActionService::ServiceError.new(**error_data)
46+
end
47+
end
48+
49+
def create_result_class
50+
demodulized_name = name.split('::').last
51+
result_class_name = "#{demodulized_name}Result"
52+
return if module_parent.const_defined?(result_class_name)
53+
54+
# Programmatically create the inheritor for the service result object
55+
# with autogenerated methods for checking for errors.
56+
errors = @errors
57+
@result_class = Class.new(SingleActionService::Result) do
58+
def self.define_error_checking_method(error)
59+
method_name = "#{error.name}_error?"
60+
61+
define_method(method_name) do
62+
error_code == error.code
63+
end
64+
end
65+
66+
errors.each do |error|
67+
define_error_checking_method(error)
68+
end
69+
end
70+
71+
module_parent.const_set(result_class_name, @result_class)
1172
end
73+
74+
def define_methods_to_create_error_results
75+
@errors.each do |error_object|
76+
result_method_name = "#{error_object.name}_error"
77+
define_method(result_method_name) do |data = nil|
78+
error(code: error_object.code, data: data)
79+
end
80+
end
81+
end
82+
83+
def result_class
84+
@result_class ||= SingleActionService::Result
85+
end
86+
end
87+
88+
# @return a result with a success indicator and passed data.
89+
# @param data is any data to return from service.
90+
def success(data = nil)
91+
self.class.result_class.new(true, data: data)
92+
end
93+
94+
# @return a result with an error indicator, passed data and error code.
95+
# @param data is any data to return from the service.
96+
# @param code is an error code of the occurred error.
97+
def error(data: nil, code: nil)
98+
self.class.result_class.new(false, data: data, error_code: code)
1299
end
13100
end

lib/single_action_service/exceptions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class InvalidResult < ArgumentError
77
attr_reader :result
88

99
def initialize(result)
10+
super
1011
@result = result
1112
end
1213
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module SingleActionService::ModuleHelper
2+
def module_parent
3+
module_parent_name ? constantize(module_parent_name) : Object
4+
end
5+
6+
def constantize(string)
7+
string.split('::').inject(Object) do |module_object, class_name|
8+
module_object.const_get(class_name)
9+
end
10+
end
11+
12+
def module_parent_name
13+
if defined?(@parent_name)
14+
@parent_name
15+
else
16+
parent_name = name =~ /::[^:]+\z/ ? -$` : nil
17+
@parent_name = parent_name unless frozen?
18+
parent_name
19+
end
20+
end
21+
end
Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
module SingleActionService
2-
class Result
3-
attr_accessor :status, :data, :error_code
1+
class SingleActionService::Result
2+
attr_accessor :status, :data, :error_code
43

5-
def initialize(status, data: nil, error_code: nil)
6-
@status = status
7-
@data = data
8-
@error_code = error_code
9-
end
4+
def initialize(status, data: nil, error_code: nil)
5+
@status = status
6+
@data = data
7+
@error_code = error_code
8+
end
109

11-
def success?
12-
status
13-
end
10+
def success?
11+
status
12+
end
1413

15-
def error?
16-
!success?
17-
end
14+
def error?
15+
!success?
16+
end
1817

19-
def data!
20-
return data if success?
18+
def data!
19+
return data if success?
2120

22-
raise InvalidResult.new(self)
23-
end
21+
raise SingleActionService::InvalidResult, self
2422
end
2523
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class SingleActionService::ServiceError
2+
attr_accessor :code, :name
3+
4+
def initialize(code: nil, name: nil)
5+
@code = code
6+
@name = name
7+
end
8+
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module SingleActionService
2-
VERSION = "0.3.0"
2+
VERSION = '0.4.0'.freeze
33
end

0 commit comments

Comments
 (0)