launchdarkly-server-sdk 6.3.0 → 8.0.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 (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