Skip to content

Commit a67d321

Browse files
committed
fix: main-mappers-and-tests
1 parent a7216f5 commit a67d321

File tree

8 files changed

+622
-36
lines changed

8 files changed

+622
-36
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "spec/engine-test-data"]
22
path = spec/engine-test-data
33
url = [email protected]:Flagsmith/engine-test-data.git
4-
branch = v1.0.0
4+
branch = main

dev_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require 'bundler/setup'
5+
require_relative 'lib/flagsmith'
6+
7+
flagsmith = Flagsmith::Client.new(
8+
environment_key: ''
9+
)
10+
11+
begin
12+
flags = flagsmith.get_environment_flags
13+
14+
beta_users_flag = flags['beta_users']
15+
16+
if beta_users_flag
17+
puts "Flag found!"
18+
else
19+
puts "error getting flag environment"
20+
end
21+
22+
puts "-" * 50
23+
puts "All flags"
24+
flags.all_flags.each do |flag|
25+
puts " - #{flag.feature_name}: enabled=#{flag.enabled?}, value=#{flag.value.inspect}"
26+
end
27+
28+
rescue StandardError => e
29+
puts "Error: #{e.message}"
30+
puts e.backtrace.join("\n")
31+
end

lib/flagsmith/engine/core.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require_relative 'segments/evaluator'
1212
require_relative 'segments/models'
1313
require_relative 'utils/hash_func'
14+
require_relative 'evaluationContext/mappers'
1415

1516
module Flagsmith
1617
module Engine
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module Flagsmith
4+
module Engine
5+
module EvaluationContext
6+
# Core evaluation logic for the evaluation context
7+
# This will contain the main evaluation engine logic similar to Node SDK
8+
module Core
9+
# Get evaluation result from evaluation context
10+
#
11+
# @param evaluation_context [Hash] The evaluation context
12+
# @return [Hash] Evaluation result with flags and segments
13+
def self.get_evaluation_result(evaluation_context)
14+
# TODO: Implement core evaluation logic
15+
{
16+
flags: {},
17+
segments: []
18+
}
19+
end
20+
end
21+
end
22+
end
23+
end
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# frozen_string_literal: true
2+
3+
module Flagsmith
4+
module Engine
5+
module EvaluationContext
6+
module Mappers
7+
# Using integer constant instead of -Float::INFINITY because the JSON serializer rejects infinity values
8+
HIGHEST_PRIORITY = 0
9+
WEAKEST_PRIORITY = 99_999_999
10+
#
11+
# @param environment [Flagsmith::Engine::Environment] The environment model
12+
# @param identity [Flagsmith::Engine::Identity, nil] Optional identity model
13+
# @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
14+
# @return [Hash] Evaluation context with environment, features, segments, and optionally identity
15+
def self.get_evaluation_context(environment, identity = nil, override_traits = nil)
16+
environment_context = map_environment_model_to_evaluation_context(environment)
17+
identity_context = identity ? map_identity_model_to_identity_context(identity, override_traits) : nil
18+
19+
context = environment_context.dup
20+
context[:identity] = identity_context if identity_context
21+
22+
context
23+
end
24+
25+
# Maps environment model to evaluation context
26+
#
27+
# @param environment [Flagsmith::Engine::Environment] The environment model
28+
# @return [Hash] Context with :environment, :features, and :segments keys
29+
def self.map_environment_model_to_evaluation_context(environment)
30+
environment_context = {
31+
key: environment.api_key,
32+
name: environment.project.name
33+
}
34+
35+
# Map feature states to features hash
36+
features = {}
37+
environment.feature_states.each do |fs|
38+
# Map multivariate values if present
39+
variants = nil
40+
if fs.multivariate_feature_state_values&.any?
41+
variants = fs.multivariate_feature_state_values.map do |mv|
42+
{
43+
value: mv.multivariate_feature_option.value,
44+
weight: mv.percentage_allocation,
45+
priority: mv.id || uuid_to_big_int(mv.mv_fs_value_uuid)
46+
}
47+
end
48+
end
49+
50+
feature_hash = {
51+
key: fs.django_id&.to_s || fs.uuid,
52+
feature_key: fs.feature.id.to_s,
53+
name: fs.feature.name,
54+
enabled: fs.enabled,
55+
value: fs.get_value
56+
}
57+
58+
feature_hash[:variants] = variants if variants
59+
feature_hash[:priority] = fs.feature_segment.priority if fs.feature_segment&.priority
60+
feature_hash[:metadata] = { flagsmithId: fs.feature.id }
61+
62+
features[fs.feature.name] = feature_hash
63+
end
64+
65+
# Map segments from project
66+
segments = {}
67+
environment.project.segments.each do |segment|
68+
overrides = segment.feature_states.map do |fs|
69+
override_hash = {
70+
key: fs.django_id&.to_s || fs.uuid,
71+
feature_key: fs.feature.id.to_s,
72+
name: fs.feature.name,
73+
enabled: fs.enabled,
74+
value: fs.get_value
75+
}
76+
override_hash[:priority] = fs.feature_segment.priority if fs.feature_segment&.priority
77+
override_hash
78+
end
79+
80+
segments[segment.id.to_s] = {
81+
key: segment.id.to_s,
82+
name: segment.name,
83+
rules: segment.rules.map { |rule| map_segment_rule_model_to_rule(rule) },
84+
overrides: overrides,
85+
metadata: {
86+
source: 'API',
87+
flagsmith_id: segment.id
88+
}
89+
}
90+
end
91+
92+
# Map identity overrides to segments
93+
if environment.identity_overrides&.any?
94+
identity_override_segments = map_identity_overrides_to_segments(environment.identity_overrides)
95+
segments.merge!(identity_override_segments)
96+
end
97+
98+
{
99+
environment: environment_context,
100+
features: features,
101+
segments: segments
102+
}
103+
end
104+
105+
def self.uuid_to_big_int(uuid)
106+
uuid.gsub('-', '').to_i(16)
107+
end
108+
109+
# Maps identity model to identity context
110+
#
111+
# @param identity [Flagsmith::Engine::Identity] The identity model
112+
# @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
113+
# @return [Hash] Identity context with :identifier, :key, and :traits
114+
def self.map_identity_model_to_identity_context(identity, override_traits = nil)
115+
# Use override traits if provided, otherwise use identity's traits
116+
traits = override_traits || identity.identity_traits
117+
118+
# Map traits to a hash with trait key => trait value
119+
traits_hash = {}
120+
traits.each do |trait|
121+
traits_hash[trait.trait_key] = trait.trait_value
122+
end
123+
124+
{
125+
identifier: identity.identifier,
126+
key: identity.django_id&.to_s || identity.composite_key,
127+
traits: traits_hash
128+
}
129+
end
130+
131+
# Maps segment rule model to rule hash
132+
#
133+
# @param rule [Flagsmith::Engine::Segments::Rule] The segment rule model
134+
# @return [Hash] Mapped rule with :type, :conditions, and :rules
135+
def self.map_segment_rule_model_to_rule(rule)
136+
result = {
137+
type: rule.type
138+
}
139+
140+
# Map conditions if present
141+
if rule.conditions&.any?
142+
result[:conditions] = rule.conditions.map do |condition|
143+
{
144+
property: condition.property,
145+
operator: condition.operator,
146+
value: condition.value
147+
}
148+
end
149+
else
150+
result[:conditions] = []
151+
end
152+
153+
if rule.rules&.any?
154+
result[:rules] = rule.rules.map { |nested_rule| map_segment_rule_model_to_rule(nested_rule) }
155+
else
156+
result[:rules] = []
157+
end
158+
159+
result
160+
end
161+
162+
# Maps identity overrides to segments
163+
#
164+
# @param identity_overrides [Array<Flagsmith::Engine::Identity>] Array of identity override models
165+
# @return [Hash] Segments hash for identity overrides
166+
def self.map_identity_overrides_to_segments(identity_overrides)
167+
require 'digest'
168+
169+
segments = {}
170+
features_to_identifiers = {}
171+
172+
identity_overrides.each do |identity|
173+
next if identity.identity_features.nil? || !identity.identity_features.any?
174+
175+
# Sort features by name for consistent hashing
176+
sorted_features = identity.identity_features.to_a.sort_by { |fs| fs.feature.name }
177+
178+
# Create override keys for hashing
179+
overrides_key = sorted_features.map do |fs|
180+
{
181+
feature_key: fs.feature.id.to_s,
182+
name: fs.feature.name,
183+
enabled: fs.enabled,
184+
value: fs.get_value,
185+
priority: WEAKEST_PRIORITY,
186+
metadata: {
187+
flagsmithId: fs.feature.id
188+
}
189+
}
190+
end
191+
192+
# Create hash of the overrides to group identities with same overrides
193+
overrides_hash = Digest::SHA1.hexdigest(overrides_key.to_json)
194+
195+
features_to_identifiers[overrides_hash] ||= { identifiers: [], overrides: overrides_key }
196+
features_to_identifiers[overrides_hash][:identifiers] << identity.identifier
197+
end
198+
199+
# Create segments for each unique set of overrides
200+
features_to_identifiers.each do |overrides_hash, data|
201+
segment_key = "identity_override_#{overrides_hash}"
202+
203+
segments[segment_key] = {
204+
key: segment_key,
205+
name: 'identity_override',
206+
rules: [
207+
{
208+
type: 'ALL',
209+
conditions: [
210+
{
211+
property: '$.identity.identifier',
212+
operator: 'IN',
213+
value: data[:identifiers].join(',')
214+
}
215+
],
216+
rules: []
217+
}
218+
],
219+
metadata: {
220+
source: 'identity_override'
221+
},
222+
overrides: data[:overrides]
223+
}
224+
end
225+
226+
segments
227+
end
228+
end
229+
end
230+
end
231+
end
232+

spec/engine-test-data

Submodule engine-test-data updated 190 files

spec/engine/e2e/engine_spec.rb

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,49 @@
22

33
require 'spec_helper'
44

5-
def load_test_cases(filepath)
6-
data = JSON.parse(File.open(filepath).read, symbolize_names: true)
7-
environment = Flagsmith::Engine::Environment.build(data[:environment])
8-
9-
data[:identities_and_responses].map do |test_case|
10-
identity = Flagsmith::Engine::Identity.build(test_case[:identity])
11-
{
12-
environment: environment,
13-
identity: identity,
14-
response: test_case[:response]
15-
}
16-
end
5+
def get_test_files
6+
test_data_dir = File.join(APP_ROOT, 'spec/engine-test-data/test_cases')
7+
Dir.glob(File.join(test_data_dir, '*.{json,jsonc}')).sort
8+
end
9+
10+
def parse_jsonc(content)
11+
# Simple JSONC parser: remove single-line comments
12+
# JSON.parse will handle the rest
13+
cleaned = content.lines.reject { |line| line.strip.start_with?('//') }.join
14+
JSON.parse(cleaned, symbolize_names: true)
15+
end
16+
17+
def load_test_file(filepath)
18+
content = File.read(filepath)
19+
parse_jsonc(content)
1720
end
1821

1922
RSpec.describe Flagsmith::Engine do
20-
load_test_cases(
21-
File.join(APP_ROOT, 'spec/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json')
22-
).each do |test_case|
23-
engine = Flagsmith::Engine::Engine.new
24-
json_flags = test_case.dig(:response, :flags).sort_by { |json| json.dig(:feature, :name) }
25-
feature_states = engine.get_identity_feature_states(test_case[:environment], test_case[:identity]).sort_by { |fs| fs.feature.name }
26-
27-
it { expect(feature_states.length).to eq(json_flags.length) }
28-
29-
json_flags.each.with_index do |json_flag, index|
30-
describe "feature state with ID #{json_flag.dig(:feature, :id)}" do
31-
subject { feature_states[index] }
32-
33-
context '#enabled?' do
34-
it { expect(subject.enabled?).to eq(json_flag[:enabled]) }
35-
end
36-
37-
context '#get_value' do
38-
it {
39-
expect(subject.get_value(test_case[:identity].django_id)).to eq(json_flag[:feature_state_value])
40-
}
41-
end
23+
test_files = get_test_files
24+
25+
raise "No test files found" if test_files.empty?
26+
27+
test_files.each do |filepath|
28+
test_name = File.basename(filepath, File.extname(filepath))
29+
30+
describe test_name do
31+
it 'should produce the expected evaluation result' do
32+
test_case = load_test_file(filepath)
33+
34+
test_evaluation_context = test_case[:context]
35+
test_expected_result = test_case[:result]
36+
37+
# TODO: Build environment/identity models and map to evaluation context
38+
evaluation_context = test_evaluation_context
39+
40+
# TODO: Implement evaluation logic
41+
evaluation_result = {}
42+
43+
# For now, verify the context structure is valid
44+
expect(evaluation_context).to eq(test_evaluation_context)
45+
46+
# TODO: Uncomment when evaluation is implemented
47+
# expect(evaluation_result).to eq(test_expected_result)
4248
end
4349
end
4450
end

0 commit comments

Comments
 (0)