ldclient-rb 0.8.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +3 -3
- data/ext/mkrf_conf.rb +11 -0
- data/ldclient-rb.gemspec +3 -1
- data/lib/ldclient-rb.rb +6 -2
- data/lib/ldclient-rb/{store.rb → cache_store.rb} +0 -0
- data/lib/ldclient-rb/config.rb +27 -29
- data/lib/ldclient-rb/evaluation.rb +265 -0
- data/lib/ldclient-rb/events.rb +75 -0
- data/lib/ldclient-rb/feature_store.rb +60 -0
- data/lib/ldclient-rb/ldclient.rb +92 -303
- data/lib/ldclient-rb/polling.rb +56 -0
- data/lib/ldclient-rb/requestor.rb +56 -0
- data/lib/ldclient-rb/stream.rb +40 -140
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/feature.json +27 -58
- data/spec/ldclient_spec.rb +20 -226
- data/spec/stream_spec.rb +7 -63
- metadata +28 -7
- data/lib/ldclient-rb/settings.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e6c80abac366d30d919bd5fe4cc1e4457adc55f
|
4
|
+
data.tar.gz: 6c2a232ee5f1c33ef3f86f1511e37a3f8dd34579
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d9d45ecf32ff2580a8c716646f2b685b66c2786a0e66cd93eb914044bb7a4d67add695ff8d06308d1d51e618962587ddded79cd3287b58d65ef1eec5635b23f
|
7
|
+
data.tar.gz: 2b28b6e04bf7ecc6ad677ffad40421c06428b3c2b15334dd996dfe21f327694c8f27406d1ee057316ea6cbbd8b9ce84831f0165d27dd5eeb4d03bada8b96e195
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
|
+
|
5
|
+
## [2.0.0] - 2016-08-08
|
6
|
+
### Added
|
7
|
+
- Support for multivariate feature flags. In addition to booleans, feature flags can now return numbers, strings, dictionaries, or arrays via the `variation` method.
|
8
|
+
- New `all_flags` method returns all flag values for a specified user.
|
9
|
+
- If streaming is disabled, the client polls for feature flag changes. If streaming is disabled, the client will default to polling LaunchDarkly every second for updates. The poll interval is configurable via `poll_interval`.
|
10
|
+
- New `secure_mode_hash` function computes a hash suitable for the new LaunchDarkly JavaScript client's secure mode feature.
|
11
|
+
- Support for extremely large feature flags. When a large feature flag changes, the stream will include a directive to fetch the updated flag.
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- You can now initialize the LaunchDarkly client with an optional timeout (specified in seconds). This will block initialization until the client has finished bootstrapping and is able to serve feature flags.
|
15
|
+
- The streaming implementation (`StreamProcessor`) uses [Celluloid](https://github.com/celluloid/celluloid) under the hood instead of [EventMachine](https://github.com/eventmachine/eventmachine). The dependency on EventMachine has been removed.
|
16
|
+
- The `store` option has been renamed to `cache_store`.
|
17
|
+
- Offline mode can no longer be set dynamically. Instead, at configuration time, the `offline` parameter can be set to put the client in offline mode. It is no longer possible to dynamically change whether the client is online and offline (via `set_online` and `set_offline`). Call `offline?` to determine whether or not the client is offline.
|
18
|
+
- The `debug_stream` configuration option has been removed.
|
19
|
+
- The `log_timings` configuration option has been removed.
|
20
|
+
|
21
|
+
### Deprecated
|
22
|
+
- The `toggle` call has been deprecated in favor of `variation`.
|
23
|
+
|
24
|
+
### Removed
|
25
|
+
- `update_user_flag_setting` has been removed. To change user settings, use the LaunchDarkly REST API.
|
data/README.md
CHANGED
@@ -23,10 +23,10 @@ gem install ldclient-rb
|
|
23
23
|
require 'ldclient-rb'
|
24
24
|
```
|
25
25
|
|
26
|
-
2. Create a new LDClient with your
|
26
|
+
2. Create a new LDClient with your SDK key:
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
client = LaunchDarkly::LDClient.new("
|
29
|
+
client = LaunchDarkly::LDClient.new("your_sdk_key")
|
30
30
|
```
|
31
31
|
|
32
32
|
### Ruby on Rails
|
@@ -36,7 +36,7 @@ client = LaunchDarkly::LDClient.new("your_api_key")
|
|
36
36
|
1. Initialize the launchdarkly client in `config/initializers/launchdarkly.rb`:
|
37
37
|
|
38
38
|
```ruby
|
39
|
-
Rails.configuration.ld_client = LaunchDarkly::LDClient.new("
|
39
|
+
Rails.configuration.ld_client = LaunchDarkly::LDClient.new("your_sdk_key")
|
40
40
|
```
|
41
41
|
|
42
42
|
2. You may want to include a function in your ApplicationController
|
data/ext/mkrf_conf.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
|
4
|
+
# From http://stackoverflow.com/questions/5830835/how-to-add-openssl-dependency-to-gemspec
|
5
|
+
# the whole reason this file exists: to return an error if openssl
|
6
|
+
# isn't installed.
|
7
|
+
require 'openssl'
|
8
|
+
|
9
|
+
f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w") # create dummy rakefile to indicate success
|
10
|
+
f.write("task :default\n")
|
11
|
+
f.close
|
data/ldclient-rb.gemspec
CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
|
+
spec.extensions = 'ext/mkrf_conf.rb'
|
20
21
|
|
21
22
|
spec.add_development_dependency "bundler", "~> 1.7"
|
22
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
@@ -30,5 +31,6 @@ Gem::Specification.new do |spec|
|
|
30
31
|
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
|
31
32
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.0"
|
32
33
|
spec.add_runtime_dependency "hashdiff", "~> 0.2"
|
33
|
-
spec.add_runtime_dependency "ld-
|
34
|
+
spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.5"
|
35
|
+
spec.add_runtime_dependency "waitutil", "0.2"
|
34
36
|
end
|
data/lib/ldclient-rb.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require "ldclient-rb/version"
|
2
|
-
require "ldclient-rb/
|
2
|
+
require "ldclient-rb/evaluation"
|
3
3
|
require "ldclient-rb/ldclient"
|
4
|
-
require "ldclient-rb/
|
4
|
+
require "ldclient-rb/cache_store"
|
5
5
|
require "ldclient-rb/config"
|
6
6
|
require "ldclient-rb/newrelic"
|
7
7
|
require "ldclient-rb/stream"
|
8
|
+
require "ldclient-rb/polling"
|
9
|
+
require "ldclient-rb/events"
|
10
|
+
require "ldclient-rb/feature_store"
|
11
|
+
require "ldclient-rb/requestor"
|
File without changes
|
data/lib/ldclient-rb/config.rb
CHANGED
@@ -31,9 +31,15 @@ module LaunchDarkly
|
|
31
31
|
# connections in seconds.
|
32
32
|
# @option opts [Float] :connect_timeout (2) The connect timeout for network
|
33
33
|
# connections in seconds.
|
34
|
-
# @option opts [Object] :
|
34
|
+
# @option opts [Object] :cache_store A cache store for the Faraday HTTP caching
|
35
35
|
# library. Defaults to the Rails cache in a Rails environment, or a
|
36
36
|
# thread-safe in-memory store otherwise.
|
37
|
+
# @option opts [Boolean] :offline (false) Whether the client should be initialized in
|
38
|
+
# offline mode. In offline mode, default values are returned for all flags and no
|
39
|
+
# remote network requests are made.
|
40
|
+
# @option opts [Float] :poll_interval (30) The number of seconds between polls for flag updates
|
41
|
+
# if streaming is off.
|
42
|
+
# @option opts [Boolean] :stream (true) Whether or not the streaming API should be used to receive flag updates.
|
37
43
|
#
|
38
44
|
# @return [type] [description]
|
39
45
|
def initialize(opts = {})
|
@@ -42,14 +48,14 @@ module LaunchDarkly
|
|
42
48
|
@events_uri = (opts[:events_uri] || Config.default_events_uri).chomp("/")
|
43
49
|
@capacity = opts[:capacity] || Config.default_capacity
|
44
50
|
@logger = opts[:logger] || Config.default_logger
|
45
|
-
@
|
51
|
+
@cache_store = opts[:cache_store] || Config.default_cache_store
|
46
52
|
@flush_interval = opts[:flush_interval] || Config.default_flush_interval
|
47
53
|
@connect_timeout = opts[:connect_timeout] || Config.default_connect_timeout
|
48
54
|
@read_timeout = opts[:read_timeout] || Config.default_read_timeout
|
49
55
|
@feature_store = opts[:feature_store] || Config.default_feature_store
|
50
56
|
@stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
|
51
|
-
@
|
52
|
-
@
|
57
|
+
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
|
58
|
+
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > 1 ? opts[:poll_interval] : Config.default_poll_interval
|
53
59
|
end
|
54
60
|
|
55
61
|
#
|
@@ -79,13 +85,9 @@ module LaunchDarkly
|
|
79
85
|
@stream
|
80
86
|
end
|
81
87
|
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
#
|
86
|
-
# @return [Boolean] True if we should debug streaming mode
|
87
|
-
def debug_stream?
|
88
|
-
@debug_stream
|
88
|
+
# TODO docs
|
89
|
+
def offline?
|
90
|
+
@offline
|
89
91
|
end
|
90
92
|
|
91
93
|
#
|
@@ -95,6 +97,11 @@ module LaunchDarkly
|
|
95
97
|
# @return [Float] The configured number of seconds between flushes of the event buffer.
|
96
98
|
attr_reader :flush_interval
|
97
99
|
|
100
|
+
#
|
101
|
+
# The number of seconds to wait before polling for feature flag updates. This option has no
|
102
|
+
# effect unless streaming is disabled
|
103
|
+
attr_reader :poll_interval
|
104
|
+
|
98
105
|
#
|
99
106
|
# The configured logger for the LaunchDarkly client. The client library uses the log to
|
100
107
|
# print warning and error messages.
|
@@ -117,7 +124,7 @@ module LaunchDarkly
|
|
117
124
|
# 'read' and 'write' requests.
|
118
125
|
#
|
119
126
|
# @return [Object] The configured store for the Faraday HTTP caching library.
|
120
|
-
attr_reader :
|
127
|
+
attr_reader :cache_store
|
121
128
|
|
122
129
|
#
|
123
130
|
# The read timeout for network connections in seconds.
|
@@ -132,16 +139,7 @@ module LaunchDarkly
|
|
132
139
|
attr_reader :connect_timeout
|
133
140
|
|
134
141
|
#
|
135
|
-
#
|
136
|
-
# level on the configured logger. This can be very verbose.
|
137
|
-
#
|
138
|
-
# @return [Boolean] True if timing information should be logged.
|
139
|
-
def log_timings?
|
140
|
-
@log_timings
|
141
|
-
end
|
142
|
-
|
143
|
-
#
|
144
|
-
# TODO docs
|
142
|
+
# A store for feature flag configuration rules.
|
145
143
|
#
|
146
144
|
attr_reader :feature_store
|
147
145
|
|
@@ -170,7 +168,7 @@ module LaunchDarkly
|
|
170
168
|
"https://events.launchdarkly.com"
|
171
169
|
end
|
172
170
|
|
173
|
-
def self.
|
171
|
+
def self.default_cache_store
|
174
172
|
defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
|
175
173
|
end
|
176
174
|
|
@@ -190,20 +188,20 @@ module LaunchDarkly
|
|
190
188
|
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
|
191
189
|
end
|
192
190
|
|
193
|
-
def self.default_log_timings
|
194
|
-
false
|
195
|
-
end
|
196
|
-
|
197
191
|
def self.default_stream
|
198
192
|
true
|
199
193
|
end
|
200
194
|
|
201
195
|
def self.default_feature_store
|
202
|
-
|
196
|
+
InMemoryFeatureStore.new
|
203
197
|
end
|
204
198
|
|
205
|
-
def self.
|
199
|
+
def self.default_offline
|
206
200
|
false
|
207
201
|
end
|
202
|
+
|
203
|
+
def self.default_poll_interval
|
204
|
+
1
|
205
|
+
end
|
208
206
|
end
|
209
207
|
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
|
5
|
+
module Evaluation
|
6
|
+
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
7
|
+
|
8
|
+
OPERATORS = {
|
9
|
+
in:
|
10
|
+
lambda do |a, b|
|
11
|
+
a == b
|
12
|
+
end,
|
13
|
+
endsWith:
|
14
|
+
lambda do |a, b|
|
15
|
+
(a.is_a? String) && (a.end_with? b)
|
16
|
+
end,
|
17
|
+
startsWith:
|
18
|
+
lambda do |a, b|
|
19
|
+
(a.is_a? String) && (a.start_with? b)
|
20
|
+
end,
|
21
|
+
matches:
|
22
|
+
lambda do |a, b|
|
23
|
+
(b.is_a? String) && !(Regexp.new b).match(a).nil?
|
24
|
+
end,
|
25
|
+
contains:
|
26
|
+
lambda do |a, b|
|
27
|
+
(a.is_a? String) && (a.include? b)
|
28
|
+
end,
|
29
|
+
lessThan:
|
30
|
+
lambda do |a, b|
|
31
|
+
(a.is_a? Numeric) && (a < b)
|
32
|
+
end,
|
33
|
+
lessThanOrEqual:
|
34
|
+
lambda do |a, b|
|
35
|
+
(a.is_a? Numeric) && (a <= b)
|
36
|
+
end,
|
37
|
+
greaterThan:
|
38
|
+
lambda do |a, b|
|
39
|
+
(a.is_a? Numeric) && (a > b)
|
40
|
+
end,
|
41
|
+
greaterThanOrEqual:
|
42
|
+
lambda do |a, b|
|
43
|
+
(a.is_a? Numeric) && (a >= b)
|
44
|
+
end,
|
45
|
+
before:
|
46
|
+
lambda do |a, b|
|
47
|
+
begin
|
48
|
+
if a.is_a? String
|
49
|
+
a = DateTime.rfc3339(a).strftime('%Q').to_i
|
50
|
+
end
|
51
|
+
if b.is_a? String
|
52
|
+
b = DateTime.rfc3339(b).strftime('%Q').to_i
|
53
|
+
end
|
54
|
+
(a.is_a? Numeric) ? a < b : false
|
55
|
+
rescue => e
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end,
|
59
|
+
after:
|
60
|
+
lambda do |a, b|
|
61
|
+
begin
|
62
|
+
if a.is_a? String
|
63
|
+
a = DateTime.rfc3339(a).strftime('%Q').to_i
|
64
|
+
end
|
65
|
+
if b.is_a? String
|
66
|
+
b = DateTime.rfc3339(b).strftime('%Q').to_i
|
67
|
+
end
|
68
|
+
(a.is_a? Numeric) ? a > b : false
|
69
|
+
rescue => e
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
class EvaluationError < StandardError
|
76
|
+
end
|
77
|
+
|
78
|
+
# Evaluates a feature flag, returning a hash containing the evaluation result and any events
|
79
|
+
# generated during prerequisite evaluation. Raises EvaluationError if the flag is not well-formed
|
80
|
+
# Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
|
81
|
+
# In that case, the caller should return the default value.
|
82
|
+
def evaluate(flag, user, store)
|
83
|
+
if flag.nil?
|
84
|
+
raise EvaluationError, "Flag does not exist"
|
85
|
+
end
|
86
|
+
|
87
|
+
if user.nil? || user[:key].nil?
|
88
|
+
raise EvaluationError, "Invalid user"
|
89
|
+
end
|
90
|
+
|
91
|
+
events = []
|
92
|
+
|
93
|
+
if flag[:on]
|
94
|
+
res = eval_internal(flag, user, store, events)
|
95
|
+
|
96
|
+
return {value: res, events: events} if !res.nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
if !flag[:offVariation].nil? && flag[:offVariation] < flag[:variations].length
|
100
|
+
value = flag[:variations][flag[:offVariation]]
|
101
|
+
return {value: value, events: events}
|
102
|
+
end
|
103
|
+
|
104
|
+
{value: nil, events: events}
|
105
|
+
end
|
106
|
+
|
107
|
+
def eval_internal(flag, user, store, events)
|
108
|
+
failed_prereq = false
|
109
|
+
# Evaluate prerequisites, if any
|
110
|
+
if !flag[:prerequisites].nil?
|
111
|
+
flag[:prerequisites].each do |prerequisite|
|
112
|
+
|
113
|
+
prereq_flag = store.get(prerequisite[:key])
|
114
|
+
|
115
|
+
if prereq_flag.nil? || !prereq_flag[:on]
|
116
|
+
failed_prereq = true
|
117
|
+
else
|
118
|
+
begin
|
119
|
+
prereq_res = eval_internal(prereq_flag, user, store, events)
|
120
|
+
variation = get_variation(prereq_flag, prerequisite[:variation])
|
121
|
+
events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
|
122
|
+
if prereq_res.nil? || prereq_res!= variation
|
123
|
+
failed_prereq = true
|
124
|
+
end
|
125
|
+
rescue => exn
|
126
|
+
@config.logger.error("[LDClient] Error evaluating prerequisite: #{exn.inspect}")
|
127
|
+
failed_prereq = true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
if failed_prereq
|
133
|
+
return nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
# The prerequisites were satisfied.
|
137
|
+
# Now walk through the evaluation steps and get the correct
|
138
|
+
# variation index
|
139
|
+
eval_rules(flag, user)
|
140
|
+
end
|
141
|
+
|
142
|
+
def eval_rules(flag, user)
|
143
|
+
# Check user target matches
|
144
|
+
if !flag[:targets].nil?
|
145
|
+
flag[:targets].each do |target|
|
146
|
+
if !target[:values].nil?
|
147
|
+
target[:values].each do |value|
|
148
|
+
return get_variation(flag, target[:variation]) if value == user[:key]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Check custom rules
|
155
|
+
if !flag[:rules].nil?
|
156
|
+
flag[:rules].each do |rule|
|
157
|
+
return variation_for_user(rule, user, flag) if rule_match_user(rule, user)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Check the fallthrough rule
|
162
|
+
if !flag[:fallthrough].nil?
|
163
|
+
return variation_for_user(flag[:fallthrough], user, flag)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Not even the fallthrough matched-- return the off variation or default
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_variation(flag, index)
|
171
|
+
if index >= flag[:variations].length
|
172
|
+
raise EvaluationError, "Invalid variation index"
|
173
|
+
end
|
174
|
+
flag[:variations][index]
|
175
|
+
end
|
176
|
+
|
177
|
+
def rule_match_user(rule, user)
|
178
|
+
return false if !rule[:clauses]
|
179
|
+
|
180
|
+
rule[:clauses].each do |clause|
|
181
|
+
return false if !clause_match_user(clause, user)
|
182
|
+
end
|
183
|
+
|
184
|
+
return true
|
185
|
+
end
|
186
|
+
|
187
|
+
def clause_match_user(clause, user)
|
188
|
+
val = user_value(user, clause[:attribute])
|
189
|
+
return false if val.nil?
|
190
|
+
|
191
|
+
op = OPERATORS[clause[:op].to_sym]
|
192
|
+
|
193
|
+
if op.nil?
|
194
|
+
raise EvaluationError, "Unsupported operator #{clause[:op]} in evaluation"
|
195
|
+
end
|
196
|
+
|
197
|
+
if val.is_a? Enumerable
|
198
|
+
val.each do |v|
|
199
|
+
return maybe_negate(clause, true) if match_any(op, v, clause[:values])
|
200
|
+
end
|
201
|
+
return maybe_negate(clause, false)
|
202
|
+
end
|
203
|
+
|
204
|
+
maybe_negate(clause, match_any(op, val, clause[:values]))
|
205
|
+
end
|
206
|
+
|
207
|
+
def variation_for_user(rule, user, flag)
|
208
|
+
if !rule[:variation].nil? # fixed variation
|
209
|
+
return get_variation(flag, rule[:variation])
|
210
|
+
elsif !rule[:rollout].nil? # percentage rollout
|
211
|
+
rollout = rule[:rollout]
|
212
|
+
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
213
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
|
214
|
+
sum = 0;
|
215
|
+
rollout[:variations].each do |variate|
|
216
|
+
sum += variate[:weight].to_f / 100000.0
|
217
|
+
return get_variation(flag, variate[:variation]) if bucket < sum
|
218
|
+
end
|
219
|
+
nil
|
220
|
+
else # the rule isn't well-formed
|
221
|
+
raise EvaluationError, "Rule does not define a variation or rollout"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def bucket_user(user, key, bucket_by, salt)
|
226
|
+
return nil unless user[:key]
|
227
|
+
|
228
|
+
id_hash = user_value(user, bucket_by)
|
229
|
+
|
230
|
+
if user[:secondary]
|
231
|
+
id_hash += "." + user[:secondary]
|
232
|
+
end
|
233
|
+
|
234
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
235
|
+
|
236
|
+
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
237
|
+
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
238
|
+
end
|
239
|
+
|
240
|
+
def user_value(user, attribute)
|
241
|
+
attribute = attribute.to_sym
|
242
|
+
|
243
|
+
if BUILTINS.include? attribute
|
244
|
+
user[attribute]
|
245
|
+
elsif !user[:custom].nil?
|
246
|
+
user[:custom][attribute]
|
247
|
+
else
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def maybe_negate(clause, b)
|
253
|
+
clause[:negate] ? !b : b
|
254
|
+
end
|
255
|
+
|
256
|
+
def match_any(op, value, values)
|
257
|
+
values.each do |v|
|
258
|
+
return true if op.call(value, v)
|
259
|
+
end
|
260
|
+
return false
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|