wingify-fme-ruby-sdk 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/resources/debug_messages.json +21 -0
  3. data/lib/resources/error_messages.json +63 -0
  4. data/lib/resources/info_messages.json +43 -0
  5. data/lib/resources/warn_messages.json +1 -0
  6. data/lib/vwo.rb +40 -0
  7. data/lib/wingify/api/get_flag.rb +244 -0
  8. data/lib/wingify/api/set_attribute.rb +57 -0
  9. data/lib/wingify/api/track_event.rb +80 -0
  10. data/lib/wingify/constants/constants.rb +106 -0
  11. data/lib/wingify/decorators/storage_decorator.rb +82 -0
  12. data/lib/wingify/enums/api_enum.rb +23 -0
  13. data/lib/wingify/enums/campaign_type_enum.rb +19 -0
  14. data/lib/wingify/enums/debug_category_enum.rb +20 -0
  15. data/lib/wingify/enums/decision_types_enum.rb +18 -0
  16. data/lib/wingify/enums/event_enum.rb +22 -0
  17. data/lib/wingify/enums/headers_enum.rb +20 -0
  18. data/lib/wingify/enums/hooks_enum.rb +17 -0
  19. data/lib/wingify/enums/http_method_enum.rb +18 -0
  20. data/lib/wingify/enums/log_level_enum.rb +21 -0
  21. data/lib/wingify/enums/log_level_to_number.rb +27 -0
  22. data/lib/wingify/enums/status_enum.rb +19 -0
  23. data/lib/wingify/enums/storage_enum.rb +22 -0
  24. data/lib/wingify/enums/url_enum.rb +22 -0
  25. data/lib/wingify/models/campaign/campaign_model.rb +192 -0
  26. data/lib/wingify/models/campaign/feature_model.rb +111 -0
  27. data/lib/wingify/models/campaign/impact_campaign_model.rb +38 -0
  28. data/lib/wingify/models/campaign/metric_model.rb +44 -0
  29. data/lib/wingify/models/campaign/rule_model.rb +56 -0
  30. data/lib/wingify/models/campaign/variable_model.rb +51 -0
  31. data/lib/wingify/models/campaign/variation_model.rb +137 -0
  32. data/lib/wingify/models/gateway_service_model.rb +39 -0
  33. data/lib/wingify/models/schemas/settings_schema_validation.rb +104 -0
  34. data/lib/wingify/models/settings/settings_model.rb +101 -0
  35. data/lib/wingify/models/storage/storage_data_model.rb +44 -0
  36. data/lib/wingify/models/user/context_model.rb +154 -0
  37. data/lib/wingify/models/user/context_vwo_model.rb +38 -0
  38. data/lib/wingify/models/user/get_flag_response.rb +50 -0
  39. data/lib/wingify/models/vwo_options_model.rb +129 -0
  40. data/lib/wingify/packages/decision_maker/decision_maker.rb +60 -0
  41. data/lib/wingify/packages/logger/core/log_manager.rb +92 -0
  42. data/lib/wingify/packages/logger/core/transport_manager.rb +76 -0
  43. data/lib/wingify/packages/logger/log_message_builder.rb +70 -0
  44. data/lib/wingify/packages/logger/logger.rb +38 -0
  45. data/lib/wingify/packages/logger/transports/console_transport.rb +49 -0
  46. data/lib/wingify/packages/network_layer/client/network_client.rb +276 -0
  47. data/lib/wingify/packages/network_layer/handlers/request_handler.rb +37 -0
  48. data/lib/wingify/packages/network_layer/manager/network_manager.rb +145 -0
  49. data/lib/wingify/packages/network_layer/models/global_request_model.rb +105 -0
  50. data/lib/wingify/packages/network_layer/models/request_model.rb +179 -0
  51. data/lib/wingify/packages/network_layer/models/response_model.rb +62 -0
  52. data/lib/wingify/packages/segmentation_evaluator/core/segmentation_manager.rb +77 -0
  53. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
  54. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
  55. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +33 -0
  56. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +218 -0
  57. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +415 -0
  58. data/lib/wingify/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
  59. data/lib/wingify/packages/storage/connector.rb +26 -0
  60. data/lib/wingify/packages/storage/storage.rb +47 -0
  61. data/lib/wingify/services/batch_event_queue.rb +179 -0
  62. data/lib/wingify/services/campaign_decision_service.rb +161 -0
  63. data/lib/wingify/services/hooks_service.rb +51 -0
  64. data/lib/wingify/services/logger_service.rb +114 -0
  65. data/lib/wingify/services/settings_service.rb +178 -0
  66. data/lib/wingify/services/storage_service.rb +66 -0
  67. data/lib/wingify/utils/batch_event_dispatcher_util.rb +178 -0
  68. data/lib/wingify/utils/brand_context.rb +33 -0
  69. data/lib/wingify/utils/brand_util.rb +53 -0
  70. data/lib/wingify/utils/campaign_util.rb +284 -0
  71. data/lib/wingify/utils/data_type_util.rb +105 -0
  72. data/lib/wingify/utils/debugger_service_util.rb +40 -0
  73. data/lib/wingify/utils/decision_util.rb +259 -0
  74. data/lib/wingify/utils/event_util.rb +55 -0
  75. data/lib/wingify/utils/function_util.rb +141 -0
  76. data/lib/wingify/utils/gateway_service_util.rb +101 -0
  77. data/lib/wingify/utils/impression_util.rb +66 -0
  78. data/lib/wingify/utils/log_message_util.rb +42 -0
  79. data/lib/wingify/utils/meg_util.rb +357 -0
  80. data/lib/wingify/utils/network_util.rb +503 -0
  81. data/lib/wingify/utils/rule_evaluation_util.rb +57 -0
  82. data/lib/wingify/utils/settings_util.rb +38 -0
  83. data/lib/wingify/utils/usage_stats_util.rb +119 -0
  84. data/lib/wingify/utils/uuid_util.rb +96 -0
  85. data/lib/wingify/wingify_builder.rb +261 -0
  86. data/lib/wingify/wingify_client.rb +227 -0
  87. data/lib/wingify.rb +117 -0
  88. metadata +327 -0
