launchdarkly-server-sdk 5.8.2 → 6.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. 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
@@ -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, @store, @config.logger, @event_factory_default)
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 ? { kind: 'ERROR', errorKind: 'EXCEPTION' } : nil, details_only_if_tracked)
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('CLIENT_NOT_READY', default)
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('CLIENT_NOT_READY', default)
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('FLAG_NOT_FOUND', default)
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('USER_NOT_SPECIFIED', default)
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, @store, @config.logger, event_factory)
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('EXCEPTION', default)
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
@@ -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
- @client = Util.new_http_client(@config.base_uri, @config)
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
- @client.finish
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
- req = Net::HTTP::Get.new(uri)
46
- Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
47
- req["Connection"] = "keep-alive"
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
- req["If-None-Match"] = cached.etag
57
+ headers["If-None-Match"] = cached.etag
51
58
  end
52
- res = @client.request(req)
53
- status = res.code.to_i
54
- @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
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(res.body, res["content-type"])
64
- etag = res["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
- JSON.parse(body, symbolize_names: true)
77
+ body
68
78
  end
69
79
 
70
80
  def fix_encoding(body, content_type)
@@ -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
- @feature_store.init({
86
- FEATURES => message[:data][:flags],
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
- @feature_store.upsert(kind, data[:data])
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
@@ -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
- uri = URI(uri_s)
24
- client = Net::HTTP.new(uri.hostname, uri.port)
25
- client.use_ssl = true if uri.scheme == "https"
26
- client.open_timeout = config.connect_timeout
27
- client.read_timeout = config.read_timeout
28
- client
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)
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.8.2"
2
+ VERSION = "6.0.0"
3
3
  end
@@ -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