active_call 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +11 -0
- data/README.md +107 -25
- data/lib/active_call/base.rb +70 -1
- data/lib/active_call/concerns/request_errorable.rb +17 -0
- data/lib/active_call/concerns/validation_errorable.rb +16 -0
- data/lib/active_call/error.rb +13 -0
- data/lib/active_call/version.rb +1 -1
- data/lib/active_call.rb +6 -5
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e129c0113e02eef8e739a830213f343eddb585c6798f4237f127256b1a5ac419
|
4
|
+
data.tar.gz: 550cdfc7e5285f9dc353003eb570fc89ae1d36d54025af035ab2a4751b61658a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91d837ea7bead55372e4eaf777f84f35ac4ad3a9183633e9bc9042d14cdf26ae02c425a30391ff4dc85116d66d38edb62202fa114c5657c9c5bee60fa02bde83
|
7
|
+
data.tar.gz: 4025432ebc6ef1dbf2cbe5129b4e235808ab91ded26d192c04b7253bca008423e6d054808e3f774421e86bc42465e005ee0e4bfd26241ab19e685fc6ffc219d2
|
data/.rubocop.yml
CHANGED
@@ -15,6 +15,9 @@ AllCops:
|
|
15
15
|
- 'vendor/**/*'
|
16
16
|
- 'gem/**/*'
|
17
17
|
|
18
|
+
Layout/LineEndStringConcatenationIndentation:
|
19
|
+
EnforcedStyle: indented
|
20
|
+
|
18
21
|
Lint/MissingSuper:
|
19
22
|
AllowedParentClasses:
|
20
23
|
- ActiveCall::Base
|
@@ -23,6 +26,11 @@ Metrics/BlockLength:
|
|
23
26
|
Exclude:
|
24
27
|
- spec/*/**.rb
|
25
28
|
|
29
|
+
Metrics/MethodLength:
|
30
|
+
AllowedMethods:
|
31
|
+
- call
|
32
|
+
- call!
|
33
|
+
|
26
34
|
RSpec/ExampleLength:
|
27
35
|
Enabled: false
|
28
36
|
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.0] - 2025-03-20
|
4
|
+
|
5
|
+
- Added method `.call!` with a bang, which will raise an `ActiveCall::ValidationError` exception when validation fails and an `ActiveCall::RequestError` exception when errors were added to the service object in the `after_call` callback.
|
6
|
+
- Use new method `success?` instead of `valid?`.
|
7
|
+
- Method `valid?` will return `true` if the service object passed validation and was able to make the `call` method.
|
8
|
+
- Use `validate, on: :response` to validate the response object.
|
9
|
+
- Raise `NotImplementedError` when `call` is not defined in subclasses.
|
10
|
+
- Use `self.abstract_class = true` to treat the class as a base class that does not define a `call` method.
|
11
|
+
- Adding a `@bang` instance variable on the service objects to determine if `call` or `call!` was invoked.
|
12
|
+
- Don't set `@response` if the object is an `Enumerable`. The response will be set in `each` and not `call`.
|
13
|
+
|
3
14
|
## [0.1.0] - 2025-03-08
|
4
15
|
|
5
16
|
- Initial release
|
data/README.md
CHANGED
@@ -4,46 +4,64 @@ Active Call provides a standardized way to create service objects.
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
8
|
-
|
9
7
|
Install the gem and add to the application's Gemfile by executing:
|
10
8
|
|
11
9
|
```bash
|
12
|
-
bundle add
|
10
|
+
bundle add active_call
|
13
11
|
```
|
14
12
|
|
15
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
14
|
|
17
15
|
```bash
|
18
|
-
gem install
|
16
|
+
gem install active_call
|
19
17
|
```
|
20
18
|
|
21
19
|
## Usage
|
22
20
|
|
23
21
|
Your child classes should inherit from `ActiveCall::Base`.
|
24
22
|
|
25
|
-
|
23
|
+
You can add your own service object classes in your gem's `lib` folder or your project's `app/services` folder.
|
26
24
|
|
27
25
|
Each service object must define only one public method named `call`.
|
28
26
|
|
29
|
-
|
27
|
+
### Logic Flow
|
28
|
+
|
29
|
+
1. **Before** invoking `call`.
|
30
|
+
|
31
|
+
- Validate the request with `validates`.
|
32
|
+
|
33
|
+
- Use the `before_call` hook to set up anything **after validation** passes.
|
34
|
+
|
35
|
+
2. **During** `call` invocation.
|
36
|
+
|
37
|
+
- A `response` attribute gets set with the result of the `call` method.
|
38
|
+
|
39
|
+
3. **After** invoking `call`.
|
40
|
+
|
41
|
+
- Validate the response with `validate, on: :response`.
|
30
42
|
|
31
|
-
|
43
|
+
- Use the `after_call` hook to set up anything **after response validation** passes.
|
32
44
|
|
33
|
-
|
45
|
+
### Example Service Object
|
34
46
|
|
35
47
|
Define a service object with optional validations and callbacks.
|
36
48
|
|
37
49
|
```ruby
|
38
50
|
require 'active_call'
|
39
51
|
|
40
|
-
class YourGemName::SomeResource::
|
52
|
+
class YourGemName::SomeResource::CreateService < ActiveCall::Base
|
41
53
|
attr_reader :message
|
42
54
|
|
43
55
|
validates :message, presence: true
|
44
56
|
|
57
|
+
validate on: :response do
|
58
|
+
errors.add(:message, :invalid, message: 'cannot be baz') if response[:foo] == 'baz'
|
59
|
+
end
|
60
|
+
|
45
61
|
before_call :strip_message
|
46
62
|
|
63
|
+
after_call :log_response
|
64
|
+
|
47
65
|
def initialize(message: nil)
|
48
66
|
@message = message
|
49
67
|
end
|
@@ -57,41 +75,89 @@ class YourGemName::SomeResource::GetService < ActiveCall::Base
|
|
57
75
|
def strip_message
|
58
76
|
@message.strip!
|
59
77
|
end
|
78
|
+
|
79
|
+
def log_response
|
80
|
+
puts "Successfully called #{response}"
|
81
|
+
end
|
60
82
|
end
|
61
83
|
```
|
62
84
|
|
63
|
-
|
85
|
+
### Using `.call`
|
86
|
+
|
87
|
+
You will get an **errors** object when validation fails.
|
64
88
|
|
65
89
|
```ruby
|
66
|
-
service = YourGemName::SomeResource::
|
67
|
-
service.
|
68
|
-
service.
|
90
|
+
service = YourGemName::SomeResource::CreateService.call(message: '')
|
91
|
+
service.success? # => false
|
92
|
+
service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
93
|
+
service.errors.full_messages # => ["Message can't be blank"]
|
94
|
+
service.response # => nil
|
69
95
|
```
|
70
96
|
|
71
|
-
|
97
|
+
A **response** object on a successful `call` invocation.
|
72
98
|
|
73
99
|
```ruby
|
74
|
-
service = YourGemName::SomeResource::
|
75
|
-
service.
|
76
|
-
service.
|
77
|
-
service.response # => nil
|
100
|
+
service = YourGemName::SomeResource::CreateService.call(message: ' bar ')
|
101
|
+
service.success? # => true
|
102
|
+
service.response # => {:foo=>"bar"}
|
78
103
|
```
|
79
104
|
|
80
|
-
|
105
|
+
And an **errors** object if you added errors during the `validate, on: :response` validation.
|
81
106
|
|
82
107
|
```ruby
|
83
|
-
|
108
|
+
service = YourGemName::SomeResource::CreateService.call(message: 'baz')
|
109
|
+
service.success? # => false
|
110
|
+
service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
111
|
+
service.errors.full_messages # => ["Message cannot be baz"]
|
112
|
+
service.response # => {:foo=>"baz"}
|
113
|
+
```
|
114
|
+
|
115
|
+
### Using `.call!`
|
84
116
|
|
117
|
+
An `ActiveCall::ValidationError` **exception** gets raised when validation fails.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
begin
|
121
|
+
service = YourGemName::SomeResource::CreateService.call!(message: '')
|
122
|
+
rescue ActiveCall::ValidationError => exception
|
123
|
+
exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
124
|
+
exception.errors.full_messages # => ["Message can't be blank"]
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
A **response** object on a successful `call` invocation.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
service = YourGemName::SomeResource::CreateService.call!(message: ' bar ')
|
132
|
+
service.success? # => true
|
133
|
+
service.response # => {:foo=>"bar"}
|
134
|
+
```
|
135
|
+
|
136
|
+
And an `ActiveCall::RequestError` **exception** gets raised if you added errors during the `validate, on: :response` validation.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
begin
|
140
|
+
service = YourGemName::SomeResource::CreateService.call!(message: 'baz')
|
141
|
+
rescue ActiveCall::RequestError => exception
|
142
|
+
exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
143
|
+
exception.errors.full_messages # => ["Message cannot be baz"]
|
144
|
+
exception.response # => {:foo=>"baz"}
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
## Configuration
|
149
|
+
|
150
|
+
If you have secrets, use a **configuration** block.
|
151
|
+
|
152
|
+
```ruby
|
85
153
|
class YourGemName::BaseService < ActiveCall::Base
|
86
|
-
|
154
|
+
self.abstract_class = true
|
87
155
|
|
88
|
-
|
89
|
-
Net::HTTP.get_response(URI("http://example.com/api?#{URI.encode_www_form(api_key: api_key)}"))
|
90
|
-
end
|
156
|
+
config_accessor :api_key, default: ENV['API_KEY'], instance_writer: false
|
91
157
|
end
|
92
158
|
```
|
93
159
|
|
94
|
-
Then in your application code you can
|
160
|
+
Then in your application code you can overwite the configuration defaults.
|
95
161
|
|
96
162
|
```ruby
|
97
163
|
YourGemName::BaseService.configure do |config|
|
@@ -99,6 +165,18 @@ YourGemName::BaseService.configure do |config|
|
|
99
165
|
end
|
100
166
|
```
|
101
167
|
|
168
|
+
And implement a service object like so.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
require 'net/http'
|
172
|
+
|
173
|
+
class YourGemName::SomeResource::CreateService < YourGemName::BaseService
|
174
|
+
def call
|
175
|
+
Net::HTTP.get_response(URI("http://example.com/api?#{URI.encode_www_form(api_key: api_key)}"))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
102
180
|
## Gem Creation
|
103
181
|
|
104
182
|
To create your own gem for a service.
|
@@ -121,6 +199,10 @@ spec.add_dependency 'active_call'
|
|
121
199
|
|
122
200
|
Now start adding your service objects in the `lib` directory and make sure they inherit from `ActiveCall::Base`.
|
123
201
|
|
202
|
+
## Gems Using Active Call
|
203
|
+
|
204
|
+
- [Zoho Sign](https://github.com/kobusjoubert/zoho_sign)
|
205
|
+
|
124
206
|
## Development
|
125
207
|
|
126
208
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/active_call/base.rb
CHANGED
@@ -10,15 +10,84 @@ class ActiveCall::Base
|
|
10
10
|
define_model_callbacks :call
|
11
11
|
|
12
12
|
class << self
|
13
|
+
# Set your class to `self.abstract_class = true` if the class is used as a base class and not as a service object.
|
14
|
+
# Abstract classes are not meant to be instantiated directly, but rather inherited from.
|
15
|
+
# The `call` method doesn't need to be implemented in abstract classes.
|
16
|
+
#
|
17
|
+
# Example:
|
18
|
+
#
|
19
|
+
# class YourGemName::BaseService < ActiveCall::Base
|
20
|
+
# self.abstract_class = true
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class YourGemName::SomeResource::CreateService < YourGemName::BaseService
|
24
|
+
# def call
|
25
|
+
# # Implementation specific to this service.
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
attr_accessor :abstract_class
|
30
|
+
|
31
|
+
def abstract_class?
|
32
|
+
@abstract_class == true
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: Refactor `call` and `call!`. The only differences are the two lines raising exceptions.
|
13
36
|
def call(...)
|
14
37
|
service_object = new(...)
|
15
|
-
|
38
|
+
service_object.instance_variable_set(:@bang, false)
|
39
|
+
return service_object if service_object.invalid?(except_on: :response)
|
40
|
+
|
41
|
+
service_object.run_callbacks(:call) do
|
42
|
+
next if service_object.is_a?(Enumerable)
|
43
|
+
|
44
|
+
service_object.instance_variable_set(:@response, service_object.call)
|
45
|
+
service_object.validate(:response)
|
46
|
+
|
47
|
+
return service_object unless service_object.success?
|
48
|
+
end
|
49
|
+
|
50
|
+
service_object
|
51
|
+
end
|
52
|
+
|
53
|
+
def call!(...)
|
54
|
+
service_object = new(...)
|
55
|
+
service_object.instance_variable_set(:@bang, true)
|
56
|
+
raise ActiveCall::ValidationError, service_object.errors if service_object.invalid?(except_on: :response)
|
16
57
|
|
17
58
|
service_object.run_callbacks(:call) do
|
59
|
+
next if service_object.is_a?(Enumerable)
|
60
|
+
|
18
61
|
service_object.instance_variable_set(:@response, service_object.call)
|
62
|
+
service_object.validate(:response)
|
63
|
+
|
64
|
+
unless service_object.success?
|
65
|
+
raise ActiveCall::RequestError.new(service_object.response, service_object.errors)
|
66
|
+
end
|
19
67
|
end
|
20
68
|
|
21
69
|
service_object
|
22
70
|
end
|
23
71
|
end
|
72
|
+
|
73
|
+
def success?
|
74
|
+
errors.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
def valid?(context = nil)
|
78
|
+
return true if response
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def bang?
|
84
|
+
!!@bang
|
85
|
+
end
|
86
|
+
|
87
|
+
def call
|
88
|
+
return if self.class.abstract_class?
|
89
|
+
|
90
|
+
raise NotImplementedError, 'Subclasses must implement a call method. If this is an abstract base class, set ' \
|
91
|
+
'`self.abstract_class = true`.'
|
92
|
+
end
|
24
93
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCall::RequestErrorable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attr_reader :response, :errors
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(response = nil, errors = ActiveModel::Errors.new(self), message = nil)
|
11
|
+
@response = response
|
12
|
+
@errors = errors
|
13
|
+
message ||= errors.full_messages.to_sentence.presence || 'Request failed'
|
14
|
+
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCall::ValidationErrorable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attr_reader :errors
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(errors = ActiveModel::Errors.new(self), message = nil)
|
11
|
+
@errors = errors
|
12
|
+
message ||= errors.full_messages.to_sentence.presence || 'Validation failed'
|
13
|
+
|
14
|
+
super(message)
|
15
|
+
end
|
16
|
+
end
|
data/lib/active_call/version.rb
CHANGED
data/lib/active_call.rb
CHANGED
@@ -3,11 +3,12 @@
|
|
3
3
|
require 'active_model'
|
4
4
|
require 'zeitwerk'
|
5
5
|
|
6
|
-
require_relative 'active_call/version'
|
7
|
-
|
8
6
|
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.ignore("#{__dir__}/active_call/error.rb")
|
8
|
+
loader.collapse("#{__dir__}/active_call/concerns")
|
9
9
|
loader.setup
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
require_relative 'active_call/error'
|
12
|
+
require_relative 'active_call/version'
|
13
|
+
|
14
|
+
module ActiveCall; end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_call
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kobus Joubert
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -53,6 +53,9 @@ files:
|
|
53
53
|
- Rakefile
|
54
54
|
- lib/active_call.rb
|
55
55
|
- lib/active_call/base.rb
|
56
|
+
- lib/active_call/concerns/request_errorable.rb
|
57
|
+
- lib/active_call/concerns/validation_errorable.rb
|
58
|
+
- lib/active_call/error.rb
|
56
59
|
- lib/active_call/version.rb
|
57
60
|
- sig/active_call.rbs
|
58
61
|
homepage: https://github.com/kobusjoubert/active_call
|