tungsten-runtime 0.1.4 → 0.1.6
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 +4 -4
- data/README.md +191 -9
- data/lib/tungsten/runtime/auth.rb +35 -2
- data/lib/tungsten/runtime/client_core.rb +195 -20
- data/lib/tungsten/runtime/descriptors.rb +12 -3
- data/lib/tungsten/runtime/json.rb +10 -0
- data/lib/tungsten/runtime/macros.rb +1 -1
- data/lib/tungsten/runtime/oauth.rb +133 -36
- data/lib/tungsten/runtime/oauth_inputs.rb +136 -0
- data/lib/tungsten/runtime/oauth_sessions.rb +521 -0
- data/lib/tungsten/runtime/options.rb +35 -11
- data/lib/tungsten/runtime/pages.rb +3 -1
- data/lib/tungsten/runtime/schema.rb +49 -19
- data/lib/tungsten/runtime/serialize.rb +43 -8
- data/lib/tungsten/runtime/stream.rb +3 -3
- data/lib/tungsten/runtime/version.rb +1 -1
- data/lib/tungsten/runtime.rb +2 -0
- metadata +3 -1
|
@@ -12,14 +12,20 @@ module Tungsten
|
|
|
12
12
|
# before any network call, success bodies, page items and events after.
|
|
13
13
|
#
|
|
14
14
|
# Issues come structure first, then constraint violations, then unknown
|
|
15
|
-
# members. Types are strict (no coercion); string lengths count
|
|
16
|
-
# code
|
|
15
|
+
# members. Types are strict (no coercion); string lengths count Unicode
|
|
16
|
+
# code points (a surrogate pair is one, a lone surrogate is one).
|
|
17
|
+
# `pattern` is an ECMAScript expression run on Onigmo, Ruby's
|
|
17
18
|
# engine: `^` and `$` outside a character class are anchored to the
|
|
18
19
|
# whole string (`\A`, `\z`) as in ECMAScript without the `m` flag; other
|
|
19
20
|
# differences (Unicode property and case-folding details, `\d`, `\w` and
|
|
20
21
|
# `\s` being ASCII-only in Ruby) are Onigmo's, and a pattern Onigmo
|
|
21
|
-
# cannot compile is not checked
|
|
22
|
+
# cannot compile is not checked: the value is accepted and the pattern is
|
|
23
|
+
# reported (see `validate`).
|
|
22
24
|
class Schema
|
|
25
|
+
# A pattern that could not be checked: the value was accepted without
|
|
26
|
+
# it. It never holds the value.
|
|
27
|
+
PatternNote = Data.define(:path, :pattern, :reason)
|
|
28
|
+
|
|
23
29
|
DATE = "(?:(?:[0-9][0-9][2468][048]|[0-9][0-9][13579][26]|[0-9][0-9]0[48]|[02468][048]00" \
|
|
24
30
|
"|[13579][26]00)-02-29|[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])" \
|
|
25
31
|
"|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|(?:02)-(?:0[1-9]|1[0-9]|2[0-8])))"
|
|
@@ -41,15 +47,22 @@ module Tungsten
|
|
|
41
47
|
|
|
42
48
|
class << self
|
|
43
49
|
# The compiled pattern of an ECMAScript source, or nil when Onigmo
|
|
44
|
-
# cannot compile it.
|
|
50
|
+
# cannot compile it (see `pattern_error`).
|
|
45
51
|
def pattern(source)
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
compile(source)[0]
|
|
53
|
+
end
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
# Why Onigmo cannot compile the source, or nil when it can.
|
|
56
|
+
def pattern_error(source)
|
|
57
|
+
compile(source)[1]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def compile(source)
|
|
61
|
+
@patterns_lock.synchronize do
|
|
62
|
+
@patterns[source] ||= begin
|
|
63
|
+
[Regexp.new(translate(source)), nil]
|
|
64
|
+
rescue RegexpError, ArgumentError => e
|
|
65
|
+
[nil, "it is not a regular expression this runtime can compile (Onigmo: #{e.message.sub(/: \/.*\z/m, '')})"]
|
|
53
66
|
end
|
|
54
67
|
end
|
|
55
68
|
end
|
|
@@ -91,18 +104,21 @@ module Tungsten
|
|
|
91
104
|
@defs = defs || {}
|
|
92
105
|
end
|
|
93
106
|
|
|
94
|
-
# The issues of a value, none when it is valid.
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
# The issues of a value, none when it is valid. `notes`, when given (an
|
|
108
|
+
# Array), receives a PatternNote for each pattern that could not be
|
|
109
|
+
# checked (once per path and pattern); the value was accepted without
|
|
110
|
+
# it. Without `notes` the call is silent.
|
|
111
|
+
def validate(value, notes = nil)
|
|
112
|
+
run(@schema, value, notes)
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
# Whether the value is valid.
|
|
100
116
|
def accepts?(value) = validate(value).empty?
|
|
101
117
|
|
|
102
|
-
def run(schema, value)
|
|
118
|
+
def run(schema, value, notes = nil)
|
|
103
119
|
# Descriptors share validators across clients. Keep each validation's
|
|
104
120
|
# issue lists on its own worker, including recursive union probes.
|
|
105
|
-
self.class.new(schema, @defs).send(:run_validation, schema, value)
|
|
121
|
+
self.class.new(schema, @defs).send(:run_validation, schema, value, notes)
|
|
106
122
|
end
|
|
107
123
|
|
|
108
124
|
# Whether `schema` admits `value`, in a fresh run (union variants).
|
|
@@ -112,7 +128,9 @@ module Tungsten
|
|
|
112
128
|
|
|
113
129
|
private
|
|
114
130
|
|
|
115
|
-
def run_validation(schema, value)
|
|
131
|
+
def run_validation(schema, value, notes)
|
|
132
|
+
@notes = notes
|
|
133
|
+
@noted = {}
|
|
116
134
|
@structure = []
|
|
117
135
|
@constraints = []
|
|
118
136
|
@unknown = []
|
|
@@ -282,12 +300,24 @@ module Tungsten
|
|
|
282
300
|
value.is_a?(Float) && value == value.truncate && value.abs < Json::MAX_SAFE ? value.to_i.to_s : Json.number_text(value)
|
|
283
301
|
end
|
|
284
302
|
|
|
303
|
+
def unchecked(path, source, reason)
|
|
304
|
+
return unless @notes
|
|
305
|
+
return if @noted.key?([path, source])
|
|
306
|
+
|
|
307
|
+
@noted[[path, source]] = true
|
|
308
|
+
@notes << PatternNote.new(path: path, pattern: source, reason: reason)
|
|
309
|
+
end
|
|
310
|
+
|
|
285
311
|
def string_limits(schema, text, path)
|
|
286
|
-
n = Json.
|
|
312
|
+
n = Json.code_point_length(text)
|
|
287
313
|
limit(path, "must have at least #{schema['min_length']} characters") if schema["min_length"] && n < schema["min_length"]
|
|
288
314
|
limit(path, "must have at most #{schema['max_length']} characters") if schema["max_length"] && n > schema["max_length"]
|
|
289
|
-
if (source = schema["pattern"])
|
|
290
|
-
|
|
315
|
+
if (source = schema["pattern"])
|
|
316
|
+
if (re = Schema.pattern(source))
|
|
317
|
+
limit(path, "must match the pattern /#{source}/") unless re.match?(text)
|
|
318
|
+
else
|
|
319
|
+
unchecked(path, source, Schema.pattern_error(source))
|
|
320
|
+
end
|
|
291
321
|
end
|
|
292
322
|
format = FORMATS[schema["format"]]
|
|
293
323
|
limit(path, "must be a valid #{schema['format']}") if format && !format.match?(text)
|
|
@@ -14,7 +14,7 @@ module Tungsten
|
|
|
14
14
|
Encoded = Data.define(:empty, :bytes, :parts, :content_type, :display, :hashed)
|
|
15
15
|
EMPTY = Encoded.new(empty: true, bytes: nil, parts: nil, content_type: nil, display: nil, hashed: nil)
|
|
16
16
|
|
|
17
|
-
# One multipart part: `kind` :text, :json or :file.
|
|
17
|
+
# One multipart part: `kind` :text, :json (a typed text part) or :file.
|
|
18
18
|
Part = Data.define(:name, :kind, :text, :data, :filename, :content_type)
|
|
19
19
|
|
|
20
20
|
module_function
|
|
@@ -141,8 +141,36 @@ module Tungsten
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
#
|
|
144
|
+
# RFC 6265 cookie-octets.
|
|
145
|
+
COOKIE_OCTET = /\A[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]\z/
|
|
146
|
+
|
|
147
|
+
# A cookie style value as written; only characters a cookie cannot
|
|
148
|
+
# carry are percent-encoded.
|
|
149
|
+
def cookie_text(text) = text.to_s.each_char.map { |c| COOKIE_OCTET.match?(c) ? c : encode_component(c) }.join
|
|
150
|
+
|
|
151
|
+
# OpenAPI 3.2 cookie style: n=v, n=v1,v2 or exploded n=v1; n=v2 and
|
|
152
|
+
# k=v; k2=v2.
|
|
153
|
+
def cookie_style(p, value)
|
|
154
|
+
case value
|
|
155
|
+
when Array
|
|
156
|
+
items = value.reject(&:nil?).map { |v| cookie_text(scalar(v)) }
|
|
157
|
+
return items.map { |i| "#{p.wire}=#{i}" }.join("; ") if p.explode && !items.empty?
|
|
158
|
+
|
|
159
|
+
"#{p.wire}=#{items.join(',')}"
|
|
160
|
+
when Hash
|
|
161
|
+
pairs = value.reject { |_, v| v.nil? }.map { |k, v| [cookie_text(k), cookie_text(scalar(v))] }
|
|
162
|
+
return pairs.map { |k, v| "#{k}=#{v}" }.join("; ") if p.explode && !pairs.empty?
|
|
163
|
+
|
|
164
|
+
"#{p.wire}=#{pairs.flatten.join(',')}"
|
|
165
|
+
else "#{p.wire}=#{cookie_text(scalar(value))}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# name=value of a cookie parameter (form style, values encoded; or
|
|
170
|
+
# cookie style).
|
|
145
171
|
def cookie_param(p, value)
|
|
172
|
+
return cookie_style(p, value) if p.style == "cookie"
|
|
173
|
+
|
|
146
174
|
case value
|
|
147
175
|
when Array then "#{p.wire}=#{encoded_items(value).join(',')}"
|
|
148
176
|
when Hash then "#{p.wire}=#{encoded_pairs(value).flatten.join(',')}"
|
|
@@ -212,7 +240,7 @@ module Tungsten
|
|
|
212
240
|
text = pairs.map { |k, v| "#{encode_form_component(k)}=#{encode_form_component(v)}" }.join("&")
|
|
213
241
|
Encoded.new(empty: false, bytes: text.b, parts: nil, content_type: media, display: redact.call(pairs.to_h),
|
|
214
242
|
hashed: text.b)
|
|
215
|
-
when "multipart" then multipart(value, redact)
|
|
243
|
+
when "multipart" then multipart(value, redact, desc.parts)
|
|
216
244
|
when "bytes"
|
|
217
245
|
bin = Json.binary_of(value)
|
|
218
246
|
data =
|
|
@@ -242,7 +270,7 @@ module Tungsten
|
|
|
242
270
|
end
|
|
243
271
|
end
|
|
244
272
|
|
|
245
|
-
def multipart(value, redact)
|
|
273
|
+
def multipart(value, redact, declared_parts = {})
|
|
246
274
|
unless value.is_a?(Hash)
|
|
247
275
|
return SerializationError.new(parameter: "body", expected: "an object of multipart fields", value: value)
|
|
248
276
|
end
|
|
@@ -254,10 +282,12 @@ module Tungsten
|
|
|
254
282
|
|
|
255
283
|
list = item.is_a?(Array) ? item.reject(&:nil?) : [item]
|
|
256
284
|
shown = []
|
|
285
|
+
declared = declared_parts[k.to_s]
|
|
286
|
+
declared_json = !declared.nil? && Classify.json_media?(declared.split(";", 2).first.to_s.strip.downcase)
|
|
257
287
|
list.each do |x|
|
|
258
288
|
bin = Json.binary_of(x)
|
|
259
289
|
if !UNSET.equal?(bin)
|
|
260
|
-
content_type = bin.content_type || "application/octet-stream"
|
|
290
|
+
content_type = bin.content_type || declared || "application/octet-stream"
|
|
261
291
|
unless valid_media_type?(content_type)
|
|
262
292
|
return SerializationError.new(parameter: "body", expected: "multipart file content types that are valid media types",
|
|
263
293
|
value: value)
|
|
@@ -265,12 +295,17 @@ module Tungsten
|
|
|
265
295
|
|
|
266
296
|
# Keep the checked header value stable until the body is sent.
|
|
267
297
|
content_type = content_type.dup.freeze
|
|
268
|
-
shown << binary_display(bin.bytesize, bin.content_type)
|
|
298
|
+
shown << binary_display(bin.bytesize, bin.content_type || declared)
|
|
269
299
|
parts << Part.new(name: k, kind: :file, text: nil, data: bin.data, filename: bin.filename || k,
|
|
270
300
|
content_type: content_type)
|
|
271
301
|
elsif x.is_a?(Hash)
|
|
272
|
-
parts << Part.new(name: k, kind: :json, text: Json.text(x), data: nil, filename: nil,
|
|
302
|
+
parts << Part.new(name: k, kind: :json, text: Json.text(x), data: nil, filename: nil,
|
|
303
|
+
content_type: declared_json ? declared : "application/json")
|
|
273
304
|
shown << x
|
|
305
|
+
elsif declared
|
|
306
|
+
parts << Part.new(name: k, kind: :json, text: declared_json ? Json.text(x) : scalar(x), data: nil,
|
|
307
|
+
filename: nil, content_type: declared)
|
|
308
|
+
shown << scalar(x)
|
|
274
309
|
else
|
|
275
310
|
parts << Part.new(name: k, kind: :text, text: scalar(x), data: nil, filename: nil, content_type: nil)
|
|
276
311
|
shown << scalar(x)
|
|
@@ -295,7 +330,7 @@ module Tungsten
|
|
|
295
330
|
out << "Content-Type: #{p.content_type}\r\n\r\n".b << p.data.b
|
|
296
331
|
when :json
|
|
297
332
|
out << %(Content-Disposition: form-data; name="#{quote.call(p.name)}"\r\n).b
|
|
298
|
-
out << "Content-Type:
|
|
333
|
+
out << "Content-Type: #{p.content_type}\r\n\r\n".b << p.text.b
|
|
299
334
|
else
|
|
300
335
|
out << %(Content-Disposition: form-data; name="#{quote.call(p.name)}"\r\n\r\n).b << p.text.b
|
|
301
336
|
end
|
|
@@ -278,7 +278,7 @@ module Tungsten
|
|
|
278
278
|
return FailedStream.new(Runtime.diag(d.id, "VALIDATION_FAILED").param("opts").expected("call options")
|
|
279
279
|
.remedy("Pass the call options as a Hash of idempotency_key, confirm, timeout_ms, verify and headers (#{e.message}).").err)
|
|
280
280
|
end
|
|
281
|
-
EventStream.new(self, d, args, o, decode)
|
|
281
|
+
EventStream.new(self, d, args, with_call_deadline(o), decode)
|
|
282
282
|
end
|
|
283
283
|
|
|
284
284
|
# A stream that fails before it starts.
|
|
@@ -463,14 +463,14 @@ module Tungsten
|
|
|
463
463
|
Err.new(error: Util.scrub_diagnostic(b.status(o.meta.status).request_id(o.meta.request_id).retrying("never")
|
|
464
464
|
.attempts(o.meta.attempts).build, o.secrets))
|
|
465
465
|
end
|
|
466
|
-
value = Json.try_parse(ev.data)
|
|
466
|
+
value = op.stream["text"] ? ev.data : Json.try_parse(ev.data)
|
|
467
467
|
if UNSET.equal?(value)
|
|
468
468
|
return failure.call(Runtime.diag(op.id, "UNEXPECTED_RESPONSE").param("events[#{index}]")
|
|
469
469
|
.received(Util.envelope_value(ev.data, false)).expected("JSON in the data of every event")
|
|
470
470
|
.remedy("Event #{index} of the stream (event \"#{ev.event}\") does not carry JSON in its data.#{after_effect} Events before it were delivered."))
|
|
471
471
|
end
|
|
472
472
|
if @validate_responses != "off" && op.stream["event"]
|
|
473
|
-
issues = judge(op.stream["event"], value)
|
|
473
|
+
issues = judge(op.stream["event"], value, op.id, "events[#{index}]")
|
|
474
474
|
unless issues.empty?
|
|
475
475
|
issue = issues.first
|
|
476
476
|
path_text = Runtime.issue_path(issue.path)
|
data/lib/tungsten/runtime.rb
CHANGED
|
@@ -41,6 +41,8 @@ require_relative "runtime/expr"
|
|
|
41
41
|
require_relative "runtime/client_core"
|
|
42
42
|
require_relative "runtime/auth"
|
|
43
43
|
require_relative "runtime/oauth"
|
|
44
|
+
require_relative "runtime/oauth_inputs"
|
|
45
|
+
require_relative "runtime/oauth_sessions"
|
|
44
46
|
require_relative "runtime/pages"
|
|
45
47
|
require_relative "runtime/macros"
|
|
46
48
|
require_relative "runtime/sse"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tungsten-runtime
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Prince Boachie
|
|
@@ -47,6 +47,8 @@ files:
|
|
|
47
47
|
- lib/tungsten/runtime/json.rb
|
|
48
48
|
- lib/tungsten/runtime/macros.rb
|
|
49
49
|
- lib/tungsten/runtime/oauth.rb
|
|
50
|
+
- lib/tungsten/runtime/oauth_inputs.rb
|
|
51
|
+
- lib/tungsten/runtime/oauth_sessions.rb
|
|
50
52
|
- lib/tungsten/runtime/options.rb
|
|
51
53
|
- lib/tungsten/runtime/pages.rb
|
|
52
54
|
- lib/tungsten/runtime/schema.rb
|