action_figure 0.5.0 → 0.6.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: 2afc60d26a2b7f19aa7a5e8e1e438dba5f0400688c9dd0b5ec184f0307484366
4
- data.tar.gz: 346713da33afd14d6ea9d7862fbd7e71030d116e467664d6c73f19397e9a54c0
3
+ metadata.gz: 6c29461ca24fe48d0143c6c861a738e37fa04fa9954b0fcc3336f8154dc16a30
4
+ data.tar.gz: 93e527838e129363aafbba74751660831a54084cb159571fa71f0b9ef11322fc
5
5
  SHA512:
6
- metadata.gz: 2dba3a5b85d8f8bd55ebcfca08fb9eeedb508e2ff2d5f16cd8c971c0e3eb5586ab614922b341c90aaed4ad4e3607eefd71c7bd76e881592b45f5a1ac4688e9f8
7
- data.tar.gz: 9a796394dc06f986ec4cbeef0e7e9cf3e3a9a80aaa15b51edb16201e89db815e86f8ccc3b816366543c7a8878327fd8f2be9fee11a4fbfe4adc47fade36e3879
6
+ metadata.gz: 311b1a051ee7caec1aece62143607ed2484720ab6a04398b66f091087a0febccacc91e1478c438bbca98b93d0d13cf57da6f12d8ae3caf47dd8bbb0b642c7994
7
+ data.tar.gz: 4bd0d145727af9f7373301ad26abc7b65a08041071c07317cf3fb88a818961be09e834de1b857a5256c6012b2965e0aa2bf1da24943cd2219689416d14be5a22
data/README.md CHANGED
@@ -5,127 +5,27 @@ Fully-articulated controller actions.
5
5
  ---
6
6
  > #### Table of Contents
