Skip to content

Commit d32ca1e

Browse files
authored
config v6 support (#32)
* config v6 support * test data * evaluation log tests (no rest only test) * tests * rollout tests * lint fix * sonarcloud * fix tests on ruby 2.7 + add ruby 3.2, 3.3 to the CI * compile fix on ruby 3.2 * extend_config_with_inline_salt_and_segment > fixup_config_salt_and_segments * review fixes
1 parent bb49279 commit d32ca1e

File tree

116 files changed

+6437
-541
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+6437
-541
lines changed

.github/workflows/ruby-ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ jobs:
1616
runs-on: ubuntu-latest
1717
strategy:
1818
matrix:
19-
ruby-versions: [ 2.4, 2.5, 2.6, 2.7, '3.0', '3.1' ]
19+
ruby-versions: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3' ]
20+
2021
steps:
2122
- uses: actions/checkout@v3
2223
- name: Set up Ruby

.github/workflows/snyk.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
snyk:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v2
15+
- uses: actions/checkout@v3
1616
- name: Set up Ruby
1717
uses: ruby/setup-ruby@v1
1818
with:

.sonarcloud.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sonar.sources=lib
2+
sonar.tests=spec

lib/configcat/config.rb

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
module ConfigCat
2+
CONFIG_FILE_NAME = 'config_v6'
3+
SERIALIZATION_FORMAT_VERSION = 'v2'
4+
5+
# Config
6+
PREFERENCES = 'p'
7+
SEGMENTS = 's'
8+
FEATURE_FLAGS = 'f'
9+
10+
# Preferences
11+
BASE_URL = 'u'
12+
REDIRECT = 'r'
13+
SALT = 's'
14+
15+
# Segment
16+
SEGMENT_NAME = 'n' # The first 4 characters of the Segment's name
17+
SEGMENT_CONDITIONS = 'r' # The list of segment rule conditions (has a logical AND relation between the items).
18+
19+
# Segment Condition (User Condition)
20+
COMPARISON_ATTRIBUTE = 'a' # The attribute of the user object that should be used to evaluate this rule
21+
COMPARATOR = 'c'
22+
23+
# Feature flag (Evaluation Formula)
24+
SETTING_TYPE = 't' # 0 = bool, 1 = string, 2 = int, 3 = double
25+
PERCENTAGE_RULE_ATTRIBUTE = 'a' # Percentage rule evaluation hashes this attribute of the User object to calculate the buckets
26+
TARGETING_RULES = 'r' # Targeting Rules (Logically connected by OR)
27+
PERCENTAGE_OPTIONS = 'p' # Percentage Options without conditions
28+
VALUE = 'v'
29+
VARIATION_ID = 'i'
30+
INLINE_SALT = 'inline_salt'
31+
32+
# Targeting Rule (Evaluation Rule)
33+
CONDITIONS = 'c'
34+
SERVED_VALUE = 's' # Value and Variation ID
35+
TARGETING_RULE_PERCENTAGE_OPTIONS = 'p'
36+
37+
# Condition
38+
USER_CONDITION = 'u'
39+
SEGMENT_CONDITION = 's' # Segment targeting rule
40+
PREREQUISITE_FLAG_CONDITION = 'p' # Prerequisite flag targeting rule
41+
42+
# Segment Condition
43+
SEGMENT_INDEX = 's'
44+
SEGMENT_COMPARATOR = 'c'
45+
INLINE_SEGMENT = 'inline_segment'
46+
47+
# Prerequisite Flag Condition
48+
PREREQUISITE_FLAG_KEY = 'f'
49+
PREREQUISITE_COMPARATOR = 'c'
50+
51+
# Percentage Option
52+
PERCENTAGE = 'p'
53+
54+
# Value
55+
BOOL_VALUE = 'b'
56+
STRING_VALUE = 's'
57+
INT_VALUE = 'i'
58+
DOUBLE_VALUE = 'd'
59+
STRING_LIST_VALUE = 'l'
60+
UNSUPPORTED_VALUE = 'unsupported_value'
61+
62+
module Config
63+
def self.is_type_mismatch(value, ruby_type)
64+
is_float_int_mismatch = \
65+
(value.is_a?(Float) && ruby_type == Integer) || \
66+
(value.is_a?(Integer) && ruby_type == Float)
67+
68+
is_bool_mismatch = value.is_a?(TrueClass) && ruby_type == FalseClass || \
69+
value.is_a?(FalseClass) && ruby_type == TrueClass
70+
71+
if value.class != ruby_type && !is_float_int_mismatch && !is_bool_mismatch
72+
return true
73+
end
74+
75+
return false
76+
end
77+
78+
def self.get_value(dictionary, setting_type)
79+
value_descriptor = dictionary[VALUE]
80+
if value_descriptor.nil?
81+
raise 'Value is missing'
82+
end
83+
84+
expected_value_type, expected_ruby_type = SettingType.get_type_info(setting_type)
85+
if expected_value_type.nil?
86+
raise 'Unsupported setting type'
87+
end
88+
89+
value = value_descriptor[expected_value_type]
90+
if value.nil? || is_type_mismatch(value, expected_ruby_type)
91+
raise "Setting value is not of the expected type #{expected_ruby_type}"
92+
end
93+
94+
return value
95+
end
96+
97+
def self.get_value_type(dictionary)
98+
value = dictionary[VALUE]
99+
if !value.nil?
100+
if !value[BOOL_VALUE].nil?
101+
return TrueClass
102+
end
103+
if !value[STRING_VALUE].nil?
104+
return String
105+
end
106+
if !value[INT_VALUE].nil?
107+
return Integer
108+
end
109+
if !value[DOUBLE_VALUE].nil?
110+
return Float
111+
end
112+
end
113+
114+
return nil
115+
end
116+
117+
def self.fixup_config_salt_and_segments(config)
118+
"""
119+
Adds the inline salt and segment to the config.
120+
When using flag overrides, the original salt and segment indexes may become invalid. Therefore, we copy the
121+
object references to the locations where they are referenced and use these references instead of the indexes.
122+
"""
123+
salt = config.fetch(PREFERENCES, {}).fetch(SALT, '')
124+
segments = config[SEGMENTS] || []
125+
settings = config[FEATURE_FLAGS] || {}
126+
settings.each do |_, setting|
127+
next unless setting.is_a?(Hash)
128+
129+
# add salt
130+
setting[INLINE_SALT] = salt
131+
132+
# add segment to the segment conditions
133+
targeting_rules = setting[TARGETING_RULES] || []
134+
targeting_rules.each do |targeting_rule|
135+
conditions = targeting_rule[CONDITIONS] || []
136+
conditions.each do |condition|
137+
segment_condition = condition[SEGMENT_CONDITION]
138+
if segment_condition
139+
segment_index = segment_condition[SEGMENT_INDEX]
140+
segment = segments[segment_index]
141+
segment_condition[INLINE_SEGMENT] = segment
142+
end
143+
end
144+
end
145+
end
146+
end
147+
end
148+
149+
class SettingType
150+
BOOL = 0
151+
STRING = 1
152+
INT = 2
153+
DOUBLE = 3
154+
155+
@@setting_type_mapping = {
156+
SettingType::BOOL => [BOOL_VALUE, TrueClass],
157+
SettingType::STRING => [STRING_VALUE, String],
158+
SettingType::INT => [INT_VALUE, Integer],
159+
SettingType::DOUBLE => [DOUBLE_VALUE, Float]
160+
}
161+
162+
def self.get_type_info(setting_type)
163+
return @@setting_type_mapping[setting_type] || [nil, nil]
164+
end
165+
166+
def self.from_type(object_type)
167+
if object_type == TrueClass || object_type == FalseClass
168+
return BOOL
169+
elsif object_type == String
170+
return STRING
171+
elsif object_type == Integer
172+
return INT
173+
elsif object_type == Float
174+
return DOUBLE
175+
end
176+
177+
return nil
178+
end
179+
180+
def self.to_type(setting_type)
181+
return get_type_info(setting_type)[1]
182+
end
183+
184+
def self.to_value_type(setting_type)
185+
return get_type_info(setting_type)[0]
186+
end
187+
end
188+
189+
module PrerequisiteComparator
190+
EQUALS = 0
191+
NOT_EQUALS = 1
192+
end
193+
194+
module SegmentComparator
195+
IS_IN = 0
196+
IS_NOT_IN = 1
197+
end
198+
199+
module Comparator
200+
IS_ONE_OF = 0
201+
IS_NOT_ONE_OF = 1
202+
CONTAINS_ANY_OF = 2
203+
NOT_CONTAINS_ANY_OF = 3
204+
IS_ONE_OF_SEMVER = 4
205+
IS_NOT_ONE_OF_SEMVER = 5
206+
LESS_THAN_SEMVER = 6
207+
LESS_THAN_OR_EQUAL_SEMVER = 7
208+
GREATER_THAN_SEMVER = 8
209+
GREATER_THAN_OR_EQUAL_SEMVER = 9
210+
EQUALS_NUMBER = 10
211+
NOT_EQUALS_NUMBER = 11
212+
LESS_THAN_NUMBER = 12
213+
LESS_THAN_OR_EQUAL_NUMBER = 13
214+
GREATER_THAN_NUMBER = 14
215+
GREATER_THAN_OR_EQUAL_NUMBER = 15
216+
IS_ONE_OF_HASHED = 16
217+
IS_NOT_ONE_OF_HASHED = 17
218+
BEFORE_DATETIME = 18
219+
AFTER_DATETIME = 19
220+
EQUALS_HASHED = 20
221+
NOT_EQUALS_HASHED = 21
222+
STARTS_WITH_ANY_OF_HASHED = 22
223+
NOT_STARTS_WITH_ANY_OF_HASHED = 23
224+
ENDS_WITH_ANY_OF_HASHED = 24
225+
NOT_ENDS_WITH_ANY_OF_HASHED = 25
226+
ARRAY_CONTAINS_ANY_OF_HASHED = 26
227+
ARRAY_NOT_CONTAINS_ANY_OF_HASHED = 27
228+
EQUALS = 28
229+
NOT_EQUALS = 29
230+
STARTS_WITH_ANY_OF = 30
231+
NOT_STARTS_WITH_ANY_OF = 31
232+
ENDS_WITH_ANY_OF = 32
233+
NOT_ENDS_WITH_ANY_OF = 33
234+
ARRAY_CONTAINS_ANY_OF = 34
235+
ARRAY_NOT_CONTAINS_ANY_OF = 35
236+
end
237+
238+
COMPARATOR_TEXTS = [
239+
'IS ONE OF', # IS_ONE_OF
240+
'IS NOT ONE OF', # IS_NOT_ONE_OF
241+
'CONTAINS ANY OF', # CONTAINS_ANY_OF
242+
'NOT CONTAINS ANY OF', # NOT_CONTAINS_ANY_OF
243+
'IS ONE OF', # IS_ONE_OF_SEMVER
244+
'IS NOT ONE OF', # IS_NOT_ONE_OF_SEMVER
245+
'<', # LESS_THAN_SEMVER
246+
'<=', # LESS_THAN_OR_EQUAL_SEMVER
247+
'>', # GREATER_THAN_SEMVER
248+
'>=', # GREATER_THAN_OR_EQUAL_SEMVER
249+
'=', # EQUALS_NUMBER
250+
'!=', # NOT_EQUALS_NUMBER
251+
'<', # LESS_THAN_NUMBER
252+
'<=', # LESS_THAN_OR_EQUAL_NUMBER
253+
'>', # GREATER_THAN_NUMBER
254+
'>=', # GREATER_THAN_OR_EQUAL_NUMBER
255+
'IS ONE OF', # IS_ONE_OF_HASHED
256+
'IS NOT ONE OF', # IS_NOT_ONE_OF_HASHED
257+
'BEFORE', # BEFORE_DATETIME
258+
'AFTER', # AFTER_DATETIME
259+
'EQUALS', # EQUALS_HASHED
260+
'NOT EQUALS', # NOT_EQUALS_HASHED
261+
'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF_HASHED
262+
'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF_HASHED
263+
'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF_HASHED
264+
'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF_HASHED
265+
'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF_HASHED
266+
'ARRAY NOT CONTAINS ANY OF', # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
267+
'EQUALS', # EQUALS
268+
'NOT EQUALS', # NOT_EQUALS
269+
'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF
270+
'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF
271+
'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF
272+
'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF
273+
'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF
274+
'ARRAY NOT CONTAINS ANY OF' # ARRAY_NOT_CONTAINS_ANY_OF
275+
]
276+
277+
COMPARISON_VALUES = [
278+
STRING_LIST_VALUE, # IS_ONE_OF
279+
STRING_LIST_VALUE, # IS_NOT_ONE_OF
280+
STRING_LIST_VALUE, # CONTAINS_ANY_OF
281+
STRING_LIST_VALUE, # NOT_CONTAINS_ANY_OF
282+
STRING_LIST_VALUE, # IS_ONE_OF_SEMVER
283+
STRING_LIST_VALUE, # IS_NOT_ONE_OF_SEMVER
284+
STRING_VALUE, # LESS_THAN_SEMVER
285+
STRING_VALUE, # LESS_THAN_OR_EQUAL_SEMVER
286+
STRING_VALUE, # GREATER_THAN_SEMVER
287+
STRING_VALUE, # GREATER_THAN_OR_EQUAL_SEMVER
288+
DOUBLE_VALUE, # EQUALS_NUMBER
289+
DOUBLE_VALUE, # NOT_EQUALS_NUMBER
290+
DOUBLE_VALUE, # LESS_THAN_NUMBER
291+
DOUBLE_VALUE, # LESS_THAN_OR_EQUAL_NUMBER
292+
DOUBLE_VALUE, # GREATER_THAN_NUMBER
293+
DOUBLE_VALUE, # GREATER_THAN_OR_EQUAL_NUMBER
294+
STRING_LIST_VALUE, # IS_ONE_OF_HASHED
295+
STRING_LIST_VALUE, # IS_NOT_ONE_OF_HASHED
296+
DOUBLE_VALUE, # BEFORE_DATETIME
297+
DOUBLE_VALUE, # AFTER_DATETIME
298+
STRING_VALUE, # EQUALS_HASHED
299+
STRING_VALUE, # NOT_EQUALS_HASHED
300+
STRING_LIST_VALUE, # STARTS_WITH_ANY_OF_HASHED
301+
STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF_HASHED
302+
STRING_LIST_VALUE, # ENDS_WITH_ANY_OF_HASHED
303+
STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF_HASHED
304+
STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF_HASHED
305+
STRING_LIST_VALUE, # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
306+
STRING_VALUE, # EQUALS
307+
STRING_VALUE, # NOT_EQUALS
308+
STRING_LIST_VALUE, # STARTS_WITH_ANY_OF
309+
STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF
310+
STRING_LIST_VALUE, # ENDS_WITH_ANY_OF
311+
STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF
312+
STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF
313+
STRING_LIST_VALUE # ARRAY_NOT_CONTAINS_ANY_OF
314+
]
315+
SEGMENT_COMPARATOR_TEXTS = ['IS IN SEGMENT', 'IS NOT IN SEGMENT']
316+
PREREQUISITE_COMPARATOR_TEXTS = ['EQUALS', 'DOES NOT EQUAL']
317+
end

0 commit comments

Comments
 (0)