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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4673cf511194223bf9481aa7ed85507c9162556b20469f28c9421cc59f4230a9
4
- data.tar.gz: 20fed6531fd0ac7bc7c6e6f8ffb03d336c82e6b91131354c3c5da6e202ea56b0
3
+ metadata.gz: d67030b5b5cf7b405b842c1295c33912ebaa2b6ad1aeaefa0807e1a0f4fef105
4
+ data.tar.gz: 6b19ccf7e464102efe1e70ab0501ea6bb3aa8a781880f1492cca1bfeee9de576
5
5
  SHA512:
6
- metadata.gz: 0064c55032419b699535d0f19cfbf90d730089f57da8a7ac360bdb96fbb7f40dc6d2a19859baec6074e180aef6ef290a45bb818ba872a5418f2ad8c0432786cf
7
- data.tar.gz: bea908f7aeb097617edaf7694a5fffd21e8ef6f3a90f8fdcebd8e4efd23cf90fba289df598914c35540b16400b7ae46dd6ce4c4bd5df29e1b42feb1ad6dabfa2
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
+ [![Gem Version](https://badge.fury.io/rb/active_call.svg?icon=si%3Arubygems)](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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- Now you can start adding your own service object classes in your gem's `lib` folder.
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
- A `response` attribute is set with the result of the `call` method.
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
- An `errors` object will be set if you specified any validations that failed before the `call` method could be invoked.
41
+ 3. **After** invoking `call`.
32
42
 
33
- There is also a `before_call` hook to set up anything before invoking the `call` method. This only happens after all validations have passed.
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 YourGemName::SomeResource::GetService < ActiveCall::Base
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
- You will get a **response** object.
87
+ ### Using `call`
88
+
89
+ You will get an **errors** object when validation fails.
64
90
 
65
91
  ```ruby
66
- service = YourGemName::SomeResource::GetService.call(message: ' bar ')
67
- service.valid? # => true
68
- service.response # => { foo: 'bar' }
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
- And an **errors** object when validation failed.
99
+ A **response** object on a successful `call` invocation.
72
100
 
73
101
  ```ruby
74
- service = YourGemName::SomeResource::GetService.call(message: '')
75
- service.valid? # => false
76
- service.errors.full_messages # => ["Message can't be blank"]
77
- service.response # => nil
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
- require 'net/http'
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 overrite the configuration defaults.
162
+ Then in your application code you can override the configuration defaults.
95
163
 
96
164
  ```ruby
97
- YourGemName::BaseService.configure do |config|
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.
@@ -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
- return service_object if service_object.invalid?
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCall
4
+ class Error < StandardError; end
5
+
6
+ class ValidationError < Error
7
+ include ActiveCall::ValidationErrorable
8
+ end
9
+
10
+ class RequestError < Error
11
+ include ActiveCall::RequestErrorable
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCall
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.1'
5
5
  end
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
- module ActiveCall
12
- class Error < StandardError; end
13
- end
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.0
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-10 00:00:00.000000000 Z
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: