launchdarkly-server-sdk 6.3.0 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -0,0 +1,230 @@
1
+ require 'ldclient-rb/impl/migrations/migrator'
2
+
3
+ module LaunchDarkly
4
+ #
5
+ # Namespace for feature-flag based technology migration support.
6
+ #
7
+ module Migrations
8
+ # Symbol representing the old origin, or the old technology source you are migrating away from.
9
+ ORIGIN_OLD = :old
10
+ # Symbol representing the new origin, or the new technology source you are migrating towards.
11
+ ORIGIN_NEW = :new
12
+
13
+ # Symbol defining a read-related operation
14
+ OP_READ = :read
15
+ # Symbol defining a write-related operation
16
+ OP_WRITE = :write
17
+
18
+ STAGE_OFF = :off
19
+ STAGE_DUALWRITE = :dualwrite
20
+ STAGE_SHADOW = :shadow
21
+ STAGE_LIVE = :live
22
+ STAGE_RAMPDOWN = :rampdown
23
+ STAGE_COMPLETE = :complete
24
+
25
+ VALID_OPERATIONS = [
26
+ OP_READ,
27
+ OP_WRITE,
28
+ ]
29
+
30
+ VALID_ORIGINS = [
31
+ ORIGIN_OLD,
32
+ ORIGIN_NEW,
33
+ ]
34
+
35
+ VALID_STAGES = [
36
+ STAGE_OFF,
37
+ STAGE_DUALWRITE,
38
+ STAGE_SHADOW,
39
+ STAGE_LIVE,
40
+ STAGE_RAMPDOWN,
41
+ STAGE_COMPLETE,
42
+ ]
43
+
44
+ #
45
+ # The OperationResult wraps the {LaunchDarkly::Result} class to tie an operation origin to a result.
46
+ #
47
+ class OperationResult
48
+ extend Forwardable
49
+ def_delegators :@result, :value, :error, :exception, :success?
50
+
51
+ #
52
+ # @param origin [Symbol]
53
+ # @param result [LaunchDarkly::Result]
54
+ #
55
+ def initialize(origin, result)
56
+ @origin = origin
57
+ @result = result
58
+ end
59
+
60
+ #
61
+ # @return [Symbol] The origin this result is associated with.
62
+ #
63
+ attr_reader :origin
64
+ end
65
+
66
+ #
67
+ # A write result contains the operation results against both the authoritative and non-authoritative origins.
68
+ #
69
+ # Authoritative writes are always executed first. In the event of a failure, the non-authoritative write will not
70
+ # be executed, resulting in a nil value in the final WriteResult.
71
+ #
72
+ class WriteResult
73
+ #
74
+ # @param authoritative [OperationResult]
75
+ # @param nonauthoritative [OperationResult, nil]
76
+ #
77
+ def initialize(authoritative, nonauthoritative = nil)
78
+ @authoritative = authoritative
79
+ @nonauthoritative = nonauthoritative
80
+ end
81
+
82
+ #
83
+ # Returns the operation result for the authoritative origin.
84
+ #
85
+ # @return [OperationResult]
86
+ #
87
+ attr_reader :authoritative
88
+
89
+ #
90
+ # Returns the operation result for the non-authoritative origin.
91
+ #
92
+ # This result might be nil as the non-authoritative write does not execute in every stage, and will not execute
93
+ # if the authoritative write failed.
94
+ #
95
+ # @return [OperationResult, nil]
96
+ #
97
+ attr_reader :nonauthoritative
98
+ end
99
+
100
+
101
+ #
102
+ # The migration builder is used to configure and construct an instance of a
103
+ # {LaunchDarkly::Interfaces::Migrations::Migrator}. This migrator can be used to perform LaunchDarkly assisted
104
+ # technology migrations through the use of migration-based feature flags.
105
+ #
106
+ class MigratorBuilder
107
+ EXECUTION_SERIAL = :serial
108
+ EXECUTION_RANDOM = :random
109
+ EXECUTION_PARALLEL = :parallel
110
+
111
+ VALID_EXECUTION_ORDERS = [EXECUTION_SERIAL, EXECUTION_RANDOM, EXECUTION_PARALLEL]
112
+ private_constant :VALID_EXECUTION_ORDERS
113
+
114
+ #
115
+ # @param client [LaunchDarkly::LDClient]
116
+ #
117
+ def initialize(client)
118
+ @client = client
119
+
120
+ # Default settings as required by the spec
121
+ @read_execution_order = EXECUTION_PARALLEL
122
+ @measure_latency = true
123
+ @measure_errors = true
124
+
125
+ @read_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
126
+ @write_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
127
+ end
128
+
129
+ #
130
+ # The read execution order influences the parallelism and execution order for read operations involving multiple
131
+ # origins.
132
+ #
133
+ # @param order [Symbol]
134
+ #
135
+ def read_execution_order(order)
136
+ return unless VALID_EXECUTION_ORDERS.include? order
137
+
138
+ @read_execution_order = order
139
+ end
140
+
141
+ #
142
+ # Enable or disable latency tracking for migration operations. This latency information can be sent upstream to
143
+ # LaunchDarkly to enhance migration visibility.
144
+ #
145
+ # @param enabled [Boolean]
146
+ #
147
+ def track_latency(enabled)
148
+ @measure_latency = !!enabled
149
+ end
150
+
151
+ #
152
+ # Enable or disable error tracking for migration operations. This error information can be sent upstream to
153
+ # LaunchDarkly to enhance migration visibility.
154
+ #
155
+ # @param enabled [Boolean]
156
+ #
157
+ def track_errors(enabled)
158
+ @measure_errors = !!enabled
159
+ end
160
+
161
+ #
162
+ # Read can be used to configure the migration-read behavior of the resulting
163
+ # {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
164
+ #
165
+ # Users are required to provide two different read methods -- one to read from the old migration origin, and one
166
+ # to read from the new origin. Additionally, customers can opt-in to consistency tracking by providing a
167
+ # comparison function.
168
+ #
169
+ # Depending on the migration stage, one or both of these read methods may be called.
170
+ #
171
+ # The read methods should accept a single nullable parameter. This parameter is a payload passed through the
172
+ # {LaunchDarkly::Interfaces::Migrations::Migrator#read} method. This method should return a {LaunchDarkly::Result}
173
+ # instance.
174
+ #
175
+ # The consistency method should accept 2 parameters of any type. These parameters are the results of executing the
176
+ # read operation against the old and new origins. If both operations were successful, the consistency method will
177
+ # be invoked. This method should return true if the two parameters are equal, or false otherwise.
178
+ #
179
+ # @param old_read [#call]
180
+ # @param new_read [#call]
181
+ # @param comparison [#call, nil]
182
+ #
183
+ def read(old_read, new_read, comparison = nil)
184
+ return unless old_read.respond_to?(:call) && old_read.arity == 1
185
+ return unless new_read.respond_to?(:call) && new_read.arity == 1
186
+ return unless comparison.nil? || (comparison.respond_to?(:call) && comparison.arity == 2)
187
+
188
+ @read_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_read, new_read, comparison)
189
+ end
190
+
191
+ #
192
+ # Write can be used to configure the migration-write behavior of the resulting
193
+ # {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
194
+ #
195
+ # Users are required to provide two different write methods -- one to write to the old migration origin, and one
196
+ # to write to the new origin.
197
+ #
198
+ # Depending on the migration stage, one or both of these write methods may be called.
199
+ #
200
+ # The write methods should accept a single nullable parameter. This parameter is a payload passed through the
201
+ # {LaunchDarkly::Interfaces::Migrations::Migrator#write} method. This method should return a {LaunchDarkly::Result}
202
+ # instance.
203
+ #
204
+ # @param old_write [#call]
205
+ # @param new_write [#call]
206
+ #
207
+ def write(old_write, new_write)
208
+ return unless old_write.respond_to?(:call) && old_write.arity == 1
209
+ return unless new_write.respond_to?(:call) && new_write.arity == 1
210
+
211
+ @write_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_write, new_write, nil)
212
+ end
213
+
214
+ #
215
+ # Build constructs a {LaunchDarkly::Interfaces::Migrations::Migrator} instance to support migration-based reads
216
+ # and writes. A string describing any failure conditions will be returned if the build fails.
217
+ #
218
+ # @return [LaunchDarkly::Interfaces::Migrations::Migrator, string]
219
+ #
220
+ def build
221
+ return "client not provided" if @client.nil?
222
+ return "read configuration not provided" if @read_config.nil?
223
+ return "write configuration not provided" if @write_config.nil?
224
+
225
+ LaunchDarkly::Impl::Migrations::Migrator.new(@client, @read_execution_order, @read_config, @write_config, @measure_latency, @measure_errors)
226
+ end
227
+ end
228
+
229
+ end
230
+ end
@@ -17,7 +17,7 @@ module LaunchDarkly
17
17
  # Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
