flagnetic 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +276 -0
- data/lib/flagnetic/backoff.rb +29 -0
- data/lib/flagnetic/changes.rb +88 -0
- data/lib/flagnetic/client.rb +609 -0
- data/lib/flagnetic/controller_helpers.rb +97 -0
- data/lib/flagnetic/detail.rb +73 -0
- data/lib/flagnetic/eval/evaluation.rb +382 -0
- data/lib/flagnetic/eval/instant.rb +71 -0
- data/lib/flagnetic/eval/murmur3.rb +70 -0
- data/lib/flagnetic/eval/operators.rb +225 -0
- data/lib/flagnetic/eval/regex.rb +647 -0
- data/lib/flagnetic/eval/ruleset.rb +267 -0
- data/lib/flagnetic/eval/semver.rb +80 -0
- data/lib/flagnetic/eval/types.rb +148 -0
- data/lib/flagnetic/eval/unicode_tables.rb +180 -0
- data/lib/flagnetic/eval.rb +38 -0
- data/lib/flagnetic/event_batch.rb +119 -0
- data/lib/flagnetic/exposures.rb +342 -0
- data/lib/flagnetic/fallback_chain.rb +230 -0
- data/lib/flagnetic/generations.rb +50 -0
- data/lib/flagnetic/global.rb +59 -0
- data/lib/flagnetic/local_overrides.rb +65 -0
- data/lib/flagnetic/logging.rb +62 -0
- data/lib/flagnetic/railtie.rb +146 -0
- data/lib/flagnetic/recorder.rb +135 -0
- data/lib/flagnetic/reporter.rb +94 -0
- data/lib/flagnetic/sse_parser.rb +94 -0
- data/lib/flagnetic/store.rb +34 -0
- data/lib/flagnetic/stream.rb +65 -0
- data/lib/flagnetic/summaries.rb +257 -0
- data/lib/flagnetic/testing.rb +160 -0
- data/lib/flagnetic/transport.rb +106 -0
- data/lib/flagnetic/uuid.rb +25 -0
- data/lib/flagnetic/version.rb +16 -0
- data/lib/flagnetic/worker.rb +270 -0
- data/lib/flagnetic.rb +34 -0
- metadata +79 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flagnetic
|
|
4
|
+
# Eval is the evaluator; see flagnetic/eval.rb.
|
|
5
|
+
module Eval
|
|
6
|
+
# Values reads context and condition values as the evaluator compares
|
|
7
|
+
# them.
|
|
8
|
+
module Values
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# number reads a context or condition value as a number: an Integer,
|
|
12
|
+
# Float, Rational or other real Numeric, never a String ("5" is
|
|
13
|
+
# wrong-typed). NaN and the infinities are not numbers.
|
|
14
|
+
def number(value)
|
|
15
|
+
return nil unless value.is_a?(Numeric) && value.real?
|
|
16
|
+
|
|
17
|
+
f = value.to_f
|
|
18
|
+
f.nan? || f.infinite? ? nil : f
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# utf8 returns a string whose bytes compare as UTF-8, whatever encoding
|
|
22
|
+
# the caller's string carries. Strings compare byte-wise.
|
|
23
|
+
def utf8(str)
|
|
24
|
+
str.encoding == Encoding::UTF_8 ? str : str.dup.force_encoding(Encoding::UTF_8)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# strings reads a list whose every element is a string.
|
|
28
|
+
def strings(value)
|
|
29
|
+
return nil unless value.is_a?(Array) && value.all?(String)
|
|
30
|
+
|
|
31
|
+
value.map { |s| utf8(s) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
private_constant :Values
|
|
35
|
+
|
|
36
|
+
# Operators compiles an attribute condition into a matcher of a context
|
|
37
|
+
# value. The matcher receives nil for an absent attribute. An absent,
|
|
38
|
+
# null or wrong-typed value makes every operator false except
|
|
39
|
+
# not_exists, which is then true; there is no coercion.
|
|
40
|
+
module Operators
|
|
41
|
+
# Error is an operator undefined for the attribute type, a condition
|
|
42
|
+
# value of the wrong JSON type, or a pattern outside RE2.
|
|
43
|
+
class Error < StandardError; end
|
|
44
|
+
|
|
45
|
+
EXTRACT = {
|
|
46
|
+
"string" => ->(v) { Values.utf8(v) if v.is_a?(String) },
|
|
47
|
+
"number" => ->(v) { Values.number(v) },
|
|
48
|
+
"boolean" => ->(v) { v if [true, false].include?(v) },
|
|
49
|
+
"semver" => ->(v) { Semver.parse(v) if v.is_a?(String) },
|
|
50
|
+
"date" => ->(v) { Instant.from_context(v) },
|
|
51
|
+
"list" => ->(v) { Values.strings(v) }
|
|
52
|
+
}.freeze
|
|
53
|
+
COMPARISONS = %w[eq neq lt lte gt gte].freeze
|
|
54
|
+
|
|
55
|
+
# KNOWN is every operator this evaluator knows, for any attribute
|
|
56
|
+
# type. One outside it is an unsupported construct, typically sent by
|
|
57
|
+
# a server newer than the evaluator; a known operator applied to a
|
|
58
|
+
# type it is not defined for is a defect of the ruleset instead.
|
|
59
|
+
KNOWN = %w[eq neq lt lte gt gte in not_in contains not_contains starts_with ends_with matches before after
|
|
60
|
+
contains_any contains_all is_empty exists not_exists].freeze
|
|
61
|
+
private_constant :EXTRACT, :COMPARISONS
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
# check returns why this evaluator cannot apply op with value to an
|
|
66
|
+
# attribute of type, or nil when it can: op must be defined for type
|
|
67
|
+
# and value must have the JSON type op needs. It does not reject what
|
|
68
|
+
# the evaluator treats as a false condition, such as an unparseable
|
|
69
|
+
# semantic version or date in the value.
|
|
70
|
+
def check(type, op, value)
|
|
71
|
+
compile(type, op, value)
|
|
72
|
+
nil
|
|
73
|
+
rescue Error => e
|
|
74
|
+
e.message
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def compile(type, op, value)
|
|
78
|
+
extract = EXTRACT[type]
|
|
79
|
+
raise Error, "unknown attribute type #{type.inspect}" unless extract
|
|
80
|
+
|
|
81
|
+
case op
|
|
82
|
+
when "exists" then return ->(v) { !extract.call(v).nil? }
|
|
83
|
+
when "not_exists" then return ->(v) { extract.call(v).nil? }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test = send(:"#{type}_test", op, value)
|
|
87
|
+
raise Error, "operator #{op.inspect} is not defined for type #{type}" unless test
|
|
88
|
+
|
|
89
|
+
lambda do |v|
|
|
90
|
+
x = extract.call(v)
|
|
91
|
+
!x.nil? && test.call(x)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def string_test(op, value)
|
|
96
|
+
case op
|
|
97
|
+
when "eq", "neq", "contains", "not_contains", "starts_with", "ends_with"
|
|
98
|
+
want = string_value(op, value)
|
|
99
|
+
want_bytes = want.b
|
|
100
|
+
f = case op
|
|
101
|
+
when "eq", "neq" then ->(s) { s == want }
|
|
102
|
+
when "contains", "not_contains" then ->(s) { s.b.include?(want_bytes) }
|
|
103
|
+
when "starts_with" then ->(s) { s.b.start_with?(want_bytes) }
|
|
104
|
+
else ->(s) { s.b.end_with?(want_bytes) }
|
|
105
|
+
end
|
|
106
|
+
negate(f, %w[neq not_contains].include?(op))
|
|
107
|
+
when "in", "not_in"
|
|
108
|
+
set = strings_value(op, value)
|
|
109
|
+
negate(->(s) { set.include?(s) }, op == "not_in")
|
|
110
|
+
when "matches"
|
|
111
|
+
pattern = regex(string_value(op, value))
|
|
112
|
+
->(s) { pattern.match?(s) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def number_test(op, value)
|
|
117
|
+
case op
|
|
118
|
+
when *COMPARISONS
|
|
119
|
+
want = json_number(value)
|
|
120
|
+
raise Error, "operator #{op.inspect} needs a number value" unless want
|
|
121
|
+
|
|
122
|
+
comparison(op) { |x| x <=> want }
|
|
123
|
+
when "in", "not_in"
|
|
124
|
+
set = value.map { |e| json_number(e) } if value.is_a?(Array)
|
|
125
|
+
raise Error, "operator #{op.inspect} needs a list of numbers" unless set&.none?(nil)
|
|
126
|
+
|
|
127
|
+
negate(->(x) { set.include?(x) }, op == "not_in")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def boolean_test(op, value)
|
|
132
|
+
return unless op == "eq"
|
|
133
|
+
raise Error, "operator #{op.inspect} needs a boolean value" unless [true, false].include?(value)
|
|
134
|
+
|
|
135
|
+
->(x) { x == value }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def semver_test(op, value)
|
|
139
|
+
case op
|
|
140
|
+
when *COMPARISONS
|
|
141
|
+
want = Semver.parse(string_value(op, value))
|
|
142
|
+
# An unparseable version makes the condition false.
|
|
143
|
+
return ->(_) { false } unless want
|
|
144
|
+
|
|
145
|
+
comparison(op) { |x| x <=> want }
|
|
146
|
+
when "in", "not_in"
|
|
147
|
+
set = strings_value(op, value).map { |s| Semver.parse(s) }
|
|
148
|
+
# An unparseable version makes the condition false, as it does for
|
|
149
|
+
# eq and neq: skipping it instead would make not_in ["garbage"]
|
|
150
|
+
# match every version.
|
|
151
|
+
return ->(_) { false } if set.any?(&:nil?)
|
|
152
|
+
|
|
153
|
+
negate(->(x) { set.any? { |v| (x <=> v).zero? } }, op == "not_in")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def date_test(op, value)
|
|
158
|
+
return unless %w[before after].include?(op)
|
|
159
|
+
raise Error, "operator #{op.inspect} needs an RFC 3339 string" unless value.is_a?(String)
|
|
160
|
+
|
|
161
|
+
want = Instant.parse_rfc3339(value)
|
|
162
|
+
# An unparseable date makes the condition false.
|
|
163
|
+
return ->(_) { false } unless want
|
|
164
|
+
|
|
165
|
+
op == "before" ? ->(x) { x < want } : ->(x) { x > want }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def list_test(op, value)
|
|
169
|
+
case op
|
|
170
|
+
when "contains", "not_contains"
|
|
171
|
+
want = string_value(op, value)
|
|
172
|
+
negate(->(x) { x.include?(want) }, op == "not_contains")
|
|
173
|
+
when "contains_any"
|
|
174
|
+
wants = strings_value(op, value)
|
|
175
|
+
->(x) { wants.any? { |w| x.include?(w) } }
|
|
176
|
+
when "contains_all"
|
|
177
|
+
wants = strings_value(op, value)
|
|
178
|
+
->(x) { wants.all? { |w| x.include?(w) } }
|
|
179
|
+
when "is_empty"
|
|
180
|
+
lambda(&:empty?)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# json_number reads a condition value as a number: only what JSON
|
|
185
|
+
# decodes to one.
|
|
186
|
+
def json_number(value)
|
|
187
|
+
Values.number(value) if value.is_a?(Integer) || value.is_a?(Float)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def string_value(op, value)
|
|
191
|
+
raise Error, "operator #{op.inspect} needs a string value" unless value.is_a?(String)
|
|
192
|
+
|
|
193
|
+
Values.utf8(value)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def strings_value(op, value)
|
|
197
|
+
Values.strings(value) or raise Error, "operator #{op.inspect} needs a list of strings"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def regex(pattern)
|
|
201
|
+
Regex.compile(pattern)
|
|
202
|
+
rescue Regex::Error => e
|
|
203
|
+
raise Error, "operator \"matches\": #{e.message}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def negate(test, negated)
|
|
207
|
+
negated ? ->(x) { !test.call(x) } : test
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# comparison builds eq, neq, lt, lte, gt or gte over a three-way
|
|
211
|
+
# comparison of the context value against the condition value.
|
|
212
|
+
def comparison(op, &cmp)
|
|
213
|
+
case op
|
|
214
|
+
when "eq" then ->(x) { cmp.call(x).zero? }
|
|
215
|
+
when "neq" then ->(x) { !cmp.call(x).zero? }
|
|
216
|
+
when "lt" then ->(x) { cmp.call(x).negative? }
|
|
217
|
+
when "lte" then ->(x) { !cmp.call(x).positive? }
|
|
218
|
+
when "gt" then ->(x) { cmp.call(x).positive? }
|
|
219
|
+
else ->(x) { !cmp.call(x).negative? }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
private_constant :Operators
|
|
224
|
+
end
|
|
225
|
+
end
|