optimizely-sdk 5.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -127
  4. data/lib/optimizely/bucketer.rb +156 -156
  5. data/lib/optimizely/condition_tree_evaluator.rb +123 -123
  6. data/lib/optimizely/config/datafile_project_config.rb +558 -558
  7. data/lib/optimizely/config/proxy_config.rb +34 -34
  8. data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
  9. data/lib/optimizely/config_manager/http_project_config_manager.rb +340 -340
  10. data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
  12. data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
  13. data/lib/optimizely/decide/optimizely_decision.rb +60 -60
  14. data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
  15. data/lib/optimizely/decision_service.rb +589 -563
  16. data/lib/optimizely/error_handler.rb +39 -39
  17. data/lib/optimizely/event/batch_event_processor.rb +235 -235
  18. data/lib/optimizely/event/entity/conversion_event.rb +44 -44
  19. data/lib/optimizely/event/entity/decision.rb +38 -38
  20. data/lib/optimizely/event/entity/event_batch.rb +86 -86
  21. data/lib/optimizely/event/entity/event_context.rb +50 -50
  22. data/lib/optimizely/event/entity/impression_event.rb +48 -48
  23. data/lib/optimizely/event/entity/snapshot.rb +33 -33
  24. data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
  25. data/lib/optimizely/event/entity/user_event.rb +22 -22
  26. data/lib/optimizely/event/entity/visitor.rb +36 -36
  27. data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
  28. data/lib/optimizely/event/event_factory.rb +156 -156
  29. data/lib/optimizely/event/event_processor.rb +25 -25
  30. data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
  31. data/lib/optimizely/event/user_event_factory.rb +88 -88
  32. data/lib/optimizely/event_builder.rb +221 -221
  33. data/lib/optimizely/event_dispatcher.rb +69 -69
  34. data/lib/optimizely/exceptions.rb +193 -193
  35. data/lib/optimizely/helpers/constants.rb +459 -459
  36. data/lib/optimizely/helpers/date_time_utils.rb +30 -30
  37. data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
  38. data/lib/optimizely/helpers/group.rb +31 -31
  39. data/lib/optimizely/helpers/http_utils.rb +68 -68
  40. data/lib/optimizely/helpers/sdk_settings.rb +61 -61
  41. data/lib/optimizely/helpers/validator.rb +236 -236
  42. data/lib/optimizely/helpers/variable_type.rb +67 -67
  43. data/lib/optimizely/logger.rb +46 -46
  44. data/lib/optimizely/notification_center.rb +174 -174
  45. data/lib/optimizely/notification_center_registry.rb +71 -71
  46. data/lib/optimizely/odp/lru_cache.rb +114 -114
  47. data/lib/optimizely/odp/odp_config.rb +102 -102
  48. data/lib/optimizely/odp/odp_event.rb +75 -75
  49. data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
  50. data/lib/optimizely/odp/odp_event_manager.rb +286 -286
  51. data/lib/optimizely/odp/odp_manager.rb +159 -159
  52. data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
  53. data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
  54. data/lib/optimizely/optimizely_config.rb +273 -273
  55. data/lib/optimizely/optimizely_factory.rb +183 -184
  56. data/lib/optimizely/optimizely_user_context.rb +238 -238
  57. data/lib/optimizely/params.rb +31 -31
  58. data/lib/optimizely/project_config.rb +99 -99
  59. data/lib/optimizely/semantic_version.rb +166 -166
  60. data/lib/optimizely/user_condition_evaluator.rb +391 -391
  61. data/lib/optimizely/user_profile_service.rb +35 -35
  62. data/lib/optimizely/user_profile_tracker.rb +64 -0
  63. data/lib/optimizely/version.rb +21 -21
  64. data/lib/optimizely.rb +1326 -1262
  65. metadata +8 -5