18
18
  # this returns a value: true if the job was submitted, false if all workers are busy.
19
19
  def post
20
- if !@semaphore.try_acquire(1)
20
+ unless @semaphore.try_acquire(1)
21
21
  return
22
22
  end
23
23
  @pool.post do
@@ -1,6 +1,7 @@
1
1
  require "ldclient-rb/impl/repeating_task"
2
2
 
3
3
  require "concurrent/atomics"
4
+ require "json"
4
5
  require "thread"
5
6
 
6
7
  module LaunchDarkly
@@ -27,30 +28,75 @@ module LaunchDarkly
27
28
  end
28
29
 
29
30
  def stop
30
- @task.stop
31
- @config.logger.info { "[LDClient] Polling connection stopped" }
31
+ stop_with_error_info
32
32
  end
33
33
 
34
34
  def poll
35
35
  begin
36
36
  all_data = @requestor.request_all_data
37
37
  if all_data
38
- @config.feature_store.init(all_data)
38
+ update_sink_or_data_store.init(all_data)
39
39
  if @initialized.make_true
40
40
  @config.logger.info { "[LDClient] Polling connection initialized" }
41
41
  @ready.set
42
42
  end
43
43
  end
44
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
45
+ rescue JSON::ParserError => e
46
+ @config.logger.error { "[LDClient] JSON parsing failed for polling response." }
47
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
48
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
49
+ 0,
50
+ e.to_s,
51
+ Time.now
52
+ )
53
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
44
54
  rescue UnexpectedResponseError => e
