optimizely-sdk 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb7a6c83f167e9c4e5166a6a4d8d4a9d4a0da2d4
4
- data.tar.gz: 0a6b87960154256a3073c319be67779a395df7e4
3
+ metadata.gz: b6172194b87939a24b453c42267ee0431b7e86f4
4
+ data.tar.gz: 26fabaaa1ff0a7d6ea4aa2d6728b75dbe85d646c
5
5
  SHA512:
6
- metadata.gz: 1b47592ea9b7a94640a3d3d2f876eea4578b726bfc7a644a4ef2427493b510d00e7ebd7f2475cb9fca69c36167afb5691a58465ce639e863caf960e900821e8f
7
- data.tar.gz: d4165dbdb6a9982d9fcd6582c34acbab71a9e41a31ec44037a8aead5462f6daad1142b97ee61ab116e5433836bd283c6356c1658885a7c06513c5246f3297091
6
+ metadata.gz: e7deb33a0386fa18352dc926a9920cd1a7a56373dfb5e7f1a03f1161013b867add78d13cdf051d3cde9762710bb9ee9adbc77669dee0a08ca9b9bda461df8f5e
7
+ data.tar.gz: 2c17a1ca3bef13c7c44e53c1a50ff32991f7abd966d7b9cd7af57708f395ea95a69ecab24090af936488ea21c8810b83c826ba0c01481058bd61a6f5da3d7fbd
data/lib/optimizely.rb CHANGED
@@ -22,6 +22,9 @@ require_relative 'optimizely/config_manager/static_project_config_manager'
22
22
  require_relative 'optimizely/decision_service'
23
23
  require_relative 'optimizely/error_handler'
24
24
  require_relative 'optimizely/event_builder'
25
+ require_relative 'optimizely/event/forwarding_event_processor'
26
+ require_relative 'optimizely/event/event_factory'
27
+ require_relative 'optimizely/event/user_event_factory'
25
28
  require_relative 'optimizely/event_dispatcher'
26
29
  require_relative 'optimizely/exceptions'
27
30
  require_relative 'optimizely/helpers/constants'
@@ -35,8 +38,8 @@ module Optimizely
35
38
  class Project
36
39
  attr_reader :notification_center
37
40
  # @api no-doc
38
- attr_reader :config_manager, :decision_service, :error_handler,
39
- :event_builder, :event_dispatcher, :logger
41
+ attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
42
+ :event_processor, :logger, :stopped
40
43
 
41
44
  # Constructor for Projects.
42
45
  #
@@ -49,8 +52,9 @@ module Optimizely
49
52
  # @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
50
53
  # @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
51
54
  # Must provide at least one of datafile or sdk_key.
52
- # @param config_manager - Optional Responds to get_config.
55
+ # @param config_manager - Optional Responds to 'config' method.
53
56
  # @param notification_center - Optional Instance of NotificationCenter.
57
+ # @param event_processor - Optional Responds to process.
54
58
 
55
59
  def initialize(
56
60
  datafile = nil,
@@ -61,7 +65,8 @@ module Optimizely
61
65
  user_profile_service = nil,
62
66
  sdk_key = nil,
63
67
  config_manager = nil,
64
- notification_center = nil
68
+ notification_center = nil,
69
+ event_processor = nil
65
70
  )
66
71
  @logger = logger || NoOpLogger.new
67
72
  @error_handler = error_handler || NoOpErrorHandler.new
@@ -77,7 +82,7 @@ module Optimizely
77
82
 
78
83
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
79
84
 
80
- @config_manager = if config_manager.respond_to?(:get_config)
85
+ @config_manager = if config_manager.respond_to?(:config)
81
86
  config_manager
82
87
  elsif sdk_key
