hanikamu-operation 0.1.2 → 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: c0b23bd074385d4c3f45c13b8c247fd39e3b367567bc64b03a2a55db400f3b2a
4
- data.tar.gz: d2c55ea2d5f6aac49462c6c964ccb2d81c568b37ac3cb85143285521eb85ab86
3
+ metadata.gz: b935969d9ffb9b34935f54e2f9d8adf907603906916a4c1b555499bd25e97f28
4
+ data.tar.gz: 2fada67131e16535eb5c6480c3a80999315efd0306753aae615a6a582d9cb43d
5
5
  SHA512:
6
- metadata.gz: '049de88ca1deb2c3604300762ce87fb3b7dad4657f6ffcbd553c33cc086cda780971b5859e84be171ca6800dca2a75c9cbb0de439bef7ad2999bda6c96e953e8'
7
- data.tar.gz: 80f4dfb5e621dd5c914fc853596828277f940c13cbbc1935d30ed64ea2b42ceb1c79bbeeb1ab14d135a768b581bca2d77a83e2c9fbcaa1a5f9dc5728852cb88e
6
+ metadata.gz: cc5970b13fd6511a024b14071b273350604ef811f8bb944ca03f6839ed7f03051d106c63f550225f2b4de181d60c7698f545ecd1219faa1bb813aaa76be340a3
7
+ data.tar.gz: a691099d6fb2be177d4e19bcfff74e9760716465b2a9ceaff2fa0df33a63027e199377749e285f4a8b9cad02224c93e347e4b229eab60f159aa2d4bc5e585454
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Development environment runs via docker-compose. Use Makefile targets instead of local commands.
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Docker-Compose Development Environment
7
+
8
+ This project runs entirely inside Docker. Never run Ruby, Bundler, or Redis commands directly on the host.
9
+
10
+ ## Makefile Targets
11
+
12
+ Use these instead of raw commands:
13
+
14
+ | Task | Command |
15
+ |------|---------|
16
+ | Run tests | `make rspec` |
17
+ | Run linter (auto-fix) | `make cops` |
18
+ | Open a shell in the container | `make shell` |
19
+ | Open an IRB console | `make console` |
20
+ | Install gems & rebuild | `make bundle` |
21
+ | Build the Docker image | `make build` |
22
+
23
+ ## Important
24
+
25
+ - All `make` targets use `docker-compose run --rm app` under the hood.
26
+ - Redis is provided as a linked service (`redis://redis:6379/15`) — no local Redis needed.
27
+ - After changing the Gemfile or gemspec, run `make build` to reinstall dependencies.
28
+ - Never use `bundle exec` directly on the host; always go through `make` or `make shell`.
29
+ - Run `make` commands directly without `cd` — use the `working_directory` parameter instead.
30
+ - After every code change, run `make rspec` to verify specs still pass before moving on.
@@ -0,0 +1,5 @@
1
+ {
2
+ "rubyLsp.rubyVersionManager": {
3
+ "identifier": "mise"
4
+ }
5
+ }
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # Base image
2
- FROM ruby:3.4.4
2
+ FROM ruby:4.0
3
3
 
4
4
  WORKDIR "/app"
5
5
 
data/Makefile CHANGED
@@ -14,6 +14,10 @@ console: ## build the image
14
14
  rspec: ## build the image
15
15
  docker-compose run --rm app bash -c "bundle exec rspec"
16
16
 
17
+ .PHONY: bundle
18
+ bundle: ## install gems and rebuild image
19
+ docker-compose run --rm app bundle install
20
+ $(MAKE) build
17
21
 
18
22
  .PHONY: cops
19
23
  cops: ## build the image
data/README.md CHANGED
@@ -66,7 +66,7 @@ Requires Ruby 3.4.0 or later.
66
66
 
67
67
  ```ruby
68
68
  # Gemfile
69
- gem 'hanikamu-operation', '~> 0.1.2'
69
+ gem 'hanikamu-operation', '~> 0.2.0'
70
70
  ```
71
71
 
72
72
  ```bash
@@ -135,7 +135,7 @@ Requires Ruby 3.4.0 or later.
135
135
 
136
136
  ```ruby
137
137
  # Gemfile
138
- gem 'hanikamu-operation', '~> 0.1.2'
138
+ gem 'hanikamu-operation', '~> 0.2.0'
139
139
  ```
140
140
 
141
141
  ```bash
@@ -332,6 +332,38 @@ def mutex_lock
332
332
  end