55
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
56
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, e.status, nil, Time.now)
45
57
  message = Util.http_error_message(e.status, "polling request", "will retry")
46
- @config.logger.error { "[LDClient] #{message}" };
47
- if !Util.http_error_recoverable?(e.status)
58
+ @config.logger.error { "[LDClient] #{message}" }
59
+
60
+ if Util.http_error_recoverable?(e.status)
61
+ @config.data_source_update_sink&.update_status(
62
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
63
+ error_info
64
+ )
65
+ else
48
66
  @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
49
- stop
67
+ stop_with_error_info error_info
50
68
  end
51
69
  rescue StandardError => e
52
70
  Util.log_exception(@config.logger, "Exception while polling", e)
71
+ @config.data_source_update_sink&.update_status(
72
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
73
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, e.to_s, Time.now)
74
+ )
53
75
  end
54
76
  end
77
+
78
+ #
79
+ # The original implementation of this class relied on the feature store
80
+ # directly, which we are trying to move away from. Customers who might have
81
+ # instantiated this directly for some reason wouldn't know they have to set
82
+ # the config's sink manually, so we have to fall back to the store if the
83
+ # sink isn't present.
84
+ #
85
+ # The next major release should be able to simplify this structure and
86
+ # remove the need for fall back to the data store because the update sink
87
+ # should always be present.
88
+ #
89
+ private def update_sink_or_data_store
90
+ @config.data_source_update_sink || @config.feature_store
91
+ end
92
+
93
+ #
94
+ # @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
95
+ #
96
+ private def stop_with_error_info(error_info = nil)
97
+ @task.stop
98
+ @config.logger.info { "[LDClient] Polling connection stopped" }
99
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
100
+ end
55
101
  end
