active_call 0.1.0 → 0.2.1
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 +15 -0
- data/README.md +111 -26
- data/lib/active_call/base.rb +127 -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 +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d67030b5b5cf7b405b842c1295c33912ebaa2b6ad1aeaefa0807e1a0f4fef105
|
4
|
+
data.tar.gz: 6b19ccf7e464102efe1e70ab0501ea6bb3aa8a781880f1492cca1bfeee9de576
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b0aea3b486558cd5d81ead4cbc1e75c1ae97f1edef31b3d9ed796b04cdb78e24e6aeda57227ade6c927f7a90599dfe16c06dc8065b98dcbb340db8a27f279e4
|
7
|
+
data.tar.gz: 46f8fa215650ff0373ada16c79a7bd3f2f5a73ff8aaea30081a6e59ade1a869ac3aff735c132d87bc3774708ef13c2dc7829d802774b238f6acd74a061589063
|
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,20 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.1] - 2025-03-25
|
4
|
+
|
5
|
+
- Gemspec `changelog_uri` fixed.
|
6
|
+
|
7
|
+
## [0.2.0] - 2025-03-20
|
8
|
+
|
9
|
+
- 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 `validate on: :response` block.
|
10
|
+
- Use new method `success?` instead of `valid?`.
|
11
|
+
- Method `valid?` will return `true` if the service object passed validation and was able to make the `call` method.
|
12
|
+
- Use `validate, on: :response` to validate the response object.
|
13
|
+
- Raise `NotImplementedError` when `call` is not defined in subclasses.
|
14
|
+
- Use `self.abstract_class = true` to treat the class as a base class that does not define a `call` method.
|
15
|
+
- Adding a `@bang` instance variable on the service objects to determine if `call` or `call!` was invoked.
|
16
|
+
- Don't set `@response` if the object is an `Enumerable`. The response will be set in `each` and not `call`.
|
17
|
+
|
3
18
|
## [0.1.0] - 2025-03-08
|
4
19
|
|
5
20
|
- Initial release
|
data/README.md
CHANGED
@@ -1,49 +1,69 @@
|
|
1
1
|
# Active Call
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/active_call)
|
4
|
+
|
3
5
|
Active Call provides a standardized way to create service objects.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
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
9
|
Install the gem and add to the application's Gemfile by executing:
|
10
10
|
|
11
11
|
```bash
|
12
|
-
bundle add
|
12
|
+
bundle add active_call
|
13
13
|
```
|
14
14
|
|
15
15
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
16
|
|
17
17
|
```bash
|
18
|
-
gem install
|
18
|
+
gem install active_call
|
19
19
|
```
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
23
|
Your child classes should inherit from `ActiveCall::Base`.
|
24
24
|
|
25
|
-
|
25
|
+
You can add your own service object classes in your gem's `lib` folder or your project's `app/services` folder.
|
26
26
|
|
27
27
|
Each service object must define only one public method named `call`.
|
28
28
|
|
29
|
-
|
29
|
+
### Logic Flow
|
30
|
+
|
31
|
+
1. **Before** invoking `call`.
|
32
|
+
|
33
|
+
- Validate the request with `validates`.
|
34
|
+
|
35
|
+
- Use the `before_call` hook to set up anything **after validation** passes.
|
36
|
+
|
37
|
+
2. **During** `call` invocation.
|
38
|
+
|
39
|
+
- A `response` attribute gets set with the result of the `call` method.
|
30
40
|
|
31
|
-
|
41
|
+
3. **After** invoking `call`.
|
32
42
|
|
33
|
-
|
43
|
+
- Validate the response with `validate, on: :response`.
|
44
|
+
|
45
|
+
- Use the `after_call` hook to set up anything **after response validation** passes.
|
46
|
+
|
47
|
+
### Example Service Object
|
34
48
|
|
35
49
|
Define a service object with optional validations and callbacks.
|
36
50
|
|
37
51
|
```ruby
|
38
52
|
require 'active_call'
|
39
53
|
|
40
|
-
class
|
54
|
+
class YourGem::SomeResource::CreateService < ActiveCall::Base
|
41
55
|
attr_reader :message
|
42
56
|
|
43
57
|
validates :message, presence: true
|
44
58
|
|
59
|
+
validate on: :response do
|
60
|
+
errors.add(:message, :invalid, message: 'cannot be baz') if response[:foo] == 'baz'
|
61
|
+
end
|
62
|
+
|
45
63
|
before_call :strip_message
|
46
64
|
|
65
|
+
after_call :log_response
|
66
|
+
|
47
67
|
def initialize(message: nil)
|
48
68
|
@message = message
|
49
69
|
end
|
@@ -57,48 +77,108 @@ class YourGemName::SomeResource::GetService < ActiveCall::Base
|
|
57
77
|
def strip_message
|
58
78
|
@message.strip!
|
59
79
|
end
|
80
|
+
|
81
|
+
def log_response
|
82
|
+
puts "Successfully called #{response}"
|
83
|
+
end
|
60
84
|
end
|
61
85
|
```
|
62
86
|
|
63
|
-
|
87
|
+
### Using `call`
|
88
|
+
|
89
|
+
You will get an **errors** object when validation fails.
|
64
90
|
|
65
91
|
```ruby
|
66
|
-
service =
|
67
|
-
service.
|
68
|
-
service.
|
92
|
+
service = YourGem::SomeResource::CreateService.call(message: '')
|
93
|
+
service.success? # => false
|
94
|
+
service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
95
|
+
service.errors.full_messages # => ["Message can't be blank"]
|
96
|
+
service.response # => nil
|
69
97
|
```
|
70
98
|
|
71
|
-
|
99
|
+
A **response** object on a successful `call` invocation.
|
72
100
|
|
73
101
|
```ruby
|
74
|
-
service =
|
75
|
-
service.
|
76
|
-
service.
|
77
|
-
|
102
|
+
service = YourGem::SomeResource::CreateService.call(message: ' bar ')
|
103
|
+
service.success? # => true
|
104
|
+
service.response # => {:foo=>"bar"}
|
105
|
+
```
|
106
|
+
|
107
|
+
And an **errors** object if you added errors during the `validate, on: :response` validation.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
service = YourGem::SomeResource::CreateService.call(message: 'baz')
|
111
|
+
service.success? # => false
|
112
|
+
service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
113
|
+
service.errors.full_messages # => ["Message cannot be baz"]
|
114
|
+
service.response # => {:foo=>"baz"}
|
115
|
+
```
|
116
|
+
|
117
|
+
### Using `call!`
|
118
|
+
|
119
|
+
An `ActiveCall::ValidationError` **exception** gets raised when validation fails.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
begin
|
123
|
+
service = YourGem::SomeResource::CreateService.call!(message: '')
|
124
|
+
rescue ActiveCall::ValidationError => exception
|
125
|
+
exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
126
|
+
exception.errors.full_messages # => ["Message can't be blank"]
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
A **response** object on a successful `call` invocation.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
service = YourGem::SomeResource::CreateService.call!(message: ' bar ')
|
134
|
+
service.success? # => true
|
135
|
+
service.response # => {:foo=>"bar"}
|
136
|
+
```
|
137
|
+
|
138
|
+
And an `ActiveCall::RequestError` **exception** gets raised if you added errors during the `validate, on: :response` validation.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
begin
|
142
|
+
service = YourGem::SomeResource::CreateService.call!(message: 'baz')
|
143
|
+
rescue ActiveCall::RequestError => exception
|
144
|
+
exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
145
|
+
exception.errors.full_messages # => ["Message cannot be baz"]
|
146
|
+
exception.response # => {:foo=>"baz"}
|
147
|
+
end
|
78
148
|
```
|
79
149
|
|
150
|
+
## Configuration
|
151
|
+
|
80
152
|
If you have secrets, use a **configuration** block.
|
81
153
|
|
82
154
|
```ruby
|
83
|
-
|
155
|
+
class YourGem::BaseService < ActiveCall::Base
|
156
|
+
self.abstract_class = true
|
84
157
|
|
85
|
-
class YourGemName::BaseService < ActiveCall::Base
|
86
158
|
config_accessor :api_key, default: ENV['API_KEY'], instance_writer: false
|
87
|
-
|
88
|
-
def call
|
89
|
-
Net::HTTP.get_response(URI("http://example.com/api?#{URI.encode_www_form(api_key: api_key)}"))
|
90
|
-
end
|
91
159
|
end
|
92
160
|
```
|
93
161
|
|
94
|
-
Then in your application code you can
|
162
|
+
Then in your application code you can override the configuration defaults.
|
95
163
|
|
96
164
|
```ruby
|
97
|
-
|
165
|
+
YourGem::BaseService.configure do |config|
|
98
166
|
config.api_key = Rails.application.credentials.api_key || ENV['API_KEY']
|
99
167
|
end
|
100
168
|
```
|
101
169
|
|
170
|
+
And implement a service object like so.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
require 'net/http'
|
174
|
+
|
175
|
+
class YourGem::SomeResource::CreateService < YourGem::BaseService
|
176
|
+
def call
|
177
|
+
Net::HTTP.get_response(URI("http://example.com/api?#{URI.encode_www_form(api_key: api_key)}"))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
102
182
|
## Gem Creation
|
103
183
|
|
104
184
|
To create your own gem for a service.
|
@@ -121,6 +201,11 @@ spec.add_dependency 'active_call'
|
|
121
201
|
|
122
202
|
Now start adding your service objects in the `lib` directory and make sure they inherit from `ActiveCall::Base`.
|
123
203
|
|
204
|
+
## Gems Using Active Call
|
205
|
+
|
206
|
+
- [nCino KYC DocFox](https://github.com/kobusjoubert/doc_fox)
|
207
|
+
- [Zoho Sign](https://github.com/kobusjoubert/zoho_sign)
|
208
|
+
|
124
209
|
## Development
|
125
210
|
|
126
211
|
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,141 @@ 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
|
+
# ==== Examples
|
18
|
+
#
|
19
|
+
# class YourGem::BaseService < ActiveCall::Base
|
20
|
+
# self.abstract_class = true
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class YourGem::SomeResource::CreateService < YourGem::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.
|
36
|
+
|
37
|
+
# Using `call`
|
38
|
+
#
|
39
|
+
# ==== Examples
|
40
|
+
#
|
41
|
+
# You will get an `errors` object when validation fails.
|
42
|
+
#
|
43
|
+
# service = YourGem::SomeResource::CreateService.call(message: '')
|
44
|
+
# service.success? # => false
|
45
|
+
# service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
46
|
+
# service.errors.full_messages # => ["Message can't be blank"]
|
47
|
+
# service.response # => nil
|
48
|
+
#
|
49
|
+
# A `response` object on a successful `call` invocation.
|
50
|
+
#
|
51
|
+
# service = YourGem::SomeResource::CreateService.call(message: ' bar ')
|
52
|
+
# service.success? # => true
|
53
|
+
# service.response # => {:foo=>"bar"}
|
54
|
+
#
|
55
|
+
# And an `errors` object if you added errors during the `validate, on: :response` validation.
|
56
|
+
#
|
57
|
+
# service = YourGem::SomeResource::CreateService.call(message: 'baz')
|
58
|
+
# service.success? # => false
|
59
|
+
# service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
60
|
+
# service.errors.full_messages # => ["Message cannot be baz"]
|
61
|
+
# service.response # => {:foo=>"baz"}
|
62
|
+
#
|
13
63
|
def call(...)
|
14
64
|
service_object = new(...)
|
15
|
-
|
65
|
+
service_object.instance_variable_set(:@bang, false)
|
66
|
+
return service_object if service_object.invalid?(except_on: :response)
|
16
67
|
|
17
68
|
service_object.run_callbacks(:call) do
|
69
|
+
next if service_object.is_a?(Enumerable)
|
70
|
+
|
18
71
|
service_object.instance_variable_set(:@response, service_object.call)
|
72
|
+
service_object.validate(:response)
|
73
|
+
|
74
|
+
return service_object unless service_object.success?
|
19
75
|
end
|
20
76
|
|
21
77
|
service_object
|
22
78
|
end
|
79
|
+
|
80
|
+
# Using `call!`
|
81
|
+
#
|
82
|
+
# ==== Examples
|
83
|
+
#
|
84
|
+
# An `ActiveCall::ValidationError` exception gets raised when validation fails.
|
85
|
+
#
|
86
|
+
# begin
|
87
|
+
# service = YourGem::SomeResource::CreateService.call!(message: '')
|
88
|
+
# rescue ActiveCall::ValidationError => exception
|
89
|
+
# exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]>
|
90
|
+
# exception.errors.full_messages # => ["Message can't be blank"]
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# A `response` object on a successful `call` invocation.
|
94
|
+
#
|
95
|
+
# service = YourGem::SomeResource::CreateService.call!(message: ' bar ')
|
96
|
+
# service.success? # => true
|
97
|
+
# service.response # => {:foo=>"bar"}
|
98
|
+
#
|
99
|
+
# And an `ActiveCall::RequestError` exception gets raised if you added errors during the `validate, on: :response`
|
100
|
+
# validation.
|
101
|
+
#
|
102
|
+
# begin
|
103
|
+
# service = YourGem::SomeResource::CreateService.call!(message: 'baz')
|
104
|
+
# rescue ActiveCall::RequestError => exception
|
105
|
+
# exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]>
|
106
|
+
# exception.errors.full_messages # => ["Message cannot be baz"]
|
107
|
+
# exception.response # => {:foo=>"baz"}
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
def call!(...)
|
111
|
+
service_object = new(...)
|
112
|
+
service_object.instance_variable_set(:@bang, true)
|
113
|
+
raise ActiveCall::ValidationError, service_object.errors if service_object.invalid?(except_on: :response)
|
114
|
+
|
115
|
+
service_object.run_callbacks(:call) do
|
116
|
+
next if service_object.is_a?(Enumerable)
|
117
|
+
|
118
|
+
service_object.instance_variable_set(:@response, service_object.call)
|
119
|
+
service_object.validate(:response)
|
120
|
+
|
121
|
+
unless service_object.success?
|
122
|
+
raise ActiveCall::RequestError.new(service_object.response, service_object.errors)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
service_object
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def success?
|
131
|
+
errors.empty?
|
132
|
+
end
|
133
|
+
|
134
|
+
def valid?(context = nil)
|
135
|
+
return true if response
|
136
|
+
|
137
|
+
super
|
138
|
+
end
|
139
|
+
|
140
|
+
def bang?
|
141
|
+
!!@bang
|
142
|
+
end
|
143
|
+
|
144
|
+
def call
|
145
|
+
return if self.class.abstract_class?
|
146
|
+
|
147
|
+
raise NotImplementedError, 'Subclasses must implement a call method. If this is an abstract base class, set ' \
|
148
|
+
'`self.abstract_class = true`.'
|
23
149
|
end
|
24
150
|
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.1
|
4
|
+
version: 0.2.1
|
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-25 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
|
@@ -63,7 +66,7 @@ metadata:
|
|
63
66
|
rubygems_mfa_required: 'true'
|
64
67
|
homepage_uri: https://github.com/kobusjoubert/active_call
|
65
68
|
source_code_uri: https://github.com/kobusjoubert/active_call
|
66
|
-
changelog_uri: https://github.com/kobusjoubert/active_call/CHANGELOG.md
|
69
|
+
changelog_uri: https://github.com/kobusjoubert/active_call/blob/main/CHANGELOG.md
|
67
70
|
post_install_message:
|
68
71
|
rdoc_options: []
|
69
72
|
require_paths:
|