rails_soft_lock 0.2.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: 9f6443555d20169a04a8c16dbe970b56fca4f3befe6a840bea0f1385e6783524
4
- data.tar.gz: '08382ac46d6a3fe067e4899721da2b217ccd74065bf76061886509694ab6c85a'
3
+ metadata.gz: '059033a8b8115c0c301d705a1cfdeb412038ca9d7c5901fdba21400c591592c1'
4
+ data.tar.gz: febced8d04b074e5acb9a62d913e55d3e1e1b0e6c4328d957249e7bd7f98cb60
5
5
  SHA512:
6
- metadata.gz: a1bd7a8346ccfc3e17b12f632847627bd313ae27bb2cc641e437524f932d708cd93f2edcf9e7694a8e5b9df81efab8b1c76006d67fb7508fe5ad5e8df48797d6
7
- data.tar.gz: 892296a9f41bf10c6c61eca479b42bd6f79892cb2dfbe9762f978080b634630b34f97178759259f0efa20db7bbed25ac059403a83e44cf3febcc3175932f6fd6
6
+ metadata.gz: f0b8ea015e43aafd3c624831ad2fbbbde4fa50b733268ec51f2e00654e76165c3653a5112177b79cc537da785cc1603af8eb273227bac012856c69d6ce01ac2b
7
+ data.tar.gz: 42658703a0637fb10038f0bac0bdba3e3c06a710b60bb1539e2435c59ea1f27917c25ba8d06476672c2e64864c6f2cd50aaec52da6392dc3ce89057debeba91f
data/.reek.yml ADDED
@@ -0,0 +1,4 @@
1
+ detectors:
2
+ UncommunicativeVariableName:
3
+ accept:
4
+ - e
data/Rakefile CHANGED
@@ -9,4 +9,8 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ require "reek/rake/task"
13
+
14
+ Reek::Rake::Task.new
15
+
16
+ task default: %i[spec rubocop reek]
@@ -2,41 +2,38 @@
2
2
 
3
3
  # lib/rails_soft_lock/configuration.rb
4
4
  module RailsSoftLock
5
- # Configuration class for RailsSoftLock gem.
5
+ # Configuration for global settings like adapter and user class.
6
6
  class Configuration
7
- # List of supported adapters.
7
+ # Supported storage adapters.
8
8
  VALID_ADAPTERS = %i[redis nats memcached].freeze
9
- attr_reader :adapter
10
- # :reek:Attribute
11
- attr_accessor :adapter_options
9
+
10
+ attr_reader :adapter, :adapter_options
12
11
  # :reek:Attribute
13
12
  attr_writer :locked_by_class
14
13
 
14
+ # Initializes configuration with default values.
15
15
  def initialize
16
- @adapter = :redis # Default adapter
17
- @adapter_options ||= RedisConfig.default_adapter_options # Default adapter options
16
+ @adapter = :redis
17
+ @adapter_options = RedisConfig.default_adapter_options
18
18
  @locked_by_class = locked_by_class || "User"
19
19
  end
20
20
 
21
- def acts_as_locked_by(attribute = :lock_attribute, scope: -> { "none" })
22
- @acts_as_locked_options = { by: attribute, scope: scope }
23
- end
24
-
25
- def acts_as_locked_attribute
26
- @acts_as_locked_options&.[](:by) || :lock_attribute
27
- end
28
-
29
- def acts_as_locked_scope
30
- @acts_as_locked_options&.[](:scope)&.call || "none"
21
+ def adapter_options=(options = {})
22
+ @adapter_options = options.empty? ? RedisConfig.default_adapter_options : options
31
23
  end
32
24
 
25
+ # Returns the locker class constant (e.g., User).
26
+ #
27
+ # @return [Class]
33
28
  def locked_by_class
34
29
  @locked_by_class.is_a?(String) ? @locked_by_class.constantize : @locked_by_class
35
30
  end
36
31
 