83
88
  HTTPProjectConfigManager.new(
@@ -91,8 +96,14 @@ module Optimizely
91
96
  else
92
97
  StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
93
98
  end
99
+
94
100
  @decision_service = DecisionService.new(@logger, @user_profile_service)
95
- @event_builder = EventBuilder.new(@logger)
101
+
102
+ @event_processor = if event_processor.respond_to?(:process)
103
+ event_processor
104
+ else
105
+ ForwardingEventProcessor.new(@event_dispatcher, @logger, @notification_center)
106
+ end
96
107
  end
97
108
 
98
109
  # Buckets visitor and sends impression event to Optimizely.
@@ -243,20 +254,17 @@ module Optimizely
243
254
  return nil
244
255
  end
245
256
 
246
- conversion_event = @event_builder.create_conversion_event(config, event, user_id, attributes, event_tags)
257
+ user_event = UserEventFactory.create_conversion_event(config, event, user_id, attributes, event_tags)
258
+ @event_processor.process(user_event)
247
259
  @logger.log(Logger::INFO, "Tracking event '#{event_key}' for user '#{user_id}'.")
248
- @logger.log(Logger::INFO,
249
- "Dispatching conversion event to URL #{conversion_event.url} with params #{conversion_event.params}.")
250
- begin
251
- @event_dispatcher.dispatch_event(conversion_event)
252
- rescue => e
253
- @logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
254
- end
255
260
 
256
- @notification_center.send_notifications(
257
- NotificationCenter::NOTIFICATION_TYPES[:TRACK],
258
- event_key, user_id, attributes, event_tags, conversion_event
259
- )
261
+ if @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:TRACK]).positive?
262
+ log_event = EventFactory.create_log_event(user_event, @logger)
263
+ @notification_center.send_notifications(
264
+ NotificationCenter::NOTIFICATION_TYPES[:TRACK],
265
+ event_key, user_id, attributes, event_tags, log_event
266
+ )
267
+ end
260
268
  nil
261
269
  end
262
270
 
@@ -369,6 +377,32 @@ module Optimizely
369
377
  enabled_features
370
378
  end
371
379
 
380
+ # Get the value of the specified variable in the feature flag.
381
+ #
382
+ # @param feature_flag_key - String key of feature flag the variable belongs to
383
+ # @param variable_key - String key of variable for which we are getting the value
384
+ # @param user_id - String user ID
385
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
386
+ #
387
+ # @return [*] the type-casted variable value.
388
+ # @return [nil] if the feature flag or variable are not found.
389
+
390
+ def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
391
+ unless is_valid
392
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
393
+ return nil
394
+ end
395
+ variable_value = get_feature_variable_for_type(
396
+ feature_flag_key,
397
+ variable_key,
398
+ nil,
399
+ user_id,
400
+ attributes
401
+ )
402
+
403
+ variable_value
404
+ end
405
+
372
406
  # Get the String value of the specified variable in the feature flag.
373
407
  #
374
408
  # @param feature_flag_key - String key of feature flag the variable belongs to
@@ -481,6 +515,14 @@ module Optimizely
481
515
  config.is_a?(Optimizely::ProjectConfig)
482
516
  end
483
517
 
518
+ def close
519
+ return if @stopped
520
+
521
+ @stopped = true
522
+ @config_manager.stop! if @config_manager.respond_to?(:stop!)
523
+ @event_processor.stop! if @event_processor.respond_to?(:stop!)
524
+ end
525
+
484
526
  private
485
527
 
486
528
  def get_variation_with_config(experiment_key, user_id, attributes, config)
@@ -556,6 +598,9 @@ module Optimizely
556
598
  return nil if variable.nil?
557
599
 
558
600
  feature_enabled = false
601
+
602
+ # If variable_type is nil, set it equal to variable['type']
603
+ variable_type ||= variable['type']
559
604
  # Returns nil if type differs
560
605
  if variable['type'] != variable_type
561
606
  @logger.log(Logger::WARN,
@@ -663,18 +708,16 @@ module Optimizely
663
708
  def send_impression(config, experiment, variation_key, user_id, attributes = nil)
664
709
  experiment_key = experiment['key']
665
710
  variation_id = config.get_variation_id_from_key(experiment_key, variation_key)
666
- impression_event = @event_builder.create_impression_event(config, experiment, variation_id, user_id, attributes)
667
- @logger.log(Logger::INFO,
668
- "Dispatching impression event to URL #{impression_event.url} with params #{impression_event.params}.")
669
- begin
670
- @event_dispatcher.dispatch_event(impression_event)
671
- rescue => e
672
- @logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
673
- end
711
+ user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, user_id, attributes)
712
+ @event_processor.process(user_event)
713
+ return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
714
+
715
+ @logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
674
716
  variation = config.get_variation_from_id(experiment_key, variation_id)
