Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a67d321
fix: main-mappers-and-tests
Zaimwa9 Oct 23, 2025
dc19da6
fix: added-metadata
Zaimwa9 Oct 23, 2025
d536c81
fix: renaming
Zaimwa9 Oct 23, 2025
79b3176
fix: comments
Zaimwa9 Oct 23, 2025
45b926e
fix: remoevd-redundant-test
Zaimwa9 Oct 23, 2025
9ea952d
feat: reviewed-engine-test-todos
Zaimwa9 Oct 23, 2025
b9d14a8
Update lib/flagsmith/engine/evaluation/mappers.rb
Zaimwa9 Oct 24, 2025
cb3558b
feat: use-overrides-key-hash
Zaimwa9 Oct 24, 2025
58bfe8b
feat: removed-identifiers-join
Zaimwa9 Oct 24, 2025
3d7fa00
feat: flagsmith-id-in-snake-case
Zaimwa9 Oct 24, 2025
075a8a4
feat: run-lint
Zaimwa9 Oct 28, 2025
73b841c
feat: renamed-module-evaluation
Zaimwa9 Oct 28, 2025
51894ad
feat: fixed-priority-0-being-skipped
Zaimwa9 Oct 28, 2025
e1eabd2
feat: reverted-to-hash
Zaimwa9 Oct 30, 2025
0386836
feat: linter
Zaimwa9 Nov 10, 2025
d86ba90
feat: split-functions-and-module-for-linting
Zaimwa9 Nov 10, 2025
f8ea862
feat: get-rid-of-extra-map-nested-rule-function
Zaimwa9 Nov 10, 2025
b2c3d30
feat: renaming-identity-methods
Zaimwa9 Nov 10, 2025
7de46c2
feat: fixed-forgotten-func-renaming
Zaimwa9 Nov 10, 2025
10b256c
Update lib/flagsmith/engine/evaluation/mappers/environment.rb
Zaimwa9 Nov 10, 2025
6c8dc0b
feat: added-name-env-model-and-fixture
Zaimwa9 Nov 10, 2025
1a908fe
feat: moved-mappers-to-engine-namespace
Zaimwa9 Nov 10, 2025
0c9650a
feat: removed-feature-key
Zaimwa9 Nov 11, 2025
4b106a7
feat: linter
Zaimwa9 Nov 11, 2025
f1026a2
feat: get evaluation get result (#88)
Zaimwa9 Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
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
31 changes: 31 additions & 0 deletions dev_test.rb
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
1 change: 1 addition & 0 deletions lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require_relative 'segments/evaluator'
require_relative 'segments/models'
require_relative 'utils/hash_func'
require_relative 'evaluationContext/mappers'

module Flagsmith
module Engine
Expand Down
21 changes: 21 additions & 0 deletions lib/flagsmith/engine/evaluation/core.rb
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
233 changes: 233 additions & 0 deletions lib/flagsmith/engine/evaluation/mappers.rb
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)
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,
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)

features_to_identifiers[overrides_hash] ||= { identifiers: [], overrides: overrides_key }
features_to_identifiers[overrides_hash][:identifiers] << identity.identifier
end

# Create segments for each unique set of overrides
features_to_identifiers.each do |overrides_hash, data|
segment_key = "identity_override_#{overrides_hash}"

segments[segment_key] = {
key: segment_key,
name: 'identity_override',
rules: [
{
type: 'ALL',
conditions: [
{
property: '$.identity.identifier',
operator: 'IN',
value: data[:identifiers].join(',')
}
],
rules: []
}
],
metadata: {
source: 'identity_override'
},
overrides: data[:overrides]
}
end

segments
end
end
end
end
end

2 changes: 1 addition & 1 deletion spec/engine-test-data
Submodule engine-test-data updated 190 files
69 changes: 35 additions & 34 deletions spec/engine/e2e/engine_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,44 @@

require 'spec_helper'

def load_test_cases(filepath)
data = JSON.parse(File.open(filepath).read, symbolize_names: true)
environment = Flagsmith::Engine::Environment.build(data[:environment])

data[:identities_and_responses].map do |test_case|
identity = Flagsmith::Engine::Identity.build(test_case[:identity])
{
environment: environment,
identity: identity,
response: test_case[:response]
}
end
def get_test_files
test_data_dir = File.join(APP_ROOT, 'spec/engine-test-data/test_cases')
Dir.glob(File.join(test_data_dir, '*.{json,jsonc}')).sort
end

def parse_jsonc(content)
# Simple JSONC parser: remove single-line comments
# JSON.parse will handle the rest
cleaned = content.lines.reject { |line| line.strip.start_with?('//') }.join
JSON.parse(cleaned, symbolize_names: true)
end

def load_test_file(filepath)
content = File.read(filepath)
parse_jsonc(content)
end

RSpec.describe Flagsmith::Engine do
load_test_cases(
File.join(APP_ROOT, 'spec/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json')
).each do |test_case|
engine = Flagsmith::Engine::Engine.new
json_flags = test_case.dig(:response, :flags).sort_by { |json| json.dig(:feature, :name) }
feature_states = engine.get_identity_feature_states(test_case[:environment], test_case[:identity]).sort_by { |fs| fs.feature.name }

it { expect(feature_states.length).to eq(json_flags.length) }

json_flags.each.with_index do |json_flag, index|
describe "feature state with ID #{json_flag.dig(:feature, :id)}" do
subject { feature_states[index] }

context '#enabled?' do
it { expect(subject.enabled?).to eq(json_flag[:enabled]) }
end

context '#get_value' do
it {
expect(subject.get_value(test_case[:identity].django_id)).to eq(json_flag[:feature_state_value])
}
end
test_files = get_test_files

raise "No test files found" if test_files.empty?

test_files.each do |filepath|
test_name = File.basename(filepath, File.extname(filepath))

describe test_name do
it 'should produce the expected evaluation result' do
test_case = load_test_file(filepath)

test_evaluation_context = test_case[:context]
test_expected_result = test_case[:result]

# TODO: Implement evaluation logic
evaluation_result = {}


# TODO: Uncomment when evaluation is implemented
# expect(evaluation_result).to eq(test_expected_result)
end
end
end
Expand Down
Loading
Loading