333
333
  ```
334
334
 
335
+ **Conditional locking with `:if` / `:unless`**:
336
+
337
+ When the lock key is derived from an optional attribute, a `nil` value produces a degenerate shared key (e.g., `"Order$"`) that serializes all operations. Use `:if` or `:unless` to skip the Redis lock entirely when it isn't needed:
338
+
339
+ ```ruby
340
+ class ProcessOrderOperation < Hanikamu::Operation
341
+ attribute :order_id?, Types::Params::Integer.optional
342
+
343
+ within_mutex(:mutex_lock, if: -> { order_id.present? })
344
+
345
+ def execute
346
+ # ...
347
+ response(successful: true)
348
+ end
349
+
350
+ def mutex_lock
351
+ "Order$#{order_id}"
352
+ end
353
+ end
354
+ ```
355
+
356
+ - When `order_id` is present, the lock is acquired on `"Order$42"` as usual.
357
+ - When `order_id` is `nil`, no Redis round-trip occurs — the operation runs without a lock.
358
+
359
+ The `:unless` option works inversely:
360
+
361
+ ```ruby
362
+ within_mutex(:mutex_lock, unless: -> { order_id.nil? })
363
+ ```
364
+
365
+ The lambda is evaluated via `instance_exec` on the operation instance, so it has access to all attributes and methods. Providing both `:if` and `:unless` raises `ArgumentError`.
366
+
335
367
  ### Database Transactions with `within_transaction`
336
368
 
337
369
  Ensure multiple database changes succeed or fail together atomically. If any database operation raises an exception, all changes are rolled back.
@@ -6,7 +6,7 @@ module Hanikamu
6
6
  class Operation < Hanikamu::Service
7
7
  include ActiveModel::Validations
8
8
 
9
- Error = Class.new(Hanikamu::Service::Error)
9
+ class Error < Hanikamu::Service::Error; end
10
10
 
11
11
  # Error classes
12
12
  class FormError < Hanikamu::Service::Error
@@ -91,9 +91,15 @@ module Hanikamu
91
91
  end
92
92
 
93
93
  # DSL methods
94
- def within_mutex(lock_key, expire_milliseconds: nil)
94
+ def within_mutex(lock_key, expire_milliseconds: nil, if: nil, unless: nil)
95
+ if_condition = binding.local_variable_get(:if)
96
+ unless_condition = binding.local_variable_get(:unless)
97
+ validate_mutex_conditions!(if_condition, unless_condition)
98
+
95
99
  @_mutex_lock_key = lock_key
96
100
  @_mutex_expire_milliseconds = expire_milliseconds || Hanikamu::Operation.config.mutex_expire_milliseconds
101
+ @_mutex_if_condition = if_condition
102
+ @_mutex_unless_condition = unless_condition
97
103
  end
98
104
 
99
105
  def within_transaction(klass)
@@ -144,7 +150,24 @@ module Hanikamu
144
150
  end
145
151
  # rubocop:enable Metrics/MethodLength
146
152
 
147
- attr_reader :_mutex_lock_key, :_mutex_expire_milliseconds, :_transaction_klass, :_block
153
+ attr_reader :_mutex_lock_key, :_mutex_expire_milliseconds, :_mutex_if_condition, :_mutex_unless_condition,
154
+ :_transaction_klass, :_block
155
+
156
+ private
157
+
158
+ def validate_mutex_conditions!(if_condition, unless_condition)
159
+ if if_condition && unless_condition
160
+ raise ArgumentError, "Cannot specify both :if and :unless conditions for within_mutex"
161
+ end
162
+
163
+ if if_condition && !if_condition.respond_to?(:call)
164
+ raise ArgumentError, "within_mutex :if condition must be a callable (e.g. a Proc or lambda)"
165
+ end
166
+
167
+ return unless unless_condition && !unless_condition.respond_to?(:call)
168
+
169
+ raise ArgumentError, "within_mutex :unless condition must be a callable (e.g. a Proc or lambda)"
170
+ end
148
171
  end
149
172
 
150
173
  def call!(&block)
@@ -167,15 +190,11 @@ module Hanikamu
167
190
  end
168
191
 
169
192
  def within_mutex!(&)
170
- return yield if _lock_key.nil?
171
-
172
- Hanikamu::Operation.redis_lock.lock!(_lock_key, self.class._mutex_expire_milliseconds, &)
173
- end
174
-
175
- def _lock_key
176
- return if self.class._mutex_lock_key.blank?
193
+ return yield if self.class._mutex_lock_key.blank?
194
+ return yield unless _should_apply_mutex?
177
195
 
178
- public_send(self.class._mutex_lock_key)
196
+ lock_key = public_send(self.class._mutex_lock_key)
197
+ Hanikamu::Operation.redis_lock.lock!(lock_key, self.class._mutex_expire_milliseconds, &)
179
198
  end
180
199
 
181
200
  def within_transaction!(&)
@@ -186,6 +205,13 @@ module Hanikamu
186
205
 
187
206
  private
188
207
 
208
+ def _should_apply_mutex?
209
+ return false if self.class._mutex_if_condition && !instance_exec(&self.class._mutex_if_condition)
210
+ return false if self.class._mutex_unless_condition && instance_exec(&self.class._mutex_unless_condition)
211
+
212
+ true
213
+ end
214
+
189
215
  def transaction_class
190
216
  return if self.class._transaction_klass.nil?
191
217
  return ActiveRecord::Base if self.class._transaction_klass == :base
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanikamu-operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolai Seerup
8
8
  - Alejandro Jimenez
9
- autorequire:
10
9
  bindir: exe
11
10
  cert_chain: []
12
- date: 2025-12-05 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activemodel
@@ -102,11 +101,12 @@ dependencies:
102
101
  description: |
103
102
  Ruby gem for building robust service operations with guard validations, distributed mutex locks via Redlock,
104
103
  database transactions, and comprehensive error handling. Thread-safe and designed for production Rails applications.
105
- email:
106
104
  executables: []
107
105
  extensions: []
108
106
  extra_rdoc_files: []
109
107
  files:
108
+ - ".cursor/rules/docker-environment.mdc"
109
+ - ".vscode/settings.json"
110
110
  - CHANGELOG.md
111
111
  - Dockerfile
112
112
  - LICENSE.txt
@@ -124,7 +124,6 @@ metadata:
124
124
  source_code_uri: https://github.com/Hanikamu/hanikamu-operation
125
125
  changelog_uri: https://github.com/Hanikamu/hanikamu-operation/blob/main/CHANGELOG.md
126
126
  rubygems_mfa_required: 'true'
127
- post_install_message:
128
127
  rdoc_options: []
129
128
  require_paths:
130
129
  - lib
@@ -139,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
138
  - !ruby/object:Gem::Version
140
139
  version: '0'
141
140
  requirements: []
142
- rubygems_version: 3.5.3
143
- signing_key:
141
+ rubygems_version: 4.0.3
144
142
  specification_version: 4
145
143
  summary: Service objects with guards, distributed locks, and transactions
146
144
  test_files: []