56
102
  end
@@ -0,0 +1,274 @@
1
+ module LaunchDarkly
2
+ #
3
+ # Reference is an attribute name or path expression identifying a value
4
+ # within a Context.
5
+ #
6
+ # This type is mainly intended to be used internally by LaunchDarkly SDK and
7
+ # service code, where efficiency is a major concern so it's desirable to do
8
+ # any parsing or preprocessing just once. Applications are unlikely to need
9
+ # to use the Reference type directly.
10
+ #
11
+ # It can be used to retrieve a value with LDContext.get_value_for_reference()
12
+ # or to identify an attribute or nested value that should be considered
13
+ # private.
14
+ #
15
+ # Parsing and validation are done at the time that the Reference is
16
+ # constructed. If a Reference instance was created from an invalid string, it
17
+ # is considered invalid and its {Reference#error} attribute will return a
18
+ # non-nil error.
19
+ #
20
+ # ## Syntax
21
+ #
22
+ # The string representation of an attribute reference in LaunchDarkly JSON
23
+ # data uses the following syntax:
24
+ #
25
+ # If the first character is not a slash, the string is interpreted literally
26
+ # as an attribute name. An attribute name can contain any characters, but
27
+ # must not be empty.
28
+ #
29
+ # If the first character is a slash, the string is interpreted as a
30
+ # slash-delimited path where the first path component is an attribute name,
31
+ # and each subsequent path component is the name of a property in a JSON
32
+ # object. Any instances of the characters "/" or "~" in a path component are
33
+ # escaped as "~1" or "~0" respectively. This syntax deliberately resembles
34
+ # JSON Pointer, but no JSON Pointer behaviors other than those mentioned here
35
+ # are supported.
36
+ #
37
+ # ## Examples
38
+ #
39
+ # Suppose there is a context whose JSON implementation looks like this:
40
+ #
41
+ # {
42
+ # "kind": "user",
43
+ # "key": "value1",
44
+ # "address": {
45
+ # "street": {
46
+ # "line1": "value2",
47
+ # "line2": "value3"
48
+ # },
49
+ # "city": "value4"
50
+ # },
51
+ # "good/bad": "value5"
52
+ # }
53
+ #
54
+ # The attribute references "key" and "/key" would both point to "value1".
55
+ #
56
+ # The attribute reference "/address/street/line1" would point to "value2".
57
+ #
58
+ # The attribute references "good/bad" and "/good~1bad" would both point to
59
+ # "value5".
60
+ #
61
+ class Reference
62
+ ERR_EMPTY = 'empty reference'
63
+ private_constant :ERR_EMPTY
64
+
65
+ ERR_INVALID_ESCAPE_SEQUENCE = 'invalid escape sequence'
66
+ private_constant :ERR_INVALID_ESCAPE_SEQUENCE
67
+
68
+ ERR_DOUBLE_TRAILING_SLASH = 'double or trailing slash'
69
+ private_constant :ERR_DOUBLE_TRAILING_SLASH
70
+
71
+ #
72
+ # Returns nil for a valid Reference, or a non-nil error value for an
73
+ # invalid Reference.
74
+ #
75
+ # A Reference is invalid if the input string is empty, or starts with a
76
+ # slash but is not a valid slash-delimited path, or starts with a slash and
77
+ # contains an invalid escape sequence.
78
+ #
79
+ # Otherwise, the Reference is valid, but that does not guarantee that such
80
+ # an attribute exists in any given Context. For instance,
81
+ # Reference.create("name") is a valid Reference, but a specific Context
82
+ # might or might not have a name.
83
+ #
84
+ # See comments on the Reference type for more details of the attribute
85
+ # reference syntax.
86
+ #
87
+ # @return [String, nil]
88
+ #
89
+ attr_reader :error
90
+
91
+ #
92
+ # Returns the attribute reference as a string, in the same format provided
93
+ # to {#create}.
94
+ #
95
+ # If the Reference was created with {#create}, this value is identical to
96
+ # the original string. If it was created with {#create_literal}, the value
97
+ # may be different due to unescaping (for instance, an attribute whose name
98
+ # is "/a" would be represented as "~1a").
99
+ #
100
+ # @return [String, nil]
101
+ #
102
+ attr_reader :raw_path
103
+
104
+ def initialize(raw_path, components = [], error = nil)
105
+ @raw_path = raw_path
106
+ # @type [Array<Symbol>]
107
+ @components = components
108
+ @error = error
109
+ end
110
+ private_class_method :new
111
+
112
+ #
113
+ # Creates a Reference from a string. For the supported syntax and examples,
114
+ # see comments on the Reference type.
115
+ #
116
+ # This constructor always returns a Reference that preserves the original
117
+ # string, even if validation fails, so that accessing {#raw_path} (or
118
+ # serializing the Reference to JSON) will produce the original string. If
119
+ # validation fails, {#error} will return a non-nil error and any SDK method
120
+ # that takes this Reference as a parameter will consider it invalid.
121
+ #
122
+ # @param value [String, Symbol]
123
+ # @return [Reference]
124
+ #
125
+ def self.create(value)
126
+ unless value.is_a?(String) || value.is_a?(Symbol)
127
+ return new(value, [], ERR_EMPTY)
128
+ end
129
+
130
+ value = value.to_s if value.is_a?(Symbol)
131
+
132
+ return new(value, [], ERR_EMPTY) if value.empty? || value == "/"
133
+
134
+ unless value.start_with? "/"
135
+ return new(value, [value.to_sym])
136
+ end
137
+
138
+ if value.end_with? "/"
139
+ return new(value, [], ERR_DOUBLE_TRAILING_SLASH)
140
+ end
141
+
142
+ components = []
143
+ value[1..].split("/").each do |component|
144
+ if component.empty?
145
+ return new(value, [], ERR_DOUBLE_TRAILING_SLASH)
146
+ end
147
+
148
+ path, error = unescape_path(component)
149
+
150
+ if error
151
+ return new(value, [], error)
152
+ end
153
+
154
+ components << path.to_sym
155
+ end
156
+
157
+ new(value, components)
158
+ end
159
+
160
+ #
161
+ # create_literal is similar to {#create} except that it always
162
+ # interprets the string as a literal attribute name, never as a
163
+ # slash-delimited path expression. There is no escaping or unescaping, even
164
+ # if the name contains literal '/' or '~' characters. Since an attribute
165
+ # name can contain any characters, this method always returns a valid
166
+ # Reference unless the name is empty.
167
+ #
168
+ # For example: Reference.create_literal("name") is exactly equivalent to
169
+ # Reference.create("name"). Reference.create_literal("a/b") is exactly
170
+ # equivalent to Reference.create("a/b") (since the syntax used by {#create}
171
+ # treats the whole string as a literal as long as it does not start with a
172
+ # slash), or to Reference.create("/a~1b").
173
+ #
174
+ # @param value [String, Symbol]
175
+ # @return [Reference]
176
+ #
177
+ def self.create_literal(value)
178
+ unless value.is_a?(String) || value.is_a?(Symbol)
179
+ return new(value, [], ERR_EMPTY)
180
+ end
181
+
182
+ value = value.to_s if value.is_a?(Symbol)
183
+
184
+ return new(value, [], ERR_EMPTY) if value.empty?
185
+ return new(value, [value.to_sym]) if value[0] != '/'
186
+
187
+ escaped = "/" + value.gsub('~', '~0').gsub('/', '~1')
188
+ new(escaped, [value.to_sym])
189
+ end
190
+
191
+ #
192
+ # Returns the number of path components in the Reference.
193
+ #
194
+ # For a simple attribute reference such as "name" with no leading slash,
195
+ # this returns 1.
196
+ #
197
+ # For an attribute reference with a leading slash, it is the number of
198
+ # slash-delimited path components after the initial slash. For instance,
199
+ # NewRef("/a/b").Depth() returns 2.
200
+ #
201
+ # @return [Integer]
202
+ #
203
+ def depth
204
+ @components.size
205
+ end
206
+
207
+ #
208
+ # Retrieves a single path component from the attribute reference.
209
+ #
210
+ # For a simple attribute reference such as "name" with no leading slash, if
211
+ # index is zero, {#component} returns the attribute name as a symbol.
212
+ #
213
+ # For an attribute reference with a leading slash, if index is non-negative
214
+ # and less than {#depth}, Component returns the path component as a symbol.
215
+ #
216
+ # If index is out of range, it returns nil.
217
+ #
218
+ # Reference.create("a").component(0) # returns "a"
219
+ # Reference.create("/a/b").component(1) # returns "b"
220
+ #
221
+ # @param index [Integer]
222
+ # @return [Symbol, nil]
223
+ #
224
+ def component(index)
225
+ return nil if index < 0 || index >= depth
226
+
227
+ @components[index]
228
+ end
229
+
230
+ #
231
+ # Performs unescaping of attribute reference path components:
232
+ #
233
+ # "~1" becomes "/"
234
+ # "~0" becomes "~"
235
+ # "~" followed by any character other than "0" or "1" is invalid
236
+ #
237
+ # This method returns an array of two values. The first element of the
238
+ # array is the path if unescaping was valid; otherwise, it will be nil. The
239
+ # second value is an error string, or nil if the unescaping was successful.
240
+ #
241
+ # @param path [String]
242
+ # @return [Array([String, nil], [String, nil])] Returns a fixed size array.
243
+ #
244
+ private_class_method def self.unescape_path(path)
245
+ # If there are no tildes then there's definitely nothing to do
246
+ return path, nil unless path.include? '~'
247
+
248
+ out = ""
249
+ i = 0
250
+ while i < path.size
251
+ if path[i] != "~"
252
+ out << path[i]
253
+ i += 1
254
+ next
255
+ end
256
+
257
+ return nil, ERR_INVALID_ESCAPE_SEQUENCE if i + 1 == path.size
258
+
259
+ case path[i + 1]
260
+ when '0'
261
+ out << "~"
262
+ when '1'
263
+ out << '/'
264
+ else
265
+ return nil, ERR_INVALID_ESCAPE_SEQUENCE
266
+ end
267
+
268
+ i += 2
269
+ end
270
+
271
+ [out, nil]
272
+ end
273
+ end
274
+ end
@@ -31,9 +31,9 @@ module LaunchDarkly
31
31
 
