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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4673cf511194223bf9481aa7ed85507c9162556b20469f28c9421cc59f4230a9
4
- data.tar.gz: 20fed6531fd0ac7bc7c6e6f8ffb03d336c82e6b91131354c3c5da6e202ea56b0
3
+ metadata.gz: e129c0113e02eef8e739a830213f343eddb585c6798f4237f127256b1a5ac419
4
+ data.tar.gz: 550cdfc7e5285f9dc353003eb570fc89ae1d36d54025af035ab2a4751b61658a
5
5
  SHA512:
6
- metadata.gz: 0064c55032419b699535d0f19cfbf90d730089f57da8a7ac360bdb96fbb7f40dc6d2a19859baec6074e180aef6ef290a45bb818ba872a5418f2ad8c0432786cf
7
- data.tar.gz: bea908f7aeb097617edaf7694a5fffd21e8ef6f3a90f8fdcebd8e4efd23cf90fba289df598914c35540b16400b7ae46dd6ce4c4bd5df29e1b42feb1ad6dabfa2
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- Now you can start adding your own service object classes in your gem's `lib` folder.
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
- A `response` attribute is set with the result of the `call` method.
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
- An `errors` object will be set if you specified any validations that failed before the `call` method could be invoked.
43
+ - Use the `after_call` hook to set up anything **after response validation** passes.
32
44
 
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.
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::GetService < ActiveCall::Base
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
- You will get a **response** object.
85
+ ### Using `.call`
86
+
87
+ You will get an **errors** object when validation fails.
64
88
 
65
89
  ```ruby
66
- service = YourGemName::SomeResource::GetService.call(message: ' bar ')
67
- service.valid? # => true
68
- service.response # => { foo: 'bar' }
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
- And an **errors** object when validation failed.
97
+ A **response** object on a successful `call` invocation.
72
98
 
73
99
  ```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
100
+ service = YourGemName::SomeResource::CreateService.call(message: ' bar ')
101
+ service.success? # => true
102
+ service.response # => {:foo=>"bar"}
78
103
  ```
79
104
 
80
- If you have secrets, use a **configuration** block.
105
+ And an **errors** object if you added errors during the `validate, on: :response` validation.
81
106
 
82
107
  ```ruby
83
- require 'net/http'
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
- config_accessor :api_key, default: ENV['API_KEY'], instance_writer: false
154
+ self.abstract_class = true
87
155
 
88
- def call
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 overrite the configuration defaults.
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.
@@ -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
- return service_object if service_object.invalid?
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
@@ -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.0'
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.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-10 00:00:00.000000000 Z
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