contextual_config 0.1.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 +7 -0
- data/.claude/settings.local.json +14 -0
- data/.rspec +3 -0
- data/CLAUDE.md +66 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +295 -0
- data/LICENSE.txt +21 -0
- data/README.md +782 -0
- data/Rakefile +10 -0
- data/contextual_config.gemspec +43 -0
- data/lib/contextual_config/concern/configurable.rb +112 -0
- data/lib/contextual_config/concern/lookupable.rb +129 -0
- data/lib/contextual_config/concern/schema_driven_validation.rb +152 -0
- data/lib/contextual_config/configuration.rb +77 -0
- data/lib/contextual_config/generators.rb +4 -0
- data/lib/contextual_config/module_registry.rb +144 -0
- data/lib/contextual_config/services/contextual_matcher.rb +201 -0
- data/lib/contextual_config/version.rb +3 -0
- data/lib/contextual_config.rb +71 -0
- data/lib/generators/contextual_config/configurable_table/USAGE +33 -0
- data/lib/generators/contextual_config/configurable_table/configurable_table_generator.rb +68 -0
- data/lib/generators/contextual_config/configurable_table/templates/migration.rb.tt +52 -0
- metadata +194 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
3
|
+
module ContextualConfig
|
|
4
|
+
module Services
|
|
5
|
+
class ContextualMatcher
|
|
6
|
+
# --- Public Class Methods ---
|
|
7
|
+
|
|
8
|
+
# Finds the single best matching configuration record from a list of candidates.
|
|
9
|
+
# Candidates are expected to be pre-sorted by their 'priority' (higher priority first).
|
|
10
|
+
#
|
|
11
|
+
# @param candidates [ActiveRecord::Relation, Array<ActiveRecord::Base>] A collection of candidate configuration records.
|
|
12
|
+
# @param context [Hash] The current runtime context.
|
|
13
|
+
# @return [ActiveRecord::Base, nil] The best matching record, or nil if no suitable match.
|
|
14
|
+
def self.find_best_match(candidates:, context:)
|
|
15
|
+
log_matching_attempt('find_best_match', candidates.count, context)
|
|
16
|
+
|
|
17
|
+
best_match_record = nil
|
|
18
|
+
highest_specificity_score = -1 # Initialize with a value lower than any possible score
|
|
19
|
+
|
|
20
|
+
candidates.each do |candidate_record|
|
|
21
|
+
next unless candidate_record.respond_to?(:scoping_rules) # Ensure it's a configurable record
|
|
22
|
+
|
|
23
|
+
next unless matches_context?(candidate_record.scoping_rules, context)
|
|
24
|
+
|
|
25
|
+
current_specificity_score = calculate_specificity_score(candidate_record.scoping_rules, context)
|
|
26
|
+
|
|
27
|
+
# Higher specificity wins.
|
|
28
|
+
# If specificity is the same, the one encountered first wins due to pre-sorting by priority.
|
|
29
|
+
if current_specificity_score > highest_specificity_score
|
|
30
|
+
highest_specificity_score = current_specificity_score
|
|
31
|
+
best_match_record = candidate_record
|
|
32
|
+
elsif current_specificity_score == highest_specificity_score && best_match_record.nil?
|
|
33
|
+
# This case handles if the very first matching record has a specificity of 0 (e.g. global rule)
|
|
34
|
+
# and becomes the initial best_match_record.
|
|
35
|
+
best_match_record = candidate_record
|
|
36
|
+
end
|
|
37
|
+
# If current_specificity_score == highest_specificity_score and best_match_record is not nil,
|
|
38
|
+
# the existing best_match_record (which came earlier in the pre-sorted list,
|
|
39
|
+
# thus having higher or equal priority) is preferred.
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
log_matching_result('find_best_match', best_match_record)
|
|
43
|
+
best_match_record
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Finds all configuration records that match the given context.
|
|
47
|
+
#
|
|
48
|
+
# @param candidates [ActiveRecord::Relation, Array<ActiveRecord::Base>] A collection of candidate configuration records.
|
|
49
|
+
# @param context [Hash] The current runtime context.
|
|
50
|
+
# @return [Array<ActiveRecord::Base>] An array of all matching records, maintaining original sort order (priority).
|
|
51
|
+
def self.find_all_matches(candidates:, context:)
|
|
52
|
+
log_matching_attempt('find_all_matches', candidates.count, context)
|
|
53
|
+
|
|
54
|
+
matches = candidates.select do |candidate_record|
|
|
55
|
+
candidate_record.respond_to?(:scoping_rules) &&
|
|
56
|
+
matches_context?(candidate_record.scoping_rules, context)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
log_matching_result('find_all_matches', matches)
|
|
60
|
+
matches
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# --- Private Class Methods ---
|
|
64
|
+
|
|
65
|
+
# Checks if a given set of scoping_rules matches the provided context.
|
|
66
|
+
#
|
|
67
|
+
# @param scoping_rules [Hash, nil] The rules from a configuration record.
|
|
68
|
+
# @param context [Hash] The runtime context.
|
|
69
|
+
# @return [Boolean] True if all rules match or if rules are empty/nil, false otherwise.
|
|
70
|
+
def self.matches_context?(scoping_rules, context)
|
|
71
|
+
return true if scoping_rules.blank? # An empty rule set is a global match.
|
|
72
|
+
|
|
73
|
+
scoping_rules.all? do |dimension_key, rule_value|
|
|
74
|
+
dimension_sym = dimension_key.to_sym
|
|
75
|
+
context_value = context[dimension_sym]
|
|
76
|
+
|
|
77
|
+
# --- Special Handling for 'timing' Dimension ---
|
|
78
|
+
if dimension_sym == :timing
|
|
79
|
+
# Check if timing evaluation is enabled
|
|
80
|
+
if ContextualConfig.configuration.timing_evaluation_enabled?
|
|
81
|
+
evaluate_timing_rule(rule_value, context[:current_date])
|
|
82
|
+
else
|
|
83
|
+
true # Skip timing evaluation when disabled
|
|
84
|
+
end
|
|
85
|
+
# --- Add other special dimension handlers here as elif/else if ---
|
|
86
|
+
# Example:
|
|
87
|
+
# elsif dimension_sym == :employee_location_country
|
|
88
|
+
# # Assuming rule_value is an array of allowed countries and context_value is the employee's country
|
|
89
|
+
# evaluate_location_country_rule(rule_value, context_value)
|
|
90
|
+
else
|
|
91
|
+
# --- Default Generic Dimension Matching ---
|
|
92
|
+
# If the rule specifies a dimension, the context *must* have a value for that dimension,
|
|
93
|
+
# and the values must match.
|
|
94
|
+
# If you want to treat a missing key in context as "doesn't matter for this rule",
|
|
95
|
+
# this logic would need to change. Current logic: rule implies context must provide.
|
|
96
|
+
context.key?(dimension_sym) && context_value == rule_value
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Calculates a specificity score for a set of scoping_rules against a context.
|
|
102
|
+
# A simple score: the number of explicitly defined (and matching) dimensions in the rules.
|
|
103
|
+
#
|
|
104
|
+
# @param scoping_rules [Hash, nil] The rules from a configuration record.
|
|
105
|
+
# @param context [Hash] The runtime context (used here to ensure we only score dimensions that could match).
|
|
106
|
+
# @return [Integer] The specificity score.
|
|
107
|
+
def self.calculate_specificity_score(scoping_rules, context)
|
|
108
|
+
return 0 if scoping_rules.blank? # Global rules have the lowest specificity.
|
|
109
|
+
|
|
110
|
+
# Score is the number of rules that actively match a corresponding key in the context.
|
|
111
|
+
scoping_rules.count do |dimension_key, rule_value|
|
|
112
|
+
dimension_sym = dimension_key.to_sym
|
|
113
|
+
context_value = context[dimension_sym]
|
|
114
|
+
|
|
115
|
+
if dimension_sym == :timing
|
|
116
|
+
# Timing rule contributes to specificity if it's valid and matches
|
|
117
|
+
context.key?(:current_date) && evaluate_timing_rule(rule_value, context[:current_date])
|
|
118
|
+
else
|
|
119
|
+
# Other rules contribute if context has the key and values match
|
|
120
|
+
context.key?(dimension_sym) && context_value == rule_value
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Evaluates a 'timing' rule.
|
|
126
|
+
# Assumes rule_value is a hash like { "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD" }.
|
|
127
|
+
# Dates are inclusive. current_date must be provided in the context.
|
|
128
|
+
#
|
|
129
|
+
# @param timing_rule_value [Hash] The timing rule definition.
|
|
130
|
+
# @param current_date [Date, String, nil] The current date from the context.
|
|
131
|
+
# @return [Boolean] True if the current_date falls within the rule's range (or if range is unbounded appropriately).
|
|
132
|
+
def self.evaluate_timing_rule(timing_rule_value, current_date)
|
|
133
|
+
return false unless timing_rule_value.is_a?(Hash) # Rule must be a hash
|
|
134
|
+
return false if current_date.nil? # Context must provide current_date
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
# Ensure current_date is a Date object
|
|
138
|
+
effective_current_date = current_date.is_a?(Date) ? current_date : Date.parse(current_date.to_s)
|
|
139
|
+
|
|
140
|
+
rule_start_date_str = timing_rule_value['start_date'] || timing_rule_value[:start_date]
|
|
141
|
+
rule_end_date_str = timing_rule_value['end_date'] || timing_rule_value[:end_date]
|
|
142
|
+
|
|
143
|
+
# Parse rule dates if they exist
|
|
144
|
+
start_date = rule_start_date_str ? Date.parse(rule_start_date_str.to_s) : nil
|
|
145
|
+
end_date = rule_end_date_str ? Date.parse(rule_end_date_str.to_s) : nil
|
|
146
|
+
|
|
147
|
+
# Perform checks
|
|
148
|
+
matches = true
|
|
149
|
+
matches &&= (effective_current_date >= start_date) if start_date
|
|
150
|
+
matches &&= (effective_current_date <= end_date) if end_date
|
|
151
|
+
matches
|
|
152
|
+
rescue ArgumentError # Invalid date string
|
|
153
|
+
log_timing_error(timing_rule_value, current_date)
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Log matching attempt details
|
|
159
|
+
def self.log_matching_attempt(operation, candidate_count, context)
|
|
160
|
+
return unless ContextualConfig.configuration.enable_logging?
|
|
161
|
+
|
|
162
|
+
logger = ContextualConfig.configuration.effective_logger
|
|
163
|
+
logger.info(
|
|
164
|
+
"ContextualMatcher: #{operation} - Candidates: #{candidate_count}, Context: #{context}"
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Log matching results
|
|
169
|
+
def self.log_matching_result(operation, result)
|
|
170
|
+
return unless ContextualConfig.configuration.enable_logging?
|
|
171
|
+
|
|
172
|
+
logger = ContextualConfig.configuration.effective_logger
|
|
173
|
+
result_description = if result.is_a?(Array)
|
|
174
|
+
"#{result.count} matches found"
|
|
175
|
+
elsif result
|
|
176
|
+
"Match found: #{result.key} (Priority: #{result.priority})"
|
|
177
|
+
else
|
|
178
|
+
'No match found'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
logger.info("ContextualMatcher: #{operation} - #{result_description}")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Log timing rule evaluation errors
|
|
185
|
+
def self.log_timing_error(timing_rule_value, current_date)
|
|
186
|
+
return unless ContextualConfig.configuration.enable_logging?
|
|
187
|
+
|
|
188
|
+
logger = ContextualConfig.configuration.effective_logger
|
|
189
|
+
logger.warn(
|
|
190
|
+
"ContextualMatcher: Invalid date format in timing rule: #{timing_rule_value} or current_date: #{current_date}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Placeholder for other specific rule evaluators if needed
|
|
195
|
+
# def self.evaluate_location_country_rule(rule_value_array, context_value_string)
|
|
196
|
+
# return false unless rule_value_array.is_a?(Array)
|
|
197
|
+
# rule_value_array.include?(context_value_string)
|
|
198
|
+
# end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative 'contextual_config/version'
|
|
2
|
+
require 'active_support'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
module ContextualConfig
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Autoload concerns
|
|
9
|
+
autoload :Configurable, 'contextual_config/concern/configurable'
|
|
10
|
+
autoload :Lookupable, 'contextual_config/concern/lookupable'
|
|
11
|
+
autoload :SchemaDrivenValidation, 'contextual_config/concern/schema_driven_validation'
|
|
12
|
+
|
|
13
|
+
# Autoload services
|
|
14
|
+
autoload :ContextualMatcher, 'contextual_config/services/contextual_matcher'
|
|
15
|
+
|
|
16
|
+
# Autoload configuration classes
|
|
17
|
+
autoload :Configuration, 'contextual_config/configuration'
|
|
18
|
+
autoload :ModuleRegistry, 'contextual_config/module_registry'
|
|
19
|
+
|
|
20
|
+
module Concern
|
|
21
|
+
autoload :Configurable, 'contextual_config/concern/configurable'
|
|
22
|
+
autoload :Lookupable, 'contextual_config/concern/lookupable'
|
|
23
|
+
autoload :SchemaDrivenValidation, 'contextual_config/concern/schema_driven_validation'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Services
|
|
27
|
+
autoload :ContextualMatcher, 'contextual_config/services/contextual_matcher'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Global configuration for the gem
|
|
32
|
+
def configuration
|
|
33
|
+
@configuration ||= Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Configure the gem
|
|
37
|
+
def configure
|
|
38
|
+
yield(configuration) if block_given?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Reset configuration (useful for testing)
|
|
42
|
+
def reset_configuration!
|
|
43
|
+
@configuration = Configuration.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Module registry access
|
|
47
|
+
def registry
|
|
48
|
+
@registry ||= ModuleRegistry.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Register a module
|
|
52
|
+
def register_module(module_name, &block)
|
|
53
|
+
registry.register(module_name, &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get a registered module
|
|
57
|
+
def get_module(module_name)
|
|
58
|
+
registry.get(module_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get all registered modules
|
|
62
|
+
def registered_modules
|
|
63
|
+
registry.all
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Load Rails generators if Rails is available
|
|
69
|
+
if defined?(Rails)
|
|
70
|
+
require 'contextual_config/generators'
|
|
71
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'rails/generators/active_record'
|
|
2
|
+
|
|
3
|
+
module ContextualConfig
|
|
4
|
+
module Generators
|
|
5
|
+
# Generates a migration to create a table suitable for models
|
|
6
|
+
# that will include ContextualConfig's concerns.
|
|
7
|
+
#
|
|
8
|
+
# To run this generator:
|
|
9
|
+
# rails generate contextual_config:configurable_table YourModelName
|
|
10
|
+
# rails generate contextual_config:configurable_table Namespace::YourModelName
|
|
11
|
+
# rails generate contextual_config:configurable_table UserPreference --table-name user_prefs
|
|
12
|
+
#
|
|
13
|
+
class ConfigurableTableGenerator < Rails::Generators::NamedBase
|
|
14
|
+
include ActiveRecord::Generators::Migration # Provides #migration_template
|
|
15
|
+
|
|
16
|
+
# Thor class options
|
|
17
|
+
class_option :table_name, type: :string, desc: 'Specify the table name directly, overriding the default.'
|
|
18
|
+
|
|
19
|
+
# Define where to find generator templates (e.g., migration.rb.tt)
|
|
20
|
+
source_root File.expand_path('templates', __dir__)
|
|
21
|
+
|
|
22
|
+
# The main method executed by the generator
|
|
23
|
+
def create_migration_file
|
|
24
|
+
# migration_template will copy the template file to the application's db/migrate directory.
|
|
25
|
+
# It automatically handles the timestamp for the migration filename.
|
|
26
|
+
# Instance variables set here (like @migration_table_name) are available in the template.
|
|
27
|
+
@migration_table_name = determine_table_name_for_migration
|
|
28
|
+
|
|
29
|
+
migration_template('migration.rb.tt', # Template file name
|
|
30
|
+
"db/migrate/create_#{@migration_table_name}.rb", # Destination path
|
|
31
|
+
migration_version: migration_version_string) # For Rails 5+ migration versions
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Required by ActiveRecord::Generators::Migration to create timestamped migration filenames.
|
|
35
|
+
def self.next_migration_number(dirname)
|
|
36
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Determines the actual table name to be used in the migration.
|
|
42
|
+
# Uses the --table-name option if provided, otherwise derives from the generator's 'name' argument.
|
|
43
|
+
def determine_table_name_for_migration
|
|
44
|
+
# `name` is the original argument to the generator (e.g., "Finance::Configuration" or "UserPreference")
|
|
45
|
+
# `table_name` is a helper from NamedBase (e.g., "user_preferences" for "UserPreference")
|
|
46
|
+
custom_name = options[:table_name]
|
|
47
|
+
return custom_name.underscore if custom_name.present?
|
|
48
|
+
|
|
49
|
+
# For namespaced models like "Finance::Configuration", `name.underscore` gives "finance/configuration".
|
|
50
|
+
# We want "finance_configurations" as the table name.
|
|
51
|
+
name.underscore.tr('/', '_').pluralize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Provides the Rails migration version string, e.g., "[7.0]".
|
|
55
|
+
def migration_version_string
|
|
56
|
+
"[#{ActiveRecord::Migration.current_version}]" if ActiveRecord::Migration.respond_to?(:current_version)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Helper method accessible in the ERB template to form the migration class name.
|
|
60
|
+
# e.g., CreateFinanceConfigurations
|
|
61
|
+
public
|
|
62
|
+
|
|
63
|
+
def migration_class_name
|
|
64
|
+
"Create#{determine_table_name_for_migration.camelize}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# This file is a template used by the ContextualConfig::ConfigurableTableGenerator.
|
|
2
|
+
# It will be processed to create a migration file in your application's db/migrate directory.
|
|
3
|
+
|
|
4
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version_string %>
|
|
5
|
+
def change
|
|
6
|
+
create_table :<%= @migration_table_name %> do |t|
|
|
7
|
+
# --- Standard columns for ContextualConfig ---
|
|
8
|
+
# 'key' uniquely identifies a configuration entry within its type/scope.
|
|
9
|
+
t.string :key, null: false
|
|
10
|
+
|
|
11
|
+
# 'config_data' stores the main JSON payload of the configuration.
|
|
12
|
+
t.jsonb :config_data, null: false, default: {}
|
|
13
|
+
|
|
14
|
+
# 'scoping_rules' stores the JSON defining when this configuration is applicable.
|
|
15
|
+
t.jsonb :scoping_rules, null: false, default: {}
|
|
16
|
+
|
|
17
|
+
# 'priority' helps resolve conflicts if multiple configurations match.
|
|
18
|
+
# Lower numbers generally indicate higher priority.
|
|
19
|
+
t.integer :priority, null: false, default: 100
|
|
20
|
+
|
|
21
|
+
# 'is_active' allows for soft-deleting or disabling configurations.
|
|
22
|
+
t.boolean :is_active, null: false, default: true
|
|
23
|
+
|
|
24
|
+
# 'description' provides a human-readable summary of the configuration's purpose.
|
|
25
|
+
t.text :description
|
|
26
|
+
|
|
27
|
+
# 'type' column is standard for Single Table Inheritance (STI).
|
|
28
|
+
# Include this if the model using this table (e.g., Finance::Configuration)
|
|
29
|
+
# will itself be a parent class for more specific configuration types
|
|
30
|
+
# (e.g., Finance::TaxConfig, Finance::ReportConfig).
|
|
31
|
+
t.string :type
|
|
32
|
+
|
|
33
|
+
t.timestamps null: false
|
|
34
|
+
# --- End of standard columns ---
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# --- Recommended Indexes ---
|
|
38
|
+
# Index for typical lookups by key, potentially scoped by STI type.
|
|
39
|
+
# Using a unique name for the index to avoid conflicts if multiple tables are generated.
|
|
40
|
+
add_index :<%= @migration_table_name %>, [:type, :key], unique: true, name: 'idx_<%= @migration_table_name.first(40) %>_on_type_and_key'
|
|
41
|
+
# Note: If 'type' is not used or key should be globally unique for this table,
|
|
42
|
+
# a simpler index might be `add_index :<%= @migration_table_name %>, :key, unique: true`
|
|
43
|
+
|
|
44
|
+
# Index to efficiently query active configurations by priority.
|
|
45
|
+
add_index :<%= @migration_table_name %>, [:is_active, :priority], name: 'idx_<%= @migration_table_name.first(40) %>_on_active_priority'
|
|
46
|
+
|
|
47
|
+
# Optional: GIN indexes for jsonb columns if you frequently query directly into them.
|
|
48
|
+
# These can be large and have write performance implications, so add if needed.
|
|
49
|
+
# add_index :<%= @migration_table_name %>, :config_data, using: :gin, name: 'idx_<%= @migration_table_name.first(40) %>_on_config_data'
|
|
50
|
+
# add_index :<%= @migration_table_name %>, :scoping_rules, using: :gin, name: 'idx_<%= @migration_table_name.first(40) %>_on_scoping_rules'
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: contextual_config
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- bazinga012
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: json-schema
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '6.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop-rails
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '2.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '2.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop-rspec
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: sqlite3
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '2.1'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.1'
|
|
138
|
+
description: ContextualConfig provides a flexible framework for managing configurations
|
|
139
|
+
that can be applied based on contextual rules, priorities, and scoping. Perfect
|
|
140
|
+
for complex applications requiring dynamic configuration resolution.
|
|
141
|
+
email:
|
|
142
|
+
- vishal18593@gmail.com
|
|
143
|
+
executables: []
|
|
144
|
+
extensions: []
|
|
145
|
+
extra_rdoc_files: []
|
|
146
|
+
files:
|
|
147
|
+
- ".claude/settings.local.json"
|
|
148
|
+
- ".rspec"
|
|
149
|
+
- CLAUDE.md
|
|
150
|
+
- Gemfile
|
|
151
|
+
- Gemfile.lock
|
|
152
|
+
- LICENSE.txt
|
|
153
|
+
- README.md
|
|
154
|
+
- Rakefile
|
|
155
|
+
- contextual_config.gemspec
|
|
156
|
+
- lib/contextual_config.rb
|
|
157
|
+
- lib/contextual_config/concern/configurable.rb
|
|
158
|
+
- lib/contextual_config/concern/lookupable.rb
|
|
159
|
+
- lib/contextual_config/concern/schema_driven_validation.rb
|
|
160
|
+
- lib/contextual_config/configuration.rb
|
|
161
|
+
- lib/contextual_config/generators.rb
|
|
162
|
+
- lib/contextual_config/module_registry.rb
|
|
163
|
+
- lib/contextual_config/services/contextual_matcher.rb
|
|
164
|
+
- lib/contextual_config/version.rb
|
|
165
|
+
- lib/generators/contextual_config/configurable_table/USAGE
|
|
166
|
+
- lib/generators/contextual_config/configurable_table/configurable_table_generator.rb
|
|
167
|
+
- lib/generators/contextual_config/configurable_table/templates/migration.rb.tt
|
|
168
|
+
homepage: https://github.com/bazinga012/contextual_config
|
|
169
|
+
licenses:
|
|
170
|
+
- MIT
|
|
171
|
+
metadata:
|
|
172
|
+
allowed_push_host: https://rubygems.org
|
|
173
|
+
homepage_uri: https://github.com/bazinga012/contextual_config
|
|
174
|
+
source_code_uri: https://github.com/bazinga012/contextual_config
|
|
175
|
+
changelog_uri: https://github.com/bazinga012/contextual_config/blob/main/CHANGELOG.md
|
|
176
|
+
rubygems_mfa_required: 'true'
|
|
177
|
+
rdoc_options: []
|
|
178
|
+
require_paths:
|
|
179
|
+
- lib
|
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: 3.0.0
|
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
|
+
requirements:
|
|
187
|
+
- - ">="
|
|
188
|
+
- !ruby/object:Gem::Version
|
|
189
|
+
version: '0'
|
|
190
|
+
requirements: []
|
|
191
|
+
rubygems_version: 3.6.9
|
|
192
|
+
specification_version: 4
|
|
193
|
+
summary: A Ruby gem for context-aware configuration management
|
|
194
|
+
test_files: []
|