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.
@@ -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 UTF-16
16
- # code units. `pattern` is an ECMAScript expression run on Onigmo, Ruby's
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
- @patterns_lock.synchronize do
47
- return @patterns[source] if @patterns.key?(source)
52
+ compile(source)[0]
53
+ end
48
54
 
49
- @patterns[source] = begin
50
- Regexp.new(translate(source))
51
- rescue RegexpError, ArgumentError
52
- nil
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
- def validate(value)
96
- run(@schema, value)
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.utf16_length(text)
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"]) && (re = Schema.pattern(source)) && !re.match?(text)
290
- limit(path, "must match the pattern /#{source}/")
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
- # name=value of a cookie parameter.
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, content_type: 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: application/json\r\n\r\n".b << p.text.b
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)
@@ -4,6 +4,6 @@
4
4
  module Tungsten
5
5
  module Runtime
6
6
  # The runtime's version, sent as X-Tungsten-Runtime: tungsten-rb/<version>.
7
- VERSION = "0.1.4"
7
+ VERSION = "0.1.6"
8
8
  end
9
9
  end
@@ -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
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