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.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +7 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- 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
|
-
|
20
|
+
unless @semaphore.try_acquire(1)
|
21
21
|
return
|
22
22
|
end
|
23
23
|
@pool.post do
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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))
|
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
|
-
|
97
|
+
[parts[0], charset]
|
100
98
|
end
|
101
99
|
end
|
102
100
|
end
|