@@ -0,0 +1,218 @@
1
+ # Copyright 2024-2026 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+ require_relative '../../../decorators/storage_decorator'
17
+ require_relative '../../../models/settings/settings_model'
18
+ require_relative '../../../models/user/context_model'
19
+ require_relative '../../../models/campaign/feature_model'
20
+ require_relative '../../../services/storage_service'
21
+ require_relative '../../../services/logger_service'
22
+ require_relative '../enums/segment_operator_value_enum'
23
+ require_relative '../core/segmentation_manager'
24
+ require_relative '../utils/segment_util'
25
+ require_relative './segment_operand_evaluator'
26
+ require_relative '../../../enums/log_level_enum'
27
+ require_relative '../../../enums/api_enum'
28
+
29
+ class SegmentEvaluator
30
+ attr_accessor :context, :settings, :feature
31
+
32
+ def initialize(context = nil, settings = nil, feature = nil)
33
+ @context = context
34
+ @settings = settings
35
+ @feature = feature
36
+ end
37
+
38
+ # Validates if the segmentation defined in the DSL is applicable based on the provided properties.
39
+ # @param dsl [Hash] The DSL node to evaluate
40
+ # @param properties [Hash] The properties to evaluate the DSL against
41
+ # @return [Boolean] True if the segmentation is valid, false otherwise
42
+ def is_segmentation_valid(dsl, properties)
43
+ key_value = get_key_value(dsl)
44
+ return false unless key_value
45
+
46
+ operator = key_value[:key]
47
+ sub_dsl = key_value[:value]
48
+
49
+ case operator
50
+ when SegmentOperatorValueEnum::NOT
51
+ !is_segmentation_valid(sub_dsl, properties)
52
+ when SegmentOperatorValueEnum::AND
53
+ every(sub_dsl, properties)
54
+ when SegmentOperatorValueEnum::OR
55
+ some(sub_dsl, properties)
56
+ when SegmentOperatorValueEnum::CUSTOM_VARIABLE
57
+ SegmentOperandEvaluator.new.evaluate_custom_variable_dsl(sub_dsl, properties)
58
+ when SegmentOperatorValueEnum::USER
59
+ SegmentOperandEvaluator.new.evaluate_user_dsl(sub_dsl, properties)
60
+ when SegmentOperatorValueEnum::UA
61
+ SegmentOperandEvaluator.new.evaluate_user_agent_dsl(sub_dsl, @context)
62
+ when SegmentOperatorValueEnum::IP
63
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::IP)
64
+ when SegmentOperatorValueEnum::BROWSER_VERSION
65
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::BROWSER_VERSION)
66
+ when SegmentOperatorValueEnum::OS_VERSION
67
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::OS_VERSION)
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ # Evaluates if any of the DSL nodes are valid using the OR logic.
74
+ # @param dsl_nodes [Array<Hash>] The DSL nodes to evaluate
75
+ # @param custom_variables [Hash] The custom variables
76
+ # @return [Boolean] True if any of the DSL nodes are valid, false otherwise
77
+ def some(dsl_nodes, custom_variables)
78
+ ua_parser_map = {}
79
+ key_count = 0
80
+ is_ua_parser = false
81
+
82
+ dsl_nodes.each do |dsl|
83
+ dsl.each do |key, value|
84
+ if [SegmentOperatorValueEnum::OPERATING_SYSTEM, SegmentOperatorValueEnum::BROWSER_AGENT,
85
+ SegmentOperatorValueEnum::DEVICE_TYPE, SegmentOperatorValueEnum::DEVICE].include?(key)
86
+ is_ua_parser = true
87
+ ua_parser_map[key] ||= []
88
+ ua_parser_map[key] += Array(value).map(&:to_s)
89
+ key_count += 1
90
+ end
91
+
92
+ if key == SegmentOperatorValueEnum::FEATURE_ID
93
+ feature_id_object = dsl[key]
94
+ feature_id_key = feature_id_object.keys.first
95
+ feature_id_value = feature_id_object[feature_id_key]
96
+
97
+ if %w[on off].include?(feature_id_value)
98
+ feature = @settings.get_features.find { |f| f.get_id == feature_id_key.to_i }
99
+ if feature
100
+ feature_key = feature.get_key
101
+ result = check_in_user_storage(@settings, feature_key, @context)
102
+ return !result if feature_id_value == 'off'
103
+ return result
104
+ else
105
+ LoggerService.log(LogLevelEnum::INFO, "Feature not found with featureIdKey: #{feature_id_key}", nil)
106
+ return nil
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ return check_user_agent_parser(ua_parser_map) if is_ua_parser && key_count == dsl_nodes.length
113
+ return true if is_segmentation_valid(dsl, custom_variables)
114
+ end
115
+
116
+ false
117
+ end
118
+
119
+ # Evaluates all DSL nodes using the AND logic.
120
+ # @param dsl_nodes [Array<Hash>] The DSL nodes to evaluate
121
+ # @param custom_variables [Hash] The custom variables
122
+ # @return [Boolean] True if all DSL nodes are valid, false otherwise
123
+ def every(dsl_nodes, custom_variables)
124
+ location_map = {}
125
+ dsl_nodes.each do |dsl|
126
+ if dsl.keys.any? { |key| [SegmentOperatorValueEnum::COUNTRY, SegmentOperatorValueEnum::REGION,
127
+ SegmentOperatorValueEnum::CITY].include?(key) }
128
+ add_location_values_to_map(dsl, location_map)
129
+ return check_location_pre_segmentation(location_map) if location_map.keys.length == dsl_nodes.length
130
+ next
131
+ end
132
+ return false unless is_segmentation_valid(dsl, custom_variables)
133
+ end
134
+ true
135
+ end
136
+
137
+ # Adds the location values to the map
138
+ # @param dsl [Hash] The DSL node
139
+ # @param location_map [Hash] The location map
140
+ def add_location_values_to_map(dsl, location_map)
141
+ location_map[SegmentOperatorValueEnum::COUNTRY] = dsl[SegmentOperatorValueEnum::COUNTRY] if dsl.key?(SegmentOperatorValueEnum::COUNTRY)
142
+ location_map[SegmentOperatorValueEnum::REGION] = dsl[SegmentOperatorValueEnum::REGION] if dsl.key?(SegmentOperatorValueEnum::REGION)
143
+ location_map[SegmentOperatorValueEnum::CITY] = dsl[SegmentOperatorValueEnum::CITY] if dsl.key?(SegmentOperatorValueEnum::CITY)
144
+ end
145
+
146
+ # Checks if the location pre-segmentation is valid
147
+ # @param location_map [Hash] The location map
148
+ # @return [Boolean] True if the location pre-segmentation is valid, false otherwise
149
+ def check_location_pre_segmentation(location_map)
150
+ unless @context&.get_ip_address
151
+ LoggerService.log(LogLevelEnum::ERROR, 'INVALID_IP_ADDRESS_IN_CONTEXT_FOR_PRE_SEGMENTATION', { an: ApiEnum::GET_FLAG, sId: @context.get_session_id, uuid: @context.get_uuid})
152
+ return false
153
+ end
154
+
155
+ unless @context&.get_vwo&.get_location
156
+ return false
157
+ end
158
+
159
+ values_match(location_map, @context.get_vwo.get_location)
160
+ end
161
+
162
+ # Checks if the user agent parser is valid
163
+ # @param ua_parser_map [Hash] The user agent parser map
164
+ # @return [Boolean] True if the user agent parser is valid, false otherwise
165
+ def check_user_agent_parser(ua_parser_map)
166
+ unless @context&.get_user_agent
167
+ LoggerService.log(LogLevelEnum::ERROR, 'INVALID_USER_AGENT_IN_CONTEXT_FOR_PRE_SEGMENTATION', { an: ApiEnum::GET_FLAG, sId: @context.get_session_id, uuid: @context.get_uuid})
168
+ return false
169
+ end
170
+
171
+ unless @context&.get_vwo&.get_ua_info
172
+ return false
173
+ end
174
+
175
+ check_value_present(ua_parser_map, @context.get_vwo.get_ua_info)
176
+ end
177
+
178
+ # Checks if the feature key is present in the user's storage
179
+ # @param settings [SettingsModel] The settings for the VWO instance
180
+ # @param feature_key [String] The key of the feature to check
181
+ # @param context [ContextModel] The context for the evaluation
182
+ # @return [Boolean] True if the feature key is present in the user's storage, false otherwise
183
+ def check_in_user_storage(settings, feature_key, context)
184
+ storage_service = StorageService.new
185
+ stored_data = StorageDecorator.new.get_feature_from_storage(feature_key, context, storage_service)
186
+ stored_data.is_a?(Hash) && !stored_data.empty?
187
+ end
188
+
189
+ # Checks if the expected values are present in the actual values
190
+ # @param expected_map [Hash] The expected values
191
+ # @param actual_map [Hash] The actual values
192
+ # @return [Boolean] True if the expected values are present in the actual values, false otherwise
193
+ def check_value_present(expected_map, actual_map)
194
+ actual_map.each do |key, actual_value|
195
+ next unless expected_map.key?(key)
196
+
197
+ expected_values = expected_map[key].map(&:downcase)
198
+
199
+ return true if expected_values.any? { |val| val.start_with?('wildcard(') && val.end_with?(')') && actual_value.match?(Regexp.new(val[9..-2].gsub('*', '.*'), Regexp::IGNORECASE)) }
200
+ return true if expected_values.include?(actual_value.downcase)
201
+ end
202
+ false
203
+ end
204
+
205
+ # Checks if the expected location values match the user's location
206
+ # @param expected_location_map [Hash] The expected location values
207
+ # @param user_location [Hash] The user's location values
208
+ # @return [Boolean] True if the expected location values match the user's location, false otherwise
209
+ def values_match(expected_location_map, user_location)
210
+ expected_location_map.all? { |key, value| normalize_value(value) == normalize_value(user_location[key]) }
211
+ end
212
+
213
+ def normalize_value(value)
214
+ return nil if value.nil?
215
+
216
+ value.to_s.gsub(/^"|"$/, '').strip
217
+ end
218
+ end
@@ -0,0 +1,415 @@
1
+ # Copyright 2024-2026 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require_relative '../utils/segment_util'
16
+ require_relative '../enums/segment_operand_value_enum'
17
+ require_relative '../enums/segment_operand_regex_enum'
18
+ require_relative '../enums/segment_operator_value_enum'
19
+ require_relative '../../../utils/data_type_util'
20
+ require_relative '../../../utils/gateway_service_util'
21
+ require_relative '../../../enums/url_enum'
22
+ require_relative '../../../services/logger_service'
23
+ require_relative '../../../models/user/context_model'
24
+ require_relative '../../../enums/log_level_enum'
25
+ require_relative '../../../enums/api_enum'
26
+
27
+ # SegmentOperandEvaluator class provides methods to evaluate different types of DSL (Domain Specific Language)
28
+ # expressions based on the segment conditions defined for custom variables, user IDs, and user agents.
29
+ class SegmentOperandEvaluator
30
+ # Regex pattern to check if a string contains non-numeric characters (except decimal point)
31
+ NON_NUMERIC_PATTERN = /[^0-9.]/
32
+
33
+ # Evaluates the custom variable DSL
34
+ # @param dsl_operand_value [Hash] The operand value to evaluate
35
+ # @param properties [Hash] The properties to evaluate the operand against
36
+ # @return [Boolean] True if the operand value matches the tag value, false otherwise
37
+ def evaluate_custom_variable_dsl(dsl_operand_value, properties)
38
+ key_value = get_key_value(dsl_operand_value)
39
+ return false unless key_value
40
+
41
+ operand_key = key_value[:key].to_sym
42
+ operand = key_value[:value]
43
+
44
+ return false unless properties.key?(operand_key)
45
+
46
+ if operand.include?('inlist')
47
+ match = operand.match(/inlist\(([^)]+)\)/)
48
+ unless match
49
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_ATTRIBUTE_LIST_FORMAT", { an: ApiEnum::GET_FLAG, sId: @context.get_session_id, uuid: @context.get_uuid})
50
+ return false
51
+ end
52
+
53
+ tag_value = properties[operand_key]
54
+ attribute_value = pre_process_tag_value(tag_value)
55
+ list_id = match[1]
56
+
57
+ query_params_obj = { attribute: attribute_value, listId: list_id }
58
+
59
+ begin
60
+ res = get_from_gateway_service(query_params_obj, UrlEnum::ATTRIBUTE_CHECK)
61
+ if res.nil? || res == false || res == 'false' || (res.is_a?(Hash) && res[:status] == 0)
62
+ return false
63
+ end
64
+ return res
65
+ rescue StandardError => e
66
+ LoggerService.log(LogLevelEnum::ERROR, "ERROR_FETCHING_DATA_FROM_GATEWAY", { err: e.message, an: ApiEnum::GET_FLAG, sId: @context.get_session_id, uuid: @context.get_uuid})
67
+ return false
68
+ end
69
+
70
+ false
71
+ else
72
+ tag_value = properties[operand_key]
73
+ tag_value = pre_process_tag_value(tag_value)
74
+ processed_operand = pre_process_operand_value(operand)
75
+ processed_values = process_values(processed_operand[:operand_value], tag_value)
76
+ tag_value = processed_values[:tag_value]
77
+ extract_result(processed_operand[:operand_type], processed_values[:operand_value], tag_value)
78
+ end
79
+ end
80
+
81
+ # Evaluates the user DSL
82
+ # @param dsl_operand_value [String] The operand value to evaluate
83
+ # @param properties [Hash] The properties to evaluate the operand against
84
+ # @return [Boolean] True if the operand value matches the tag value, false otherwise
85
+ def evaluate_user_dsl(dsl_operand_value, properties)
86
+ users = dsl_operand_value.split(',')
87
+ users.any? { |user| user.strip == properties["_vwoUserId"].to_s }
88
+ end
89
+
90
+ # Evaluates the user agent DSL
91
+ # @param dsl_operand_value [String] The operand value to evaluate
92
+ # @param context [ContextModel] The context to evaluate the operand against
93
+ # @return [Boolean] True if the operand value matches the tag value, false otherwise
94
+ def evaluate_user_agent_dsl(dsl_operand_value, context)
95
+ operand = dsl_operand_value
96
+ unless context.get_user_agent
97
+ LoggerService.log(LogLevelEnum::INFO, 'To Evaluate UserAgent segmentation, please provide userAgent in context', nil)
98
+ return false
99
+ end
100
+
101
+ tag_value = CGI.unescape(context.get_user_agent)
102
+ processed_operand = pre_process_operand_value(operand)
103
+ processed_values = process_values(processed_operand[:operand_value], tag_value)
104
+ tag_value = processed_values[:tag_value]
105
+ extract_result(processed_operand[:operand_type], processed_values[:operand_value], tag_value)
106
+ end
107
+
108
+ # Evaluates a given string tag value against a DSL operand value.
109
+ # @param dsl_operand_value [String] The DSL operand string (e.g., "contains(\"value\")").
110
+ # @param context [ContextModel] The context object containing the value to evaluate.
111
+ # @param operand_type [String] The type of operand being evaluated (ip_address, browser_version, os_version).
112
+ # @return [Boolean] True if tag value matches DSL operand criteria, false otherwise.
113
+ def evaluate_string_operand_dsl(dsl_operand_value, context, operand_type)
114
+ operand = dsl_operand_value.to_s
115
+
116
+ # Determine the tag value based on operand type
117
+ tag_value = get_tag_value_for_operand_type(context, operand_type)
118
+
119
+ if tag_value.nil?
120
+ log_missing_context_error(operand_type)
121
+ return false
122
+ end
123
+
124
+ operand_type_and_value = pre_process_operand_value(operand)
125
+ processed_values = process_values(operand_type_and_value[:operand_value], tag_value, operand_type)
126
+ processed_tag_value = processed_values[:tag_value]
127
+
128
+ extract_result(
129
+ operand_type_and_value[:operand_type],
130
+ processed_values[:operand_value].to_s.strip.gsub(/"/, ''),
131
+ processed_tag_value
132
+ )
133
+ end
134
+
135
+ # Evaluates IP address DSL expression.
136
+ # @param dsl_operand_value [String] The DSL expression for the IP address.
137
+ # @param context [ContextModel] The context object containing the IP address.
138
+ # @return [Boolean] True if the IP address matches the DSL condition, otherwise false.
139
+ def evaluate_ip_dsl(dsl_operand_value, context)
140
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::IP)
141
+ end
142
+
143
+ # Evaluates browser version DSL expression.
144
+ # @param dsl_operand_value [String] The DSL expression for the browser version.
145
+ # @param context [ContextModel] The context object containing the user agent info.
146
+ # @return [Boolean] True if the browser version matches the DSL condition, otherwise false.
147
+ def evaluate_browser_version_dsl(dsl_operand_value, context)
148
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::BROWSER_VERSION)
149
+ end
150
+
151
+ # Evaluates OS version DSL expression.
152
+ # @param dsl_operand_value [String] The DSL expression for the OS version.
153
+ # @param context [ContextModel] The context object containing the user agent info.
154
+ # @return [Boolean] True if the OS version matches the DSL condition, otherwise false.
155
+ def evaluate_os_version_dsl(dsl_operand_value, context)
156
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::OS_VERSION)
157
+ end
158
+
159
+ # Gets the appropriate tag value based on the operand type.
160
+ # @param context [ContextModel] The context object.
161
+ # @param operand_type [String] The type of operand.
162
+ # @return [String, nil] The tag value or nil if not available.
163
+ def get_tag_value_for_operand_type(context, operand_type)
164
+ case operand_type
165
+ when SegmentOperatorValueEnum::IP
166
+ context.get_ip_address
167
+ when SegmentOperatorValueEnum::BROWSER_VERSION
168
+ get_browser_version_from_context(context)
169
+ else
170
+ # Default works for OS version
171
+ get_os_version_from_context(context)
172
+ end
173
+ end
174
+
175
+ # Gets browser version from context.
176
+ # @param context [ContextModel] The context object.
177
+ # @return [String, nil] The browser version or nil if not available.
178
+ def get_browser_version_from_context(context)
179
+ user_agent = context.get_vwo&.get_ua_info
180
+ return nil unless user_agent && user_agent.is_a?(Hash) && !user_agent.empty?
181
+
182
+ # Assuming UserAgent dictionary contains browser_version
183
+ if user_agent.key?('browser_version')
184
+ return user_agent['browser_version']&.to_s
185
+ end
186
+ nil
187
+ end
188
+
189
+ # Gets OS version from context.
190
+ # @param context [ContextModel] The context object.
191
+ # @return [String, nil] The OS version or nil if not available.
192
+ def get_os_version_from_context(context)
193
+ user_agent = context.get_vwo&.get_ua_info
194
+ return nil unless user_agent && user_agent.is_a?(Hash) && !user_agent.empty?
195
+
196
+ # Assuming UserAgent dictionary contains os_version
197
+ if user_agent.key?('os_version')
198
+ return user_agent['os_version']&.to_s
199
+ end
200
+ nil
201
+ end
202
+
203
+ # Logs appropriate error message for missing context.
204
+ # @param operand_type [String] The type of operand.
205
+ def log_missing_context_error(operand_type)
206
+ case operand_type
207
+ when SegmentOperatorValueEnum::IP
208
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate IP segmentation, please provide ipAddress in context', nil)
209
+ when SegmentOperatorValueEnum::BROWSER_VERSION
210
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate browser version segmentation, please provide userAgent in context', nil)
211
+ else
212
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate OS version segmentation, please provide userAgent in context', nil)
213
+ end
214
+ end
215
+
216
+ # Pre-processes the tag value to ensure it is in the correct format for evaluation.
217
+ # @param tag_value [Any] The value to be processed.
218
+ # @return [String, Boolean] The processed tag value, either as a string or a boolean.
219
+ def pre_process_tag_value(tag_value)
220
+ # Default to empty string if undefined
221
+ if tag_value.nil?
222
+ tag_value = ''
223
+ end
224
+ # Convert boolean values to boolean type
225
+ if DataTypeUtil.is_boolean(tag_value)
226
+ tag_value = tag_value ? true : false
227
+ end
228
+ # Convert all non-null values to string
229
+ unless tag_value.nil?
230
+ tag_value = tag_value.to_s
231
+ end
232
+ tag_value
233
+ end
234
+
235
+ # Pre-processes the operand value
236
+ # @param operand [String] The operand to pre-process
237
+ # @return [Hash] The pre-processed operand value
238
+ def pre_process_operand_value(operand)
239
+ case operand
240
+ when /#{SegmentOperandRegexEnum::LOWER_MATCH}/
241
+ { operand_type: SegmentOperandValueEnum::LOWER_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::LOWER_MATCH) }
242
+ when /#{SegmentOperandRegexEnum::WILDCARD_MATCH}/
243
+ value = extract_operand_value(operand, SegmentOperandRegexEnum::WILDCARD_MATCH)
244
+ starting_star = match_with_regex(value, SegmentOperandRegexEnum::STARTING_STAR)
245
+ ending_star = match_with_regex(value, SegmentOperandRegexEnum::ENDING_STAR)
246
+
247
+ # Determine specific wildcard type
248
+ if starting_star && ending_star
249
+ type = SegmentOperandValueEnum::STARTING_ENDING_STAR_VALUE
250
+ elsif starting_star
251
+ type = SegmentOperandValueEnum::STARTING_STAR_VALUE
252
+ elsif ending_star
253
+ type = SegmentOperandValueEnum::ENDING_STAR_VALUE
254
+ end
255
+
256
+ # Remove wildcard characters from the operand value
257
+ value = value
258
+ .gsub(Regexp.new(SegmentOperandRegexEnum::STARTING_STAR), '')
259
+ .gsub(Regexp.new(SegmentOperandRegexEnum::ENDING_STAR), '')
260
+
261
+ { operand_type: type, operand_value: value }
262
+ when /#{SegmentOperandRegexEnum::REGEX_MATCH}/
263
+ { operand_type: SegmentOperandValueEnum::REGEX_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::REGEX_MATCH) }
264
+ when /#{SegmentOperandRegexEnum::GREATER_THAN_MATCH}/
265
+ { operand_type: SegmentOperandValueEnum::GREATER_THAN_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::GREATER_THAN_MATCH) }
266
+ when /#{SegmentOperandRegexEnum::LESS_THAN_MATCH}/
267
+ { operand_type: SegmentOperandValueEnum::LESS_THAN_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::LESS_THAN_MATCH) }
268
+ when /#{SegmentOperandRegexEnum::GREATER_THAN_EQUAL_TO_MATCH}/
269
+ { operand_type: SegmentOperandValueEnum::GREATER_THAN_EQUAL_TO_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::GREATER_THAN_EQUAL_TO_MATCH) }
270
+ when /#{SegmentOperandRegexEnum::LESS_THAN_EQUAL_TO_MATCH}/
271
+ { operand_type: SegmentOperandValueEnum::LESS_THAN_EQUAL_TO_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::LESS_THAN_EQUAL_TO_MATCH) }
272
+ else
273
+ { operand_type: SegmentOperandValueEnum::EQUAL_VALUE, operand_value: operand }
274
+ end
275
+ end
276
+
277
+ # Extracts the operand value from the operand
278
+ # @param operand [String] The operand to extract the value from
279
+ # @param regex [String] The regex to match the operand against
280
+ # @return [String] The extracted operand value
281
+ def extract_operand_value(operand, regex)
282
+ match_result = match_with_regex(operand, regex)
283
+ match_result && match_result[1] ? match_result[1] : ''
284
+ end
285
+
286
+ # Processes numeric values from operand and tag values, converting them to strings.
287
+ # @param operand_value [Any] The operand value to process.
288
+ # @param tag_value [Any] The tag value to process.
289
+ # @param operand_type [String] The type of operand being evaluated (optional).
290
+ # @return [Hash] An object containing the processed operand and tag values as strings.
291
+ def process_values(operand_value, tag_value, operand_type = nil)
292
+ if [SegmentOperatorValueEnum::IP, SegmentOperatorValueEnum::BROWSER_VERSION, SegmentOperatorValueEnum::OS_VERSION].include?(operand_type)
293
+ return {
294
+ operand_value: operand_value,
295
+ tag_value: tag_value
296
+ }
297
+ end
298
+
299
+ # Convert operand and tag values to floats
300
+ if NON_NUMERIC_PATTERN.match?(tag_value.to_s)
301
+ return {
302
+ operand_value: operand_value,
303
+ tag_value: tag_value
304
+ }
305
+ end
306
+
307
+ processed_operand_value = operand_value.to_f
308
+ processed_tag_value = tag_value.to_f
309
+
310
+ # Return original values if conversion fails
311
+ if processed_operand_value == 0 && operand_value.to_s != '0' && operand_value.to_s != '0.0'
312
+ return {
313
+ operand_value: operand_value,
314
+ tag_value: tag_value
315
+ }
316
+ end
317
+
318
+ if processed_tag_value == 0 && tag_value.to_s != '0' && tag_value.to_s != '0.0'
319
+ return {
320
+ operand_value: operand_value,
321
+ tag_value: tag_value
322
+ }
323
+ end
324
+
325
+ # Convert numeric values back to strings
326
+ {
327
+ operand_value: processed_operand_value.to_s,
328
+ tag_value: processed_tag_value.to_s
329
+ }
330
+ end
331
+
332
+ # Extracts the result from the operand value and tag value
333
+ # @param operand_type [Symbol] The type of operand
334
+ # @param operand_value [String] The value of the operand
335
+ # @param tag_value [String] The value of the tag
336
+ # @return [Boolean] True if the operand value matches the tag value, false otherwise
337
+ def extract_result(operand_type, operand_value, tag_value)
338
+ result = false
339
+
340
+ return false if tag_value.nil?
341
+
342
+ # Ensure operand_value and tag_value are strings
343
+ operand_value_str = operand_value.to_s
344
+ tag_value_str = tag_value.to_s
345
+
346
+ case operand_type
347
+ when SegmentOperandValueEnum::LOWER_VALUE
348
+ result = operand_value_str.downcase == tag_value_str.downcase
349
+ when SegmentOperandValueEnum::STARTING_ENDING_STAR_VALUE
350
+ result = tag_value_str.include?(operand_value_str)
351
+ when SegmentOperandValueEnum::STARTING_STAR_VALUE
352
+ result = tag_value_str.end_with?(operand_value_str)
353
+ when SegmentOperandValueEnum::ENDING_STAR_VALUE
354
+ result = tag_value_str.start_with?(operand_value_str)
355
+ when SegmentOperandValueEnum::REGEX_VALUE
356
+ begin
357
+ pattern = Regexp.new(operand_value_str)
358
+ result = pattern.match?(tag_value_str)
359
+ rescue StandardError
360
+ result = false
361
+ end
362
+ when SegmentOperandValueEnum::GREATER_THAN_VALUE
363
+ result = compare_versions(tag_value_str, operand_value_str) > 0
364
+ when SegmentOperandValueEnum::GREATER_THAN_EQUAL_TO_VALUE
365
+ result = compare_versions(tag_value_str, operand_value_str) >= 0
366
+ when SegmentOperandValueEnum::LESS_THAN_VALUE
367
+ result = compare_versions(tag_value_str, operand_value_str) < 0
368
+ when SegmentOperandValueEnum::LESS_THAN_EQUAL_TO_VALUE
369
+ result = compare_versions(tag_value_str, operand_value_str) <= 0
370
+ else
371
+ # For version-like strings, use version comparison; otherwise use string comparison
372
+ if version_string?(tag_value_str) && version_string?(operand_value_str)
373
+ result = compare_versions(tag_value_str, operand_value_str) == 0
374
+ else
375
+ result = tag_value_str == operand_value_str
376
+ end
377
+ end
378
+
379
+ result
380
+ end
381
+
382
+ # Checks if a string appears to be a version string (contains only digits and dots).
383
+ # @param str [String] The string to check.
384
+ # @return [Boolean] True if the string appears to be a version string.
385
+ def version_string?(str)
386
+ /^(\d+\.)*\d+$/.match?(str)
387
+ end
388
+
389
+ # Compares two version strings using semantic versioning rules.
390
+ # Supports formats like "1.2.3", "1.0", "2.1.4.5", etc.
391
+ # @param version1 [String] First version string.
392
+ # @param version2 [String] Second version string.
393
+ # @return [Integer] -1 if version1 < version2, 0 if equal, 1 if version1 > version2.
394
+ def compare_versions(version1, version2)
395
+ # Split versions by dots and convert to integers
396
+ parts1 = version1.split('.').map { |part| part.match?(/^\d+$/) ? part.to_i : 0 }
397
+ parts2 = version2.split('.').map { |part| part.match?(/^\d+$/) ? part.to_i : 0 }
398
+
399
+ # Find the maximum length to handle different version formats
400
+ max_length = [parts1.length, parts2.length].max
401
+
402
+ (0...max_length).each do |i|
403
+ part1 = i < parts1.length ? parts1[i] : 0
404
+ part2 = i < parts2.length ? parts2[i] : 0
405
+
406
+ if part1 < part2
407
+ return -1
408
+ elsif part1 > part2
409
+ return 1
410
+ end
411
+ end
412
+
413
+ 0 # Versions are equal
414
+ end
415
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright 2024-2026 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+
17
+ # Utility function to check if a value is an object (excluding arrays and other types)
18
+ def is_object?(val)
19
+ val.is_a?(Hash)
20
+ end
21
+
22
+ # Extracts the first key-value pair from the provided object.
23
+ # @param obj [Hash] The object from which to extract the key-value pair.
24
+ # @return [Hash, nil] A hash containing the first key and value, or nil if input is not a hash.
25
+ def get_key_value(obj)
26
+ return nil unless is_object?(obj)
27
+
28
+ key = obj.keys.first
29
+ return nil unless key
30
+
31
+ { key: key, value: obj[key] }
32
+ end
33
+
34
+ # Matches a string against a regular expression and returns the match result.
35
+ # @param string [String] The string to match against the regex.
36
+ # @param regex [String] The regex pattern as a string.
37
+ # @return [Array, nil] The results of the regex match, or nil if an error occurs.
38
+ def match_with_regex(string, regex)
39
+ begin
40
+ return string.match(Regexp.new(regex))
41
+ rescue StandardError
42
+ return nil
43
+ end
44
+ end