attribute_guard 1.0.1 → 1.1.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: 00ca78d9fb2f442ce8c5d58b0950e6ae2406e7317cc0060b572083d85705b247
4
- data.tar.gz: 447523ace443fcdeeb1312ea118e2c39dc0e8195cecb5e647aed1f70f4cb0c41
3
+ metadata.gz: 88ec907e7391560c5a2690875407a3caa1ba387c6b52896d222d17964babcec9
4
+ data.tar.gz: 909244a8bc6e31c80574f27923e88f76b94e7a16720c75878b1ab83bf2b560f0
5
5
  SHA512:
6
- metadata.gz: d4c1f7e90bb839de650cf4f272c6269106136bd03e91f86674bd73b9d00c3da76ec43741b8389c57d4fc9cef5d505e27ced1e548e823941b34e89048a583fee6
7
- data.tar.gz: 11a93fe37a9db10b1de8e41e511f06d80abbc4cbe2670f193f6d7208cd9247218f6245c9d696aef716da9e55e5a26fd24e1a4a4112cb4f634620df66972d3413
6
+ metadata.gz: d00b0a3d6cee7b7cf3aa246129214dad28218536284a6d411aebd718711aed7f381921a6d5db196e02a89e9377f902fc096440efef218d93f3e94ed99f0e09bb
7
+ data.tar.gz: 0a378c08a53d9b858581739ccdcec49a06d75ec1df1dd49681ea50a979d848abebe16a6087cbe669048c69dd714ba665e50fd835978b6c6b974943b217cf0756
data/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.1.1
8
+
9
+ ### Fixed
10
+
11
+ - Fixed keyword arguments not being forwarded through the prepended constructor, which raised an `ArgumentError` when including `AttributeGuard` in a class whose `initialize` method takes keyword arguments.
12
+ - `unlock_attributes` now returns the record when called with an empty attribute list instead of `nil` so method chaining works as documented.
13
+ - Unlocked attributes are no longer shared between a record and its `dup` or `clone`; unlocking attributes on a copy no longer unlocks them on the original.
14
+ - `lock_attributes` now raises an `ArgumentError` if an invalid mode is specified instead of silently treating it as `:error`.
15
+
16
+ ### Changed
17
+
18
+ - Minimum required Ruby version is now 2.6.
19
+
20
+ ## 1.1.0
21
+
22
+ ### Added
23
+
24
+ - Added :raise mode that raises an error if a locked attribute has been changed instead of adding a validation error.
25
+
26
+ ### Changed
27
+
28
+ - Changed gem dependency from `activerecord` to `activemodel`. You can now use locked attributes with ActiveModel classes that include `ActiveModel::Validations` and `ActiveModel::Dirty and implement a `new_record?` method.
29
+
7
30
  ## 1.0.1
8
31
 
9
32
  ### Added
data/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  [![Continuous Integration](https://github.com/bdurand/attribute_guard/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/attribute_guard/actions/workflows/continuous_integration.yml)
4
4
  [![Regression Test](https://github.com/bdurand/attribute_guard/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/attribute_guard/actions/workflows/regression_test.yml)
5
5
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
+ [![Gem Version](https://badge.fury.io/rb/attribute_guard.svg)](https://badge.fury.io/rb/attribute_guard)
6
7
 
7
- This Ruby gem provides an extension for ActiveRecord allowing you to declare certain attributes in a model to be locked. Locked attributes cannot be changed once a record is created unless you explicitly allow changes.
8
+ This Ruby gem provides an extension for ActiveRecord/ActiveModel allowing you to declare certain attributes in a model to be locked. Locked attributes cannot be changed once a record is created unless you explicitly allow changes.
8
9
 
9
10
  This feature can be used for a couple of different purposes.
10
11
 
@@ -118,24 +119,35 @@ record.update!(status: "canceled") # raises ActiveRecord::RecordInvalid error
118
119
 
119
120
  ### Modes
120
121
 
121
- The default behavior when a locked attribute is changed is to add a validation error to the record. You can change this behavior with the `mode` option when locking attributes.
122
+ The default behavior when a locked attribute is changed is to add a validation error to the record. You can change this behavior with the `mode` option when locking attributes. You still need to validate the record to trigger the locked attribute check, regardless of the mode.
122
123
 
123
124
  ```ruby
124
125
  class MyModel
