|
5 | 5 |
|
6 | 6 | require_relative 'environments/models' |
7 | 7 | require_relative 'features/models' |
| 8 | +require_relative 'features/constants' |
8 | 9 | require_relative 'identities/models' |
9 | 10 | require_relative 'organisations/models' |
10 | 11 | require_relative 'projects/models' |
11 | 12 | require_relative 'segments/evaluator' |
12 | 13 | require_relative 'segments/models' |
13 | 14 | require_relative 'utils/hash_func' |
14 | 15 | require_relative 'evaluation/mappers' |
15 | | -require_relative 'evaluation/core' |
16 | 16 |
|
17 | 17 | module Flagsmith |
| 18 | + # Core evaluation logic for feature flags |
18 | 19 | module Engine |
19 | | - # Flags engine methods |
20 | | - # NOTE: This class is kept for backwards compatibility but no longer contains |
21 | | - # the old model-based evaluation methods. Use the context-based evaluation |
22 | | - # via Flagsmith::Engine::Evaluation::Core.get_evaluation_result instead. |
23 | | - class Engine |
24 | | - include Flagsmith::Engine::Segments::Evaluator |
| 20 | + extend self |
| 21 | + include Flagsmith::Engine::Utils::HashFunc |
| 22 | + include Flagsmith::Engine::Features::TargetingReasons |
| 23 | + include Flagsmith::Engine::Segments::Evaluator |
| 24 | + |
| 25 | + # Get evaluation result from evaluation context |
| 26 | + # |
| 27 | + # @param evaluation_context [Hash] The evaluation context |
| 28 | + # @return [Hash] Evaluation result with flags and segments |
| 29 | + # returns EvaluationResultWithMetadata |
| 30 | + def get_evaluation_result(evaluation_context) |
| 31 | + segments, segment_overrides = evaluate_segments(evaluation_context) |
| 32 | + flags = evaluate_features(evaluation_context, segment_overrides) |
| 33 | + { |
| 34 | + flags: flags, |
| 35 | + segments: segments |
| 36 | + } |
| 37 | + end |
| 38 | + |
| 39 | + # Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; } |
| 40 | + def evaluate_segments(evaluation_context) |
| 41 | + return [], {} if evaluation_context[:segments].nil? |
| 42 | + |
| 43 | + identity_segments = get_segments_from_context(evaluation_context) |
| 44 | + |
| 45 | + segments = identity_segments.map do |segment| |
| 46 | + result = { |
| 47 | + name: segment[:name] |
| 48 | + } |
| 49 | + |
| 50 | + result[:metadata] = segment[:metadata] if segment[:metadata] |
| 51 | + |
| 52 | + result |
| 53 | + end |
| 54 | + |
| 55 | + segment_overrides = process_segment_overrides(identity_segments) |
| 56 | + |
| 57 | + [segments, segment_overrides] |
| 58 | + end |
| 59 | + |
| 60 | + # Returns Record<string: override.name, SegmentOverride> |
| 61 | + def process_segment_overrides(identity_segments) |
| 62 | + segment_overrides = {} |
| 63 | + |
| 64 | + identity_segments.each do |segment| |
| 65 | + next unless segment[:overrides] |
| 66 | + |
| 67 | + overrides_list = segment[:overrides].is_a?(Array) ? segment[:overrides] : [] |
| 68 | + |
| 69 | + overrides_list.each do |override| |
| 70 | + next unless should_apply_override(override, segment_overrides) |
| 71 | + |
| 72 | + segment_overrides[override[:name]] = { |
| 73 | + feature: override, |
| 74 | + segment_name: segment[:name] |
| 75 | + } |
| 76 | + end |
| 77 | + end |
| 78 | + |
| 79 | + segment_overrides |
| 80 | + end |
| 81 | + |
| 82 | + # returns EvaluationResultFlags<Metadata> |
| 83 | + def evaluate_features(evaluation_context, segment_overrides) |
| 84 | + flags = {} |
| 85 | + |
| 86 | + (evaluation_context[:features] || {}).each_value do |feature| |
| 87 | + segment_override = segment_overrides[feature[:name]] |
| 88 | + final_feature = segment_override ? segment_override[:feature] : feature |
| 89 | + has_override = !segment_override.nil? |
| 90 | + |
| 91 | + # Evaluate feature value |
| 92 | + evaluated = evaluate_feature_value(final_feature, get_identity_key(evaluation_context)) |
| 93 | + |
| 94 | + # Build flag result |
| 95 | + flag_result = { |
| 96 | + name: final_feature[:name], |
| 97 | + enabled: final_feature[:enabled], |
| 98 | + value: evaluated[:value] |
| 99 | + } |
| 100 | + |
| 101 | + # Add metadata if present |
| 102 | + flag_result[:metadata] = final_feature[:metadata] if final_feature[:metadata] |
| 103 | + |
| 104 | + # Set reason |
| 105 | + flag_result[:reason] = evaluated[:reason] || |
| 106 | + (has_override ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{segment_override[:segment_name]}" : TARGETING_REASON_DEFAULT) |
| 107 | + flags[final_feature[:name].to_sym] = flag_result |
| 108 | + end |
| 109 | + |
| 110 | + flags |
| 111 | + end |
| 112 | + |
| 113 | + # Returns {value: any; reason?: string} |
| 114 | + def evaluate_feature_value(feature, identity_key = nil) |
| 115 | + return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key |
| 116 | + |
| 117 | + { value: feature[:value], reason: nil } |
| 118 | + end |
| 119 | + |
| 120 | + # Returns {value: any; reason?: string} |
| 121 | + def get_multivariate_feature_value(feature, identity_key) |
| 122 | + percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key]) |
| 123 | + sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY } |
| 124 | + |
| 125 | + start_percentage = 0 |
| 126 | + sorted_variants.each do |variant| |
| 127 | + limit = start_percentage + variant[:weight] |
| 128 | + if start_percentage <= percentage_value && percentage_value < limit |
| 129 | + return { |
| 130 | + value: variant[:value], |
| 131 | + reason: "#{TARGETING_REASON_SPLIT}; weight=#{variant[:weight]}" |
| 132 | + } |
| 133 | + end |
| 134 | + start_percentage = limit |
| 135 | + end |
| 136 | + |
| 137 | + { value: feature[:value], reason: nil } |
| 138 | + end |
| 139 | + |
| 140 | + # returns boolean |
| 141 | + def should_apply_override(override, existing_overrides) |
| 142 | + current_override = existing_overrides[override[:name]] |
| 143 | + !current_override || is_stronger_priority?(override[:priority], current_override[:feature][:priority]) |
| 144 | + end |
| 145 | + |
| 146 | + private |
| 147 | + |
| 148 | + # Extract identity key from evaluation context |
| 149 | + # |
| 150 | + # @param evaluation_context [Hash] The evaluation context |
| 151 | + # @return [String, nil] The identity key or nil if no identity |
| 152 | + def get_identity_key(evaluation_context) |
| 153 | + return nil unless evaluation_context[:identity] |
| 154 | + |
| 155 | + evaluation_context[:identity][:key] || |
| 156 | + "#{evaluation_context[:environment][:key]}_#{evaluation_context[:identity][:identifier]}" |
| 157 | + end |
| 158 | + |
| 159 | + # returns boolean |
| 160 | + def is_stronger_priority?(priority_a, priority_b) |
| 161 | + (priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY) |
25 | 162 | end |
26 | 163 | end |
27 | 164 | end |
0 commit comments