717
+ log_event = EventFactory.create_log_event(user_event, @logger)
675
718
  @notification_center.send_notifications(
676
719
  NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
677
- experiment, user_id, attributes, variation, impression_event
720
+ experiment, user_id, attributes, variation, log_event
678
721
  )
679
722
  end
680
723
 
@@ -30,7 +30,7 @@ module Optimizely
30
30
  class HTTPProjectConfigManager < ProjectConfigManager
31
31
  # Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
32
32
 
33
- attr_reader :config
33
+ attr_reader :stopped
34
34
 
35
35
  # Initialize config manager. One of sdk_key or url has to be set to be able to use.
36
36
  #
@@ -38,9 +38,9 @@ module Optimizely
38
38
  # datafile: Optional JSON string representing the project.
39
39
  # polling_interval - Optional floating point number representing time interval in seconds
40
40
  # at which to request datafile and set ProjectConfig.
41
- # blocking_timeout -
42
- # auto_update -
43
- # start_by_default -
41
+ # blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized.
42
+ # auto_update - Boolean indicates to run infinitely or only once.
43
+ # start_by_default - Boolean indicates to start by default AsyncScheduler.
44
44
  # url - Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
45
45
  # url_template - Optional string template which in conjunction with sdk_key
46
46
  # determines URL from where to fetch the datafile.
@@ -72,6 +72,7 @@ module Optimizely
72
72
  @last_modified = nil
73
73
  @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
74
74
  @async_scheduler.start! if start_by_default == true
75
+ @stopped = false
75
76
  @skip_json_validation = skip_json_validation
76
77
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
77
78
  @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
@@ -84,20 +85,36 @@ module Optimizely
84
85
  end
85
86
 
86
87
  def start!
88
+ if @stopped
89
+ @logger.log(Logger::WARN, 'Not starting. Already stopped.')
90
+ return
91
+ end
92
+
87
93
  @async_scheduler.start!
94
+ @stopped = false
88
95
  end
89
96
 
90
97
  def stop!
98
+ if @stopped
99
+ @logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
100
+ return
101
+ end
102
+
91
103
  @async_scheduler.stop!
104
+ @config = nil
105
+ @stopped = true
92
106
  end
93
107
 
94
- def get_config
108
+ def config
95
109
  # Get Project Config.
96
110
 
111
+ # if stopped is true, then simply return @config.
97
112
  # If the background datafile polling thread is running. and config has been initalized,
98
- # we simply return config.
113
+ # we simply return @config.
99
114
  # If it is not, we wait and block maximum for @blocking_timeout.
100
115
  # If thread is not running, we fetch the datafile and update config.
116
+ return @config if @stopped
117
+
101
118
  if @async_scheduler.running
102
119
  return @config if ready?
103
120
 
@@ -196,7 +213,7 @@ module Optimizely
196
213
  return
197
214
  end
198
215
 
199
- unless polling_interval.is_a? Integer
216
+ unless polling_interval.is_a? Numeric
200
217
  @logger.log(
201
218
  Logger::ERROR,
202
219
  "Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
@@ -205,7 +222,7 @@ module Optimizely
205
222
  return
206
223
  end
207
224
 
208
- unless polling_interval.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
225
+ unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
209
226
  @logger.log(
210
227
  Logger::DEBUG,
211
228
  "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
@@ -218,9 +235,9 @@ module Optimizely
218
235
  end
219
236
 
220
237
  def blocking_timeout(blocking_timeout)
221
- # Sets time in seconds to block the get_config call until config has been initialized.
238
+ # Sets time in seconds to block the config call until config has been initialized.
222
239
  #
223
- # blocking_timeout - Time in seconds after which to update datafile.
240
+ # blocking_timeout - Time in seconds to block the config call.
224
241
 
225
242
  # If valid set given timeout, default blocking_timeout otherwise.
226
243
 
@@ -229,7 +246,7 @@ module Optimizely
229
246
  Logger::DEBUG,
230
247
  "Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
231
248
  )
232
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
249
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
233
250
  return
234
251
  end
235
252
 
@@ -238,7 +255,7 @@ module Optimizely
238
255
  Logger::ERROR,
239
256
  "Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
240
257
  )