7
7
  > [Installation](#installation)<br>
8
- > [Quick Start](#quick-start)<br>
9
8
  > [How It Works](#how-it-works)<br>
10
9
  > [Features](#features)<br>
11
- > [Full Example](#full-example)<br>
10
+ > [Quick Start](#quick-start)<br>
12
11
  > [Design Philosophy](#design-philosophy)<br>
13
12
  > [Requirements](#requirements)<br>
14
13
  > [License](#license)
15
14
  ---
16
15
 
17
- **ActionFigure** replaces gnarly controller method logic with explicit, purpose-driven operation classes. Each action validates its input, executes its logic, and returns a render-ready hash making your controller action methods one-liners and behavior easily testable.
18
-
19
- ## Installation
20
-
21
- Add to your Gemfile and `bundle install`:
22
-
23
- ```ruby
24
- gem "action_figure"
25
- ```
26
-
27
- ## Quick Start
28
-
29
- **1. Start with what the action should do.**
30
-
31
- ```ruby
32
- # spec/actions/users/create_action_spec.rb
33
- RSpec.describe Users::CreateAction do
34
- it "creates a user with valid parameters" do
35
- company = Company.create!(name: "Acme")
36
-
37
- # Note: Extra keyword arguments like company: are injected as context alongside params:
38
- result = Users::CreateAction.call(
39
- params: { user: { name: "Tad", email: "tad@example.com" } },
40
- company: company
41
- )
42
-
43
- # Results are render-ready hashes (JSend formatted in this case)
44
- # => { json: { status: "success", data: { name: "Tad", ... } }, status: :created }
45
- expect(result).to be_Created
46
- expect(result[:json][:data]).to include("name" => "Tad", "email" => "tad@example.com")
47
- expect(User.find_by(email: "tad@example.com")).to be_persisted
48
- end
49
-
50
- it "fails when name is missing" do
51
- company = Company.create!(name: "Acme")
52
- result = Users::CreateAction.call(
53
- params: { user: { email: "tad@example.com" } },
54
- company: company
55
- )
56
-
57
- # Validation failures short-circuit before #call executes
58
- # => { json: { status: "fail", data: { user: { name: ["is missing"] } } },
59
- # status: :unprocessable_content }
60
- expect(result).to be_UnprocessableContent
61
- expect(result[:json][:data][:user][:name]).to include("is missing")
62
- expect(User.find_by(email: "tad@example.com")).not_to be_persisted
63
- end
64
- end
65
- ```
66
-
67
- **2. Define the action class.**
68
-
69
- ```ruby
70
- # app/actions/users/create_action.rb
71
- class Users::CreateAction
72
- include ActionFigure[:jsend]
73
-
74
- params_schema do
75
- required(:user).hash do
76
- required(:name).filled(:string)
77
- required(:email).filled(:string)
78
- end
79
- end
80
-
81
- def call(params:, company:)
82
- user = company.users.create(params[:user])
83
- return UnprocessableContent(errors: user.errors.messages) if user.errors.any?
84
-
85
- Created(resource: user.as_json(only: %i[id name email]))
86
- end
87
- end
88
- ```
89
-
90
- **3. Call it from your controller.**
16
+ **ActionFigure** extracts controller actions into classes that validate params, orchestrate work, and return render-ready responses. Your controller becomes:
91
17
 
92
18
  ```ruby
93
- class UsersController < ApplicationController
19
+ class OrdersController < ApplicationController
94
20
  def create
95
- render Users::CreateAction.call(params:, company: current_company)
21
+ render Orders::CreateAction.create(params:, current_user:)
96
22
  end
97
23
  end
98
24
  ```
99
25
 
100
- ## How It Works
101
-
102
- Every action class has three responsibilities:
103
-
104
- 1. **Check params** — `params_schema` validates structure and types, `rules` enforces validation rules. If either fails, the formatter returns an error response and `#call` is never invoked.
105
- 2. **Orchestrate** — `#call` coordinates the work: creating records, calling service objects, enqueuing jobs, or anything else your operation requires. The action is the entry point, not necessarily where all the logic lives.
106
- 3. **Return a formatted response** — response helpers like `Created(resource:)` and `NotFound(errors:)` return render-ready hashes that go straight to `render` in your controller.
107
-
108
- ## Features
109
-
110
- | Feature | Description |
111
- |---------|-------------|
112
- | [Validation](docs/validation.md) | Two-layer validation powered by dry-validation: structural schemas with type coercion, plus validation rules. Includes cross-parameter helpers like `one_rule`, `all_rule`, and `implies_rule`. |
113
- | [Response Formatters](docs/response-formatters.md) | Four built-in formats: Default, JSend, JSON:API, and Wrapped. Each provides response helpers (`Ok`, `Created`, `NotFound`, etc.) that return render-ready hashes. |
114
- | [Custom Formatters](docs/custom-formatters.md) | Define your own response envelope by implementing the formatter interface. Registration validates your module at load time. |
115
- | [Actions](docs/actions.md) | Custom entry points (`entry_point :search`), context injection via keyword arguments, per-class API versioning, and no-params actions. |
116
- | [Configuration](docs/configuration.md) | Global defaults for response format, parameter strictness, and API version. All overridable per-class. |
117
- | [Notifications](docs/activesupport-notifications.md) | Opt-in `ActiveSupport::Notifications` events for every action call. Emits action class, outcome status, and duration on the `process.action_figure` event. |
118
- | [Testing](docs/testing.md) | Minitest assertions (`assert_Ok`, `assert_Created`, ...) and RSpec matchers (`be_Ok`, `be_Created`, ...) for expressive status checks. |
119
- | [Integration Patterns](docs/integration-patterns.md) | Recipes for serializers (Blueprinter, Alba, Oj Serializers), authorization (Pundit, CanCanCan), and pagination (cursor, Pagy). |
120
-
121
- ## Full Example
122
-
123
- Here is a more complete action showing how validation, authorization, and response formatting work together.
124
-
125
- **The action class:**
26
+ The action class owns everything that used to be scattered across the controller method, strong params, model callbacks, and ad-hoc response building:
126
27
 
127
28
  ```ruby
128
- # app/actions/orders/create_action.rb
129
29
  class Orders::CreateAction
130
30
  include ActionFigure[:wrapped]
131
31
 
@@ -142,7 +42,7 @@ class Orders::CreateAction
142
42
  "gift fields must be provided together or not at all")
143
43
  end
144
44
 
145
- def call(params:, current_user:)
45
+ def create(params:, current_user:)
146
46
  if current_user.unpaid_balance?
147
47
  return Forbidden(errors: { base: ["unpaid balance on account"] })
148
48
  end
@@ -163,22 +63,9 @@ class Orders::CreateAction
163
63
  end
164
64
  ```
165
65
 
166
- **The controller:**
66
+ Param validation, cross-field rules, authorization, error handling, and response formatting — all in one place, all testable without a request:
167
67
 
168
68
  ```ruby
169
- class OrdersController < ApplicationController
170
- def create
171
- render Orders::CreateAction.call(params:, current_user:)
172
- end
173
- end
174
- ```
175
-
176
- **Testing it:**
177
-
178
- ```ruby
179
- # test/actions/orders/create_action_test.rb
180
- require "action_figure/testing/minitest"
181
-
182
69
  class Orders::CreateActionTest < Minitest::Test
183
70
  include ActionFigure::Testing::Minitest
184
71
 
@@ -186,20 +73,19 @@ class Orders::CreateActionTest < Minitest::Test
186
73
  user = User.create!(name: "Tad")
187
74
  item = Item.create!(name: "Widget", price: 29.00)
188
75
 
189
- result = Orders::CreateAction.call(
76
+ result = Orders::CreateAction.create(
190
77
  params: { item_id: item.id, quantity: 2 },
191
78
  current_user: user
192
79
  )
193
80
 
194
81
  assert_Created(result)
195
82
  assert_equal item.id, result[:json][:data]["item_id"]
196
- assert_equal 2, result[:json][:data]["quantity"]
197
83
  end
198
84
 
199
85
  def test_forbidden_with_unpaid_balance
200
- user = User.create!(name: "Tud", balance: -1)
86
+ user = User.create!(name: "Tad", balance: -1)
201
87
 
202
- result = Orders::CreateAction.call(
88
+ result = Orders::CreateAction.create(
203
89
  params: { item_id: 1, quantity: 1 },
204
90
  current_user: user
205
91
  )
@@ -211,7 +97,7 @@ class Orders::CreateActionTest < Minitest::Test
211
97
  def test_not_found_when_item_missing
212
98
  user = User.create!(name: "Tad")
213
99
 
214
- result = Orders::CreateAction.call(
100
+ result = Orders::CreateAction.create(
215
101
  params: { item_id: 999, quantity: 1 },
216
102
  current_user: user
217
103
  )
@@ -220,49 +106,135 @@ class Orders::CreateActionTest < Minitest::Test
220
106
  assert_includes result[:json][:errors][:item_id], "item not found"
221
107
  end
222
108
 
223
- def test_surfaces_model_validation_errors
109
+ def test_rejects_partial_gift_fields
224
110
  user = User.create!(name: "Tad")
225
- item = Item.create!(name: "Widget", price: 29.00, stock: 0)
111
+ item = Item.create!(name: "Widget", price: 29.00)
226
112
 
227
- result = Orders::CreateAction.call(
228
- params: { item_id: item.id, quantity: 5 },
113
+ result = Orders::CreateAction.create(
114
+ params: { item_id: item.id, quantity: 1, gift_message: "Enjoy!" },
229
115
  current_user: user
230
116
  )
231
117
 
232
118
  assert_UnprocessableContent(result)
233
- assert_includes result[:json][:errors][:quantity], "exceeds available stock"
119
+ assert_includes result[:json][:errors][:gift_message],
120
+ "gift fields must be provided together or not at all"
234
121
  end
122
+ end
123
+ ```
235
124
 
236
- def test_rejects_partial_gift_fields
237
- user = User.create!(name: "Tad")
238
- item = Item.create!(name: "Widget", price: 29.00)
125
+ This isn't for everybody. If your controllers are already thin, or you validate through OpenAPI middleware like [committee](https://github.com/interagent/committee), you probably don't need this. ActionFigure is for teams whose controller actions have grown into tangled mixes of param wrangling, authorization checks, error handling, and response building.
239
126
 
240
- result = Orders::CreateAction.call(
241
- params: { item_id: item.id, quantity: 1, gift_message: "Enjoy!" },
242
- current_user: user
127
+ ## Installation
128
+
129
+ Add to your Gemfile and `bundle install`:
130
+
131
+ ```ruby
132
+ gem "action_figure"
133
+ ```
134
+
135
+ ## How It Works
136
+
137
+ Every action class has three responsibilities:
138
+
139
+ 1. **Check params** (optional) — when a `params_schema` is defined, it validates structure and types; `rules` enforces validation rules. If either fails, the formatter returns an error response and your action method is never invoked. Actions without a schema receive `params:` as-is.
140
+ 2. **Orchestrate** — your action method coordinates the work: creating records, calling service objects, enqueuing jobs, or anything else the action requires. The action is the entry point, not necessarily where all the logic lives.
141
+ 3. **Return a formatted response** — response helpers like `Created(resource:)` and `NotFound(errors:)` return render-ready hashes that go straight to `render` in your controller.
142
+
143
+ ## Features
144
+
145
+ | Feature | Description |
146
+ |---------|-------------|
147
+ | [Validation](docs/validation.md) | Two-layer validation powered by dry-validation: structural schemas with type coercion, plus validation rules. Includes cross-parameter helpers like `exclusive_rule`, `any_rule`, `one_rule`, and `all_rule`. |
148
+ | [Response Formatters](docs/response-formatters.md) | Four built-in formats: Default, JSend, JSON:API, and Wrapped. Each provides response helpers (`Ok`, `Created`, `NotFound`, etc.) that return render-ready hashes. |
149
+ | [Status Codes](docs/status-codes.md) | Which 4xx codes are domain concerns (handled by action classes) vs perimeter concerns (handled by middleware, router, or infrastructure). |
150
+ | [Custom Formatters](docs/custom-formatters.md) | Define your own response envelope by implementing the formatter interface. Registration validates your module at load time. |
151
+ | [Actions](docs/actions.md) | Automatic entry point discovery, context injection via keyword arguments, per-class API versioning, and `entry_point` for disambiguation. |
152
+ | [Configuration](docs/configuration.md) | Global defaults for response format, parameter strictness, and API version. All overridable per-class. |
153
+ | [Notifications](docs/activesupport-notifications.md) | Opt-in `ActiveSupport::Notifications` events for every action call. Emits action class, outcome status, and duration on the `process.action_figure` event. |
154
+ | [Testing](docs/testing.md) | Minitest assertions (`assert_Ok`, `assert_Created`, ...) and RSpec matchers (`be_Ok`, `be_Created`, ...) for expressive status checks. |
155
+ | [Integration Patterns](docs/integration-patterns.md) | Recipes for serializers (Blueprinter, Alba, Oj Serializers), authorization (Pundit, CanCanCan), and pagination (cursor, Pagy). |
156
+
157
+ ## Quick Start
158
+
159
+ **1. Define the action class.**
160
+
161
+ ```ruby
162
+ # app/actions/users/create_action.rb
163
+ class Users::CreateAction
164
+ include ActionFigure[:jsend]
165
+
166
+ params_schema do
167
+ required(:user).hash do
168
+ required(:name).filled(:string)
169
+ required(:email).filled(:string)
170
+ end
171
+ end
172
+
173
+ def create(params:, company:)
174
+ user = company.users.create(params[:user])
175
+ return UnprocessableContent(errors: user.errors.messages) if user.errors.any?
176
+
177
+ Created(resource: user.as_json(only: %i[id name email]))
178
+ end
179
+ end
180
+ ```
181
+
182
+ **2. Call it from your controller.**
183
+
184
+ ```ruby
185
+ class UsersController < ApplicationController
186
+ def create
187
+ render Users::CreateAction.create(params:, company: current_company)
188
+ end
189
+ end
190
+ ```
191
+
192
+ **3. Test it directly.**
193
+
194
+ ```ruby
195
+ class Users::CreateActionTest < Minitest::Test
196
+ include ActionFigure::Testing::Minitest
197
+
198
+ def test_creates_a_user
199
+ company = Company.create!(name: "Acme")
200
+
201
+ result = Users::CreateAction.create(
202
+ params: { user: { name: "Tad", email: "tad@example.com" } },
203
+ company: company
204
+ )
205
+
206
+ assert_Created(result)
207
+ assert_equal "Tad", result[:json][:data]["name"]
208
+ end
209
+
210
+ def test_fails_when_name_is_missing
211
+ company = Company.create!(name: "Acme")
212
+
213
+ result = Users::CreateAction.create(
214
+ params: { user: { email: "tad@example.com" } },
215
+ company: company
243
216
  )
244
217
 
245
218
  assert_UnprocessableContent(result)
246
- assert_includes result[:json][:errors][:gift_message],
247
- "gift fields must be provided together or not at all"
248
- assert_includes result[:json][:errors][:gift_recipient_email],
249
- "gift fields must be provided together or not at all"
219
+ assert_includes result[:json][:data][:user][:name], "is missing"
250
220
  end
251
221
  end
252
222
  ```
253
223
 
254
224
  ## Design Philosophy
255
225
 
226
+ Unlike general-purpose service object libraries, ActionFigure is scoped to controller actions — it validates params, runs your logic, and returns a hash you pass directly to `render`.
227
+
256
228
  - **Purpose over convention** — each class does one thing and names it clearly
257
229
  - **Explicit over implicit** — no magic method resolution, no inherited callbacks
258
- - **Operations own their lifecycle** — validation, execution, and response formatting live together
230
+ - **Actions own their lifecycle** — validation, execution, and response formatting live together
259
231
  - **Controllers become boring** — one-line `render` calls that delegate to action classes
260
- - **Models and Controllers stay thin** — business logic moves to purpose-built operations
232
+ - **Models and Controllers stay thin** — business logic moves to purpose-built action classes
261
233
 
262
234
  ## Requirements
263
235
 
264
236
  - Ruby >= 3.2
265
- - [dry-validation](https://dry-rb.org/gems/dry-validation/) ~> 1.10
237
+ - [dry-validation](https://dry-rb.org/gems/dry-validation/) ~> 1.10 — ActionFigure uses dry-validation for schema validation because it's the best tool for the job. There's no dependency injection container, no monads, no functional pipeline. Just a focused layer for controller actions.
266
238
  - Rails is not required, but ActionFigure is designed for Rails controller patterns
267
239
 
268
240
  ## License
data/docs/actions.md CHANGED
@@ -6,9 +6,9 @@ An ActionFigure action class is a single-purpose operation. Each class encapsula
6
6
 
7
7
  ---
8
8
 
9
- ## The Default: `call`
9
+ ## Naming Your Action Method
10
10
 
11
- Every action class gets a `.call` class method when it includes ActionFigure. It instantiates the class, runs the validation pipeline (if `params:` is provided), and delegates to the instance-level `#call` method.
11
+ ActionFigure auto-discovers your action method by name. Define one public instance method on your action class and ActionFigure registers it as the entry point -- no macro required:
12
12
 
13
13
  ```ruby
14
14
  class Users::CreateAction
@@ -21,7 +21,7 @@ class Users::CreateAction
21
21
  end
22
22
  end
23
23
 
24
- def call(params:, company:, **)
24
+ def create(params:, company:, **)
25
25
  user = company.users.create(params[:user])
26
26
  return UnprocessableContent(errors: user.errors.messages) if user.errors.any?
27
27
 
@@ -30,21 +30,31 @@ class Users::CreateAction
30
30
  end
31
31
  ```
32
32
 
33
- Wire it into a controller by passing `params:` and any additional context:
33
+ Wire it into a controller using the discovered method name:
34
34
 
35
35
  ```ruby
36
36
  class UsersController < ApplicationController
37
37
  def create
38
- render Users::CreateAction.call(params:, company: current_company)
38
+ render Users::CreateAction.create(params:, company: current_company)
39
39
  end
40
40
  end
41
41
  ```
42
42
 
43
- ---
43
+ ### How it works
44
+
45
+ ActionFigure uses a `method_added` hook to watch for public instance methods defined on the class. The first public method defined becomes the registered entry point and a matching class-level method is created for it. The full validation pipeline (`params_schema` and `rules`) still runs through the discovered entry point before your method is invoked.
44
46
 
45
- ## Custom Entry Points
47
+ ### Disambiguation with `entry_point`
46
48
 
47
- Some actions have a name that reads better than `.call`. The `entry_point` macro declares an alternative class-level method name:
49
+ If a class ends up with more than one public instance method, ActionFigure cannot determine which one to use and raises an `IndeterminantEntryPointError`:
50
+
51
+ ```
52
+ ActionFigure::IndeterminantEntryPointError: Multiple public methods defined in Orders::SearchAction:
53
+ :search and :format_results. Either make one private or declare
54
+ `entry_point :search` to disambiguate.
55
+ ```
56
+
57
+ Use the `entry_point` macro to resolve this:
48
58
 
49
59
  ```ruby
50
60
  class Orders::SearchAction
@@ -66,46 +76,34 @@ class Orders::SearchAction
66
76
  resource = orders.as_json(only: %i[id tracking_number status])
67
77
  Ok(resource:)
68
78
  end
69
- end
70
- ```
71
79
 
72
- Call it from a controller using the declared name:
80
+ private
73
81
 
74
- ```ruby
75
- class OrdersController < ApplicationController
76
- def index
77
- render Orders::SearchAction.search(params:, company: current_company)
82
+ def build_scope(company)
83
+ company.orders.active
78
84
  end
79
85
  end
80
86
  ```
81
87
 
82
- ### How it works
83
-
84
- - The instance method must match the declared entry point name (`:search` declares `.search` and expects `#search`). If an entry point is defined, any instance-level `#call` method is ignored by the class-level entry point.
85
- - The full validation pipeline still runs through the custom entry point -- `params_schema` and `rules` are applied before your method is invoked.
86
- - Calling `.call` on a class that declares a custom entry point raises a `NoMethodError` with a helpful message:
87
-
88
- ```
89
- NoMethodError: undefined method 'call' for Orders::SearchAction (use 'search' instead)
90
- ```
88
+ Only one entry point per class is allowed. A second `entry_point` declaration raises an `ArgumentError`:
91
89
 
92
- - Only one entry point per class is allowed. A second `entry_point` declaration raises an `ArgumentError`:
93
-
94
- ```
95
- ArgumentError: entry_point already defined as 'search' — each action class may declare only one entry point
96
- ```
90
+ ```
91
+ ArgumentError: entry_point already defined as 'search' — each action class may declare only one entry point
92
+ ```
97
93
 
98
94
  ---
99
95
 
100
- ## No-Params Actions
96
+ ## Actions Without a Schema
101
97
 
102
- Actions that don't need validated input simply omit `params_schema`. The validation pipeline is skipped entirely.
98
+ Actions that omit `params_schema` skip the validation pipeline entirely. Any `params:` passed through are delivered to your method as-is no coercion, no stripping, no validation.
99
+
100
+ This is useful when validation is handled upstream (e.g., Rack middleware like `committee` validating against an OpenAPI spec) or when the action simply doesn't need params:
103
101
 
104
102
  ```ruby
105
103
  class HealthCheckAction
106
104
  include ActionFigure[:jsend]
107
105
 
108
- def call
106
+ def check
109
107
  Ok(resource: { status: "healthy", time: Time.current })
110
108
  end
111
109
  end
@@ -114,17 +112,11 @@ end
114
112
  ```ruby
115
113
  class HealthController < ApplicationController
116
114
  def show
117
- render HealthCheckAction.call
115
+ render HealthCheckAction.check
118
116
  end
119
117
  end
120
118
  ```
121
119
 
122
- If you accidentally pass `params:` to an action that has no schema, ActionFigure raises immediately:
123
-
124
- ```
125
- ArgumentError: params: passed but no params_schema defined
126
- ```
127
-
128
120
  ---
129
121
 
130
122
  ## Context Injection
@@ -142,7 +134,7 @@ class Users::CreateAction
142
134
  end
143
135
  end
144
136
 
145
- def call(params:, company:, current_user:)
137
+ def create(params:, company:, current_user:)
146
138
  user = company.users.create(params[:user].merge(invited_by: current_user))
147
139
  return UnprocessableContent(errors: user.errors.messages) if user.errors.any?
148
140
 
@@ -154,7 +146,7 @@ end
154
146
  ```ruby
155
147
  class UsersController < ApplicationController
156
148
  def create
157
- render Users::CreateAction.call(
149
+ render Users::CreateAction.create(
158
150
  params:,
159
151
  company: current_company,
160
152
  current_user: current_user
@@ -175,7 +167,7 @@ A simple index action needs no params and no schema:
175
167
  class Users::IndexAction
176
168
  include ActionFigure[:jsend]
177
169
 
178
- def call(company:)
170
+ def index(company:)
179
171
  users = company.users.order(:name)
180
172
  Ok(resource: users.as_json(only: %i[id name email]))
181
173
  end
@@ -185,7 +177,7 @@ end
185
177
  ```ruby
186
178
  class UsersController < ApplicationController
187
179
  def index
188
- render Users::IndexAction.call(company: current_company)
180
+ render Users::IndexAction.index(company: current_company)
189
181
  end
190
182
  end
191
183
  ```
@@ -200,7 +192,7 @@ class Users::ShowAction
200
192
  required(:id).filled(:integer)
201
193
  end
202
194
 
203
- def call(params:, company:)
195
+ def show(params:, company:)
204
196
  user = company.users.find_by(id: params[:id])
205
197
  return NotFound(errors: { base: ["user not found"] }) unless user
206
198
 
@@ -212,7 +204,7 @@ end
212
204
  ```ruby
213
205
  class UsersController < ApplicationController
214
206
  def show
215
- render Users::ShowAction.call(params:, company: current_company)
207
+ render Users::ShowAction.show(params:, company: current_company)
216
208
  end
217
209
  end
218
210
  ```
@@ -230,7 +222,7 @@ class Users::CreateAction
230
222
  end
231
223
  end
232
224
 
233
- def call(params:, company:)
225
+ def create(params:, company:)
234
226
  user = company.users.create(params[:user])
235
227
  return UnprocessableContent(errors: user.errors.messages) unless user.persisted?
236
228
 
@@ -242,7 +234,7 @@ end
242
234
  ```ruby
243
235
  class UsersController < ApplicationController
244
236
  def create
245
- render Users::CreateAction.call(params:, company: current_company)
237
+ render Users::CreateAction.create(params:, company: current_company)
246
238
  end
247
239
  end
248
240
  ```
@@ -261,7 +253,7 @@ class Users::UpdateAction
261
253
  end
262
254
  end
263
255
 
264
- def call(params:, company:)
256
+ def update(params:, company:)
265
257
  user = company.users.find_by(id: params[:id])
266
258
  return NotFound(errors: { base: ["user not found"] }) unless user
267
259
 
@@ -276,7 +268,7 @@ end
276
268
  ```ruby
277
269
  class UsersController < ApplicationController
278
270
  def update
279
- render Users::UpdateAction.call(params:, company: current_company)
271
+ render Users::UpdateAction.update(params:, company: current_company)
280
272
  end
281
273
  end
282
274
  ```
@@ -291,7 +283,7 @@ class Users::DestroyAction
291
283
  required(:id).filled(:integer)
292
284
  end
293
285
 
294
- def call(params:, company:)
286
+ def destroy(params:, company:)
295
287
  user = company.users.find_by(id: params[:id])
296
288
  return NotFound(errors: { base: ["user not found"] }) unless user
297
289
 
@@ -304,7 +296,7 @@ end
304
296
  ```ruby
305
297
  class UsersController < ApplicationController
306
298
  def destroy
307
- render Users::DestroyAction.call(params:, company: current_company)
299
+ render Users::DestroyAction.destroy(params:, company: current_company)
308
300
  end
309
301
  end
310
302
  ```
@@ -325,7 +317,7 @@ class Users::BulkInviteAction
325
317
  required(:emails).value(:array, min_size?: 1).each(:str?)
326
318
  end
327
319
 
328
- def call(params:, company:)
320
+ def invite(params:, company:)
329
321
  result = BulkInviteService.call(emails: params[:emails], company: company)
330
322
  return UnprocessableContent(errors: result.errors) if result.failures?
331
323
 
@@ -337,7 +329,7 @@ end
337
329
  ```ruby
338
330
  class Users::InvitesController < ApplicationController
339
331
  def create
340
- render Users::BulkInviteAction.call(params:, company: current_company)
332
+ render Users::BulkInviteAction.invite(params:, company: current_company)
341
333
  end
342
334
  end
343
335
  ```
@@ -356,7 +348,7 @@ class Reports::GenerateAction
356
348
  end
357
349
  end
358
350
 
359
- def call(params:, current_user:)
351
+ def generate(params:, current_user:)
360
352
  result = ReportService.enqueue(params: params[:report], requested_by: current_user)
361
353
  return UnprocessableContent(errors: result.errors) if result.failed?
362
354
 
@@ -368,7 +360,7 @@ end
368
360
  ```ruby
369
361
  class ReportsController < ApplicationController
370
362
  def create
371
- render Reports::GenerateAction.call(params:, current_user: current_user)
363
+ render Reports::GenerateAction.generate(params:, current_user: current_user)
372
364
  end
373
365
  end
374
366
  ```
@@ -379,7 +371,7 @@ File imports work the same way — receive the file, hand it off, translate the
379
371
  class Products::ImportAction
380
372
  include ActionFigure[:jsend]
381
373
 
382
- def call(file:, company:)
374
+ def import(file:, company:)
383
375
  result = ProductImportService.call(file: file, company: company)
384
376
  return UnprocessableContent(errors: result.errors) if result.failed?
385
377
 
@@ -391,7 +383,7 @@ end
391
383
  ```ruby
392
384
  class Products::ImportsController < ApplicationController
393
385
  def create
394
- render Products::ImportAction.call(file: params[:file], company: current_company)
386
+ render Products::ImportAction.import(file: params[:file], company: current_company)
395
387
  end
396
388
  end
397
389
  ```
@@ -415,7 +407,7 @@ class Users::CreateAction
415
407
  end
416
408
  end
417
409
 
418
- def call(params:)
410
+ def create(params:)
419
411
  user = User.create(params[:user])
420
412
  return UnprocessableContent(errors: user.errors.messages) if user.errors.any?
421
413
 
@@ -435,7 +427,7 @@ Users::CreateAction.api_version #=> "2.0"
435
427
  Inside an action instance, access it through the class:
436
428
 
437
429
  ```ruby
438
- def call(params:, **)
430
+ def create(params:, **)
439
431
  if self.class.api_version == "2.0"
440
432
  # v2 behavior
441
433
  end