contextual_config 0.1.0 → 0.3.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/CLAUDE.md +2 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +66 -9
- data/Rakefile +2 -0
- data/contextual_config.gemspec +3 -1
- data/lib/contextual_config/concern/configurable.rb +9 -13
- data/lib/contextual_config/concern/lookupable.rb +98 -32
- data/lib/contextual_config/concern/schema_driven_validation.rb +5 -2
- data/lib/contextual_config/configuration.rb +2 -6
- data/lib/contextual_config/generators.rb +2 -0
- data/lib/contextual_config/module_registry.rb +1 -1
- data/lib/contextual_config/services/contextual_matcher.rb +2 -0
- data/lib/contextual_config/version.rb +3 -1
- data/lib/contextual_config.rb +3 -3
- data/lib/generators/contextual_config/configurable_table/configurable_table_generator.rb +2 -0
- data/lib/generators/contextual_config/configurable_table/templates/migration.rb.tt +6 -4
- metadata +1 -2
- data/lib/generators/contextual_config/configurable_table/USAGE +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c7a19182d63d0f03712caf91b5b996bd99af38f3a6eed254d8497ff3c78af46
|
|
4
|
+
data.tar.gz: a55ebca13917e3b7fe37f45a0d14a7279a860705ec3c1461eb0071cc1acf67c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfb97ec675a0d72ce010f4e9f809e25cc01baff02dbafbfd83a17ae62eba7bd2a5cf9dfc39fe670075c09ef17f205297fc63c074ecac3fa7b422faa08811ba95
|
|
7
|
+
data.tar.gz: 941b9502f8cbcaa055f57d668c87c622dcc0f3efd3d1ef929cf5dc2533a487820badd234557187080870d36a7c11460887c624944da9511091a8d769e85e0f7a
|
data/CLAUDE.md
CHANGED
|
@@ -18,7 +18,7 @@ ContextualConfig is a Ruby gem for context-aware configuration management built
|
|
|
18
18
|
|
|
19
19
|
### 1. Core Concerns (lib/contextual_config/concern/)
|
|
20
20
|
|
|
21
|
-
- **Configurable**: Provides validations, scopes, and basic configuration functionality. Expects database columns: `key`, `config_data` (jsonb), `scoping_rules` (jsonb), `priority` (integer), `
|
|
21
|
+
- **Configurable**: Provides validations, scopes, and basic configuration functionality. Expects database columns: `key`, `config_data` (jsonb), `scoping_rules` (jsonb), `priority` (integer), `deleted_at` (datetime)
|
|
22
22
|
- **Lookupable**: Handles configuration lookup logic with `find_applicable_config(key:, context:)` and `find_all_applicable_configs(context:)`
|
|
23
23
|
- **SchemaDrivenValidation**: Optional JSON schema validation for `config_data` and `scoping_rules`
|
|
24
24
|
|
|
@@ -51,7 +51,7 @@ t.string :key, null: false
|
|
|
51
51
|
t.jsonb :config_data, null: false, default: {}
|
|
52
52
|
t.jsonb :scoping_rules, null: false, default: {}
|
|
53
53
|
t.integer :priority, null: false, default: 100
|
|
54
|
-
t.
|
|
54
|
+
t.datetime :deleted_at
|
|
55
55
|
t.text :description
|
|
56
56
|
t.string :type # For STI if needed
|
|
57
57
|
```
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -111,7 +111,7 @@ Each configuration record contains:
|
|
|
111
111
|
- **config_data**: The actual configuration payload (JSONB)
|
|
112
112
|
- **scoping_rules**: Rules defining when this config applies (JSONB)
|
|
113
113
|
- **priority**: Lower numbers = higher priority (integer)
|
|
114
|
-
- **
|
|
114
|
+
- **deleted_at**: Soft delete timestamp (datetime, null for active)
|
|
115
115
|
- **type**: For Single Table Inheritance (string, optional)
|
|
116
116
|
|
|
117
117
|
### Context Matching
|
|
@@ -229,7 +229,7 @@ def change
|
|
|
229
229
|
t.jsonb :config_data, null: false, default: {}
|
|
230
230
|
t.jsonb :scoping_rules, null: false, default: {}
|
|
231
231
|
t.integer :priority, null: false, default: 100
|
|
232
|
-
t.
|
|
232
|
+
t.datetime :deleted_at
|
|
233
233
|
t.text :description
|
|
234
234
|
t.string :type
|
|
235
235
|
|
|
@@ -665,21 +665,71 @@ Benefits::InsuranceConfig.create!(
|
|
|
665
665
|
|
|
666
666
|
## Performance Considerations
|
|
667
667
|
|
|
668
|
+
### Caching System
|
|
669
|
+
|
|
670
|
+
ContextualConfig includes a sophisticated two-level caching system for optimal performance:
|
|
671
|
+
|
|
672
|
+
#### 1. Candidates Cache
|
|
673
|
+
Caches the database query results (list of potential configurations) to avoid repeated database hits for the same key/context combinations.
|
|
674
|
+
|
|
675
|
+
#### 2. Results Cache
|
|
676
|
+
Caches the final matched configuration after processing all matching logic, providing the fastest possible lookups for repeated requests.
|
|
677
|
+
|
|
678
|
+
### Cache Configuration
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
# Enable caching globally
|
|
682
|
+
ContextualConfig.configure do |config|
|
|
683
|
+
config.cache_enabled = true
|
|
684
|
+
config.cache_ttl = 300 # 5 minutes
|
|
685
|
+
config.cache_store = Rails.cache
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Fine-grained cache control in your models
|
|
689
|
+
class CustomConfig < ApplicationRecord
|
|
690
|
+
include ContextualConfig::Concern::Configurable
|
|
691
|
+
include ContextualConfig::Concern::Lookupable
|
|
692
|
+
|
|
693
|
+
protected
|
|
694
|
+
|
|
695
|
+
# Override to disable candidate caching for specific scenarios
|
|
696
|
+
def self.cache_candidates?(key:, context:)
|
|
697
|
+
# Only cache for stable contexts, not time-sensitive ones
|
|
698
|
+
!context.key?(:current_timestamp)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Override to disable result caching for specific scenarios
|
|
702
|
+
def self.cache_results?(key:, context:)
|
|
703
|
+
# Cache results except for admin users who need real-time data
|
|
704
|
+
context[:user_role] != 'admin'
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Override to customize candidate selection
|
|
708
|
+
def self.lookup_candidates(key:, context:)
|
|
709
|
+
# Custom logic for fetching candidates
|
|
710
|
+
active.where(key: key.to_s, department: context[:department]).order_by_priority
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
```
|
|
714
|
+
|
|
668
715
|
### Benchmarks
|
|
669
716
|
|
|
670
717
|
Based on testing with the JisrHR application:
|
|
671
718
|
|
|
672
|
-
- **Average lookup time**: ~3ms per configuration lookup
|
|
673
|
-
- **
|
|
719
|
+
- **Average lookup time**: ~3ms per configuration lookup (uncached)
|
|
720
|
+
- **Cached lookup time**: ~0.1ms per configuration lookup
|
|
721
|
+
- **100 concurrent lookups**: Completed in ~0.3 seconds (uncached), ~0.03 seconds (cached)
|
|
674
722
|
- **Database indexes**: Automatically created by the generator for optimal performance
|
|
675
|
-
- **Memory usage**: Minimal - configurations are loaded on-demand
|
|
723
|
+
- **Memory usage**: Minimal - configurations are loaded on-demand with intelligent caching
|
|
676
724
|
|
|
677
725
|
### Optimization Tips
|
|
678
726
|
|
|
679
|
-
1. **
|
|
680
|
-
2. **
|
|
681
|
-
3. **
|
|
682
|
-
4. **
|
|
727
|
+
1. **Enable caching**: Use the built-in two-level caching system for production deployments
|
|
728
|
+
2. **Use appropriate indexes**: The generator creates optimal indexes for common queries
|
|
729
|
+
3. **Customize cache behavior**: Override cache control methods for fine-grained performance tuning
|
|
730
|
+
4. **Limit scoping rule complexity**: Simpler rules = faster matching
|
|
731
|
+
5. **Use database-level constraints**: Leverage PostgreSQL JSONB indexes for complex queries
|
|
732
|
+
6. **Monitor cache hit rates**: Use logging to monitor cache effectiveness
|
|
683
733
|
|
|
684
734
|
### Production Deployment
|
|
685
735
|
|
|
@@ -740,6 +790,13 @@ Provides configuration lookup methods:
|
|
|
740
790
|
- `find_applicable_config(key:, context:)` - Find best match
|
|
741
791
|
- `find_all_applicable_configs(context:)` - Find all matches
|
|
742
792
|
|
|
793
|
+
**Protected Methods for Customization:**
|
|
794
|
+
|
|
795
|
+
- `lookup_candidates(key:, context:)` - Override to customize candidate selection for specific keys
|
|
796
|
+
- `lookup_all_candidates(context:)` - Override to customize candidate selection for all configurations
|
|
797
|
+
- `cache_candidates?(key:, context:)` - Override to control candidate caching behavior
|
|
798
|
+
- `cache_results?(key:, context:)` - Override to control result caching behavior
|
|
799
|
+
|
|
743
800
|
#### ContextualConfig::Concern::SchemaDrivenValidation
|
|
744
801
|
|
|
745
802
|
Provides JSON schema validation:
|
data/Rakefile
CHANGED
data/contextual_config.gemspec
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'lib/contextual_config/version'
|
|
2
4
|
|
|
3
5
|
Gem::Specification.new do |spec|
|
|
@@ -10,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
10
12
|
spec.description = 'ContextualConfig provides a flexible framework for managing configurations that can be applied based on contextual rules, priorities, and scoping. Perfect for complex applications requiring dynamic configuration resolution.'
|
|
11
13
|
spec.homepage = 'https://github.com/bazinga012/contextual_config'
|
|
12
14
|
spec.license = 'MIT'
|
|
13
|
-
spec.required_ruby_version = '>= 3.0.0'
|
|
15
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
14
16
|
|
|
15
17
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
16
18
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/concern'
|
|
2
4
|
|
|
3
5
|
module ContextualConfig
|
|
@@ -13,7 +15,7 @@ module ContextualConfig
|
|
|
13
15
|
# - config_data:jsonb
|
|
14
16
|
# - scoping_rules:jsonb
|
|
15
17
|
# - priority:integer (default should be a higher number for lower priority)
|
|
16
|
-
# -
|
|
18
|
+
# - deleted_at:datetime (null for active records)
|
|
17
19
|
# - type:string (if STI is to be used on the including model)
|
|
18
20
|
# - created_at:datetime
|
|
19
21
|
# - updated_at:datetime
|
|
@@ -33,9 +35,6 @@ module ContextualConfig
|
|
|
33
35
|
# It's often better to add this uniqueness validation directly in the consuming model
|
|
34
36
|
# where the scope is clearer.
|
|
35
37
|
|
|
36
|
-
# Ensures 'is_active' is either true or false.
|
|
37
|
-
validates :is_active, inclusion: { in: [true, false], message: 'is not included in the list' }
|
|
38
|
-
|
|
39
38
|
# Ensures 'priority' is an integer.
|
|
40
39
|
validates :priority, numericality: { only_integer: true, message: 'is not a number' }
|
|
41
40
|
|
|
@@ -43,7 +42,7 @@ module ContextualConfig
|
|
|
43
42
|
|
|
44
43
|
# Scope to retrieve only active configurations.
|
|
45
44
|
# Usage: YourModel.active
|
|
46
|
-
scope :active, -> { where(
|
|
45
|
+
scope :active, -> { where(deleted_at: nil) }
|
|
47
46
|
|
|
48
47
|
# Scope to order configurations by priority.
|
|
49
48
|
# Lower numbers indicate higher priority. Secondary sort by ID for deterministic ordering.
|
|
@@ -51,9 +50,8 @@ module ContextualConfig
|
|
|
51
50
|
scope :order_by_priority, -> { order(priority: :asc, id: :asc) } # 'asc' for priority means 1 is higher than 10
|
|
52
51
|
|
|
53
52
|
# --- Default Values ---
|
|
54
|
-
# It's generally recommended to set database-level defaults for `
|
|
55
|
-
#
|
|
56
|
-
# If not set at DB level, you can use `after_initialize` here, but DB defaults are safer.
|
|
53
|
+
# It's generally recommended to set database-level defaults for `priority`
|
|
54
|
+
# (e.g., a high number like 100 for low priority). `deleted_at` defaults to NULL for active records.
|
|
57
55
|
after_initialize :set_default_values, if: :new_record?
|
|
58
56
|
end
|
|
59
57
|
|
|
@@ -63,10 +61,8 @@ module ContextualConfig
|
|
|
63
61
|
|
|
64
62
|
def set_default_values
|
|
65
63
|
# Only set defaults if the model is completely new (not loaded from DB and no explicit values set)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
end
|
|
69
|
-
|
|
64
|
+
# deleted_at defaults to NULL (active), no need to set explicitly
|
|
65
|
+
|
|
70
66
|
# Override database default priority with module-specific priority if available
|
|
71
67
|
set_module_default_priority if should_use_module_default_priority?
|
|
72
68
|
end
|
|
@@ -100,7 +96,7 @@ module ContextualConfig
|
|
|
100
96
|
|
|
101
97
|
# Helper to quickly check if a configuration is currently active
|
|
102
98
|
def active?
|
|
103
|
-
|
|
99
|
+
deleted_at.nil?
|
|
104
100
|
end
|
|
105
101
|
|
|
106
102
|
# Get the module configuration for this model if available
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/concern'
|
|
2
4
|
require 'digest'
|
|
3
5
|
|
|
@@ -17,18 +19,17 @@ module ContextualConfig
|
|
|
17
19
|
# and fetches only those.
|
|
18
20
|
# @return [ActiveRecord::Base, nil] The instance of the best matching configuration, or nil if no match.
|
|
19
21
|
def find_applicable_config(key:, context:, fetch_all_of_key: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
20
|
-
# Check cache first if enabled
|
|
21
|
-
if
|
|
22
|
+
# Check result cache first if enabled
|
|
23
|
+
if cache_results?(key: key, context: context)
|
|
24
|
+
cache_key = generate_cache_key(key, context)
|
|
22
25
|
begin
|
|
23
|
-
cache_key = generate_cache_key(key, context)
|
|
24
26
|
cached_result = ContextualConfig.configuration.cache_store.read(cache_key)
|
|
25
27
|
if cached_result
|
|
26
|
-
log_lookup_attempt(key, context, '
|
|
28
|
+
log_lookup_attempt(key, context, 'result_cache_hit')
|
|
27
29
|
return cached_result
|
|
28
30
|
end
|
|
29
|
-
rescue => e
|
|
30
|
-
|
|
31
|
-
log_cache_error('read', e)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
log_lookup_attempt(key, context, "result_cache_error: #{e.message}")
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -37,13 +38,41 @@ module ContextualConfig
|
|
|
37
38
|
|
|
38
39
|
# Start with active configurations, ordered by priority (higher priority first).
|
|
39
40
|
# The `Configurable` concern is expected to provide `active` and `order_by_priority` scopes.
|
|
40
|
-
unless
|
|
41
|
-
raise(NoMethodError, "#{
|
|
41
|
+
unless respond_to?(:active) && respond_to?(:order_by_priority)
|
|
42
|
+
raise(NoMethodError, "#{name} must include ContextualConfig::Concern::Configurable to use Lookupable.")
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
# Check candidates cache if enabled
|
|
46
|
+
candidates = nil
|
|
47
|
+
if cache_candidates?(key: key, context: context)
|
|
48
|
+
candidates_cache_key = generate_candidates_cache_key(key, context)
|
|
49
|
+
begin
|
|
50
|
+
candidates = ContextualConfig.configuration.cache_store.read(candidates_cache_key)
|
|
51
|
+
log_lookup_attempt(key, context, 'candidates_cache_hit') if candidates
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
log_lookup_attempt(key, context, "candidates_cache_error: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
45
56
|
|
|
46
|
-
candidates
|
|
57
|
+
# Fetch candidates from database if not cached
|
|
58
|
+
if candidates.nil?
|
|
59
|
+
log_lookup_attempt(key, context, 'database_lookup')
|
|
60
|
+
candidates = lookup_candidates(key: key, context: context)
|
|
61
|
+
# Cache candidates if enabled
|
|
62
|
+
if cache_candidates?(key: key, context: context)
|
|
63
|
+
candidates_cache_key = generate_candidates_cache_key(key, context)
|
|
64
|
+
begin
|
|
65
|
+
ContextualConfig.configuration.cache_store.write(
|
|
66
|
+
candidates_cache_key,
|
|
67
|
+
candidates.to_a, # Convert to array to avoid caching AR relations
|
|
68
|
+
expires_in: ContextualConfig.configuration.cache_ttl
|
|
69
|
+
)
|
|
70
|
+
log_lookup_attempt(key, context, 'candidates_cache_stored')
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log_lookup_attempt(key, context, "candidates_cache_write_error: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
47
76
|
|
|
48
77
|
# Delegate to the ContextualMatcher service to find the best match from the candidates.
|
|
49
78
|
# The service will handle the logic of iterating through candidates, evaluating scoping_rules,
|
|
@@ -54,18 +83,17 @@ module ContextualConfig
|
|
|
54
83
|
)
|
|
55
84
|
|
|
56
85
|
# Cache the result if caching is enabled
|
|
57
|
-
if
|
|
86
|
+
if cache_results?(key: key, context: context) && result
|
|
87
|
+
cache_key = generate_cache_key(key, context)
|
|
58
88
|
begin
|
|
59
|
-
cache_key = generate_cache_key(key, context)
|
|
60
89
|
ContextualConfig.configuration.cache_store.write(
|
|
61
90
|
cache_key,
|
|
62
91
|
result,
|
|
63
92
|
expires_in: ContextualConfig.configuration.cache_ttl
|
|
64
93
|
)
|
|
65
|
-
log_lookup_attempt(key, context, '
|
|
66
|
-
rescue => e
|
|
67
|
-
|
|
68
|
-
log_cache_error('write', e)
|
|
94
|
+
log_lookup_attempt(key, context, 'result_cache_stored')
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
log_lookup_attempt(key, context, "result_cache_write_error: #{e.message}")
|
|
69
97
|
end
|
|
70
98
|
end
|
|
71
99
|
|
|
@@ -80,14 +108,14 @@ module ContextualConfig
|
|
|
80
108
|
# @param context [Hash] A hash representing the current context.
|
|
81
109
|
# @return [Array<ActiveRecord::Base>] An array of all matching configuration instances, ordered by priority.
|
|
82
110
|
def find_all_applicable_configs(context:)
|
|
83
|
-
unless
|
|
84
|
-
raise(NoMethodError, "#{
|
|
111
|
+
unless respond_to?(:active) && respond_to?(:order_by_priority)
|
|
112
|
+
raise(NoMethodError, "#{name} must include ContextualConfig::Concern::Configurable to use Lookupable.")
|
|
85
113
|
end
|
|
86
114
|
|
|
87
115
|
log_lookup_attempt('all_configs', context, 'all_configs_lookup')
|
|
88
116
|
|
|
89
117
|
# Fetch all active configurations of this type, ordered by priority.
|
|
90
|
-
all_candidates =
|
|
118
|
+
all_candidates = lookup_all_candidates(context: context)
|
|
91
119
|
|
|
92
120
|
# Delegate to the ContextualMatcher service.
|
|
93
121
|
ContextualConfig::Services::ContextualMatcher.find_all_matches(
|
|
@@ -96,12 +124,60 @@ module ContextualConfig
|
|
|
96
124
|
)
|
|
97
125
|
end
|
|
98
126
|
|
|
127
|
+
protected
|
|
128
|
+
|
|
129
|
+
# Override this method in your configuration class to customize candidate selection
|
|
130
|
+
# for specific key lookups. This allows fine-grained control over which configurations
|
|
131
|
+
# are considered for matching.
|
|
132
|
+
#
|
|
133
|
+
# @param key [String, Symbol] The configuration key to look for
|
|
134
|
+
# @param context [Hash] The lookup context
|
|
135
|
+
# @return [ActiveRecord::Relation] The candidates for matching
|
|
136
|
+
def lookup_candidates(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
137
|
+
active.where(key: key.to_s).order_by_priority
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Override this method in your configuration class to customize candidate selection
|
|
141
|
+
# for all configurations (used by find_all_applicable_configs).
|
|
142
|
+
#
|
|
143
|
+
# @param context [Hash] The lookup context
|
|
144
|
+
# @return [ActiveRecord::Relation] All candidates for matching
|
|
145
|
+
def lookup_all_candidates(context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
146
|
+
active.order_by_priority
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Override this method to control whether candidates should be cached.
|
|
150
|
+
# By default, candidates are cached when caching is enabled globally.
|
|
151
|
+
#
|
|
152
|
+
# @param key [String, Symbol] The configuration key
|
|
153
|
+
# @param context [Hash] The lookup context
|
|
154
|
+
# @return [Boolean] Whether to cache candidates for this lookup
|
|
155
|
+
def cache_candidates?(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
156
|
+
ContextualConfig.configuration.cache_enabled?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Override this method to control whether results should be cached.
|
|
160
|
+
# By default, results are cached when caching is enabled globally.
|
|
161
|
+
#
|
|
162
|
+
# @param key [String, Symbol] The configuration key
|
|
163
|
+
# @param context [Hash] The lookup context
|
|
164
|
+
# @return [Boolean] Whether to cache the final result for this lookup
|
|
165
|
+
def cache_results?(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
166
|
+
ContextualConfig.configuration.cache_enabled?
|
|
167
|
+
end
|
|
168
|
+
|
|
99
169
|
private
|
|
100
170
|
|
|
101
171
|
# Generate cache key for configuration lookup
|
|
102
172
|
def generate_cache_key(key, context)
|
|
103
173
|
context_hash = context.is_a?(Hash) ? context.sort.to_h : context
|
|
104
|
-
"contextual_config:#{
|
|
174
|
+
"contextual_config:#{name}:#{key}:#{Digest::MD5.hexdigest(context_hash.to_s)}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Generate cache key for candidates
|
|
178
|
+
def generate_candidates_cache_key(key, context)
|
|
179
|
+
context_hash = context.is_a?(Hash) ? context.sort.to_h : context
|
|
180
|
+
"contextual_config_candidates:#{name}:#{key}:#{Digest::MD5.hexdigest(context_hash.to_s)}"
|
|
105
181
|
end
|
|
106
182
|
|
|
107
183
|
# Log configuration lookup attempts
|
|
@@ -110,17 +186,7 @@ module ContextualConfig
|
|
|
110
186
|
|
|
111
187
|
logger = ContextualConfig.configuration.effective_logger
|
|
112
188
|
logger.info(
|
|
113
|
-
"ContextualConfig::Lookupable: #{operation} for #{
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Log cache errors
|
|
118
|
-
def log_cache_error(operation, error)
|
|
119
|
-
return unless ContextualConfig.configuration.enable_logging?
|
|
120
|
-
|
|
121
|
-
logger = ContextualConfig.configuration.effective_logger
|
|
122
|
-
logger.warn(
|
|
123
|
-
"ContextualConfig::Lookupable: Cache #{operation} error - #{error.class}: #{error.message}"
|
|
189
|
+
"ContextualConfig::Lookupable: #{operation} for #{name} - Key: #{key}, Context: #{context}"
|
|
124
190
|
)
|
|
125
191
|
end
|
|
126
192
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/concern'
|
|
2
4
|
require 'json-schema'
|
|
3
5
|
|
|
@@ -42,7 +44,7 @@ module ContextualConfig
|
|
|
42
44
|
|
|
43
45
|
begin
|
|
44
46
|
# Using fully_validate to get an array of all errors.
|
|
45
|
-
errors_found = JSON::Validator.fully_validate(schema,
|
|
47
|
+
errors_found = JSON::Validator.fully_validate(schema, config_data, strict: false, insert_defaults: false)
|
|
46
48
|
if errors_found.any?
|
|
47
49
|
errors_found.each do |error_message|
|
|
48
50
|
# Add a somewhat generic error. More specific parsing of error_message could provide better field-level errors.
|
|
@@ -67,7 +69,8 @@ module ContextualConfig
|
|
|
67
69
|
return unless schema # Skip if no schema defined
|
|
68
70
|
|
|
69
71
|
begin
|
|
70
|
-
errors_found = JSON::Validator.fully_validate(schema,
|
|
72
|
+
errors_found = JSON::Validator.fully_validate(schema, scoping_rules, strict: false,
|
|
73
|
+
insert_defaults: false)
|
|
71
74
|
if errors_found.any?
|
|
72
75
|
errors_found.each do |error_message|
|
|
73
76
|
errors.add(:scoping_rules, "is invalid - #{error_message}")
|
|
@@ -55,13 +55,9 @@ module ContextualConfig
|
|
|
55
55
|
raise(ConfigurationError, 'Cache is enabled but cache_store is not properly configured')
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
if cache_ttl <= 0
|
|
59
|
-
raise(ConfigurationError, 'cache_ttl must be a positive number')
|
|
60
|
-
end
|
|
58
|
+
raise(ConfigurationError, 'cache_ttl must be a positive number') if cache_ttl <= 0
|
|
61
59
|
|
|
62
|
-
if default_priority.negative?
|
|
63
|
-
raise(ConfigurationError, 'default_priority must be non-negative')
|
|
64
|
-
end
|
|
60
|
+
raise(ConfigurationError, 'default_priority must be non-negative') if default_priority.negative?
|
|
65
61
|
|
|
66
62
|
true
|
|
67
63
|
end
|
|
@@ -10,7 +10,7 @@ module ContextualConfig
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Register a module with its configuration
|
|
13
|
-
def register(module_name
|
|
13
|
+
def register(module_name)
|
|
14
14
|
module_sym = module_name.to_sym
|
|
15
15
|
config = @modules[module_sym] || ModuleConfig.new(module_name)
|
|
16
16
|
yield(config) if block_given?
|
data/lib/contextual_config.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'contextual_config/version'
|
|
2
4
|
require 'active_support'
|
|
3
5
|
require 'active_record'
|
|
@@ -66,6 +68,4 @@ module ContextualConfig
|
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
# Load Rails generators if Rails is available
|
|
69
|
-
if defined?(Rails)
|
|
70
|
-
require 'contextual_config/generators'
|
|
71
|
-
end
|
|
71
|
+
require 'contextual_config/generators' if defined?(Rails)
|
|
@@ -18,8 +18,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
|
18
18
|
# Lower numbers generally indicate higher priority.
|
|
19
19
|
t.integer :priority, null: false, default: 100
|
|
20
20
|
|
|
21
|
-
# '
|
|
22
|
-
|
|
21
|
+
# 'deleted_at' allows for soft-deleting configurations.
|
|
22
|
+
# NULL indicates active configuration, timestamp indicates soft-deleted.
|
|
23
|
+
t.datetime :deleted_at
|
|
23
24
|
|
|
24
25
|
# 'description' provides a human-readable summary of the configuration's purpose.
|
|
25
26
|
t.text :description
|
|
@@ -41,8 +42,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
|
41
42
|
# Note: If 'type' is not used or key should be globally unique for this table,
|
|
42
43
|
# a simpler index might be `add_index :<%= @migration_table_name %>, :key, unique: true`
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
-
|
|
45
|
+
# Functional index to efficiently query active configurations by priority.
|
|
46
|
+
# Uses expression index on (deleted_at IS NULL) for optimal soft-delete performance.
|
|
47
|
+
add_index :<%= @migration_table_name %>, ['(deleted_at IS NULL)', :priority], name: 'idx_<%= @migration_table_name.first(40) %>_on_active_priority'
|
|
46
48
|
|
|
47
49
|
# Optional: GIN indexes for jsonb columns if you frequently query directly into them.
|
|
48
50
|
# These can be large and have write performance implications, so add if needed.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: contextual_config
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bazinga012
|
|
@@ -162,7 +162,6 @@ files:
|
|
|
162
162
|
- lib/contextual_config/module_registry.rb
|
|
163
163
|
- lib/contextual_config/services/contextual_matcher.rb
|
|
164
164
|
- lib/contextual_config/version.rb
|
|
165
|
-
- lib/generators/contextual_config/configurable_table/USAGE
|
|
166
165
|
- lib/generators/contextual_config/configurable_table/configurable_table_generator.rb
|
|
167
166
|
- lib/generators/contextual_config/configurable_table/templates/migration.rb.tt
|
|
168
167
|
homepage: https://github.com/bazinga012/contextual_config
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
Description:
|
|
2
|
-
Generates a migration file for a table that's compatible with ContextualConfig concerns.
|
|
3
|
-
The table includes all necessary columns for storing contextual configurations including
|
|
4
|
-
key, config_data (JSONB), scoping_rules (JSONB), priority, is_active, and type (for STI).
|
|
5
|
-
|
|
6
|
-
Example:
|
|
7
|
-
```rails generate contextual_config:configurable_table YourModel```
|
|
8
|
-
|
|
9
|
-
This will create:
|
|
10
|
-
db/migrate/create_your_models.rb
|
|
11
|
-
|
|
12
|
-
Options:
|
|
13
|
-
--table-name=TABLE_NAME # Override the default table name
|
|
14
|
-
# Default: pluralized, underscored version of the model name
|
|
15
|
-
|
|
16
|
-
Examples:
|
|
17
|
-
rails generate contextual_config:configurable_table Finance::Configuration
|
|
18
|
-
rails generate contextual_config:configurable_table UserPreference --table-name=user_prefs
|
|
19
|
-
rails generate contextual_config:configurable_table PayrollConfig
|
|
20
|
-
|
|
21
|
-
The generated migration creates a table with:
|
|
22
|
-
- key (string): Identifier for the configuration
|
|
23
|
-
- config_data (jsonb): Main configuration payload
|
|
24
|
-
- scoping_rules (jsonb): Rules for when configuration applies
|
|
25
|
-
- priority (integer): Priority for conflict resolution
|
|
26
|
-
- is_active (boolean): Enable/disable configurations
|
|
27
|
-
- description (text): Human-readable description
|
|
28
|
-
- type (string): For Single Table Inheritance
|
|
29
|
-
- timestamps: created_at and updated_at
|
|
30
|
-
|
|
31
|
-
Indexes are automatically created for:
|
|
32
|
-
- [type, key] (unique)
|
|
33
|
-
- [is_active, priority] (for efficient querying)
|