better_model 2.0.0 → 3.0.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 +4 -4
- data/README.md +274 -208
- data/lib/better_model/archivable.rb +203 -92
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +142 -131
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +123 -96
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +103 -85
- data/lib/better_model/stateable/guard.rb +41 -21
- data/lib/better_model/stateable/transition.rb +64 -35
- data/lib/better_model/stateable.rb +43 -25
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +54 -177
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -9
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +45 -14
- data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
- data/lib/better_model/schedulable/schedule_builder.rb +0 -269
- data/lib/better_model/schedulable.rb +0 -356
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -45
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
- data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
- data/lib/generators/better_model/taggable/templates/README.tt +0 -62
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +0 -21
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "stateable_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Stateable
|
|
8
|
+
class CheckFailedError < StateableError; end
|
|
9
|
+
|
|
10
|
+
# Alias for backward compatibility
|
|
11
|
+
GuardFailedError = CheckFailedError
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Stateable
|
|
8
|
+
# Base error class for all Stateable-related errors.
|
|
9
|
+
class StateableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Statusable
|
|
8
|
+
# Base error class for all Statusable-related errors.
|
|
9
|
+
class StatusableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Traceable
|
|
8
|
+
# Base error class for all Traceable-related errors.
|
|
9
|
+
class TraceableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Validatable
|
|
8
|
+
# Base error class for all Validatable-related errors.
|
|
9
|
+
class ValidatableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Models
|
|
5
|
+
# StateTransition - Base ActiveRecord model for state transition history.
|
|
6
|
+
#
|
|
7
|
+
# This is an abstract model. Concrete classes are generated dynamically
|
|
8
|
+
# for each table (state_transitions, order_transitions, etc.).
|
|
9
|
+
#
|
|
10
|
+
# @note Table Schema
|
|
11
|
+
# t.string :transitionable_type, null: false
|
|
12
|
+
# t.integer :transitionable_id, null: false
|
|
13
|
+
# t.string :event, null: false
|
|
14
|
+
# t.string :from_state, null: false
|
|
15
|
+
# t.string :to_state, null: false
|
|
16
|
+
# t.json :metadata
|
|
17
|
+
# t.datetime :created_at, null: false
|
|
18
|
+
#
|
|
19
|
+
# @example Usage
|
|
20
|
+
# # All transitions for a model
|
|
21
|
+
# order.state_transitions
|
|
22
|
+
#
|
|
23
|
+
# @example Global queries (via dynamic classes)
|
|
24
|
+
# BetterModel::Models::StateTransitions.for_model(Order)
|
|
25
|
+
# BetterModel::Models::OrderTransitions.by_event(:confirm)
|
|
26
|
+
#
|
|
27
|
+
class StateTransition < ActiveRecord::Base
|
|
28
|
+
# Default table name (can be overridden by dynamic subclasses)
|
|
29
|
+
self.table_name = "state_transitions"
|
|
30
|
+
|
|
31
|
+
# Polymorphic association
|
|
32
|
+
belongs_to :transitionable, polymorphic: true
|
|
33
|
+
|
|
34
|
+
# Validations
|
|
35
|
+
validates :event, :from_state, :to_state, presence: true
|
|
36
|
+
|
|
37
|
+
# Scopes
|
|
38
|
+
|
|
39
|
+
# Scope for specific model.
|
|
40
|
+
#
|
|
41
|
+
# @param model_class [Class] Model class
|
|
42
|
+
# @return [ActiveRecord::Relation]
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# StateTransition.for_model(Order)
|
|
46
|
+
scope :for_model, ->(model_class) {
|
|
47
|
+
where(transitionable_type: model_class.name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Scope for specific event.
|
|
51
|
+
#
|
|
52
|
+
# @param event [Symbol, String] Event name
|
|
53
|
+
# @return [ActiveRecord::Relation]
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# StateTransition.by_event(:confirm)
|
|
57
|
+
scope :by_event, ->(event) {
|
|
58
|
+
where(event: event.to_s)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Scope for source state.
|
|
62
|
+
#
|
|
63
|
+
# @param state [Symbol, String] Source state
|
|
64
|
+
# @return [ActiveRecord::Relation]
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# StateTransition.from_state(:pending)
|
|
68
|
+
scope :from_state, ->(state) {
|
|
69
|
+
where(from_state: state.to_s)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Scope for destination state.
|
|
73
|
+
#
|
|
74
|
+
# @param state [Symbol, String] Destination state
|
|
75
|
+
# @return [ActiveRecord::Relation]
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# StateTransition.to_state(:confirmed)
|
|
79
|
+
scope :to_state, ->(state) {
|
|
80
|
+
where(to_state: state.to_s)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Scope for recent transitions.
|
|
84
|
+
#
|
|
85
|
+
# @param duration [ActiveSupport::Duration] Time duration (e.g., 7.days)
|
|
86
|
+
# @return [ActiveRecord::Relation]
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# StateTransition.recent(7.days)
|
|
90
|
+
scope :recent, ->(duration = 7.days) {
|
|
91
|
+
where("created_at >= ?", duration.ago)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Scope for transitions in a time period.
|
|
95
|
+
#
|
|
96
|
+
# @param start_time [Time, Date] Period start
|
|
97
|
+
# @param end_time [Time, Date] Period end
|
|
98
|
+
# @return [ActiveRecord::Relation]
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# StateTransition.between(1.week.ago, Time.current)
|
|
102
|
+
scope :between, ->(start_time, end_time) {
|
|
103
|
+
where(created_at: start_time..end_time)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Instance Methods
|
|
107
|
+
|
|
108
|
+
# Formatted description of the transition.
|
|
109
|
+
#
|
|
110
|
+
# @return [String] Human-readable transition description
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# transition.description # => "Order#123: pending -> confirmed (confirm)"
|
|
114
|
+
def description
|
|
115
|
+
"#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Alias for backward compatibility
|
|
119
|
+
alias_method :to_s, :description
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Models
|
|
5
|
+
# Version model for tracking changes
|
|
6
|
+
# This is the base AR model for version history
|
|
7
|
+
# Actual table_name is set dynamically in subclasses
|
|
8
|
+
class Version < ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
|
|
11
|
+
# Polymorphic association to the tracked model
|
|
12
|
+
belongs_to :item, polymorphic: true, optional: true
|
|
13
|
+
|
|
14
|
+
# Optional: belongs_to user who made the change
|
|
15
|
+
# belongs_to :updated_by, class_name: "User", optional: true
|
|
16
|
+
|
|
17
|
+
# Serialize object_changes as JSON
|
|
18
|
+
# Rails handles this automatically for json/jsonb columns
|
|
19
|
+
|
|
20
|
+
# Validations
|
|
21
|
+
validates :item_type, :event, presence: true
|
|
22
|
+
validates :event, inclusion: { in: %w[created updated destroyed] }
|
|
23
|
+
|
|
24
|
+
# Scopes
|
|
25
|
+
scope :for_item, ->(item) { where(item_type: item.class.name, item_id: item.id) }
|
|
26
|
+
scope :created_events, -> { where(event: "created") }
|
|
27
|
+
scope :updated_events, -> { where(event: "updated") }
|
|
28
|
+
scope :destroyed_events, -> { where(event: "destroyed") }
|
|
29
|
+
scope :by_user, ->(user_id) { where(updated_by_id: user_id) }
|
|
30
|
+
scope :between, ->(start_time, end_time) { where(created_at: start_time..end_time) }
|
|
31
|
+
scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
|
|
32
|
+
|
|
33
|
+
# Get the change for a specific field
|
|
34
|
+
#
|
|
35
|
+
# @param field_name [Symbol, String] Field name
|
|
36
|
+
# @return [Hash, nil] Hash with :before and :after keys
|
|
37
|
+
def change_for(field_name)
|
|
38
|
+
return nil unless object_changes
|
|
39
|
+
|
|
40
|
+
field = field_name.to_s
|
|
41
|
+
return nil unless object_changes.key?(field)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
before: object_changes[field][0],
|
|
45
|
+
after: object_changes[field][1]
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if a specific field changed in this version
|
|
50
|
+
# This method overrides ActiveRecord's changed? to accept a field_name parameter
|
|
51
|
+
#
|
|
52
|
+
# @param field_name [Symbol, String, nil] Field name (if nil, calls ActiveRecord's changed?)
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def changed?(field_name = nil)
|
|
55
|
+
return super() if field_name.nil?
|
|
56
|
+
|
|
57
|
+
object_changes&.key?(field_name.to_s) || false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get list of changed fields
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<String>]
|
|
63
|
+
def changed_fields
|
|
64
|
+
object_changes&.keys || []
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/permissible/permissible_error"
|
|
4
|
+
require_relative "errors/permissible/configuration_error"
|
|
5
|
+
|
|
6
|
+
# Permissible - Declarative permissions system for Rails models.
|
|
4
7
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
8
|
+
# This concern enables defining permissions/capabilities on models using a simple,
|
|
9
|
+
# declarative DSL, similar to the Statusable pattern but for operations.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
11
|
+
# @example Basic Usage
|
|
9
12
|
# class Article < ApplicationRecord
|
|
10
13
|
# include BetterModel::Permissible
|
|
11
14
|
#
|
|
@@ -15,7 +18,7 @@
|
|
|
15
18
|
# permit :archive, -> { is?(:published) && created_at < 1.year.ago }
|
|
16
19
|
# end
|
|
17
20
|
#
|
|
18
|
-
#
|
|
21
|
+
# @example Checking Permissions
|
|
19
22
|
# article.permit?(:delete) # => true/false
|
|
20
23
|
# article.permit_delete? # => true/false
|
|
21
24
|
# article.permit_edit? # => true/false
|
|
@@ -26,58 +29,85 @@ module BetterModel
|
|
|
26
29
|
extend ActiveSupport::Concern
|
|
27
30
|
|
|
28
31
|
included do
|
|
29
|
-
# Registry
|
|
32
|
+
# Registry of permissions defined for this class
|
|
30
33
|
class_attribute :permit_definitions
|
|
31
34
|
self.permit_definitions = {}
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
class_methods do
|
|
35
|
-
# DSL
|
|
38
|
+
# DSL to define permissions.
|
|
39
|
+
#
|
|
40
|
+
# Defines a permission check that can be evaluated against model instances.
|
|
41
|
+
# Automatically creates a convenience method permit_<permission_name>? for each permission.
|
|
36
42
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
43
|
+
# @param permission_name [Symbol, String] Permission identifier (e.g., :delete, :edit)
|
|
44
|
+
# @param condition_proc [Proc, nil] Lambda or proc that defines the condition
|
|
45
|
+
# @yield Alternative to condition_proc parameter
|
|
46
|
+
# @raise [BetterModel::Errors::Permissible::ConfigurationError] If parameters are invalid
|
|
41
47
|
#
|
|
42
|
-
#
|
|
48
|
+
# @example With lambda parameter
|
|
43
49
|
# permit :delete, -> { status != "published" }
|
|
44
|
-
#
|
|
50
|
+
#
|
|
51
|
+
# @example With block
|
|
52
|
+
# permit :edit do
|
|
53
|
+
# is?(:draft)
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# @example Complex condition
|
|
45
57
|
# permit :publish do
|
|
46
58
|
# is?(:draft) && valid?(:publication)
|
|
47
59
|
# end
|
|
48
60
|
def permit(permission_name, condition_proc = nil, &block)
|
|
49
|
-
#
|
|
50
|
-
|
|
61
|
+
# Validate parameters before converting
|
|
62
|
+
if permission_name.blank?
|
|
63
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Permission name cannot be blank"
|
|
64
|
+
end
|
|
51
65
|
|
|
52
66
|
permission_name = permission_name.to_sym
|
|
53
67
|
condition = condition_proc || block
|
|
54
|
-
raise ArgumentError, "Condition proc or block is required" unless condition
|
|
55
|
-
raise ArgumentError, "Condition must respond to call" unless condition.respond_to?(:call)
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
unless condition
|
|
70
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Condition proc or block is required"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
unless condition.respond_to?(:call)
|
|
74
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Condition must respond to call"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Register permission in registry
|
|
58
78
|
self.permit_definitions = permit_definitions.merge(permission_name => condition.freeze).freeze
|
|
59
79
|
|
|
60
|
-
#
|
|
80
|
+
# Generate dynamic method permit_#{permission_name}?
|
|
61
81
|
define_permit_method(permission_name)
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
# List all permissions defined for this class.
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<Symbol>] Array of defined permission names
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# Article.defined_permissions # => [:delete, :edit, :publish]
|
|
90
|
+
def defined_permissions = permit_definitions.keys
|
|
68
91
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
# Check if a permission is defined.
|
|
93
|
+
#
|
|
94
|
+
# @param permission_name [Symbol, String] Permission name to check
|
|
95
|
+
# @return [Boolean] true if permission is defined
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# Article.permission_defined?(:delete) # => true
|
|
99
|
+
def permission_defined?(permission_name) = permit_definitions.key?(permission_name.to_sym)
|
|
73
100
|
|
|
74
101
|
private
|
|
75
102
|
|
|
76
|
-
#
|
|
103
|
+
# Generate dynamic method permit_#{permission_name}? for each defined permission.
|
|
104
|
+
#
|
|
105
|
+
# @param permission_name [Symbol] Permission name
|
|
106
|
+
# @api private
|
|
77
107
|
def define_permit_method(permission_name)
|
|
78
108
|
method_name = "permit_#{permission_name}?"
|
|
79
109
|
|
|
80
|
-
#
|
|
110
|
+
# Avoid redefining methods if they already exist
|
|
81
111
|
return if method_defined?(method_name)
|
|
82
112
|
|
|
83
113
|
define_method(method_name) do
|
|
@@ -86,35 +116,33 @@ module BetterModel
|
|
|
86
116
|
end
|
|
87
117
|
end
|
|
88
118
|
|
|
89
|
-
#
|
|
119
|
+
# Generic method to check if a permission is granted.
|
|
90
120
|
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
121
|
+
# Evaluates the permission condition in the context of the model instance.
|
|
122
|
+
# Returns false if permission is not defined (secure by default).
|
|
93
123
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# - false se il permesso non è garantito o non è definito
|
|
124
|
+
# @param permission_name [Symbol, String] Permission name to check
|
|
125
|
+
# @return [Boolean] true if permission is granted, false otherwise
|
|
97
126
|
#
|
|
98
|
-
#
|
|
99
|
-
# article.permit?(:delete)
|
|
127
|
+
# @example
|
|
128
|
+
# article.permit?(:delete) # => true
|
|
100
129
|
def permit?(permission_name)
|
|
101
130
|
permission_name = permission_name.to_sym
|
|
102
131
|
condition = self.class.permit_definitions[permission_name]
|
|
103
132
|
|
|
104
|
-
#
|
|
133
|
+
# If permission is not defined, return false (secure by default)
|
|
105
134
|
return false unless condition
|
|
106
135
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
136
|
+
# Evaluate condition in context of model instance
|
|
137
|
+
# Errors propagate naturally - fail fast
|
|
109
138
|
instance_exec(&condition)
|
|
110
139
|
end
|
|
111
140
|
|
|
112
|
-
#
|
|
141
|
+
# Returns all available permissions for this instance with their values.
|
|
113
142
|
#
|
|
114
|
-
#
|
|
115
|
-
# - Hash con chiavi simbolo (permessi) e valori booleani (garantiti/negati)
|
|
143
|
+
# @return [Hash{Symbol => Boolean}] Hash with permission names and their granted status
|
|
116
144
|
#
|
|
117
|
-
#
|
|
145
|
+
# @example
|
|
118
146
|
# article.permissions
|
|
119
147
|
# # => { delete: true, edit: false, publish: false, archive: false }
|
|
120
148
|
def permissions
|
|
@@ -123,26 +151,49 @@ module BetterModel
|
|
|
123
151
|
end
|
|
124
152
|
end
|
|
125
153
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
# Check if instance has at least one granted permission.
|
|
155
|
+
#
|
|
156
|
+
# @return [Boolean] true if any permission is granted
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
# article.has_any_permission? # => true
|
|
160
|
+
def has_any_permission? = permissions.values.any?
|
|
130
161
|
|
|
131
|
-
#
|
|
162
|
+
# Check if instance has all specified permissions granted.
|
|
163
|
+
#
|
|
164
|
+
# @param permission_names [Array<Symbol>] Permission names to check
|
|
165
|
+
# @return [Boolean] true if all permissions are granted
|
|
166
|
+
#
|
|
167
|
+
# @example
|
|
168
|
+
# article.has_all_permissions?([:edit, :publish]) # => false
|
|
132
169
|
def has_all_permissions?(permission_names)
|
|
133
170
|
Array(permission_names).all? { |permission_name| permit?(permission_name) }
|
|
134
171
|
end
|
|
135
172
|
|
|
136
|
-
#
|
|
173
|
+
# Filter a list of permissions returning only granted ones.
|
|
174
|
+
#
|
|
175
|
+
# @param permission_names [Array<Symbol>] Permission names to filter
|
|
176
|
+
# @return [Array<Symbol>] Granted permissions
|
|
177
|
+
#
|
|
178
|
+
# @example
|
|
179
|
+
# article.granted_permissions([:edit, :delete, :publish]) # => [:edit]
|
|
137
180
|
def granted_permissions(permission_names)
|
|
138
181
|
Array(permission_names).select { |permission_name| permit?(permission_name) }
|
|
139
182
|
end
|
|
140
183
|
|
|
141
|
-
# Override
|
|
184
|
+
# Override as_json to automatically include permissions if requested.
|
|
185
|
+
#
|
|
186
|
+
# @param options [Hash] Options for as_json
|
|
187
|
+
# @option options [Boolean] :include_permissions Include permissions in JSON output
|
|
188
|
+
# @return [Hash] JSON representation
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# article.as_json(include_permissions: true)
|
|
192
|
+
# # => { ..., "permissions" => { "delete" => true, "edit" => false } }
|
|
142
193
|
def as_json(options = {})
|
|
143
194
|
result = super
|
|
144
195
|
|
|
145
|
-
# Include
|
|
196
|
+
# Include permissions if explicitly requested, converting symbol keys to strings
|
|
146
197
|
result["permissions"] = permissions.transform_keys(&:to_s) if options[:include_permissions]
|
|
147
198
|
|
|
148
199
|
result
|