37
- # Sets the adapter and validates it.
38
- # @param value [Symbol] The adapter to use.
39
- # @raise [ArgumentError] If the adapter is not supported.
32
+ # Sets the adapter (e.g., :redis, :nats, :memcached).
33
+ #
34
+ # @param value [Symbol] one of the VALID_ADAPTERS
35
+ # @raise [ArgumentError] if adapter is not valid
36
+ # @return [void]
40
37
  def adapter=(value)
41
38
  raise ArgumentError, "Adapter must be one of: #{VALID_ADAPTERS.join(", ")}" unless VALID_ADAPTERS.include?(value)
42
39
 
@@ -9,17 +9,20 @@ module RailsSoftLock
9
9
  # - object_key: The identifier of the lock instance, typically a unique database record ID
10
10
  # - object_value: The identifier of the locker that locked the record
11
11
  class LockObject
12
- # Attach adapter based on config
12
+ # Maps adapter symbols to their respective modules
13
+ ADAPTER_MAP = {
14
+ redis: RailsSoftLock::RedisAdapter
15
+ # nats: RailsSoftLock::NatsAdapter,
16
+ # memcached: RailsSoftLock::MemcachedAdapter
17
+ }.freeze
18
+
19
+ # Selects and returns the adapter module based on current configuration.
20
+ #
21
+ # @return [Module] adapter module (e.g., RedisAdapter)
22
+ # @raise [ArgumentError] if adapter is not recognized
13
23
  def self.adapter
14
- case RailsSoftLock.configuration.adapter
15
- when :redis
16
- RailsSoftLock::RedisAdapter
17
- when :nats
18
- RailsSoftLock::NatsAdapter
19
- when :memcached
20
- RailsSoftLock::MemcachedAdapter
21
- else
22
- raise ArgumentError, "Unknown adapter: #{RailsSoftLock.configuration.adapter}"
24
+ ADAPTER_MAP.fetch(RailsSoftLock.configuration.adapter) do |key|
25
+ raise ArgumentError, "Unknown adapter: #{key}"
23
26
  end
24
27
  end
25
28
 
@@ -43,7 +46,7 @@ module RailsSoftLock
43
46
  # @return [Hash] { has_locked: Boolean, locked_by: String or nil }
44
47
  def lock_or_find
45
48
  locked_object = get
46
- { has_locked: create, locked_by: locked_object || object_value }
49
+ { has_locked: !create, locked_by: locked_object || object_value }
47
50
  end
48
51
 
49
52
  # Unlocks the object
@@ -1,74 +1,119 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # lib/rails_soft_lock/model_extensions.rb
4
-
5
4
  module RailsSoftLock
6
5
  # Extend model and give methods from gem
7
6
  module ModelExtensions
8
7
  extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :locked_attribute, default: :id
11
+ class_attribute :lock_scope_proc, default: -> { "none" }
12
+
13
+ delegate :object_name, to: :class
14
+ end
15
+
9
16
  class_methods do
10
- # :reek:UtilityFunction
11
- def acts_as_locked_by(attribute = :lock_attribute, scope: -> { "none" })
12
- RailsSoftLock.configuration.acts_as_locked_by(attribute, scope: scope)
17
+ # Defines which attribute to use for locking and an optional scoping block.
18
+ #
19
+ # @param attribute [Symbol] the attribute used for locking (default: :id)
20
+ # @param scope [Proc] a proc returning the scoping value (default: -> { "none" })
21
+ # @return [void]
22
+ def acts_as_locked_by(attribute = nil, scope: nil)
23
+ unless attribute
24
+ raise InvalidArgumentError,
25
+ "[RailsSoftLock.acts_as_locked_by] Argument 'attribute' is required"
26
+ end
27
+
28
+ self.locked_attribute = attribute
29
+ self.lock_scope_proc = scope if scope
13
30
  end
14
31
 
32
+ # Fetches all locks for the model.
33
+ #
34
+ # @return [Hash]
15
35
  def all_locks
16
- RailsSoftLock::LockObject.new(object_name: object_name).all_locks
36
+ LockObject.new(object_name: object_name).all_locks
17
37
  end
18
38
 
39
+ # Unlocks a specific object key.
40
+ #
41
+ # @param object_key [String, Integer]
42
+ # @return [Boolean]
19
43
  def unlock(object_key)
