|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require_relative '../utils/hash_func' |
| 4 | +require_relative '../features/constants' |
| 5 | +require_relative '../segments/evaluator' |
| 6 | + |
| 7 | +module Flagsmith |
| 8 | + module Engine |
| 9 | + module Evaluation |
| 10 | + # Core evaluation logic for feature flags |
| 11 | + module Core |
| 12 | + extend self |
| 13 | + include Flagsmith::Engine::Utils::HashFunc |
| 14 | + include Flagsmith::Engine::Features::TargetingReasons |
| 15 | + include Flagsmith::Engine::Segments::Evaluator |
| 16 | + # Get evaluation result from evaluation context |
| 17 | + # |
| 18 | + # @param evaluation_context [Hash] The evaluation context |
| 19 | + # @return [Hash] Evaluation result with flags and segments |
| 20 | + # returns EvaluationResultWithMetadata |
| 21 | + def get_evaluation_result(evaluation_context) |
| 22 | + segments, segment_overrides = evaluate_segments(evaluation_context) |
| 23 | + flags = evaluate_features(evaluation_context, segment_overrides) |
| 24 | + { |
| 25 | + flags: flags, |
| 26 | + segments: segments |
| 27 | + } |
| 28 | + end |
| 29 | + |
| 30 | + # Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; } |
| 31 | + def evaluate_segments(evaluation_context) |
| 32 | + return [], {} if evaluation_context[:segments].nil? |
| 33 | + |
| 34 | + identity_segments = get_segments_from_context(evaluation_context) |
| 35 | + |
| 36 | + segments = identity_segments.map do |segment| |
| 37 | + { name: segment[:name], metadata: segment[:metadata] }.compact |
| 38 | + end |
| 39 | + |
| 40 | + segment_overrides = process_segment_overrides(identity_segments) |
| 41 | + |
| 42 | + [segments, segment_overrides] |
| 43 | + end |
| 44 | + |
| 45 | + # Returns Record<string: override.name, SegmentOverride> |
| 46 | + def process_segment_overrides(identity_segments) # rubocop:disable Metrics/MethodLength |
| 47 | + segment_overrides = {} |
| 48 | + |
| 49 | + identity_segments.each do |segment| |
| 50 | + Array(segment[:overrides]).each do |override| |
| 51 | + next unless should_apply_override(override, segment_overrides) |
| 52 | + |
| 53 | + segment_overrides[override[:name]] = { |
| 54 | + feature: override, |
| 55 | + segment_name: segment[:name] |
| 56 | + } |
| 57 | + end |
| 58 | + end |
| 59 | + |
| 60 | + segment_overrides |
| 61 | + end |
| 62 | + |
| 63 | + # returns EvaluationResultFlags<Metadata> |
| 64 | + def evaluate_features(evaluation_context, segment_overrides) |
| 65 | + identity_key = get_identity_key(evaluation_context) |
| 66 | + |
| 67 | + (evaluation_context[:features] || {}).each_with_object({}) do |(_, feature), flags| |
| 68 | + segment_override = segment_overrides[feature[:name]] |
| 69 | + final_feature = segment_override ? segment_override[:feature] : feature |
| 70 | + |
| 71 | + flag_result = build_flag_result(final_feature, identity_key, segment_override) |
| 72 | + flags[final_feature[:name].to_sym] = flag_result |
| 73 | + end |
| 74 | + end |
| 75 | + |
| 76 | + # Returns {value: any; reason?: string} |
| 77 | + def evaluate_feature_value(feature, identity_key = nil) |
| 78 | + return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key |
| 79 | + |
| 80 | + { value: feature[:value], reason: nil } |
| 81 | + end |
| 82 | + |
| 83 | + # Returns {value: any; reason?: string} |
| 84 | + def get_multivariate_feature_value(feature, identity_key) |
| 85 | + percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key]) |
| 86 | + sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY } |
| 87 | + |
| 88 | + variant = find_matching_variant(sorted_variants, percentage_value) |
| 89 | + variant || { value: feature[:value], reason: nil } |
| 90 | + end |
| 91 | + |
| 92 | + def find_matching_variant(sorted_variants, percentage_value) |
| 93 | + start_percentage = 0 |
| 94 | + sorted_variants.each do |variant| |
| 95 | + limit = start_percentage + variant[:weight] |
| 96 | + return { value: variant[:value], reason: "#{TARGETING_REASON_SPLIT}; weight=#{variant[:weight]}" } if start_percentage <= percentage_value && percentage_value < limit |
| 97 | + |
| 98 | + start_percentage = limit |
| 99 | + end |
| 100 | + nil |
| 101 | + end |
| 102 | + |
| 103 | + # returns boolean |
| 104 | + def should_apply_override(override, existing_overrides) |
| 105 | + current_override = existing_overrides[override[:name]] |
| 106 | + !current_override || stronger_priority?(override[:priority], current_override[:feature][:priority]) |
| 107 | + end |
| 108 | + |
| 109 | + private |
| 110 | + |
| 111 | + def build_flag_result(feature, identity_key, segment_override) |
| 112 | + evaluated = evaluate_feature_value(feature, identity_key) |
| 113 | + |
| 114 | + flag_result = { |
| 115 | + name: feature[:name], |
| 116 | + enabled: feature[:enabled], |
| 117 | + value: evaluated[:value], |
| 118 | + reason: evaluated[:reason] || (segment_override ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{segment_override[:segment_name]}" : TARGETING_REASON_DEFAULT) |
| 119 | + } |
| 120 | + |
| 121 | + flag_result[:metadata] = feature[:metadata] if feature[:metadata] |
| 122 | + flag_result |
| 123 | + end |
| 124 | + |
| 125 | + # Extract identity key from evaluation context |
| 126 | + # |
| 127 | + # @param evaluation_context [Hash] The evaluation context |
| 128 | + # @return [String, nil] The identity key or nil if no identity |
| 129 | + def get_identity_key(evaluation_context) |
| 130 | + return nil unless evaluation_context[:identity] |
| 131 | + |
| 132 | + evaluation_context[:identity][:key] || |
| 133 | + "#{evaluation_context[:environment][:key]}_#{evaluation_context[:identity][:identifier]}" |
| 134 | + end |
| 135 | + |
| 136 | + # returns boolean |
| 137 | + def stronger_priority?(priority_a, priority_b) |
| 138 | + (priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY) |
| 139 | + end |
| 140 | + end |
| 141 | + end |
| 142 | + end |
| 143 | +end |
0 commit comments