241
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
258
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
242
259
  return
243
260
  end
244
261
 
@@ -247,7 +264,7 @@ module Optimizely
247
264
  Logger::DEBUG,
248
265
  "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
249
266
  )
250
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
267
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
251
268
  return
252
269
  end
253
270
 
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require_relative 'event_processor'
19
+ require_relative '../helpers/validator'
20
+ module Optimizely
21
+ class BatchEventProcessor < EventProcessor
22
+ # BatchEventProcessor is a batched implementation of the Interface EventProcessor.
23
+ # Events passed to the BatchEventProcessor are immediately added to a EventQueue.
24
+ # The BatchEventProcessor maintains a single consumer thread that pulls events off of
25
+ # the BlockingQueue and buffers them for either a configured batch size or for a
26
+ # maximum duration before the resulting LogEvent is sent to the NotificationCenter.
27
+
28
+ attr_reader :event_queue, :event_dispatcher, :current_batch, :started, :batch_size, :flush_interval
29
+
30
+ DEFAULT_BATCH_SIZE = 10
31
+ DEFAULT_BATCH_INTERVAL = 30_000 # interval in milliseconds
32
+ DEFAULT_QUEUE_CAPACITY = 1000
33
+
34
+ FLUSH_SIGNAL = 'FLUSH_SIGNAL'
35
+ SHUTDOWN_SIGNAL = 'SHUTDOWN_SIGNAL'
36
+
37
+ def initialize(
38
+ event_queue: SizedQueue.new(DEFAULT_QUEUE_CAPACITY),
39
+ event_dispatcher: Optimizely::EventDispatcher.new,
40
+ batch_size: DEFAULT_BATCH_SIZE,
41
+ flush_interval: DEFAULT_BATCH_INTERVAL,
42
+ logger: NoOpLogger.new,
43
+ notification_center: nil
44
+ )
45
+ @event_queue = event_queue
46
+ @logger = logger
47
+ @event_dispatcher = event_dispatcher
48
+ @batch_size = if (batch_size.is_a? Integer) && positive_number?(batch_size)
49
+ batch_size
50
+ else
51
+ @logger.log(Logger::DEBUG, "Setting to default batch_size: #{DEFAULT_BATCH_SIZE}.")
52
+ DEFAULT_BATCH_SIZE
53
+ end
54
+ @flush_interval = if positive_number?(flush_interval)
55
+ flush_interval
56
+ else
57
+ @logger.log(Logger::DEBUG, "Setting to default flush_interval: #{DEFAULT_BATCH_INTERVAL} ms.")
58
+ DEFAULT_BATCH_INTERVAL
59
+ end
60
+ @notification_center = notification_center
61
+ @mutex = Mutex.new
62
+ @received = ConditionVariable.new
63
+ @current_batch = []
64
+ @started = false
65
+ start!
66
+ end
67
+
68
+ def start!
69
+ if @started == true
70
+ @logger.log(Logger::WARN, 'Service already started.')
71
+ return
72
+ end
73
+ @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
74
+ @thread = Thread.new { run }
75
+ @started = true
76
+ end
77
+
78
+ def flush
79
+ @mutex.synchronize do
80
+ @event_queue << FLUSH_SIGNAL
81
+ @received.signal
82
+ end
83
+ end
84
+
85
+ def process(user_event)
86
+ @logger.log(Logger::DEBUG, "Received userEvent: #{user_event}")
87
+
88
+ if !@started || !@thread.alive?
89
+ @logger.log(Logger::WARN, 'Executor shutdown, not accepting tasks.')
90
+ return
91
+ end
92
+
93
+ @mutex.synchronize do
94
+ begin
95
+ @event_queue << user_event
96
+ @received.signal
97
+ rescue Exception
98
+ @logger.log(Logger::WARN, 'Payload not accepted by the queue.')
99
+ return
100
+ end
101
+ end
102
+ end
103
+
104
+ def stop!
105
+ return unless @started
106
+
107
+ @mutex.synchronize do
108
+ @event_queue << SHUTDOWN_SIGNAL
109
+ @received.signal
110
+ end
111
+
112
+ @started = false
113
+ @logger.log(Logger::WARN, 'Stopping scheduler.')
114
+ @thread.exit
115
+ end
116
+
117
+ private
118
+
119
+ def run
120
+ loop do
121
+ if Helpers::DateTimeUtils.create_timestamp > @flushing_interval_deadline
122
+ @logger.log(
123
+ Logger::DEBUG,
124
+ 'Deadline exceeded flushing current batch.'
125
+ )
126
+ flush_queue!
127
+ end
128
+
129
+ item = nil
130
+
131
+ @mutex.synchronize do
132
+ @received.wait(@mutex, 0.05)
133
+ item = @event_queue.pop if @event_queue.length.positive?
134
+ end
135
+
136
+ if item.nil?
137
+ sleep(0.05)
138
+ next
139
+ end
140
+
141
+ if item == SHUTDOWN_SIGNAL
142
+ @logger.log(Logger::INFO, 'Received shutdown signal.')
143
+ break
144
+ end
145
+
146
+ if item == FLUSH_SIGNAL
147
+ @logger.log(Logger::DEBUG, 'Received flush signal.')
148
+ flush_queue!
149
+ next
150
+ end
151
+
152
+ add_to_batch(item) if item.is_a? Optimizely::UserEvent
153
+ end
154
+ rescue SignalException
155
+ @logger.log(Logger::INFO, 'Interrupted while processing buffer.')
156
+ rescue Exception => e
157
+ @logger.log(Logger::ERROR, "Uncaught exception processing buffer. #{e.message}")
158
+ ensure
159
+ @logger.log(
160
+ Logger::INFO,
161
+ 'Exiting processing loop. Attempting to flush pending events.'
162
+ )
163
+ flush_queue!
164
+ end
165
+
166
+ def flush_queue!
167
+ return if @current_batch.empty?
168
+
169
+ log_event = Optimizely::EventFactory.create_log_event(@current_batch, @logger)
170
+ begin
171
+ @event_dispatcher.dispatch_event(log_event)
172
+ @notification_center&.send_notifications(
173
+ NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT],
174
+ log_event
175
+ )
176
+ rescue StandardError => e
177
+ @logger.log(Logger::ERROR, "Error dispatching event: #{log_event} #{e.message}.")
178
+ end
179
+ @current_batch = []
180
+ end
181
+
182
+ def add_to_batch(user_event)
183
+ if should_split?(user_event)
184
+ flush_queue!
185
+ @current_batch = []
186
+ end
187
+
188
+ # Reset the deadline if starting a new batch.
189
+ @flushing_interval_deadline = (Helpers::DateTimeUtils.create_timestamp + @flush_interval) if @current_batch.empty?
190
+
191
+ @logger.log(Logger::DEBUG, "Adding user event: #{user_event} to batch.")
192
+ @current_batch << user_event
193
+ return unless @current_batch.length >= @batch_size
194
+
195
+ @logger.log(Logger::DEBUG, 'Flushing on max batch size!')
196
+ flush_queue!
197
+ end
198
+
199
+ def should_split?(user_event)
200
+ return false if @current_batch.empty?
201
+
202
+ current_context = @current_batch.last.event_context
203
+ new_context = user_event.event_context
204
+
205
+ # Revisions should match
206
+ unless current_context[:revision] == new_context[:revision]
207
+ @logger.log(Logger::DEBUG, 'Revisions mismatched: Flushing current batch.')
208
+ return true
209
+ end
210
+
211
+ # Projects should match
212
+ unless current_context[:project_id] == new_context[:project_id]
213
+ @logger.log(Logger::DEBUG, 'Project Ids mismatched: Flushing current batch.')
214
+ return true
215
+ end
216
+ false
217
+ end
218
+
219
+ def positive_number?(value)
220
+ # Returns true if the given value is positive finite number.
221
+ # false otherwise.
222
+ Helpers::Validator.finite_number?(value) && value.positive?
223
+ end
224
+ end
225
+ end