20
- RailsSoftLock::LockObject.new(object_name: object_name, object_key: object_key).unlock
44
+ LockObject.new(object_name: object_name, object_key: object_key).unlock
21
45
  end
22
46
 
47
+ # Returns the composed object name (model + scope).
48
+ #
49
+ # @return [String]
23
50
  def object_name
24
- scope = RailsSoftLock.configuration.acts_as_locked_scope
25
- "#{name}::#{scope}"
51
+ "#{name}::#{lock_scope_proc&.call}"
26
52
  end
27
53
  end
28
54
 
29
- included do # rubocop:disable Metrics/BlockLength
30
- delegate :object_name, to: :class
31
-
32
- def lock_or_find(user)
33
- lock_object_for(user).lock_or_find
34
- end
55
+ # Attempts to acquire a lock for the given user.
56
+ #
57
+ # @param user [User]
58
+ # @return [Hash]
59
+ def lock_or_find(user)
60
+ lock_object_for(user).lock_or_find
61
+ end
35
62
 
36
- def unlock(user)
37
- lock_object_for(user).unlock
38
- end
63
+ # Unlocks the object for the given user.
64
+ #
65
+ # @param user [User]
66
+ # @return [Boolean]
67
+ def unlock(user)
68
+ lock_object_for(user).unlock
69
+ end
39
70
 
40
- def locked?
41
- locked_by.present?
42
- end
71
+ # Checks if the object is locked.
72
+ #
73
+ # @return [Boolean]
74
+ def locked?
75
+ locked_by.present?
76
+ end
43
77
 
44
- def locked_by
45
- user_id = base_lock_object.locked_by&.to_i
46
- user_id ? RailsSoftLock.configuration.locked_by_class.find_by(id: user_id) : nil
47
- end
78
+ # Returns the user who locked the object, if any.
79
+ #
80
+ # @return [User, nil]
81
+ def locked_by
82
+ user_id = base_lock_object.locked_by&.to_i
83
+ user_id ? RailsSoftLock.configuration.locked_by_class.find_by(id: user_id) : nil
84
+ end
48
85
 
49
- def object_key
50
- attribute = RailsSoftLock.configuration.acts_as_locked_attribute
51
- raise ArgumentError, "No locked attribute defined" if attribute == :none
86
+ # Returns the lock attribute value for the instance.
87
+ #
88
+ # @return [Object]
89
+ def object_key
90
+ model = self.class
91
+ attribute = model.locked_attribute
52
92
 
53
- send(attribute)
93
+ if (value = try(attribute))
94
+ value
95
+ else
96
+ raise NoMethodError,
97
+ "[RailsSoftLock.object_key] Model #{model} does not respond to :#{attribute}"
54
98
  end
99
+ end
55
100
 
56
- private
101
+ private
57
102
 
58
- def lock_object_for(user)
59
- RailsSoftLock::LockObject.new(
60
- object_name: object_name,
61
- object_key: object_key,
62
- object_value: user.id.to_s
63
- )
64
- end
103
+ def lock_object_for(user)
104
+ build_lock_object(object_value: user.id.to_s)
105
+ end
65
106
 
66
- def base_lock_object
67
- RailsSoftLock::LockObject.new(
68
- object_name: object_name,
69
- object_key: object_key
70
- )
71
- end
107
+ def base_lock_object
108
+ build_lock_object
109
+ end
110
+
111
+ def build_lock_object(object_value: nil)
112
+ RailsSoftLock::LockObject.new(
113
+ object_name: object_name,
114
+ object_key: object_key,
115
+ object_value: object_value
116
+ )
72
117
  end
73
118
  end
74
119
  end
@@ -9,21 +9,9 @@ module RailsSoftLock
9
9
  # Adapter for store lock in redis
10
10
  module RedisAdapter
11
11
  # Initialize Redis client
12
- def redis_client # rubocop:disable Metrics/MethodLength
12
+ def redis_client
13
13
  @redis_client ||= begin