@@ -1,238 +1,238 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2020-2022, 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
-
19
- require 'json'
20
-
21
- module Optimizely
22
- class OptimizelyUserContext
23
- # Representation of an Optimizely User Context using which APIs are to be called.
24
-
25
- attr_reader :user_id, :forced_decisions, :optimizely_client
26
-
27
- OptimizelyDecisionContext = Struct.new(:flag_key, :rule_key)
28
- OptimizelyForcedDecision = Struct.new(:variation_key)
29
- def initialize(optimizely_client, user_id, user_attributes, identify: true)
30
- @attr_mutex = Mutex.new
31
- @forced_decision_mutex = Mutex.new
32
- @qualified_segment_mutex = Mutex.new
33
- @optimizely_client = optimizely_client
34
- @user_id = user_id
35
- @user_attributes = user_attributes.nil? ? {} : user_attributes.clone
36
- @forced_decisions = {}
37
- @qualified_segments = nil
38
-
39
- @optimizely_client&.identify_user(user_id: user_id) if identify
40
- end
41
-
42
- def clone
43
- user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes, identify: false)
44
- @forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? }
45
- @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.nil? }
46
- user_context
47
- end
48
-
49
- def user_attributes
50
- @attr_mutex.synchronize { @user_attributes.clone }
51
- end
52
-
53
- # Set an attribute for a given key
54
- #
55
- # @param key - An attribute key
56
- # @param value - An attribute value
57
-
58
- def set_attribute(attribute_key, attribute_value)
59
- @attr_mutex.synchronize { @user_attributes[attribute_key] = attribute_value }
60
- end
61
-
62
- # Returns a decision result (OptimizelyDecision) for a given flag key and a user context, which contains all data required to deliver the flag.
63
- #
64
- # If the SDK finds an error, it'll return a `decision` with nil for `variation_key`. The decision will include an error message in `reasons`
65
- #
66
- # @param key -A flag key for which a decision will be made
67
- # @param options - A list of options for decision making.
68
- #
69
- # @return [OptimizelyDecision] A decision result
70
-
71
- def decide(key, options = nil)
72
- @optimizely_client&.decide(clone, key, options)
73
- end
74
-
75
- # Returns a hash of decision results (OptimizelyDecision) for multiple flag keys and a user context.
76
- #
77
- # If the SDK finds an error for a key, the response will include a decision for the key showing `reasons` for the error.
78
- # The SDK will always return hash of decisions. When it can not process requests, it'll return an empty hash after logging the errors.
79
- #
80
- # @param keys - A list of flag keys for which the decisions will be made.
81
- # @param options - A list of options for decision making.
82
- #
83
- # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
84
-
85
- def decide_for_keys(keys, options = nil)
86
- @optimizely_client&.decide_for_keys(clone, keys, options)
87
- end
88
-
89
- # Returns a hash of decision results (OptimizelyDecision) for all active flag keys.
90
- #
91
- # @param options - A list of options for decision making.
92
- #
93
- # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
94
-
95
- def decide_all(options = nil)
96
- @optimizely_client&.decide_all(clone, options)
97
- end
98
-
99
- # Sets the forced decision (variation key) for a given flag and an optional rule.
100
- #
101
- # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
102
- # @param decision - An OptimizelyForcedDecision object containing variation key
103
- #
104
- # @return - true if the forced decision has been set successfully.
105
-
106
- def set_forced_decision(context, decision)
107
- flag_key = context[:flag_key]
108
- return false if flag_key.nil?
109
-
110
- @forced_decision_mutex.synchronize { @forced_decisions[context] = decision }
111
-
112
- true
113
- end
114
-
115
- def find_forced_decision(context)
116
- return nil if @forced_decisions.empty?
117
-
118
- decision = nil
119
- @forced_decision_mutex.synchronize { decision = @forced_decisions[context] }
120
- decision
121
- end
122
-
123
- # Returns the forced decision for a given flag and an optional rule.
124
- #
125
- # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
126
- #
127
- # @return - A variation key or nil if forced decisions are not set for the parameters.
128
-
129
- def get_forced_decision(context)
130
- find_forced_decision(context)
131
- end
132
-
133
- # Removes the forced decision for a given flag and an optional rule.
134
- #
135
- # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
136
- #
137
- # @return - true if the forced decision has been removed successfully.
138
-
139
- def remove_forced_decision(context)
140
- deleted = false
141
- @forced_decision_mutex.synchronize do
142
- if @forced_decisions.key?(context)
143
- @forced_decisions.delete(context)
144
- deleted = true
145
- end
146
- end
147
- deleted
148
- end
149
-
150
- # Removes all forced decisions bound to this user context.
151
- #
152
- # @return - true if forced decisions have been removed successfully.
153
-
154
- def remove_all_forced_decisions
155
- return false if @optimizely_client&.get_optimizely_config.nil?
156
-
157
- @forced_decision_mutex.synchronize { @forced_decisions.clear }
158
- true
159
- end
160
-
161
- # Track an event
162
- #
163
- # @param event_key - Event key representing the event which needs to be recorded.
164
-
165
- def track_event(event_key, event_tags = nil)
166
- @optimizely_client&.track(event_key, @user_id, user_attributes, event_tags)
167
- end
168
-
169
- def as_json
170
- {
171
- user_id: @user_id,
172
- attributes: @user_attributes
173
- }
174
- end
175
-
176
- def to_json(*args)
177
- as_json.to_json(*args)
178
- end
179
-
180
- # Returns An array of qualified segments for this user
181
- #
182
- # @return - An array of segments names.
183
-
184
- def qualified_segments
185
- @qualified_segment_mutex.synchronize { @qualified_segments.clone }
186
- end
187
-
188
- # Replace qualified segments with provided segments
189
- #
190
- # @param segments - An array of segment names
191
-
192
- def qualified_segments=(segments)
193
- @qualified_segment_mutex.synchronize { @qualified_segments = segments.clone }
194
- end
195
-
196
- # Checks if user is qualified for the provided segment.
197
- #
198
- # @param segment - A segment name
199
- # @return true if qualified.
200
-
201
- def qualified_for?(segment)
202
- qualified = false
203
- @qualified_segment_mutex.synchronize do
204
- break if @qualified_segments.nil? || @qualified_segments.empty?
205
-
206
- qualified = @qualified_segments.include?(segment)
207
- end
208
- qualified
209
- end
210
-
211
- # Fetch all qualified segments for the user context.
212
- #
213
- # The segments fetched will be saved in `@qualified_segments` and can be accessed any time.
214
- #
215
- # @param options - A set of options for fetching qualified segments (optional).
216
- # @param block - An optional block to call after segments have been fetched.
217
- # If a block is provided, segments will be fetched on a separate thread.
218
- # Block will be called with a boolean indicating if the fetch succeeded.
219
- # @return If no block is provided, a boolean indicating whether the fetch was successful.
220
- # Otherwise, returns a thread handle and the status boolean is passed to the block.
221
-
222
- def fetch_qualified_segments(options: [], &block)
223
- fetch_segments = lambda do |opts, callback|
224
- segments = @optimizely_client&.fetch_qualified_segments(user_id: @user_id, options: opts)
225
- self.qualified_segments = segments
226
- success = !segments.nil?
227
- callback&.call(success)
228
- success
229
- end
230
-
231
- if block_given?
232
- Thread.new(options, block, &fetch_segments)
233
- else
234
- fetch_segments.call(options, nil)
235
- end
236
- end
237
- end
238
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2020-2022, 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
+
19
+ require 'json'
20
+
21
+ module Optimizely
22
+ class OptimizelyUserContext
23
+ # Representation of an Optimizely User Context using which APIs are to be called.
24
+
25
+ attr_reader :user_id, :forced_decisions, :optimizely_client
26
+
27
+ OptimizelyDecisionContext = Struct.new(:flag_key, :rule_key)
28
+ OptimizelyForcedDecision = Struct.new(:variation_key)
29
+ def initialize(optimizely_client, user_id, user_attributes, identify: true)
30
+ @attr_mutex = Mutex.new
31
+ @forced_decision_mutex = Mutex.new
32
+ @qualified_segment_mutex = Mutex.new
33
+ @optimizely_client = optimizely_client
34
+ @user_id = user_id
35
+ @user_attributes = user_attributes.nil? ? {} : user_attributes.clone
36
+ @forced_decisions = {}
37
+ @qualified_segments = nil
38
+
39
+ @optimizely_client&.identify_user(user_id: user_id) if identify
40
+ end
41
+
42
+ def clone
43
+ user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes, identify: false)
44
+ @forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? }
45
+ @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.nil? }
46
+ user_context
47
+ end
48
+
49
+ def user_attributes
50
+ @attr_mutex.synchronize { @user_attributes.clone }
51
+ end
52
+
53
+ # Set an attribute for a given key
54
+ #
55
+ # @param key - An attribute key
56
+ # @param value - An attribute value
57
+
58
+ def set_attribute(attribute_key, attribute_value)
59
+ @attr_mutex.synchronize { @user_attributes[attribute_key] = attribute_value }
60
+ end
61
+
62
+ # Returns a decision result (OptimizelyDecision) for a given flag key and a user context, which contains all data required to deliver the flag.
63
+ #
64
+ # If the SDK finds an error, it'll return a `decision` with nil for `variation_key`. The decision will include an error message in `reasons`
65
+ #
66
+ # @param key -A flag key for which a decision will be made
67
+ # @param options - A list of options for decision making.
68
+ #
69
+ # @return [OptimizelyDecision] A decision result
70
+
71
+ def decide(key, options = nil)
72
+ @optimizely_client&.decide(clone, key, options)
73
+ end
74
+
75
+ # Returns a hash of decision results (OptimizelyDecision) for multiple flag keys and a user context.
76
+ #
77
+ # If the SDK finds an error for a key, the response will include a decision for the key showing `reasons` for the error.
78
+ # The SDK will always return hash of decisions. When it can not process requests, it'll return an empty hash after logging the errors.
79
+ #
80
+ # @param keys - A list of flag keys for which the decisions will be made.
81
+ # @param options - A list of options for decision making.
82
+ #
83
+ # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
84
+
85
+ def decide_for_keys(keys, options = nil)
86
+ @optimizely_client&.decide_for_keys(clone, keys, options)
87
+ end
88
+
89
+ # Returns a hash of decision results (OptimizelyDecision) for all active flag keys.
90
+ #
91
+ # @param options - A list of options for decision making.
92
+ #
93
+ # @return - Hash of decisions containing flag keys as hash keys and corresponding decisions as their values.
94
+
95
+ def decide_all(options = nil)
96
+ @optimizely_client&.decide_all(clone, options)
97
+ end
98
+
99
+ # Sets the forced decision (variation key) for a given flag and an optional rule.
100
+ #
101
+ # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
102
+ # @param decision - An OptimizelyForcedDecision object containing variation key
103
+ #
104
+ # @return - true if the forced decision has been set successfully.
105
+
106
+ def set_forced_decision(context, decision)
107
+ flag_key = context[:flag_key]
108
+ return false if flag_key.nil?
109
+
110
+ @forced_decision_mutex.synchronize { @forced_decisions[context] = decision }
111
+
112
+ true
113
+ end
114
+
115
+ def find_forced_decision(context)
116
+ return nil if @forced_decisions.empty?
117
+
118
+ decision = nil
119
+ @forced_decision_mutex.synchronize { decision = @forced_decisions[context] }
120
+ decision
121
+ end
122
+
123
+ # Returns the forced decision for a given flag and an optional rule.
124
+ #
125
+ # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
126
+ #
127
+ # @return - A variation key or nil if forced decisions are not set for the parameters.
128
+
129
+ def get_forced_decision(context)
130
+ find_forced_decision(context)
131
+ end
132
+
133
+ # Removes the forced decision for a given flag and an optional rule.
134
+ #
135
+ # @param context - An OptimizelyDecisionContext object containg flag key and rule key.
136
+ #
137
+ # @return - true if the forced decision has been removed successfully.
138
+
139
+ def remove_forced_decision(context)
140
+ deleted = false
141
+ @forced_decision_mutex.synchronize do
142
+ if @forced_decisions.key?(context)
143
+ @forced_decisions.delete(context)
144
+ deleted = true
145
+ end
146
+ end
147
+ deleted
148
+ end
149
+
150
+ # Removes all forced decisions bound to this user context.
151
+ #
152
+ # @return - true if forced decisions have been removed successfully.
153
+
154
+ def remove_all_forced_decisions
155
+ return false if @optimizely_client&.get_optimizely_config.nil?
156
+
157
+ @forced_decision_mutex.synchronize { @forced_decisions.clear }
158
+ true
159
+ end
160
+
161
+ # Track an event
162
+ #
163
+ # @param event_key - Event key representing the event which needs to be recorded.
164
+
165
+ def track_event(event_key, event_tags = nil)
166
+ @optimizely_client&.track(event_key, @user_id, user_attributes, event_tags)
167
+ end
168
+
169
+ def as_json
170
+ {
171
+ user_id: @user_id,
172
+ attributes: @user_attributes
173
+ }
174
+ end
175
+
176
+ def to_json(*args)
177
+ as_json.to_json(*args)
178
+ end
179
+
180
+ # Returns An array of qualified segments for this user
181
+ #
182
+ # @return - An array of segments names.
183
+
184
+ def qualified_segments
185
+ @qualified_segment_mutex.synchronize { @qualified_segments.clone }
186
+ end
187
+
188
+ # Replace qualified segments with provided segments
189
+ #
190
+ # @param segments - An array of segment names
191
+
192
+ def qualified_segments=(segments)
193
+ @qualified_segment_mutex.synchronize { @qualified_segments = segments.clone }
194
+ end
195
+
196
+ # Checks if user is qualified for the provided segment.
197
+ #
198
+ # @param segment - A segment name
199
+ # @return true if qualified.
200
+
201
+ def qualified_for?(segment)
202
+ qualified = false
203
+ @qualified_segment_mutex.synchronize do
204
+ break if @qualified_segments.nil? || @qualified_segments.empty?
205
+
206
+ qualified = @qualified_segments.include?(segment)
207
+ end
208
+ qualified
209
+ end
210
+
211
+ # Fetch all qualified segments for the user context.
212
+ #
213
+ # The segments fetched will be saved in `@qualified_segments` and can be accessed any time.
214
+ #
215
+ # @param options - A set of options for fetching qualified segments (optional).
216
+ # @param block - An optional block to call after segments have been fetched.
217
+ # If a block is provided, segments will be fetched on a separate thread.
218
+ # Block will be called with a boolean indicating if the fetch succeeded.
219
+ # @return If no block is provided, a boolean indicating whether the fetch was successful.
220
+ # Otherwise, returns a thread handle and the status boolean is passed to the block.
221
+
222
+ def fetch_qualified_segments(options: [], &block)
223
+ fetch_segments = lambda do |opts, callback|
224
+ segments = @optimizely_client&.fetch_qualified_segments(user_id: @user_id, options: opts)
225
+ self.qualified_segments = segments
226
+ success = !segments.nil?
227
+ callback&.call(success)
228
+ success
229
+ end
230
+
231
+ if block_given?
232
+ Thread.new(options, block, &fetch_segments)
233
+ else
234
+ fetch_segments.call(options, nil)
235
+ end
236
+ end
237
+ end
238
+ end
@@ -1,31 +1,31 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2016-2017, 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
- module Optimizely
19
- class Params
20
- ACCOUNT_ID = 'd'
21
- PROJECT_ID = 'a'
22
- EXPERIMENT_PREFIX = 'x'
23
- GOAL_ID = 'g'
24
- GOAL_NAME = 'n'
25
- SEGMENT_PREFIX = 's'
26
- END_USER_ID = 'u'
27
- EVENT_VALUE = 'v'
28
- SOURCE = 'src'
29
- TIME = 'time'
30
- end
31
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2016-2017, 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
+ module Optimizely
19
+ class Params
20
+ ACCOUNT_ID = 'd'
21
+ PROJECT_ID = 'a'
22
+ EXPERIMENT_PREFIX = 'x'
23
+ GOAL_ID = 'g'
24
+ GOAL_NAME = 'n'
25
+ SEGMENT_PREFIX = 's'
26
+ END_USER_ID = 'u'
27
+ EVENT_VALUE = 'v'
28
+ SOURCE = 'src'
29
+ TIME = 'time'
30
+ end
31
+ end