Class: ContextualConfig::Services::ContextualMatcher
- Inherits:
-
Object
- Object
- ContextualConfig::Services::ContextualMatcher
- Defined in:
- lib/contextual_config/services/contextual_matcher.rb
Class Method Summary collapse
-
.calculate_specificity_score(scoping_rules, context) ⇒ Integer
Calculates a specificity score for a set of scoping_rules against a context.
-
.evaluate_timing_rule(timing_rule_value, current_date) ⇒ Boolean
Evaluates a 'timing' rule.
-
.find_all_matches(candidates:, context:) ⇒ Array<ActiveRecord::Base>
Finds all configuration records that match the given context.
-
.find_best_match(candidates:, context:) ⇒ ActiveRecord::Base?
Finds the single best matching configuration record from a list of candidates.
-
.log_matching_attempt(operation, candidate_count, context) ⇒ Object
Log matching attempt details.
-
.log_matching_result(operation, result) ⇒ Object
Log matching results.
-
.log_timing_error(timing_rule_value, current_date) ⇒ Object
Log timing rule evaluation errors.
-
.matches_context?(scoping_rules, context) ⇒ Boolean
Checks if a given set of scoping_rules matches the provided context.
Class Method Details
.calculate_specificity_score(scoping_rules, context) ⇒ Integer
Calculates a specificity score for a set of scoping_rules against a context. A simple score: the number of explicitly defined (and matching) dimensions in the rules.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 109 def self.calculate_specificity_score(scoping_rules, context) return 0 if scoping_rules.blank? # Global rules have the lowest specificity. # Score is the number of rules that actively match a corresponding key in the context. scoping_rules.count do |dimension_key, rule_value| dimension_sym = dimension_key.to_sym context_value = context[dimension_sym] if dimension_sym == :timing # Timing rule contributes to specificity if it's valid and matches context.key?(:current_date) && evaluate_timing_rule(rule_value, context[:current_date]) else # Other rules contribute if context has the key and values match context.key?(dimension_sym) && context_value == rule_value end end end |
.evaluate_timing_rule(timing_rule_value, current_date) ⇒ Boolean
Evaluates a 'timing' rule. Assumes rule_value is a hash like { "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD" }. Dates are inclusive. current_date must be provided in the context.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 134 def self.evaluate_timing_rule(timing_rule_value, current_date) return false unless timing_rule_value.is_a?(Hash) # Rule must be a hash return false if current_date.nil? # Context must provide current_date begin # Ensure current_date is a Date object effective_current_date = current_date.is_a?(Date) ? current_date : Date.parse(current_date.to_s) rule_start_date_str = timing_rule_value['start_date'] || timing_rule_value[:start_date] rule_end_date_str = timing_rule_value['end_date'] || timing_rule_value[:end_date] # Parse rule dates if they exist start_date = rule_start_date_str ? Date.parse(rule_start_date_str.to_s) : nil end_date = rule_end_date_str ? Date.parse(rule_end_date_str.to_s) : nil # Perform checks matches = true matches &&= (effective_current_date >= start_date) if start_date matches &&= (effective_current_date <= end_date) if end_date matches rescue ArgumentError # Invalid date string log_timing_error(timing_rule_value, current_date) false end end |
.find_all_matches(candidates:, context:) ⇒ Array<ActiveRecord::Base>
Finds all configuration records that match the given context.
53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 53 def self.find_all_matches(candidates:, context:) log_matching_attempt('find_all_matches', candidates.count, context) matches = candidates.select do |candidate_record| candidate_record.respond_to?(:scoping_rules) && matches_context?(candidate_record.scoping_rules, context) end log_matching_result('find_all_matches', matches) matches end |
.find_best_match(candidates:, context:) ⇒ ActiveRecord::Base?
Finds the single best matching configuration record from a list of candidates. Candidates are expected to be pre-sorted by their 'priority' (higher priority first).
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 16 def self.find_best_match(candidates:, context:) log_matching_attempt('find_best_match', candidates.count, context) best_match_record = nil highest_specificity_score = -1 # Initialize with a value lower than any possible score candidates.each do |candidate_record| next unless candidate_record.respond_to?(:scoping_rules) # Ensure it's a configurable record next unless matches_context?(candidate_record.scoping_rules, context) current_specificity_score = calculate_specificity_score(candidate_record.scoping_rules, context) # Higher specificity wins. # If specificity is the same, the one encountered first wins due to pre-sorting by priority. if current_specificity_score > highest_specificity_score highest_specificity_score = current_specificity_score best_match_record = candidate_record elsif current_specificity_score == highest_specificity_score && best_match_record.nil? # This case handles if the very first matching record has a specificity of 0 (e.g. global rule) # and becomes the initial best_match_record. best_match_record = candidate_record end # If current_specificity_score == highest_specificity_score and best_match_record is not nil, # the existing best_match_record (which came earlier in the pre-sorted list, # thus having higher or equal priority) is preferred. end log_matching_result('find_best_match', best_match_record) best_match_record end |
.log_matching_attempt(operation, candidate_count, context) ⇒ Object
Log matching attempt details
161 162 163 164 165 166 167 168 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 161 def self.log_matching_attempt(operation, candidate_count, context) return unless ContextualConfig.configuration.enable_logging? logger = ContextualConfig.configuration.effective_logger logger.info( "ContextualMatcher: #{operation} - Candidates: #{candidate_count}, Context: #{context}" ) end |
.log_matching_result(operation, result) ⇒ Object
Log matching results
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 171 def self.log_matching_result(operation, result) return unless ContextualConfig.configuration.enable_logging? logger = ContextualConfig.configuration.effective_logger result_description = if result.is_a?(Array) "#{result.count} matches found" elsif result "Match found: #{result.key} (Priority: #{result.priority})" else 'No match found' end logger.info("ContextualMatcher: #{operation} - #{result_description}") end |
.log_timing_error(timing_rule_value, current_date) ⇒ Object
Log timing rule evaluation errors
187 188 189 190 191 192 193 194 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 187 def self.log_timing_error(timing_rule_value, current_date) return unless ContextualConfig.configuration.enable_logging? logger = ContextualConfig.configuration.effective_logger logger.warn( "ContextualMatcher: Invalid date format in timing rule: #{timing_rule_value} or current_date: #{current_date}" ) end |
.matches_context?(scoping_rules, context) ⇒ Boolean
Checks if a given set of scoping_rules matches the provided context.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/contextual_config/services/contextual_matcher.rb', line 72 def self.matches_context?(scoping_rules, context) return true if scoping_rules.blank? # An empty rule set is a global match. scoping_rules.all? do |dimension_key, rule_value| dimension_sym = dimension_key.to_sym context_value = context[dimension_sym] # --- Special Handling for 'timing' Dimension --- if dimension_sym == :timing # Check if timing evaluation is enabled if ContextualConfig.configuration.timing_evaluation_enabled? evaluate_timing_rule(rule_value, context[:current_date]) else true # Skip timing evaluation when disabled end # --- Add other special dimension handlers here as elif/else if --- # Example: # elsif dimension_sym == :employee_location_country # # Assuming rule_value is an array of allowed countries and context_value is the employee's country # evaluate_location_country_rule(rule_value, context_value) else # --- Default Generic Dimension Matching --- # If the rule specifies a dimension, the context *must* have a value for that dimension, # and the values must match. # If you want to treat a missing key in context as "doesn't matter for this rule", # this logic would need to change. Current logic: rule implies context must provide. context.key?(dimension_sym) && context_value == rule_value end end end |