14
- redis_config = RailsSoftLock.configuration.adapter_options.fetch(:redis, {})
15
-
16
- # Set config defaults
17
- defaults = {
18
- url: "redis://localhost:6379/0",
19
- timeout: 5
20
- }
21
- # Merge config options
22
- config = defaults.merge(redis_config)
23
-
24
- ConnectionPool::Wrapper.new do
25
- Redis.new(**config)
26
- end
14
+ ConnectionPool::Wrapper.new { Redis.new(**RailsSoftLock.configuration.adapter_options[:redis]) }
27
15
  rescue Redis::CannotConnectError => e
28
16
  raise RailsSoftLock::Error, "Failed to connect to Redis: #{e.message}"
29
17
  end
@@ -42,7 +30,7 @@ module RailsSoftLock
42
30
  transaction.hsetnx(@object_name, @object_key, @object_value)
43
31
  transaction.hget(@object_name, @object_key)
44
32
  end
45
- !result.first # has_locked - false id created
33
+ result.first # true on creation, false otherwise
46
34
  end
47
35
 
48
36
  # Updates the value for an existing key or creates a new key-value pair
@@ -18,6 +18,13 @@ module RailsSoftLock
18
18
  { redis: config_with_defaults }
19
19
  end
20
20
 
21
+ # The default Redis connection settings
22
+ # @return [Hash]
23
+ # @api private
24
+ def default_settings
25
+ { url: "redis://localhost:6379/0", timeout: 5 }
26
+ end
27
+
21
28
  # Merges default settings with any Rails-specific configuration
22
29
  # @return [Hash] Complete Redis configuration
23
30
  # @note Will return just defaults if Rails isn't available
@@ -29,13 +36,7 @@ module RailsSoftLock
29
36
  # Checks if Rails environment is available and properly configured
30
37
  # @return [Boolean]
31
38
  def rails_available?
32
- if defined?(Rails) &&
33
- Rails.respond_to?(:application) &&
34
- Rails.application
35
- true
36
- else
37
- false
38
- end
39
+ !!(defined?(Rails) && Rails.try(:application).present?)
39
40
  end
40
41
 
41
42
  # Attempts to load Redis config from Rails application
@@ -55,14 +56,7 @@ module RailsSoftLock
55
56
  # @return [Boolean]
56
57
  # @api private
57
58
  def rails_config_available?
58
- Rails.application.respond_to?(:config_for)
59
- end
60
-
61
- # The default Redis connection settings
62
- # @return [Hash]
63
- # @api private
64
- def default_settings
65
- { url: "redis://localhost:6379/0", timeout: 5 }
59
+ !!Rails.application.try(:config_for, :redis)
66
60
  end
67
61
  end
68
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSoftLock
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -8,6 +8,10 @@ require "zeitwerk"
8
8
  module RailsSoftLock
9
9
  # Error class for gem
10
10
  class Error < StandardError; end
11
+ # Error class for Argument required error
12
+ class InvalidArgumentError < Error; end
13
+ # Error class for undef attribute for Model class
14
+ class NoMethodError < Error; end
11
15
 
12
16
  def self.lock_manager(object_name:, object_key: nil, object_value: nil)
13
17
  @lock_manager ||= LockObject.new(
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_soft_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Arkhipov
8
+ - Georgy Shcherbakov
8
9
  - Vladimir Peskov
9
10
  bindir: lib
10
11
  cert_chain: []
11
- date: 2025-04-28 00:00:00.000000000 Z
12
+ date: 2025-05-17 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: connection_pool
@@ -41,11 +42,13 @@ dependencies:
41
42
  description: Using In-Memory Databases to Work with Rails Active Record Locks
42
43
  email:
43
44
  - sergey-arkhipov@ya.ru
45
+ - lordsynergymail@gmail.com
44
46
  - v.peskov@mail.ru
45
47
  executables: []
46
48
  extensions: []
47
49
  extra_rdoc_files: []
48
50
  files:
51
+ - ".reek.yml"
49
52
  - ".rspec"
50
53
  - ".rubocop.yml"
51
54
  - ".ruby-version"