32
32
  def request_all_data()
33
33
  all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
34
- Impl::Model.make_all_store_data(all_data)
34
+ Impl::Model.make_all_store_data(all_data, @config.logger)
35
35
  end
36
-
36
+
37
37
  def stop
38
38
  begin
39
39
  @http_client.close
@@ -43,21 +43,19 @@ module LaunchDarkly
43
43
 
44
44
  private
45
45
 
46
- def request_single_item(kind, path)
47
- Impl::Model.deserialize(kind, make_request(path))
48
- end
49
-
50
46
  def make_request(path)
51
- uri = URI(@config.base_uri + path)
47
+ uri = URI(
48
+ Util.add_payload_filter_key(@config.base_uri + path, @config)
49
+ )
52
50
  headers = {}
53
51
  Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
54
52
  headers["Connection"] = "keep-alive"
55
53
  cached = @cache.read(uri)
56
- if !cached.nil?
54
+ unless cached.nil?
57
55
  headers["If-None-Match"] = cached.etag
58
56
  end
59
57
  response = @http_client.request("GET", uri, {
60
- headers: headers
58
+ headers: headers,
61
59
  })
62
60
  status = response.status.code
63
61
  # must fully read body for persistent connections
@@ -72,7 +70,7 @@ module LaunchDarkly
72
70
  end
73
71
  body = fix_encoding(body, response.headers["content-type"])
74
72
  etag = response.headers["etag"]
75
- @cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
73
+ @cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
76
74
  end
77
75
  body
78
76
  end
@@ -96,7 +94,7 @@ module LaunchDarkly
96
94
  break
97
95
  end
98
96
  end
99
- return [parts[0], charset]
97
+ [parts[0], charset]
100
98
  end
101
99
  end
102
100
  end