pug-sh 0.0.1
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 +21 -0
- data/README.md +247 -0
- data/lib/pug/batcher.rb +384 -0
- data/lib/pug/client.rb +248 -0
- data/lib/pug/connect.rb +291 -0
- data/lib/pug/countries.rb +19 -0
- data/lib/pug/errors.rb +16 -0
- data/lib/pug/event_builder.rb +522 -0
- data/lib/pug/gen/buf/validate/validate_pb.rb +59 -0
- data/lib/pug/gen/common/events/v1/api_events_pb.rb +26 -0
- data/lib/pug/gen/common/events/v1/app_events_pb.rb +33 -0
- data/lib/pug/gen/common/events/v1/auth_events_pb.rb +32 -0
- data/lib/pug/gen/common/events/v1/billing_events_pb.rb +41 -0
- data/lib/pug/gen/common/events/v1/chat_events_pb.rb +56 -0
- data/lib/pug/gen/common/events/v1/commerce_events_pb.rb +38 -0
- data/lib/pug/gen/common/events/v1/discovery_events_pb.rb +31 -0
- data/lib/pug/gen/common/events/v1/error_events_pb.rb +26 -0
- data/lib/pug/gen/common/events/v1/file_events_pb.rb +29 -0
- data/lib/pug/gen/common/events/v1/form_events_pb.rb +27 -0
- data/lib/pug/gen/common/events/v1/integration_events_pb.rb +27 -0
- data/lib/pug/gen/common/events/v1/invitation_events_pb.rb +26 -0
- data/lib/pug/gen/common/events/v1/media_events_pb.rb +36 -0
- data/lib/pug/gen/common/events/v1/navigation_events_pb.rb +31 -0
- data/lib/pug/gen/common/events/v1/notification_events_pb.rb +28 -0
- data/lib/pug/gen/common/events/v1/options_pb.rb +25 -0
- data/lib/pug/gen/common/events/v1/social_events_pb.rb +25 -0
- data/lib/pug/gen/common/events/v1/support_events_pb.rb +33 -0
- data/lib/pug/gen/common/events/v1/workspace_events_pb.rb +30 -0
- data/lib/pug/gen/common/v1/filter_schema_pb.rb +29 -0
- data/lib/pug/gen/common/v1/filters_pb.rb +27 -0
- data/lib/pug/gen/common/v1/property_value_pb.rb +24 -0
- data/lib/pug/gen/common/v1/time_pb.rb +25 -0
- data/lib/pug/gen/sdk/devices/v1/devices_pb.rb +35 -0
- data/lib/pug/gen/sdk/events/v1/events_pb.rb +30 -0
- data/lib/pug/gen/sdk/profiles/v1/profiles_pb.rb +28 -0
- data/lib/pug/gen/shared/activity/v1/activity_pb.rb +46 -0
- data/lib/pug/gen/shared/insights/v1/insights_pb.rb +69 -0
- data/lib/pug/gen/shared/profiles/v1/profiles_pb.rb +43 -0
- data/lib/pug/log.rb +35 -0
- data/lib/pug/proto.rb +22 -0
- data/lib/pug/reads.rb +118 -0
- data/lib/pug/rpc.rb +73 -0
- data/lib/pug/validator.rb +475 -0
- data/lib/pug/version.rb +5 -0
- data/lib/pug/well_known.rb +33 -0
- data/lib/pug-sh.rb +3 -0
- data/lib/pug.rb +5 -0
- metadata +122 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "proto"
|
|
5
|
+
|
|
6
|
+
module Pug
|
|
7
|
+
# The subset of protovalidate the SDK's schemas use, driven by the buf.validate rules in the
|
|
8
|
+
# descriptors' options, since Ruby has no protovalidate. A rule it can't evaluate is skipped
|
|
9
|
+
# and reported by unsupported_rules, which the tests require to be empty for every schema the
|
|
10
|
+
# SDK validates: a proto sync that adopts a new rule fails CI instead of going unenforced.
|
|
11
|
+
#
|
|
12
|
+
# Each message's rules are compiled once into plain closures, since reading them back off the
|
|
13
|
+
# protobuf rule messages on every call costs more than the checks themselves.
|
|
14
|
+
module Validator
|
|
15
|
+
Violation = Struct.new(:path, :rule_id, :message) do
|
|
16
|
+
def initialize(path, rule_id, message)
|
|
17
|
+
super(path.to_s, rule_id, message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
path.empty? ? "#{message} [#{rule_id}]" : "#{path}: #{message} [#{rule_id}]"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
FieldPlan = Struct.new(:field, :name, :presence, :required, :ignore, :shape, :subtype, :nested, :convert,
|
|
26
|
+
:checks, :unsupported)
|
|
27
|
+
MessagePlan = Struct.new(:name, :fields, :oneofs, :checks, :unsupported, :active, :checked_fields)
|
|
28
|
+
|
|
29
|
+
# A map entry's path, rendered only if a violation needs it.
|
|
30
|
+
MapEntryPath = Struct.new(:parent, :name, :key) do
|
|
31
|
+
def empty? = false
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
"#{Validator.join(parent, name)}[#{key.inspect}]"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
MESSAGE_RULES = Proto.extension("buf.validate.message")
|
|
39
|
+
ONEOF_RULES = Proto.extension("buf.validate.oneof")
|
|
40
|
+
FIELD_RULES = Proto.extension("buf.validate.field")
|
|
41
|
+
|
|
42
|
+
NUMERIC = %i[float double int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64].freeze
|
|
43
|
+
RANGE = %w[const lt lte gt gte in not_in example].freeze
|
|
44
|
+
SUPPORTED = {
|
|
45
|
+
string: %w[const len min_len max_len len_bytes min_bytes max_bytes pattern prefix suffix contains
|
|
46
|
+
not_contains in not_in uuid tuuid example],
|
|
47
|
+
bool: %w[const example],
|
|
48
|
+
duration: RANGE,
|
|
49
|
+
**NUMERIC.to_h { |type| [type, %i[float double].include?(type) ? [*RANGE, "finite"] : RANGE] }
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
UUID = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
53
|
+
TUUID = /\A\h{32}\z/
|
|
54
|
+
NANOS = 1_000_000_000
|
|
55
|
+
BOUNDS = { lt: "less than", lte: "less than or equal to", gt: "greater than",
|
|
56
|
+
gte: "greater than or equal to" }.freeze
|
|
57
|
+
|
|
58
|
+
# Ruby has no CEL interpreter, so each expression the schemas use is implemented by hand and
|
|
59
|
+
# keyed by its exact text: an expression changed upstream stops matching and shows up in
|
|
60
|
+
# unsupported_rules rather than being enforced with stale semantics.
|
|
61
|
+
CEL = {
|
|
62
|
+
"this.auto_properties.all(key, key.startsWith('$'))" =>
|
|
63
|
+
->(event) { event.auto_properties.keys.all? { |key| key.start_with?("$") } },
|
|
64
|
+
"this.custom_properties.all(key, !key.startsWith('$'))" =>
|
|
65
|
+
->(event) { event.custom_properties.keys.none? { |key| key.start_with?("$") } },
|
|
66
|
+
"this.cookieless || (this.distinct_id != '' && this.session_id != '')" =>
|
|
67
|
+
->(event) { event.cookieless || (event.distinct_id != "" && event.session_id != "") },
|
|
68
|
+
"!this.startsWith('cookieless-')" =>
|
|
69
|
+
->(value) { !value.start_with?("cookieless-") }
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
@plans = {}.freeze
|
|
73
|
+
@lock = Mutex.new
|
|
74
|
+
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
def validate(message)
|
|
78
|
+
violations = []
|
|
79
|
+
check_message(message, plan(message.class.descriptor), "", violations)
|
|
80
|
+
violations
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format(violations)
|
|
84
|
+
violations.join(", ")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unsupported_rules(descriptor)
|
|
88
|
+
seen = {}
|
|
89
|
+
pending = [plan(descriptor)]
|
|
90
|
+
found = []
|
|
91
|
+
while (current = pending.pop)
|
|
92
|
+
next if seen[current.name]
|
|
93
|
+
|
|
94
|
+
seen[current.name] = true
|
|
95
|
+
found.concat(current.unsupported)
|
|
96
|
+
current.fields.each do |field_plan|
|
|
97
|
+
found.concat(field_plan.unsupported)
|
|
98
|
+
pending << plan(field_plan.subtype) if field_plan.subtype
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
found
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Plans are built under the lock and published copy-on-write, so readers never lock.
|
|
105
|
+
def plan(descriptor)
|
|
106
|
+
@plans[descriptor.name] || @lock.synchronize { build_plans(descriptor) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def check_message(message, message_plan, path, out)
|
|
110
|
+
return unless message_plan.active
|
|
111
|
+
|
|
112
|
+
message_plan.checked_fields.each { |field_plan| check_field(message, field_plan, path, out) }
|
|
113
|
+
message_plan.oneofs.each do |name|
|
|
114
|
+
next unless message.public_send(name).nil?
|
|
115
|
+
|
|
116
|
+
out << Violation.new(join(path, name), "required", "exactly one field is required in oneof")
|
|
117
|
+
end
|
|
118
|
+
message_plan.checks.each do |check|
|
|
119
|
+
failure = check.call(message)
|
|
120
|
+
out << Violation.new(path, *failure) if failure
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def check_field(message, field_plan, path, out)
|
|
125
|
+
field = field_plan.field
|
|
126
|
+
present = case field_plan.presence
|
|
127
|
+
when :explicit then field.has?(message)
|
|
128
|
+
when :repeated then field.get(message).length.positive?
|
|
129
|
+
else field.get(message) != field.default
|
|
130
|
+
end
|
|
131
|
+
if field_plan.required && !present
|
|
132
|
+
out << Violation.new(join(path, field_plan.name), "required", "value is required")
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
return unless present || (field_plan.presence == :implicit && field_plan.ignore != :IGNORE_IF_ZERO_VALUE)
|
|
136
|
+
|
|
137
|
+
value = field.get(message)
|
|
138
|
+
nested = field_plan.nested && plan(field_plan.nested)
|
|
139
|
+
# A list or map is planned only for required, then, which its presence has satisfied.
|
|
140
|
+
return if nested.nil? && field_plan.shape != :single
|
|
141
|
+
|
|
142
|
+
case field_plan.shape
|
|
143
|
+
when :map
|
|
144
|
+
value.each { |key, item| check_message(item, nested, MapEntryPath.new(path, field_plan.name, key), out) }
|
|
145
|
+
when :list
|
|
146
|
+
value.each_with_index { |item, i| check_message(item, nested, "#{join(path, field_plan.name)}[#{i}]", out) }
|
|
147
|
+
else
|
|
148
|
+
run_checks(field_plan, value, path, out) unless field_plan.checks.empty?
|
|
149
|
+
check_message(value, nested, join(path, field_plan.name), out) if nested
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def run_checks(field_plan, value, path, out)
|
|
154
|
+
value = field_plan.convert.call(value) if field_plan.convert
|
|
155
|
+
field_plan.checks.each do |check|
|
|
156
|
+
failure = check.call(value)
|
|
157
|
+
out << Violation.new(join(path, field_plan.name), *failure) if failure
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def set?(message, field)
|
|
162
|
+
if field.label == :repeated
|
|
163
|
+
field.get(message).length.positive?
|
|
164
|
+
elsif field.has_presence?
|
|
165
|
+
field.has?(message)
|
|
166
|
+
else
|
|
167
|
+
field.get(message) != field.default
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def join(path, name)
|
|
172
|
+
path.empty? ? name : "#{path}.#{name}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_plans(root)
|
|
176
|
+
return @plans[root.name] if @plans.key?(root.name)
|
|
177
|
+
|
|
178
|
+
built = {}
|
|
179
|
+
pending = [root]
|
|
180
|
+
while (descriptor = pending.pop)
|
|
181
|
+
next if @plans.key?(descriptor.name) || built.key?(descriptor.name)
|
|
182
|
+
|
|
183
|
+
built[descriptor.name] = build_plan(descriptor)
|
|
184
|
+
built[descriptor.name].fields.each { |field_plan| pending << field_plan.subtype if field_plan.subtype }
|
|
185
|
+
end
|
|
186
|
+
mark_active(built)
|
|
187
|
+
@plans = @plans.merge(built.transform_values(&:freeze)).freeze
|
|
188
|
+
@plans[root.name]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def build_plan(descriptor)
|
|
192
|
+
rules = MESSAGE_RULES.get(descriptor.options)
|
|
193
|
+
cel = rules&.cel.to_a
|
|
194
|
+
unsupported = cel.reject { |rule| CEL.key?(rule.expression) }.map { |rule| "#{descriptor.name}: cel #{rule.id}" }
|
|
195
|
+
unsupported << "#{descriptor.name}: message oneof rule" if rules&.oneof&.any?
|
|
196
|
+
unsupported << "#{descriptor.name}: cel_expression" if rules&.cel_expression&.any?
|
|
197
|
+
oneofs = []
|
|
198
|
+
descriptor.each_oneof do |oneof|
|
|
199
|
+
oneofs << oneof.name if ONEOF_RULES.get(oneof.options)&.required
|
|
200
|
+
end
|
|
201
|
+
fields = descriptor.map { |field| build_field_plan(descriptor, field) }
|
|
202
|
+
MessagePlan.new(descriptor.name, fields.freeze, oneofs.freeze, cel.filter_map { |rule| cel_check(rule) }.freeze,
|
|
203
|
+
unsupported.freeze, false, nil)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_field_plan(descriptor, field)
|
|
207
|
+
rules = FIELD_RULES.get(field.options)
|
|
208
|
+
shape = if field.label != :repeated then :single
|
|
209
|
+
elsif field.type == :message && field.subtype.options.map_entry then :map
|
|
210
|
+
else :list
|
|
211
|
+
end
|
|
212
|
+
subtype = if shape == :map
|
|
213
|
+
value = field.subtype.lookup("value")
|
|
214
|
+
value.subtype if value.type == :message
|
|
215
|
+
elsif field.type == :message
|
|
216
|
+
field.subtype
|
|
217
|
+
end
|
|
218
|
+
unsupported = unsupported_field_rules(rules, shape).map { |rule| "#{descriptor.name}.#{field.name}: #{rule}" }
|
|
219
|
+
checks = shape == :single && rules ? field_checks(rules) : []
|
|
220
|
+
convert = method(:seconds) if rules&.type == :duration
|
|
221
|
+
presence = if field.label == :repeated then :repeated
|
|
222
|
+
elsif field.has_presence? then :explicit
|
|
223
|
+
else :implicit
|
|
224
|
+
end
|
|
225
|
+
FieldPlan.new(field, field.name, presence, rules&.required || false, rules&.ignore, shape, subtype, nil, convert,
|
|
226
|
+
checks.freeze, unsupported.freeze)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def unsupported_field_rules(rules, shape)
|
|
230
|
+
return [] if rules.nil?
|
|
231
|
+
|
|
232
|
+
found = rules.cel.reject { |rule| shape == :single && CEL.key?(rule.expression) }.map { |rule| "cel #{rule.id}" }
|
|
233
|
+
found << "cel_expression" if rules.cel_expression.any?
|
|
234
|
+
type = rules.type
|
|
235
|
+
return found if type.nil?
|
|
236
|
+
return found << type.to_s if !SUPPORTED.key?(type) || shape != :single
|
|
237
|
+
|
|
238
|
+
typed = rules.public_send(type)
|
|
239
|
+
typed.class.descriptor.each do |rule|
|
|
240
|
+
found << "#{type}.#{rule.name}" if !SUPPORTED[type].include?(rule.name) && set?(typed, rule)
|
|
241
|
+
end
|
|
242
|
+
found
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# A message is worth walking only if some rule is reachable from it; recursive schemas such
|
|
246
|
+
# as google.protobuf.Struct, used for identify traits, have none and are skipped whole.
|
|
247
|
+
def mark_active(built)
|
|
248
|
+
lookup = ->(name) { built[name] || @plans[name] }
|
|
249
|
+
reaches_rule = ->(field_plan) { field_plan.subtype && lookup.call(field_plan.subtype.name).active }
|
|
250
|
+
built.each_value do |message_plan|
|
|
251
|
+
message_plan.active = message_plan.checks.any? || message_plan.oneofs.any? ||
|
|
252
|
+
message_plan.fields.any? { |field_plan| field_plan.required || field_plan.checks.any? }
|
|
253
|
+
end
|
|
254
|
+
loop do
|
|
255
|
+
reached = built.each_value.reject(&:active).select { |message_plan| message_plan.fields.any?(&reaches_rule) }
|
|
256
|
+
break if reached.empty?
|
|
257
|
+
|
|
258
|
+
reached.each { |message_plan| message_plan.active = true }
|
|
259
|
+
end
|
|
260
|
+
# Only fields with a rule, or leading to one, are walked; nested messages without any are
|
|
261
|
+
# dropped from the plan.
|
|
262
|
+
built.each_value do |message_plan|
|
|
263
|
+
message_plan.checked_fields = message_plan.fields.filter_map do |field_plan|
|
|
264
|
+
next if field_plan.ignore == :IGNORE_ALWAYS
|
|
265
|
+
|
|
266
|
+
field_plan.nested = field_plan.subtype if reaches_rule.call(field_plan)
|
|
267
|
+
field_plan if field_plan.nested || field_plan.required || field_plan.checks.any?
|
|
268
|
+
end.freeze
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Checks take the value and return nil, or the failed rule's [id, message].
|
|
273
|
+
def field_checks(rules)
|
|
274
|
+
checks = rules.cel.filter_map { |rule| cel_check(rule) }
|
|
275
|
+
type = rules.type
|
|
276
|
+
SUPPORTED.key?(type) ? checks + type_checks(type, rules.public_send(type)) : checks
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def cel_check(rule)
|
|
280
|
+
check = CEL[rule.expression]
|
|
281
|
+
failure = [rule.id, rule.message].freeze
|
|
282
|
+
check && ->(value) { failure unless check.call(value) }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def type_checks(type, rules)
|
|
286
|
+
case type
|
|
287
|
+
when :string then string_checks(rules)
|
|
288
|
+
when :bool then const_checks(type, rules)
|
|
289
|
+
else
|
|
290
|
+
checks = const_checks(type, rules) + bounds_checks(type, rules) + list_checks(type, rules)
|
|
291
|
+
if rules.respond_to?(:finite) && rules.finite
|
|
292
|
+
failure = ["#{type}.finite", "value must be finite"].freeze
|
|
293
|
+
checks << ->(value) { failure unless value.finite? }
|
|
294
|
+
end
|
|
295
|
+
checks
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def string_checks(rules)
|
|
300
|
+
checks = const_checks(:string, rules)
|
|
301
|
+
rule = lambda do |id, message, &failed|
|
|
302
|
+
failure = ["string.#{id}", message].freeze
|
|
303
|
+
checks << ->(value) { failure if failed.call(value) }
|
|
304
|
+
end
|
|
305
|
+
{ len: "", min_len: "at least ", max_len: "at most " }.each do |name, bound|
|
|
306
|
+
next unless rules.public_send(:"has_#{name}?")
|
|
307
|
+
|
|
308
|
+
limit = rules.public_send(name)
|
|
309
|
+
rule.call(name, "value length must be #{bound}#{limit} characters") do |value|
|
|
310
|
+
length_violated?(name, value.length, limit)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
{ len_bytes: "", min_bytes: "at least ", max_bytes: "at most " }.each do |name, bound|
|
|
314
|
+
next unless rules.public_send(:"has_#{name}?")
|
|
315
|
+
|
|
316
|
+
limit = rules.public_send(name)
|
|
317
|
+
rule.call(name, "value length must be #{bound}#{limit} bytes") do |value|
|
|
318
|
+
length_violated?(name, value.bytesize, limit)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
if rules.has_pattern?
|
|
322
|
+
pattern = Regexp.new(re2_anchors(rules.pattern))
|
|
323
|
+
rule.call(:pattern, "value does not match regex pattern `#{rules.pattern}`") { |value| !pattern.match?(value) }
|
|
324
|
+
end
|
|
325
|
+
if rules.has_prefix?
|
|
326
|
+
prefix = rules.prefix
|
|
327
|
+
rule.call(:prefix, "value does not have prefix `#{prefix}`") { |value| !value.start_with?(prefix) }
|
|
328
|
+
end
|
|
329
|
+
if rules.has_suffix?
|
|
330
|
+
suffix = rules.suffix
|
|
331
|
+
rule.call(:suffix, "value does not have suffix `#{suffix}`") { |value| !value.end_with?(suffix) }
|
|
332
|
+
end
|
|
333
|
+
if rules.has_contains?
|
|
334
|
+
needle = rules.contains
|
|
335
|
+
rule.call(:contains, "value does not contain substring `#{needle}`") { |value| !value.include?(needle) }
|
|
336
|
+
end
|
|
337
|
+
if rules.has_not_contains?
|
|
338
|
+
excluded = rules.not_contains
|
|
339
|
+
rule.call(:not_contains, "value contains substring `#{excluded}`") { |value| value.include?(excluded) }
|
|
340
|
+
end
|
|
341
|
+
checks.concat(list_checks(:string, rules))
|
|
342
|
+
checks.concat(uuid_checks(rules))
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def length_violated?(name, length, limit)
|
|
346
|
+
if name.start_with?("min") then length < limit
|
|
347
|
+
elsif name.start_with?("max") then length > limit
|
|
348
|
+
else length != limit
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def uuid_checks(rules)
|
|
353
|
+
shape, name, id = if rules.uuid then [UUID, "UUID", "uuid"]
|
|
354
|
+
elsif rules.tuuid then [TUUID, "trimmed UUID", "tuuid"]
|
|
355
|
+
end
|
|
356
|
+
return [] unless shape
|
|
357
|
+
|
|
358
|
+
empty = ["string.#{id}_empty", "value is empty, which is not a valid #{name}"].freeze
|
|
359
|
+
invalid = ["string.#{id}", "value must be a valid #{name}"].freeze
|
|
360
|
+
check = lambda do |value|
|
|
361
|
+
if value.empty? then empty
|
|
362
|
+
elsif !shape.match?(value) then invalid
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
[check]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def const_checks(type, rules)
|
|
369
|
+
return [] unless rules.has_const?
|
|
370
|
+
|
|
371
|
+
expected = comparable(type, rules.const)
|
|
372
|
+
shown = type == :string ? "`#{rules.const}`" : show(type, rules.const)
|
|
373
|
+
failure = ["#{type}.const", "value must equal #{shown}"].freeze
|
|
374
|
+
[->(value) { failure unless value == expected }]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# protovalidate's lt/lte/gt/gte family in one place. With both bounds, a lower bound above
|
|
378
|
+
# the upper one means "outside the gap" (the *_exclusive rules). NaN fails every comparison,
|
|
379
|
+
# which is what the double rules' explicit isNan() checks amount to.
|
|
380
|
+
def bounds_checks(type, rules)
|
|
381
|
+
lower_op = rules.greater_than
|
|
382
|
+
upper_op = rules.less_than
|
|
383
|
+
return [] if lower_op.nil? && upper_op.nil?
|
|
384
|
+
|
|
385
|
+
lower = lower_op && rules.public_send(lower_op)
|
|
386
|
+
upper = upper_op && rules.public_send(upper_op)
|
|
387
|
+
lower_bound = lower && comparable(type, lower)
|
|
388
|
+
upper_bound = upper && comparable(type, upper)
|
|
389
|
+
if lower && upper
|
|
390
|
+
exclusive = upper_bound < lower_bound
|
|
391
|
+
failure = ["#{type}.#{lower_op}_#{upper_op}#{"_exclusive" if exclusive}",
|
|
392
|
+
"value must be #{BOUNDS[lower_op]} #{show(type, lower)} #{exclusive ? "or" : "and"} " \
|
|
393
|
+
"#{BOUNDS[upper_op]} #{show(type, upper)}"].freeze
|
|
394
|
+
[lambda do |value|
|
|
395
|
+
lower_ok = satisfies?(value, lower_op, lower_bound)
|
|
396
|
+
upper_ok = satisfies?(value, upper_op, upper_bound)
|
|
397
|
+
failure unless exclusive ? lower_ok || upper_ok : lower_ok && upper_ok
|
|
398
|
+
end]
|
|
399
|
+
else
|
|
400
|
+
op = lower_op || upper_op
|
|
401
|
+
bound = lower_bound || upper_bound
|
|
402
|
+
failure = ["#{type}.#{op}", "value must be #{BOUNDS[op]} #{show(type, lower || upper)}"].freeze
|
|
403
|
+
[->(value) { failure unless satisfies?(value, op, bound) }]
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def list_checks(type, rules)
|
|
408
|
+
checks = []
|
|
409
|
+
unless rules.in.empty?
|
|
410
|
+
allowed = rules.in.map { |item| comparable(type, item) }
|
|
411
|
+
missing = ["#{type}.in", "value must be in list #{list(rules.in, type)}"].freeze
|
|
412
|
+
checks << ->(value) { missing unless allowed.include?(value) }
|
|
413
|
+
end
|
|
414
|
+
unless rules.not_in.empty?
|
|
415
|
+
denied = rules.not_in.map { |item| comparable(type, item) }
|
|
416
|
+
forbidden = ["#{type}.not_in", "value must not be in list #{list(rules.not_in, type)}"].freeze
|
|
417
|
+
checks << ->(value) { forbidden if denied.include?(value) }
|
|
418
|
+
end
|
|
419
|
+
checks
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def satisfies?(value, operator, bound)
|
|
423
|
+
case operator
|
|
424
|
+
when :lt then value < bound
|
|
425
|
+
when :lte then value <= bound
|
|
426
|
+
when :gt then value > bound
|
|
427
|
+
else value >= bound
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def comparable(type, value)
|
|
432
|
+
type == :duration ? seconds(value) : value
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def seconds(message)
|
|
436
|
+
Rational((message.seconds * NANOS) + message.nanos, NANOS)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def show(type, value)
|
|
440
|
+
case type
|
|
441
|
+
when :duration
|
|
442
|
+
total = seconds(value)
|
|
443
|
+
"#{total.denominator == 1 ? total.to_i : total.to_f}s"
|
|
444
|
+
when :float, :double then value.finite? && value == value.truncate ? value.to_i.to_s : value.to_s
|
|
445
|
+
else value.to_s
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def list(values, type)
|
|
450
|
+
"[#{values.map { |value| show(type, value) }.join(", ")}]"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# RE2 anchors ^ and $ at the ends of the text, Ruby at the ends of every line: left as is,
|
|
454
|
+
# "USD\nEUR" would pass ^[A-Z]{3}$ here and fail on the server.
|
|
455
|
+
def re2_anchors(pattern)
|
|
456
|
+
source = +""
|
|
457
|
+
escaped = in_class = false
|
|
458
|
+
pattern.each_char do |char|
|
|
459
|
+
if escaped
|
|
460
|
+
escaped = false
|
|
461
|
+
elsif char == "\\"
|
|
462
|
+
escaped = true
|
|
463
|
+
elsif char == "["
|
|
464
|
+
in_class = true
|
|
465
|
+
elsif char == "]"
|
|
466
|
+
in_class = false
|
|
467
|
+
elsif !in_class && ["^", "$"].include?(char)
|
|
468
|
+
char = char == "^" ? "\\A" : "\\z"
|
|
469
|
+
end
|
|
470
|
+
source << char
|
|
471
|
+
end
|
|
472
|
+
source
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
data/lib/pug/version.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "proto"
|
|
4
|
+
|
|
5
|
+
module Pug
|
|
6
|
+
# Like sdk-node's generated registry, the catalog holds the events whose
|
|
7
|
+
# (common.events.v1.platforms) option lists PLATFORM_SERVER or is unset; one tagged only for
|
|
8
|
+
# web, mobile or desktop is a custom event here.
|
|
9
|
+
module WellKnown
|
|
10
|
+
KIND = Proto.extension("common.events.v1.kind")
|
|
11
|
+
PLATFORMS = Proto.extension("common.events.v1.platforms")
|
|
12
|
+
|
|
13
|
+
def self.build
|
|
14
|
+
namespace = Proto::Common::Events::V1
|
|
15
|
+
classes = namespace.constants.map { |name| namespace.const_get(name) }
|
|
16
|
+
classes = classes.select { |klass| klass.is_a?(Class) && klass.respond_to?(:descriptor) }
|
|
17
|
+
classes.sort_by { |klass| klass.descriptor.name }.each_with_object({}) do |klass, catalog|
|
|
18
|
+
options = klass.descriptor.options
|
|
19
|
+
kind = KIND.get(options)
|
|
20
|
+
next if kind.empty? || catalog.key?(kind)
|
|
21
|
+
|
|
22
|
+
platforms = PLATFORMS.get(options)
|
|
23
|
+
catalog[kind] = klass if platforms.empty? || platforms.include?(:PLATFORM_SERVER)
|
|
24
|
+
end.freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
CATALOG = build
|
|
28
|
+
|
|
29
|
+
def self.[](kind)
|
|
30
|
+
CATALOG[kind]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/pug-sh.rb
ADDED
data/lib/pug.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pug-sh
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- pug.sh
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: google-protobuf
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.30'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '5'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '4.30'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '5'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: logger
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.6'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.6'
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/pug-sh.rb
|
|
53
|
+
- lib/pug.rb
|
|
54
|
+
- lib/pug/batcher.rb
|
|
55
|
+
- lib/pug/client.rb
|
|
56
|
+
- lib/pug/connect.rb
|
|
57
|
+
- lib/pug/countries.rb
|
|
58
|
+
- lib/pug/errors.rb
|
|
59
|
+
- lib/pug/event_builder.rb
|
|
60
|
+
- lib/pug/gen/buf/validate/validate_pb.rb
|
|
61
|
+
- lib/pug/gen/common/events/v1/api_events_pb.rb
|
|
62
|
+
- lib/pug/gen/common/events/v1/app_events_pb.rb
|
|
63
|
+
- lib/pug/gen/common/events/v1/auth_events_pb.rb
|
|
64
|
+
- lib/pug/gen/common/events/v1/billing_events_pb.rb
|
|
65
|
+
- lib/pug/gen/common/events/v1/chat_events_pb.rb
|
|
66
|
+
- lib/pug/gen/common/events/v1/commerce_events_pb.rb
|
|
67
|
+
- lib/pug/gen/common/events/v1/discovery_events_pb.rb
|
|
68
|
+
- lib/pug/gen/common/events/v1/error_events_pb.rb
|
|
69
|
+
- lib/pug/gen/common/events/v1/file_events_pb.rb
|
|
70
|
+
- lib/pug/gen/common/events/v1/form_events_pb.rb
|
|
71
|
+
- lib/pug/gen/common/events/v1/integration_events_pb.rb
|
|
72
|
+
- lib/pug/gen/common/events/v1/invitation_events_pb.rb
|
|
73
|
+
- lib/pug/gen/common/events/v1/media_events_pb.rb
|
|
74
|
+
- lib/pug/gen/common/events/v1/navigation_events_pb.rb
|
|
75
|
+
- lib/pug/gen/common/events/v1/notification_events_pb.rb
|
|
76
|
+
- lib/pug/gen/common/events/v1/options_pb.rb
|
|
77
|
+
- lib/pug/gen/common/events/v1/social_events_pb.rb
|
|
78
|
+
- lib/pug/gen/common/events/v1/support_events_pb.rb
|
|
79
|
+
- lib/pug/gen/common/events/v1/workspace_events_pb.rb
|
|
80
|
+
- lib/pug/gen/common/v1/filter_schema_pb.rb
|
|
81
|
+
- lib/pug/gen/common/v1/filters_pb.rb
|
|
82
|
+
- lib/pug/gen/common/v1/property_value_pb.rb
|
|
83
|
+
- lib/pug/gen/common/v1/time_pb.rb
|
|
84
|
+
- lib/pug/gen/sdk/devices/v1/devices_pb.rb
|
|
85
|
+
- lib/pug/gen/sdk/events/v1/events_pb.rb
|
|
86
|
+
- lib/pug/gen/sdk/profiles/v1/profiles_pb.rb
|
|
87
|
+
- lib/pug/gen/shared/activity/v1/activity_pb.rb
|
|
88
|
+
- lib/pug/gen/shared/insights/v1/insights_pb.rb
|
|
89
|
+
- lib/pug/gen/shared/profiles/v1/profiles_pb.rb
|
|
90
|
+
- lib/pug/log.rb
|
|
91
|
+
- lib/pug/proto.rb
|
|
92
|
+
- lib/pug/reads.rb
|
|
93
|
+
- lib/pug/rpc.rb
|
|
94
|
+
- lib/pug/validator.rb
|
|
95
|
+
- lib/pug/version.rb
|
|
96
|
+
- lib/pug/well_known.rb
|
|
97
|
+
homepage: https://pug.sh
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata:
|
|
101
|
+
homepage_uri: https://pug.sh
|
|
102
|
+
source_code_uri: https://github.com/pug-sh/sdk-ruby
|
|
103
|
+
bug_tracker_uri: https://github.com/pug-sh/sdk-ruby/issues
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.3'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 4.0.21
|
|
119
|
+
specification_version: 4
|
|
120
|
+
summary: Pug server-side SDK for Ruby — event tracking, profile identify, and analytics
|
|
121
|
+
reads.
|
|
122
|
+
test_files: []
|