125
126
  include AttributeGuard
126
127
 
127
128
  lock_attributes :email, mode: :error
128
- lock_attributes :name: mode: :warn
129
+ lock_attributes :name, mode: :warn
130
+ lock_attributes :updated_at, mode: :raise
129
131
  lock_attributes :created_at, mode: ->(record, attribute) { raise "Created timestamp cannot be changed" }
130
132
  end
131
133
  ```
132
134
 
133
135
  * `:error` - Add a validation error to the record. This is the default.
134
136
 
135
- * `:warn` - Log a warning that the record was changed. This mode is useful to allow you soft deploy locked attributes to production on a mature project and give you information about where you may need to update code to unlock attributes.
137
+ * `:warn` - Log a warning that the record was changed. This mode is useful to allow you soft deploy locked attributes to production on a mature project and give you information about where you may need to update code to unlock attributes. If the model does not have a `logger` method that returns a `Logger`-like object, then the output will be sent to `STDERR`.
138
+
139
+ * `:raise` = Raise an `AttributeGuard::LockedAttributeError` error.
136
140
 
137
141
  * `Proc` - If you provide a `Proc` object, it will be called with the record and the attribute name when a locked attribute is changed.
138
142
 
143
+ ### Thread Safety
144
+
145
+ Unlocked attributes are tracked per model instance. Like ActiveRecord models in general, individual record instances are not thread safe, so you should not share a record between threads while unlocking attributes on it.
146
+
147
+ ### Using with ActiveModel
148
+
149
+ The gem works out of the box with ActiveRecord. You can also use it with ActiveModel classes as long as they include the `ActiveModel::Validations` and `ActiveModel::Dirty` modules. The model also needs to implement a `new_record?` method.
150
+
139
151
  ## Installation
140
152
 
141
153
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.1.1
@@ -4,11 +4,17 @@ Gem::Specification.new do |spec|
4
4
  spec.authors = ["Brian Durand"]
5
5
  spec.email = ["bbdurand@gmail.com"]
6
6
 
7
- spec.summary = "ActiveRecord extension that allows locking attributes to prevent unintended updates."
7
+ spec.summary = "ActiveRecord/ActiveModel extension that allows locking attributes to prevent unintended updates."
8
8
 
9
9
  spec.homepage = "https://github.com/bdurand/attribute_guard"
10
10
  spec.license = "MIT"
11
11
 
12
+ spec.metadata = {
13
+ "homepage_uri" => spec.homepage,
14
+ "source_code_uri" => spec.homepage,
15
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
16
+ }
17
+
12
18
  # Specify which files should be added to the gem when it is released.
13
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
14
20
  ignore_files = %w[
@@ -29,9 +35,9 @@ Gem::Specification.new do |spec|
29
35
 
30
36
  spec.require_paths = ["lib"]
31
37
 
32
- spec.required_ruby_version = ">= 2.5"
38
+ spec.required_ruby_version = ">= 2.6"
33
39
 
34
- spec.add_dependency "activerecord", ">= 5.0"
40
+ spec.add_dependency "activemodel", ">= 5.2"
35
41
 
36
42
  spec.add_development_dependency "bundler"
37
43
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  require "active_support/concern"
4
6
  require "active_support/lazy_load_hooks"
5
7
  require "active_model/validator"
@@ -36,6 +38,9 @@ end
36
38
  module AttributeGuard
37
39
  extend ActiveSupport::Concern
38
40
 
41
+ class LockedAttributeError < StandardError
42
+ end
43
+
39
44
  included do
40
45
  class_attribute :locked_attributes, default: {}, instance_accessor: false
41
46
  private_class_method :locked_attributes=
@@ -47,7 +52,7 @@ module AttributeGuard
47
52
  end
48
53
 
49
54
  module Initializer
50
- def initialize(*)
55
+ def initialize(*args, **kwargs)
51
56
  @unlocked_attributes = nil
52
57
  super
53
58
  end
@@ -56,13 +61,20 @@ module AttributeGuard
56
61
  # Validator that checks for changes to locked attributes.
57
62
  class LockedAttributesValidator < ActiveModel::Validator
58
63
  def validate(record)
64
+ unless record.respond_to?(:new_record?)
65
+ raise "AttributeGuard can only be used with models that respond to :new_record?"
66
+ end
67
+
59
68
  return if record.new_record?
60
69
 
70
+ changes = record.changes
61
71
  record.class.send(:locked_attributes).each do |attribute, params|
62
- if record.changes.include?(attribute) && record.attribute_locked?(attribute)
72
+ if changes.include?(attribute) && record.attribute_locked?(attribute)
63
73
  message, mode = params
64
74
  if mode == :warn
65
- record&.logger&.warn("Changed locked attribute #{attribute} on #{record.class.name} with id #{record.id}")
75
+ log_warning(record, attribute)
76
+ elsif mode == :raise
77
+ raise LockedAttributeError.new(error_message(record, attribute))
66
78
  elsif mode.is_a?(Proc)
67
79
  mode.call(record, attribute)
68
80
  else
@@ -71,6 +83,21 @@ module AttributeGuard
71
83
  end
72
84
  end
73
85
  end
86
+
87
+ private
88
+
89
+ def error_message(record, attribute)
90
+ "Changed locked attribute #{attribute} on #{record.class.name} with id #{record.id}"
91
+ end
92
+
93
+ def log_warning(record, attribute)
94
+ message = error_message(record, attribute)
95
+ if record.respond_to?(:logger) && record.logger.respond_to?(:warn)
96
+ record.logger.warn(message)
97
+ else
98
+ warn(message)
99
+ end
100
+ end
74
101
  end
75
102
 
76
103
  module ClassMethods
@@ -86,6 +113,10 @@ module AttributeGuard
86
113
  # @param mode [Symbol, Proc] mode to use when a locked attribute is changed
87
114
  # @return [void]
88
115
  def lock_attributes(*attributes, error: :locked, mode: :error)
116
+ unless mode == :error || mode == :warn || mode == :raise || mode.respond_to?(:call)
117
+ raise ArgumentError.new("Invalid mode: #{mode.inspect}")
118
+ end
119
+
89
120
  locked = locked_attributes.dup
90
121
  error = error.dup.freeze if error.is_a?(String)
91
122
 
@@ -113,10 +144,10 @@ module AttributeGuard
113
144
  # user.unlock_attributes(:email).update!(email: "user@example.com")
114
145
  #
115
146
  # @param attributes [Array<Symbol, String>] the attributes to unlock
116
- # @return [ActiveRecord::Base] the object itself
147
+ # @return [Object] the object itself
117
148
  def unlock_attributes(*attributes)
118
149
  attributes = attributes.flatten.map(&:to_s)
119
- return if attributes.empty?
150
+ return self if attributes.empty?
120
151
 
121
152
  @unlocked_attributes ||= Set.new
122
153
 
@@ -130,7 +161,7 @@ module AttributeGuard
130
161
  clear_unlocked_attributes if @unlocked_attributes.empty?
131
162
  end
132
163
  else
133
- @unlocked_attributes.merge(attributes)
164
+ @unlocked_attributes = @unlocked_attributes.dup.merge(attributes)
134
165
  end
135
166
 
136
167
  self
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attribute_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-11-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: activerecord
13
+ name: activemodel
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '5.0'
18
+ version: '5.2'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '5.0'
25
+ version: '5.2'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: bundler
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description:
42
40
  email:
43
41
  - bbdurand@gmail.com
44
42
  executables: []
@@ -55,8 +53,10 @@ files:
55
53
  homepage: https://github.com/bdurand/attribute_guard
56
54
  licenses:
57
55
  - MIT
58
- metadata: {}
59
- post_install_message:
56
+ metadata:
57
+ homepage_uri: https://github.com/bdurand/attribute_guard
58
+ source_code_uri: https://github.com/bdurand/attribute_guard
59
+ changelog_uri: https://github.com/bdurand/attribute_guard/blob/main/CHANGELOG.md
60
60
  rdoc_options: []
61
61
  require_paths:
62
62
  - lib
@@ -64,16 +64,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '2.5'
67
+ version: '2.6'
68
68
  required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  requirements:
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
72
  version: '0'
73
73
  requirements: []
74
- rubygems_version: 3.4.12
75
- signing_key:
74
+ rubygems_version: 4.0.3
76
75
  specification_version: 4
77
- summary: ActiveRecord extension that allows locking attributes to prevent unintended
78
- updates.
76
+ summary: ActiveRecord/ActiveModel extension that allows locking attributes to prevent
77
+ unintended updates.
79
78
  test_files: []