hanikamu-operation 0.1.1 → 0.1.2
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/CHANGELOG.md +7 -0
- data/README.md +66 -183
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0b23bd074385d4c3f45c13b8c247fd39e3b367567bc64b03a2a55db400f3b2a
|
|
4
|
+
data.tar.gz: d2c55ea2d5f6aac49462c6c964ccb2d81c568b37ac3cb85143285521eb85ab86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '049de88ca1deb2c3604300762ce87fb3b7dad4657f6ffcbd553c33cc086cda780971b5859e84be171ca6800dca2a75c9cbb0de439bef7ad2999bda6c96e953e8'
|
|
7
|
+
data.tar.gz: 80f4dfb5e621dd5c914fc853596828277f940c13cbbc1935d30ed64ea2b42ceb1c79bbeeb1ab14d135a768b581bca2d77a83e2c9fbcaa1a5f9dc5728852cb88e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,3 +7,10 @@
|
|
|
7
7
|
## [0.1.1] - 2025-11-26
|
|
8
8
|
|
|
9
9
|
- Updated Gemfile.lock
|
|
10
|
+
|
|
11
|
+
## [0.1.2] - 2025-12-05
|
|
12
|
+
|
|
13
|
+
- **Breaking Change**: Minimum Ruby version is now 3.4.0
|
|
14
|
+
- Removed `redis-client` as direct dependency (now transitive through `redlock`)
|
|
15
|
+
- Updated CI to test only Ruby 3.4
|
|
16
|
+
- Improved README with clearer FormError vs GuardError examples
|
data/README.md
CHANGED
|
@@ -8,9 +8,8 @@ A Ruby gem that extends [hanikamu-service](https://github.com/Hanikamu/hanikamu-
|
|
|
8
8
|
|
|
9
9
|
1. [Why Hanikamu::Operation?](#why-hanikamuoperation)
|
|
10
10
|
2. [Quick Start](#quick-start)
|
|
11
|
-
3. [
|
|
12
|
-
4. [
|
|
13
|
-
5. [Usage](#usage)
|
|
11
|
+
3. [Setup](#setup)
|
|
12
|
+
4. [Usage](#usage)
|
|
14
13
|
- [Basic Operation](#basic-operation)
|
|
15
14
|
- [Distributed Locking](#distributed-locking-with-within_mutex)
|
|
16
15
|
- [Database Transactions](#database-transactions-with-within_transaction)
|
|
@@ -18,14 +17,14 @@ A Ruby gem that extends [hanikamu-service](https://github.com/Hanikamu/hanikamu-
|
|
|
18
17
|
- [Guard Conditions](#guard-conditions)
|
|
19
18
|
- [Block Requirements](#block-requirements)
|
|
20
19
|
- [Complete Example](#complete-example-combining-all-features)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
5. [Error Handling](#error-handling)
|
|
21
|
+
6. [Best Practices](#best-practices)
|
|
22
|
+
7. [Configuration Reference](#configuration-reference)
|
|
23
|
+
8. [Testing](#testing)
|
|
24
|
+
9. [Development](#development)
|
|
25
|
+
10. [Contributing](#contributing)
|
|
26
|
+
11. [License](#license)
|
|
27
|
+
12. [Credits](#credits)
|
|
29
28
|
|
|
30
29
|
## Why Hanikamu::Operation?
|
|
31
30
|
|
|
@@ -63,9 +62,11 @@ Use `Hanikamu::Operation` (instead of plain `Hanikamu::Service`) when your busin
|
|
|
63
62
|
|
|
64
63
|
**1. Install the gem**
|
|
65
64
|
|
|
65
|
+
Requires Ruby 3.4.0 or later.
|
|
66
|
+
|
|
66
67
|
```ruby
|
|
67
68
|
# Gemfile
|
|
68
|
-
gem 'hanikamu-operation', '~> 0.1.
|
|
69
|
+
gem 'hanikamu-operation', '~> 0.1.2'
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
```bash
|
|
@@ -122,32 +123,19 @@ else
|
|
|
122
123
|
end
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
## Installation
|
|
126
|
-
|
|
127
|
-
Add to your application's Gemfile:
|
|
128
|
-
|
|
129
|
-
```ruby
|
|
130
|
-
gem 'hanikamu-operation', '~> 0.1.1'
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Then execute:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
bundle install
|
|
137
|
-
```
|
|
138
|
-
|
|
139
126
|
## Setup
|
|
140
127
|
|
|
141
|
-
### Rails Application Setup
|
|
128
|
+
### Rails Application Setup
|
|
142
129
|
|
|
143
130
|
Follow these steps to integrate Hanikamu::Operation into a Rails application:
|
|
144
131
|
|
|
145
132
|
**Step 1: Add the gem to your Gemfile**
|
|
146
133
|
|
|
134
|
+
Requires Ruby 3.4.0 or later.
|
|
135
|
+
|
|
147
136
|
```ruby
|
|
148
137
|
# Gemfile
|
|
149
|
-
gem 'hanikamu-operation', '~> 0.1.
|
|
150
|
-
gem 'redis-client', '~> 0.22' # Required for distributed locking
|
|
138
|
+
gem 'hanikamu-operation', '~> 0.1.2'
|
|
151
139
|
```
|
|
152
140
|
|
|
153
141
|
```bash
|
|
@@ -214,68 +202,7 @@ brew services start redis
|
|
|
214
202
|
REDIS_URL=redis://localhost:6379/0
|
|
215
203
|
```
|
|
216
204
|
|
|
217
|
-
**Step 5:
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
# Create operations directory
|
|
221
|
-
mkdir -p app/operations/users
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
```ruby
|
|
225
|
-
# app/operations/users/create_user_operation.rb
|
|
226
|
-
module Users
|
|
227
|
-
class CreateUserOperation < Hanikamu::Operation
|
|
228
|
-
attribute :email, Types::String
|
|
229
|
-
attribute :password, Types::String
|
|
230
|
-
|
|
231
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
232
|
-
validates :password, length: { minimum: 8 }
|
|
233
|
-
|
|
234
|
-
within_transaction(:base)
|
|
235
|
-
|
|
236
|
-
def execute
|
|
237
|
-
user = User.create!(
|
|
238
|
-
email: email,
|
|
239
|
-
password: password
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
response user: user
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Step 6: Use in your controller**
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
# app/controllers/users_controller.rb
|
|
252
|
-
class UsersController < ApplicationController
|
|
253
|
-
def create
|
|
254
|
-
result = Users::CreateUserOperation.call(user_params)
|
|
255
|
-
|
|
256
|
-
if result.success?
|
|
257
|
-
user = result.success.user
|
|
258
|
-
render json: { user: user }, status: :created
|
|
259
|
-
else
|
|
260
|
-
error = result.failure
|
|
261
|
-
case error
|
|
262
|
-
when Hanikamu::Operation::FormError
|
|
263
|
-
render json: { errors: error.errors.full_messages }, status: :unprocessable_entity
|
|
264
|
-
else
|
|
265
|
-
render json: { error: error.message }, status: :internal_server_error
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
private
|
|
271
|
-
|
|
272
|
-
def user_params
|
|
273
|
-
params.require(:user).permit(:email, :password)
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Step 7: Configure for production**
|
|
205
|
+
**Step 5: Production configuration**
|
|
279
206
|
|
|
280
207
|
Set your Redis URL in production (Heroku, AWS, etc.):
|
|
281
208
|
|
|
@@ -284,71 +211,11 @@ Set your Redis URL in production (Heroku, AWS, etc.):
|
|
|
284
211
|
heroku addons:create heroku-redis:mini
|
|
285
212
|
# REDIS_URL is automatically set
|
|
286
213
|
|
|
287
|
-
# Or set manually
|
|
214
|
+
# Or set manually for other providers
|
|
288
215
|
heroku config:set REDIS_URL=redis://your-redis-host:6379/0
|
|
289
216
|
```
|
|
290
217
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
If you need more control, create a detailed initializer:
|
|
294
|
-
|
|
295
|
-
```ruby
|
|
296
|
-
# config/initializers/hanikamu_operation.rb
|
|
297
|
-
require 'redis-client'
|
|
298
|
-
|
|
299
|
-
Hanikamu::Operation.configure do |config|
|
|
300
|
-
# Required: Redis client for distributed locking
|
|
301
|
-
config.redis_client = RedisClient.new(
|
|
302
|
-
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
|
|
303
|
-
reconnect_attempts: 3,
|
|
304
|
-
timeout: 1.0
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
# Optional: Customize Redlock settings (these are the defaults)
|
|
308
|
-
config.mutex_expire_milliseconds = 1500 # Lock TTL
|
|
309
|
-
config.redlock_retry_count = 6 # Number of retry attempts
|
|
310
|
-
config.redlock_retry_delay = 500 # Milliseconds between retries
|
|
311
|
-
config.redlock_retry_jitter = 50 # Random jitter to prevent thundering herd
|
|
312
|
-
config.redlock_timeout = 0.1 # Redis command timeout
|
|
313
|
-
|
|
314
|
-
# Optional: Add errors to whitelist (Redlock::LockError is always included by default)
|
|
315
|
-
config.whitelisted_errors = [CustomBusinessError]
|
|
316
|
-
end
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Redis Setup by Environment
|
|
320
|
-
|
|
321
|
-
**For Development (Docker Compose)**:
|
|
322
|
-
|
|
323
|
-
```yaml
|
|
324
|
-
# docker-compose.yml
|
|
325
|
-
services:
|
|
326
|
-
redis:
|
|
327
|
-
image: redis:7-alpine
|
|
328
|
-
volumes:
|
|
329
|
-
- redis_data:/data
|
|
330
|
-
networks:
|
|
331
|
-
- app_network
|
|
332
|
-
|
|
333
|
-
app:
|
|
334
|
-
# ... your app config
|
|
335
|
-
environment:
|
|
336
|
-
REDIS_URL: redis://redis:6379/0
|
|
337
|
-
depends_on:
|
|
338
|
-
- redis
|
|
339
|
-
networks:
|
|
340
|
-
- app_network
|
|
341
|
-
|
|
342
|
-
volumes:
|
|
343
|
-
redis_data:
|
|
344
|
-
|
|
345
|
-
networks:
|
|
346
|
-
app_network:
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
**For Production**:
|
|
350
|
-
|
|
351
|
-
Use a managed Redis service (AWS ElastiCache, Heroku Redis, Redis Labs, etc.) and set the `REDIS_URL` environment variable.
|
|
218
|
+
For advanced configuration options, see the [Configuration Reference](#configuration-reference) section below.
|
|
352
219
|
|
|
353
220
|
## Usage
|
|
354
221
|
|
|
@@ -764,52 +631,68 @@ Operations validate at three distinct levels, each serving a specific purpose:
|
|
|
764
631
|
|
|
765
632
|
### FormError vs GuardError: Practical Examples
|
|
766
633
|
|
|
767
|
-
|
|
634
|
+
Here's a complete example demonstrating the difference between form validations and guard conditions:
|
|
768
635
|
|
|
769
636
|
```ruby
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
#
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
637
|
+
class TestOperation < Hanikamu::Operation
|
|
638
|
+
attribute :sentence, Types::String
|
|
639
|
+
|
|
640
|
+
# Form validation - checks input value format/content
|
|
641
|
+
validates :sentence, presence: true
|
|
642
|
+
validates :sentence, exclusion: { in: ["form_error"], message: "is not allowed" }
|
|
643
|
+
|
|
644
|
+
# Guard validation - checks business rules/state
|
|
645
|
+
guard do
|
|
646
|
+
delegates :sentence
|
|
647
|
+
|
|
648
|
+
validates :sentence, exclusion: { in: ["guard_error"], message: "is not allowed" }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
within_mutex(:mutex_lock)
|
|
652
|
+
within_transaction(:base)
|
|
653
|
+
|
|
654
|
+
def execute
|
|
655
|
+
response(message: sentence.reverse)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def mutex_lock
|
|
659
|
+
self.class.name
|
|
660
|
+
end
|
|
661
|
+
end
|
|
788
662
|
```
|
|
789
663
|
|
|
790
|
-
**
|
|
664
|
+
**FormError Example** - Invalid input value triggers form validation:
|
|
791
665
|
|
|
792
666
|
```ruby
|
|
793
|
-
#
|
|
794
|
-
result =
|
|
795
|
-
# =>
|
|
667
|
+
# Form validation fails - input itself is invalid
|
|
668
|
+
result = TestOperation.call(sentence: "form_error")
|
|
669
|
+
# => Failure(#<Hanikamu::Operation::FormError: Sentence is not allowed>)
|
|
670
|
+
|
|
671
|
+
# Correcting the input allows the operation to proceed
|
|
672
|
+
result = TestOperation.call(sentence: "hello world")
|
|
673
|
+
# => Success(#<struct message="dlrow olleh">)
|
|
674
|
+
```
|
|
796
675
|
|
|
797
|
-
|
|
798
|
-
result = Users::CompleteUserOperation.call!(user_id: 46)
|
|
799
|
-
# => Failure(#<Hanikamu::Operation::GuardError: User has already been completed>)
|
|
676
|
+
**GuardError Example** - Valid input but business rule violated:
|
|
800
677
|
|
|
801
|
-
|
|
802
|
-
#
|
|
678
|
+
```ruby
|
|
679
|
+
# Input passes form validation, but guard fails
|
|
680
|
+
result = TestOperation.call(sentence: "guard_error")
|
|
681
|
+
# => Failure(#<Hanikamu::Operation::GuardError: Sentence is not allowed>)
|
|
682
|
+
|
|
683
|
+
# The input format is valid, but the business rule prevents execution
|
|
803
684
|
```
|
|
804
685
|
|
|
805
686
|
**Type Error Example** - Wrong argument type:
|
|
806
687
|
|
|
807
688
|
```ruby
|
|
808
|
-
# Passing wrong type raises immediately
|
|
809
|
-
|
|
689
|
+
# Passing wrong type raises immediately before any validations
|
|
690
|
+
TestOperation.call!(sentence: 123)
|
|
810
691
|
# => Raises Dry::Struct::Error
|
|
811
692
|
```
|
|
812
693
|
|
|
694
|
+
**Key Insight**: Form validations check if the *input is correct*, while guards check if the *operation can proceed* given the current state.
|
|
695
|
+
|
|
813
696
|
### Using `.call!` (Raises Exceptions)
|
|
814
697
|
|
|
815
698
|
```ruby
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanikamu-operation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolai Seerup
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2025-
|
|
12
|
+
date: 2025-12-05 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activemodel
|
|
@@ -132,7 +132,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
132
132
|
requirements:
|
|
133
133
|
- - ">="
|
|
134
134
|
- !ruby/object:Gem::Version
|
|
135
|
-
version: 3.
|
|
135
|
+
version: 3.4.0
|
|
136
136
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
137
|
requirements:
|
|
138
138
|
- - ">="
|