launchdarkly-server-sdk 5.8.2 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +28 -122
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +7 -3
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +69 -42
- data/README.md +2 -2
- data/azure-pipelines.yml +1 -1
- data/launchdarkly-server-sdk.gemspec +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +1 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/ldclient.rb +14 -9
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +10 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +89 -46
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/spec/evaluation_spec.rb +0 -789
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
module Impl
|
4
|
+
module Model
|
5
|
+
# Abstraction of deserializing a feature flag or segment that was read from a data store or
|
6
|
+
# received from LaunchDarkly.
|
7
|
+
def self.deserialize(kind, json)
|
8
|
+
return nil if json.nil?
|
9
|
+
item = JSON.parse(json, symbolize_names: true)
|
10
|
+
postprocess_item_after_deserializing!(kind, item)
|
11
|
+
item
|
12
|
+
end
|
13
|
+
|
14
|
+
# Abstraction of serializing a feature flag or segment that will be written to a data store.
|
15
|
+
# Currently we just call to_json.
|
16
|
+
def self.serialize(kind, item)
|
17
|
+
item.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
21
|
+
def self.make_all_store_data(received_data)
|
22
|
+
flags = received_data[:flags]
|
23
|
+
postprocess_items_after_deserializing!(FEATURES, flags)
|
24
|
+
segments = received_data[:segments]
|
25
|
+
postprocess_items_after_deserializing!(SEGMENTS, segments)
|
26
|
+
{ FEATURES => flags, SEGMENTS => segments }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
|
30
|
+
# or read it from a persistent data store). This allows us to precompute some derived attributes that
|
31
|
+
# will never change during the lifetime of that item.
|
32
|
+
def self.postprocess_item_after_deserializing!(kind, item)
|
33
|
+
return if !item
|
34
|
+
# Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
|
35
|
+
# to the "kind" object or the item class.
|
36
|
+
if kind.eql? FEATURES
|
37
|
+
# For feature flags, we precompute all possible parameterized EvaluationReason instances.
|
38
|
+
prereqs = item[:prerequisites]
|
39
|
+
if !prereqs.nil?
|
40
|
+
prereqs.each do |prereq|
|
41
|
+
prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rules = item[:rules]
|
45
|
+
if !rules.nil?
|
46
|
+
rules.each_index do |i|
|
47
|
+
rule = rules[i]
|
48
|
+
rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.postprocess_items_after_deserializing!(kind, items_map)
|
55
|
+
return items_map if !items_map
|
56
|
+
items_map.each do |key, item|
|
57
|
+
postprocess_item_after_deserializing!(kind, item)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
# A simple thread safe generic unbounded resource pool abstraction
|
4
|
+
class UnboundedPool
|
5
|
+
def initialize(instance_creator, instance_destructor)
|
6
|
+
@pool = Array.new
|
7
|
+
@lock = Mutex.new
|
8
|
+
@instance_creator = instance_creator
|
9
|
+
@instance_destructor = instance_destructor
|
10
|
+
end
|
11
|
+
|
12
|
+
def acquire
|
13
|
+
@lock.synchronize {
|
14
|
+
if @pool.length == 0
|
15
|
+
@instance_creator.call()
|
16
|
+
else
|
17
|
+
@pool.pop()
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def release(instance)
|
23
|
+
@lock.synchronize { @pool.push(instance) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def dispose_all
|
27
|
+
@lock.synchronize {
|
28
|
+
@pool.map { |instance| @instance_destructor.call(instance) } if !@instance_destructor.nil?
|
29
|
+
@pool.clear()
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "ldclient-rb/impl/diagnostic_events"
|
2
|
+
require "ldclient-rb/impl/evaluator"
|
2
3
|
require "ldclient-rb/impl/event_factory"
|
3
4
|
require "ldclient-rb/impl/store_client_wrapper"
|
4
5
|
require "concurrent/atomics"
|
@@ -14,7 +15,6 @@ module LaunchDarkly
|
|
14
15
|
# should create a single client instance for the lifetime of the application.
|
15
16
|
#
|
16
17
|
class LDClient
|
17
|
-
include Evaluation
|
18
18
|
include Impl
|
19
19
|
#
|
20
20
|
# Creates a new client instance that connects to LaunchDarkly. A custom
|
@@ -57,6 +57,10 @@ module LaunchDarkly
|
|
57
57
|
updated_config.instance_variable_set(:@feature_store, @store)
|
58
58
|
@config = updated_config
|
59
59
|
|
60
|
+
get_flag = lambda { |key| @store.get(FEATURES, key) }
|
61
|
+
get_segment = lambda { |key| @store.get(SEGMENTS, key) }
|
62
|
+
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
|
63
|
+
|
60
64
|
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
|
61
65
|
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
|
62
66
|
else
|
@@ -333,12 +337,13 @@ module LaunchDarkly
|
|
333
337
|
next
|
334
338
|
end
|
335
339
|
begin
|
336
|
-
result = evaluate(f, user, @
|
340
|
+
result = @evaluator.evaluate(f, user, @event_factory_default)
|
337
341
|
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
|
338
342
|
details_only_if_tracked)
|
339
343
|
rescue => exn
|
340
344
|
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
|
341
|
-
state.add_flag(f, nil, nil, with_reasons ?
|
345
|
+
state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
|
346
|
+
details_only_if_tracked)
|
342
347
|
end
|
343
348
|
end
|
344
349
|
|
@@ -376,7 +381,7 @@ module LaunchDarkly
|
|
376
381
|
# @return [EvaluationDetail]
|
377
382
|
def evaluate_internal(key, user, default, event_factory)
|
378
383
|
if @config.offline?
|
379
|
-
return error_result(
|
384
|
+
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
380
385
|
end
|
381
386
|
|
382
387
|
if !initialized?
|
@@ -384,7 +389,7 @@ module LaunchDarkly
|
|
384
389
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
385
390
|
else
|
386
391
|
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
387
|
-
detail = error_result(
|
392
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
388
393
|
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
|
389
394
|
return detail
|
390
395
|
end
|
@@ -394,20 +399,20 @@ module LaunchDarkly
|
|
394
399
|
|
395
400
|
if feature.nil?
|
396
401
|
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
|
397
|
-
detail = error_result(
|
402
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
|
398
403
|
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
|
399
404
|
return detail
|
400
405
|
end
|
401
406
|
|
402
407
|
unless user
|
403
408
|
@config.logger.error { "[LDClient] Must specify user" }
|
404
|
-
detail = error_result(
|
409
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
405
410
|
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
406
411
|
return detail
|
407
412
|
end
|
408
413
|
|
409
414
|
begin
|
410
|
-
res = evaluate(feature, user,
|
415
|
+
res = @evaluator.evaluate(feature, user, event_factory)
|
411
416
|
if !res.events.nil?
|
412
417
|
res.events.each do |event|
|
413
418
|
@event_processor.add_event(event)
|
@@ -421,7 +426,7 @@ module LaunchDarkly
|
|
421
426
|
return detail
|
422
427
|
rescue => exn
|
423
428
|
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
|
424
|
-
detail = error_result(
|
429
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
|
425
430
|
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
426
431
|
return detail
|
427
432
|
end
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -37,10 +37,7 @@ module LaunchDarkly
|
|
37
37
|
def poll
|
38
38
|
all_data = @requestor.request_all_data
|
39
39
|
if all_data
|
40
|
-
@config.feature_store.init(
|
41
|
-
FEATURES => all_data[:flags],
|
42
|
-
SEGMENTS => all_data[:segments]
|
43
|
-
})
|
40
|
+
@config.feature_store.init(all_data)
|
44
41
|
if @initialized.make_true
|
45
42
|
@config.logger.info { "[LDClient] Polling connection initialized" }
|
46
43
|
@ready.set
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "uri"
|
6
|
+
require "http"
|
4
7
|
|
5
8
|
module LaunchDarkly
|
6
9
|
# @private
|
@@ -22,37 +25,44 @@ module LaunchDarkly
|
|
22
25
|
def initialize(sdk_key, config)
|
23
26
|
@sdk_key = sdk_key
|
24
27
|
@config = config
|
25
|
-
@
|
28
|
+
@http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
|
26
29
|
@cache = @config.cache_store
|
27
30
|
end
|
28
31
|
|
29
32
|
def request_all_data()
|
30
|
-
make_request("/sdk/latest-all")
|
33
|
+
all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
|
34
|
+
Impl::Model.make_all_store_data(all_data)
|
31
35
|
end
|
32
36
|
|
33
37
|
def stop
|
34
38
|
begin
|
35
|
-
@
|
39
|
+
@http_client.close
|
36
40
|
rescue
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
40
44
|
private
|
41
45
|
|
46
|
+
def request_single_item(kind, path)
|
47
|
+
Impl::Model.deserialize(kind, make_request(path))
|
48
|
+
end
|
49
|
+
|
42
50
|
def make_request(path)
|
43
|
-
@client.start if !@client.started?
|
44
51
|
uri = URI(@config.base_uri + path)
|
45
|
-
|
46
|
-
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v|
|
47
|
-
|
52
|
+
headers = {}
|
53
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
54
|
+
headers["Connection"] = "keep-alive"
|
48
55
|
cached = @cache.read(uri)
|
49
56
|
if !cached.nil?
|
50
|
-
|
57
|
+
headers["If-None-Match"] = cached.etag
|
51
58
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
59
|
+
response = @http_client.request("GET", uri, {
|
60
|
+
headers: headers
|
61
|
+
})
|
62
|
+
status = response.status.code
|
63
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers}\n\tbody: #{res.to_s}" }
|
64
|
+
# must fully read body for persistent connections
|
65
|
+
body = response.to_s
|
56
66
|
if status == 304 && !cached.nil?
|
57
67
|
body = cached.body
|
58
68
|
else
|
@@ -60,11 +70,11 @@ module LaunchDarkly
|
|
60
70
|
if status < 200 || status >= 300
|
61
71
|
raise UnexpectedResponseError.new(status)
|
62
72
|
end
|
63
|
-
body = fix_encoding(
|
64
|
-
etag =
|
73
|
+
body = fix_encoding(body, response.headers["content-type"])
|
74
|
+
etag = response.headers["etag"]
|
65
75
|
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
|
66
76
|
end
|
67
|
-
|
77
|
+
body
|
68
78
|
end
|
69
79
|
|
70
80
|
def fix_encoding(body, content_type)
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "ld-eventsource"
|
@@ -44,7 +46,8 @@ module LaunchDarkly
|
|
44
46
|
opts = {
|
45
47
|
headers: headers,
|
46
48
|
read_timeout: READ_TIMEOUT_SECONDS,
|
47
|
-
logger: @config.logger
|
49
|
+
logger: @config.logger,
|
50
|
+
socket_factory: @config.socket_factory
|
48
51
|
}
|
49
52
|
log_connection_started
|
50
53
|
@es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
|
@@ -82,10 +85,8 @@ module LaunchDarkly
|
|
82
85
|
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
83
86
|
if method == PUT
|
84
87
|
message = JSON.parse(message.data, symbolize_names: true)
|
85
|
-
|
86
|
-
|
87
|
-
SEGMENTS => message[:data][:segments]
|
88
|
-
})
|
88
|
+
all_data = Impl::Model.make_all_store_data(message[:data])
|
89
|
+
@feature_store.init(all_data)
|
89
90
|
@initialized.make_true
|
90
91
|
@config.logger.info { "[LDClient] Stream initialized" }
|
91
92
|
@ready.set
|
@@ -94,7 +95,9 @@ module LaunchDarkly
|
|
94
95
|
for kind in [FEATURES, SEGMENTS]
|
95
96
|
key = key_for_path(kind, data[:path])
|
96
97
|
if key
|
97
|
-
|
98
|
+
data = data[:data]
|
99
|
+
Impl::Model.postprocess_item_after_deserializing!(kind, data)
|
100
|
+
@feature_store.upsert(kind, data)
|
98
101
|
break
|
99
102
|
end
|
100
103
|
end
|
data/lib/ldclient-rb/util.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require "net/http"
|
2
1
|
require "uri"
|
2
|
+
require "http"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
5
|
# @private
|
@@ -18,14 +18,18 @@ module LaunchDarkly
|
|
18
18
|
end
|
19
19
|
ret
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def self.new_http_client(uri_s, config)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
http_client_options = {}
|
24
|
+
if config.socket_factory
|
25
|
+
http_client_options["socket_class"] = config.socket_factory
|
26
|
+
end
|
27
|
+
return HTTP::Client.new(http_client_options)
|
28
|
+
.timeout({
|
29
|
+
read: config.read_timeout,
|
30
|
+
connect: config.connect_timeout
|
31
|
+
})
|
32
|
+
.persistent(uri_s)
|
29
33
|
end
|
30
34
|
|
31
35
|
def self.log_exception(logger, message, exc)
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
describe "EvaluationDetail" do
|
5
|
+
subject { EvaluationDetail }
|
6
|
+
|
7
|
+
it "sets properties" do
|
8
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).value).to eq "x"
|
9
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).variation_index).to eq 0
|
10
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).reason).to eq EvaluationReason::off
|
11
|
+
end
|
12
|
+
|
13
|
+
it "checks parameter types" do
|
14
|
+
expect { EvaluationDetail.new(nil, nil, EvaluationReason::off) }.not_to raise_error
|
15
|
+
expect { EvaluationDetail.new(nil, 0, EvaluationReason::off) }.not_to raise_error
|
16
|
+
expect { EvaluationDetail.new(nil, "x", EvaluationReason::off) }.to raise_error(ArgumentError)
|
17
|
+
expect { EvaluationDetail.new(nil, 0, { kind: "OFF" }) }.to raise_error(ArgumentError)
|
18
|
+
expect { EvaluationDetail.new(nil, 0, nil) }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "equality test" do
|
22
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).to eq EvaluationDetail.new("x", 0, EvaluationReason::off)
|
23
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("y", 0, EvaluationReason::off)
|
24
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 1, EvaluationReason::off)
|
25
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 0, EvaluationReason::fallthrough)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "EvaluationReason" do
|
30
|
+
subject { EvaluationReason }
|
31
|
+
|
32
|
+
values = [
|
33
|
+
[ EvaluationReason::off, EvaluationReason::OFF, { "kind" => "OFF" }, "OFF", nil ],
|
34
|
+
[ EvaluationReason::fallthrough, EvaluationReason::FALLTHROUGH,
|
35
|
+
{ "kind" => "FALLTHROUGH" }, "FALLTHROUGH", nil ],
|
36
|
+
[ EvaluationReason::target_match, EvaluationReason::TARGET_MATCH,
|
37
|
+
{ "kind" => "TARGET_MATCH" }, "TARGET_MATCH", nil ],
|
38
|
+
[ EvaluationReason::rule_match(1, "x"), EvaluationReason::RULE_MATCH,
|
39
|
+
{ "kind" => "RULE_MATCH", "ruleIndex" => 1, "ruleId" => "x" }, "RULE_MATCH(1,x)",
|
40
|
+
[ EvaluationReason::rule_match(2, "x"), EvaluationReason::rule_match(1, "y") ] ],
|
41
|
+
[ EvaluationReason::prerequisite_failed("x"), EvaluationReason::PREREQUISITE_FAILED,
|
42
|
+
{ "kind" => "PREREQUISITE_FAILED", "prerequisiteKey" => "x" }, "PREREQUISITE_FAILED(x)" ],
|
43
|
+
[ EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND), EvaluationReason::ERROR,
|
44
|
+
{ "kind" => "ERROR", "errorKind" => "FLAG_NOT_FOUND" }, "ERROR(FLAG_NOT_FOUND)" ]
|
45
|
+
]
|
46
|
+
values.each_index do |i|
|
47
|
+
params = values[i]
|
48
|
+
reason = params[0]
|
49
|
+
kind = params[1]
|
50
|
+
json_rep = params[2]
|
51
|
+
brief_str = params[3]
|
52
|
+
unequal_values = params[4]
|
53
|
+
|
54
|
+
describe "reason #{reason.kind}" do
|
55
|
+
it "has correct kind" do
|
56
|
+
expect(reason.kind).to eq kind
|
57
|
+
end
|
58
|
+
|
59
|
+
it "equality to self" do
|
60
|
+
expect(reason).to eq reason
|
61
|
+
end
|
62
|
+
|
63
|
+
it "inequality to others" do
|
64
|
+
values.each_index do |j|
|
65
|
+
if i != j
|
66
|
+
expect(reason).not_to eq values[j][0]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
if !unequal_values.nil?
|
70
|
+
unequal_values.each do |v|
|
71
|
+
expect(reason).not_to eq v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "JSON representation" do
|
77
|
+
expect(JSON.parse(reason.as_json.to_json)).to eq json_rep
|
78
|
+
expect(JSON.parse(reason.to_json)).to eq json_rep
|
79
|
+
end
|
80
|
+
|
81
|
+
it "brief representation" do
|
82
|
+
expect(reason.inspect).to eq brief_str
|
83
|
+
expect(reason.to_s).to eq brief_str
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "reuses singleton reasons" do
|
89
|
+
expect(EvaluationReason::off).to be EvaluationReason::off
|
90
|
+
expect(EvaluationReason::fallthrough).to be EvaluationReason::fallthrough
|
91
|
+
expect(EvaluationReason::target_match).to be EvaluationReason::target_match
|
92
|
+
expect(EvaluationReason::rule_match(1, 'x')).not_to be EvaluationReason::rule_match(1, 'x')
|
93
|
+
expect(EvaluationReason::prerequisite_failed('x')).not_to be EvaluationReason::prerequisite_failed('x')
|
94
|
+
errors = [ EvaluationReason::ERROR_CLIENT_NOT_READY, EvaluationReason::ERROR_FLAG_NOT_FOUND,
|
95
|
+
EvaluationReason::ERROR_MALFORMED_FLAG, EvaluationReason::ERROR_USER_NOT_SPECIFIED, EvaluationReason::ERROR_EXCEPTION ]
|
96
|
+
errors.each do |e|
|
97
|
+
expect(EvaluationReason::error(e)).to be EvaluationReason::error(e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "supports [] with JSON property names" do
|
102
|
+
expect(EvaluationReason::off[:kind]).to eq "OFF"
|
103
|
+
expect(EvaluationReason::off[:ruleIndex]).to be nil
|
104
|
+
expect(EvaluationReason::off[:ruleId]).to be nil
|
105
|
+
expect(EvaluationReason::off[:prerequisiteKey]).to be nil
|
106
|
+
expect(EvaluationReason::off[:errorKind]).to be nil
|
107
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleIndex]).to eq 1
|
108
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleId]).to eq "x"
|
109
|
+
expect(EvaluationReason::prerequisite_failed("x")[:prerequisiteKey]).to eq "x"
|
110
|
+
expect(EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND)[:errorKind]).to eq "FLAG_NOT_FOUND"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "freezes string properties" do
|
114
|
+
rm = EvaluationReason::rule_match(1, "x")
|
115
|
+
expect { rm.rule_id.upcase! }.to raise_error(RuntimeError)
|
116
|
+
pf = EvaluationReason::prerequisite_failed("x")
|
117
|
+
expect { pf.prerequisite_key.upcase! }.to raise_error(RuntimeError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "checks parameter types" do
|
121
|
+
expect { EvaluationReason::rule_match(nil, "x") }.to raise_error(ArgumentError)
|
122
|
+
expect { EvaluationReason::rule_match(true, "x") }.to raise_error(ArgumentError)
|
123
|
+
expect { EvaluationReason::rule_match(1, nil) }.not_to raise_error # we allow nil rule_id for backward compatibility
|
124
|
+
expect { EvaluationReason::rule_match(1, 9) }.to raise_error(ArgumentError)
|
125
|
+
expect { EvaluationReason::prerequisite_failed(nil) }.to raise_error(ArgumentError)
|
126
|
+
expect { EvaluationReason::prerequisite_failed(9) }.to raise_error(ArgumentError)
|
127
|
+
expect { EvaluationReason::error(nil) }.to raise_error(ArgumentError)
|
128
|
+
expect { EvaluationReason::error(9) }.to raise_error(ArgumentError)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "does not allow direct access to constructor" do
|
132
|
+
expect { EvaluationReason.new(:off, nil, nil, nil, nil) }.to raise_error(NoMethodError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|