-
Notifications
You must be signed in to change notification settings - Fork 6
feat: evaluation context mappers #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
a67d321
dc19da6
d536c81
79b3176
45b926e
9ea952d
b9d14a8
cb3558b
58bfe8b
3d7fa00
075a8a4
73b841c
51894ad
e1eabd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| [submodule "spec/engine-test-data"] | ||
| path = spec/engine-test-data | ||
| url = [email protected]:Flagsmith/engine-test-data.git | ||
| branch = v1.0.0 | ||
| branch = main |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| #!/usr/bin/env ruby | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'bundler/setup' | ||
| require_relative 'lib/flagsmith' | ||
|
|
||
| flagsmith = Flagsmith::Client.new( | ||
| environment_key: '' | ||
| ) | ||
|
|
||
| begin | ||
| flags = flagsmith.get_environment_flags | ||
|
|
||
| beta_users_flag = flags['beta_users'] | ||
|
|
||
| if beta_users_flag | ||
| puts "Flag found!" | ||
| else | ||
| puts "error getting flag environment" | ||
| end | ||
|
|
||
| puts "-" * 50 | ||
| puts "All flags" | ||
| flags.all_flags.each do |flag| | ||
| puts " - #{flag.feature_name}: enabled=#{flag.enabled?}, value=#{flag.value.inspect}" | ||
| end | ||
|
|
||
| rescue StandardError => e | ||
| puts "Error: #{e.message}" | ||
| puts e.backtrace.join("\n") | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Flagsmith | ||
| module Engine | ||
| module Evaluation | ||
| module Core | ||
| # Get evaluation result from evaluation context | ||
| # | ||
| # @param evaluation_context [Hash] The evaluation context | ||
| # @return [Hash] Evaluation result with flags and segments | ||
| def self.get_evaluation_result(evaluation_context) | ||
| # TODO: Implement core evaluation logic | ||
| { | ||
| flags: {}, | ||
| segments: [] | ||
| } | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,233 @@ | ||||||||||
| # frozen_string_literal: true | ||||||||||
|
|
||||||||||
| module Flagsmith | ||||||||||
| module Engine | ||||||||||
| module EvaluationContext | ||||||||||
| module Mappers | ||||||||||
| # Using integer constant instead of -Float::INFINITY because the JSON serializer rejects infinity values | ||||||||||
| HIGHEST_PRIORITY = 0 | ||||||||||
| WEAKEST_PRIORITY = 99_999_999 | ||||||||||
|
|
||||||||||
| # @param environment [Flagsmith::Engine::Environment] The environment model | ||||||||||
| # @param identity [Flagsmith::Engine::Identity, nil] Optional identity model | ||||||||||
| # @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits | ||||||||||
| # @return [Hash] Evaluation context with environment, features, segments, and optionally identity | ||||||||||
| def self.get_evaluation_context(environment, identity = nil, override_traits = nil) | ||||||||||
| environment_context = map_environment_model_to_evaluation_context(environment) | ||||||||||
| identity_context = identity ? map_identity_model_to_identity_context(identity, override_traits) : nil | ||||||||||
|
|
||||||||||
| context = environment_context.dup | ||||||||||
| context[:identity] = identity_context if identity_context | ||||||||||
|
|
||||||||||
| context | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Maps environment model to evaluation context | ||||||||||
| # | ||||||||||
| # @param environment [Flagsmith::Engine::Environment] The environment model | ||||||||||
| # @return [Hash] Context with :environment, :features, and :segments keys | ||||||||||
| def self.map_environment_model_to_evaluation_context(environment) | ||||||||||
| environment_context = { | ||||||||||
| key: environment.api_key, | ||||||||||
| name: environment.project.name | ||||||||||
| } | ||||||||||
|
|
||||||||||
| # Map feature states to features hash | ||||||||||
| features = {} | ||||||||||
| environment.feature_states.each do |fs| | ||||||||||
| # Map multivariate values if present | ||||||||||
| variants = nil | ||||||||||
| if fs.multivariate_feature_state_values&.any? | ||||||||||
| variants = fs.multivariate_feature_state_values.map do |mv| | ||||||||||
| { | ||||||||||
| value: mv.multivariate_feature_option.value, | ||||||||||
| weight: mv.percentage_allocation, | ||||||||||
| priority: mv.id || uuid_to_big_int(mv.mv_fs_value_uuid) | ||||||||||
| } | ||||||||||
| end | ||||||||||
| end | ||||||||||
|
|
||||||||||
| feature_hash = { | ||||||||||
| key: fs.django_id&.to_s || fs.uuid, | ||||||||||
| feature_key: fs.feature.id.to_s, | ||||||||||
| name: fs.feature.name, | ||||||||||
| enabled: fs.enabled, | ||||||||||
| value: fs.get_value | ||||||||||
| } | ||||||||||
|
|
||||||||||
| feature_hash[:variants] = variants if variants | ||||||||||
| feature_hash[:priority] = fs.feature_segment.priority if fs.feature_segment&.priority | ||||||||||
| feature_hash[:metadata] = { flagsmithId: fs.feature.id } | ||||||||||
|
|
||||||||||
| features[fs.feature.name] = feature_hash | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Map segments from project | ||||||||||
| segments = {} | ||||||||||
| environment.project.segments.each do |segment| | ||||||||||
| overrides = segment.feature_states.map do |fs| | ||||||||||
| override_hash = { | ||||||||||
| key: fs.django_id&.to_s || fs.uuid, | ||||||||||
| feature_key: fs.feature.id.to_s, | ||||||||||
| name: fs.feature.name, | ||||||||||
| enabled: fs.enabled, | ||||||||||
| value: fs.get_value | ||||||||||
| } | ||||||||||
| override_hash[:priority] = fs.feature_segment.priority if fs.feature_segment&.priority | ||||||||||
| override_hash[:metadata] = { flagsmithId: fs.feature.id } | ||||||||||
| override_hash | ||||||||||
| end | ||||||||||
|
|
||||||||||
| segments[segment.id.to_s] = { | ||||||||||
| key: segment.id.to_s, | ||||||||||
| name: segment.name, | ||||||||||
| rules: segment.rules.map { |rule| map_segment_rule_model_to_rule(rule) }, | ||||||||||
| overrides: overrides, | ||||||||||
| metadata: { | ||||||||||
| source: 'API', | ||||||||||
| flagsmith_id: segment.id | ||||||||||
| } | ||||||||||
| } | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Map identity overrides to segments | ||||||||||
| if environment.identity_overrides&.any? | ||||||||||
| identity_override_segments = map_identity_overrides_to_segments(environment.identity_overrides) | ||||||||||
| segments.merge!(identity_override_segments) | ||||||||||
| end | ||||||||||
|
|
||||||||||
| { | ||||||||||
| environment: environment_context, | ||||||||||
| features: features, | ||||||||||
| segments: segments | ||||||||||
| } | ||||||||||
| end | ||||||||||
|
|
||||||||||
| def self.uuid_to_big_int(uuid) | ||||||||||
| uuid.gsub('-', '').to_i(16) | ||||||||||
khvn26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| end | ||||||||||
|
|
||||||||||
| # Maps identity model to identity context | ||||||||||
| # | ||||||||||
| # @param identity [Flagsmith::Engine::Identity] The identity model | ||||||||||
| # @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits | ||||||||||
| # @return [Hash] Identity context with :identifier, :key, and :traits | ||||||||||
| def self.map_identity_model_to_identity_context(identity, override_traits = nil) | ||||||||||
| # Use override traits if provided, otherwise use identity's traits | ||||||||||
| traits = override_traits || identity.identity_traits | ||||||||||
|
|
||||||||||
| # Map traits to a hash with trait key => trait value | ||||||||||
| traits_hash = {} | ||||||||||
| traits.each do |trait| | ||||||||||
| traits_hash[trait.trait_key] = trait.trait_value | ||||||||||
| end | ||||||||||
|
|
||||||||||
| { | ||||||||||
| identifier: identity.identifier, | ||||||||||
| key: identity.django_id&.to_s || identity.composite_key, | ||||||||||
| traits: traits_hash | ||||||||||
| } | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Maps segment rule model to rule hash | ||||||||||
| # | ||||||||||
| # @param rule [Flagsmith::Engine::Segments::Rule] The segment rule model | ||||||||||
| # @return [Hash] Mapped rule with :type, :conditions, and :rules | ||||||||||
| def self.map_segment_rule_model_to_rule(rule) | ||||||||||
| result = { | ||||||||||
| type: rule.type | ||||||||||
| } | ||||||||||
|
|
||||||||||
| # Map conditions if present | ||||||||||
| if rule.conditions&.any? | ||||||||||
| result[:conditions] = rule.conditions.map do |condition| | ||||||||||
| { | ||||||||||
| property: condition.property, | ||||||||||
| operator: condition.operator, | ||||||||||
| value: condition.value | ||||||||||
| } | ||||||||||
| end | ||||||||||
| else | ||||||||||
| result[:conditions] = [] | ||||||||||
| end | ||||||||||
|
|
||||||||||
| if rule.rules&.any? | ||||||||||
| result[:rules] = rule.rules.map { |nested_rule| map_segment_rule_model_to_rule(nested_rule) } | ||||||||||
| else | ||||||||||
| result[:rules] = [] | ||||||||||
| end | ||||||||||
|
|
||||||||||
| result | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Maps identity overrides to segments | ||||||||||
| # | ||||||||||
| # @param identity_overrides [Array<Flagsmith::Engine::Identity>] Array of identity override models | ||||||||||
| # @return [Hash] Segments hash for identity overrides | ||||||||||
| def self.map_identity_overrides_to_segments(identity_overrides) | ||||||||||
| require 'digest' | ||||||||||
|
|
||||||||||
| segments = {} | ||||||||||
| features_to_identifiers = {} | ||||||||||
|
|
||||||||||
| identity_overrides.each do |identity| | ||||||||||
| next if identity.identity_features.nil? || !identity.identity_features.any? | ||||||||||
|
|
||||||||||
| # Sort features by name for consistent hashing | ||||||||||
| sorted_features = identity.identity_features.to_a.sort_by { |fs| fs.feature.name } | ||||||||||
|
|
||||||||||
| # Create override keys for hashing | ||||||||||
| overrides_key = sorted_features.map do |fs| | ||||||||||
| { | ||||||||||
| feature_key: fs.feature.id.to_s, | ||||||||||
| name: fs.feature.name, | ||||||||||
| enabled: fs.enabled, | ||||||||||
| value: fs.get_value, | ||||||||||
| priority: WEAKEST_PRIORITY, | ||||||||||
Zaimwa9 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
| metadata: { | ||||||||||
| flagsmithId: fs.feature.id | ||||||||||
| } | ||||||||||
| } | ||||||||||
| end | ||||||||||
|
|
||||||||||
| # Create hash of the overrides to group identities with same overrides | ||||||||||
| overrides_hash = Digest::SHA1.hexdigest(overrides_key.to_json) | ||||||||||
|
||||||||||
| overrides_hash = Digest::SHA1.hexdigest(overrides_key.to_json) | |
| overrides_hash = Digest::SHA1.hexdigest(overrides_key.inspect) |
- I'm not too familiar with Ruby's
Object::hash, but it could be sufficient in our case. We could drop thedigestdependency that way:
| overrides_hash = Digest::SHA1.hexdigest(overrides_key.to_json) | |
| overrides_hash = overrides_key.hash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually reverting this change to inspect as I discovered that the hash is not consistent over application restarts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not persisting this hash anywhere, so I don't think it's a requirement for it to be consistent across runtimes? Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for safety, I'd prefer this to be lower than 0. Also, our naming convention for these constants is strongest-weakest: