volcano-sdk 0.10.1 → 0.10.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59e652b75cb01f8a353cca787d8e97ecce893091202389d6eb05d16898553fe4
4
- data.tar.gz: 1aaab53a45bb753db900c0dbb5fe0d95d27c17f36a0bc4487e396a37f10e0a7c
3
+ metadata.gz: 0a5a8e6d0c0958a19588efaa73c12721eee47e710bc397ce18a4a2a383d0ba9f
4
+ data.tar.gz: ec32d121d29074aece0ed41d30599f575e90535c66ad93c82b073d1a3d1b8eaf
5
5
  SHA512:
6
- metadata.gz: f1c408840aeabf36d2880bb13f8fa9ef602ecb1fa5de7e0b3db936ca48f02ce8b8dc667c6dcf52ec4637f4418f6c9b45bbf157081cbc167406752980d1c56bc2
7
- data.tar.gz: 641095882ee98d37672f0c7c9a59a2051d98f4a392682df91753b999fffd2aae91a65a62e9c299569f5e1d3c96f73b4095247e168954ae7c75f6e36f5a71fed2
6
+ metadata.gz: ed4e6a000be0491dc2edfcb72e6bc03a87a49bdd3eb835c0d29455010852e5a645b6323e65e7740cd3a9c98898726bb0bc7ba7bb68cd5a37992f6f2049661105
7
+ data.tar.gz: b1ef9f3c793ad18bd26a7d347b8b48b85dc792d10de410d88340b945bb9f19c67a25bc3fa612f7889ca88b5962bd89fe20ef2ef4e4d1e2dee273ca5380876cd2
data/README.md CHANGED
@@ -907,3 +907,7 @@ Run `npm ci && bin/check-openapi` to verify generated provenance. Use
907
907
  `bundle exec rubocop -A` to apply safe formatting and lint fixes. Before
908
908
  changing the facade, run `bundle exec rubocop --parallel`, `bundle exec rspec`,
909
909
  and the shared Cucumber contract suite.
910
+
911
+ ## Contributing
912
+
913
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for cross-language coordination and the full verification workflow.
@@ -72,8 +72,12 @@ module Volcano
72
72
  def valid_user?(payload)
73
73
  return false unless payload.is_a?(Hash)
74
74
 
75
- required_user_fields?(payload) && valid_email_confirmation?(payload) &&
76
- valid_metadata?(payload) && valid_optional_strings?(payload) && valid_timestamps?(payload)
75
+ required_user_fields?(payload) && valid_optional_user_fields?(payload)
76
+ end
77
+
78
+ def valid_optional_user_fields?(payload)
79
+ valid_email_confirmation?(payload) && valid_metadata?(payload) &&
80
+ valid_optional_strings?(payload) && valid_timestamps?(payload)
77
81
  end
78
82
 
79
83
  def required_user_fields?(payload)
@@ -77,13 +77,16 @@ module Volcano
77
77
  provider = attributes.fetch('provider')
78
78
  linked_at = attributes.fetch('linked_at')
79
79
  updated_at = attributes.fetch('updated_at')
80
- valid = provider.is_a?(String) && !provider.strip.empty? &&
81
- linked_at.is_a?(Time) && updated_at.is_a?(Time)
80
+ valid = valid_linked_oauth_provider?(provider, linked_at, updated_at)
82
81
  raise TypeError, 'Expected complete linked OAuth providers' unless valid
83
82
 
84
83
  LinkedOAuthProvider.new(provider:, linked_at:, updated_at:)
85
84
  rescue KeyError
86
85
  raise TypeError, 'Expected complete linked OAuth providers'
87
86
  end
87
+
88
+ def valid_linked_oauth_provider?(provider, linked_at, updated_at)
89
+ complete_string?(provider) && linked_at.is_a?(Time) && updated_at.is_a?(Time)
90
+ end
88
91
  end
89
92
  end
@@ -32,15 +32,22 @@ module Volcano
32
32
 
33
33
  def freeze_oauth_api_data(value)
34
34
  case value
35
- when Hash
36
- value.to_h { |key, item| [freeze_oauth_api_data(key), freeze_oauth_api_data(item)] }.freeze
35
+ when Hash then freeze_oauth_api_data_hash(value)
37
36
  when Array
38
37
  value.map { |item| freeze_oauth_api_data(item) }.freeze
39
38
  when String
40
39
  value.dup.freeze
41
40
  else
42
- value.frozen? ? value : value.dup.freeze
41
+ freeze_oauth_api_data_scalar(value)
43
42
  end
44
43
  end
44
+
45
+ def freeze_oauth_api_data_scalar(value)
46
+ value.frozen? ? value : value.dup.freeze
47
+ end
48
+
49
+ def freeze_oauth_api_data_hash(value)
50
+ value.to_h { |key, item| [freeze_oauth_api_data(key), freeze_oauth_api_data(item)] }.freeze
51
+ end
45
52
  end
46
53
  end
@@ -75,7 +75,7 @@ module Volcano
75
75
 
76
76
  def rejected_refresh?(binding, active)
77
77
  generation, lineage, = binding
78
- @rejected_refresh == [generation, lineage] &&
78
+ @client.refresh_rejected?(generation, lineage) &&
79
79
  active.first == generation + 1 && active.last.nil?
80
80
  end
81
81
 
@@ -115,9 +115,7 @@ module Volcano
115
115
  Transport.body(refresh_response(binding.last.refresh_token), 200)
116
116
  rescue Error::AuthenticationError
117
117
  generation, owner, = binding
118
- if !owner.closing? && @client.clear_session_if_current?(generation, notifications: notifications)
119
- @rejected_refresh = [generation, owner]
120
- end
118
+ @client.reject_refresh_if_current?(generation, notifications: notifications) unless owner.closing?
121
119
  raise
122
120
  end
123
121
  end
@@ -114,6 +114,10 @@ module Volcano
114
114
  uncertain = error.nil? || error.is_a?(Error::TransportError)
115
115
  return @client.clear_session_if_current?(binding.first, lineage: binding[1]) if deletes_current && uncertain
116
116
 
117
+ deletion_binding_current?(binding, error)
118
+ end
119
+
120
+ def deletion_binding_current?(binding, error)
117
121
  active = @client.capture_session_binding
118
122
  (active.last && active[1] == binding[1]) || (error && rejected_refresh?(binding, active))
119
123
  end
@@ -46,23 +46,37 @@ module Volcano
46
46
  session_id = access_token_session_id(session.access_token)
47
47
  verified = owner.verified_pair?(session)
48
48
  return revoke_access_session(session, session_id, refresh_error, joined: joined) if session_id && !verified
49
- return refresh_error if refresh_error && !verified
50
- return unless session.refresh_token
51
49
 
52
- Transport.body(logout_response(session.refresh_token), 204)
53
- nil
50
+ refresh_revocation_error(session, refresh_error, verified: verified)
54
51
  rescue Error::VolcanoError => e
55
52
  e
56
53
  end
57
54
 
55
+ def refresh_revocation_error(session, refresh_error, verified:)
56
+ return refresh_error if refresh_error && !verified
57
+
58
+ revoke_refresh_session(session.refresh_token)
59
+ end
60
+
61
+ def revoke_refresh_session(refresh_token)
62
+ return unless refresh_token
63
+
64
+ Transport.body(logout_response(refresh_token), 204)
65
+ nil
66
+ end
67
+
58
68
  def revoke_access_session(session, session_id, refresh_error, joined:)
59
69
  error = delete_session_error(session.access_token, session_id)
60
- return error unless error&.status == 401 && session.refresh_token
70
+ return error unless refreshable_revocation?(error, session)
61
71
  return refresh_error || error if joined
62
72
 
63
73
  refreshed = parse_refresh_session(Transport.body(refresh_response(session.refresh_token), 200))
64
74
  SessionCredentials.validate_refresh(session, refreshed)
65
75
  delete_session_error(refreshed.access_token, session_id)
66
76
  end
77
+
78
+ def refreshable_revocation?(error, session)
79
+ error&.status == 401 && session.refresh_token
80
+ end
67
81
  end
68
82
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'auth_state_notifications'
4
+
3
5
  module Volcano
4
6
  # Idempotent handle for an authentication-state subscription.
5
7
  class AuthSubscription
@@ -21,14 +23,14 @@ module Volcano
21
23
 
22
24
  # Owns synchronized local session state and its subscribers.
23
25
  class AuthState
24
- CALLBACK_FAILURES = [Exception].freeze
25
- private_constant :CALLBACK_FAILURES
26
+ include AuthStateNotifications
26
27
 
27
28
  def initialize(session: nil)
28
29
  @mutex = Mutex.new
29
30
  @generation = 0
30
31
  @lineage = SessionOperations.new
31
32
  @session = session
33
+ @rejected_refresh = nil
32
34
  @callbacks = {}
33
35
  @next_callback_id = 0
34
36
  @notifications = []
@@ -50,17 +52,33 @@ module Volcano
50
52
 
51
53
  def store_if_current?(session, generation, event: :signed_in, notifications: nil, lineage: nil)
52
54
  dispatch = @mutex.synchronize do
53
- return false unless lineage.nil? ? generation == @generation : lineage == @lineage
55
+ return false unless current_binding?(generation, lineage)
54
56
  return true if session.nil? && @session.nil?
55
57
 
56
58
  enqueue_notification(replace(session, event, verified_pair: session), event, session)
57
59
  end
58
60
  return true unless dispatch
59
61
 
60
- notifications ? notifications.push(method(:drain_notifications)) : drain_notifications
62
+ dispatch_notifications(notifications)
63
+ true
64
+ end
65
+
66
+ def reject_refresh_if_current?(generation, notifications: nil)
67
+ dispatch = @mutex.synchronize do
68
+ return false unless generation == @generation && @session
69
+
70
+ # Publish rejection and credential removal as one synchronized transition.
71
+ @rejected_refresh = [generation, @lineage]
72
+ enqueue_notification(replace(nil, :signed_out), :signed_out, nil)
73
+ end
74
+ dispatch_notifications(notifications) if dispatch
61
75
  true
62
76
  end
63
77
 
78
+ def refresh_rejected?(generation, lineage)
79
+ @mutex.synchronize { @rejected_refresh == [generation, lineage] }
80
+ end
81
+
64
82
  def update_user_if_current?(user, generation)
65
83
  @mutex.synchronize do
66
84
  return false unless generation == @generation && @session
@@ -81,6 +99,10 @@ module Volcano
81
99
 
82
100
  private
83
101
 
102
+ def current_binding?(generation, lineage) = lineage.nil? ? generation == @generation : lineage == @lineage
103
+
104
+ def dispatch_notifications(queue) = queue ? queue.push(method(:drain_notifications)) : drain_notifications
105
+
84
106
  def replace(session, event, verified_pair: nil)
85
107
  SessionCredentials.validate_refresh(@session, session) if event == :token_refreshed
86
108
  @session = session
@@ -89,59 +111,5 @@ module Volcano
89
111
  @lineage = SessionOperations.new(verified_pair) if session && event != :token_refreshed
90
112
  @callbacks.keys
91
113
  end
92
-
93
- def register(callback)
94
- @next_callback_id += 1
95
- @callbacks[@next_callback_id] = callback
96
- [@next_callback_id, @session]
97
- end
98
-
99
- def unsubscribe(callback_id)
100
- @mutex.synchronize { @callbacks.delete(callback_id) }
101
- end
102
-
103
- def enqueue_notification(callbacks, event, session)
104
- return false if event.nil? || callbacks.empty?
105
-
106
- @notifications << [callbacks, event, session]
107
- return false if @dispatching_notifications
108
-
109
- @dispatching_notifications = true
110
- end
111
-
112
- def drain_notifications
113
- failure = nil
114
- loop do
115
- notification = next_notification
116
- break unless notification
117
-
118
- current_failure = notify(*notification)
119
- failure ||= current_failure
120
- end
121
- raise failure if failure
122
- end
123
-
124
- def next_notification
125
- @mutex.synchronize do
126
- if @notifications.empty?
127
- @dispatching_notifications = false
128
- return
129
- end
130
- @notifications.shift
131
- end
132
- end
133
-
134
- def notify(callback_ids, event, session)
135
- failure = nil
136
- callback_ids.each do |callback_id|
137
- @mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
138
- rescue StandardError => e
139
- Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
140
- rescue *CALLBACK_FAILURES => e
141
- unsubscribe(callback_id)
142
- failure ||= e
143
- end
144
- failure
145
- end
146
114
  end
147
115
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Queues auth events under the state mutex and invokes subscribers outside it.
5
+ module AuthStateNotifications
6
+ CALLBACK_FAILURES = [Exception].freeze
7
+ private_constant :CALLBACK_FAILURES
8
+
9
+ private
10
+
11
+ def register(callback)
12
+ @next_callback_id += 1
13
+ @callbacks[@next_callback_id] = callback
14
+ [@next_callback_id, @session]
15
+ end
16
+
17
+ def unsubscribe(callback_id) = @mutex.synchronize { @callbacks.delete(callback_id) }
18
+
19
+ def enqueue_notification(callbacks, event, session)
20
+ return false if event.nil? || callbacks.empty?
21
+
22
+ @notifications << [callbacks, event, session]
23
+ return false if @dispatching_notifications
24
+
25
+ @dispatching_notifications = true
26
+ end
27
+
28
+ def drain_notifications
29
+ failure = nil
30
+ loop do
31
+ notification = next_notification
32
+ break unless notification
33
+
34
+ current_failure = notify(*notification)
35
+ failure ||= current_failure
36
+ end
37
+ raise failure if failure
38
+ end
39
+
40
+ def next_notification
41
+ @mutex.synchronize do
42
+ if @notifications.empty?
43
+ @dispatching_notifications = false
44
+ return
45
+ end
46
+ @notifications.shift
47
+ end
48
+ end
49
+
50
+ def notify(callback_ids, event, session)
51
+ failure = nil
52
+ callback_ids.each do |callback_id|
53
+ @mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
54
+ rescue StandardError => e
55
+ Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
56
+ rescue *CALLBACK_FAILURES => e
57
+ unsubscribe(callback_id)
58
+ failure ||= e
59
+ end
60
+ failure
61
+ end
62
+ end
63
+ private_constant :AuthStateNotifications
64
+ end
@@ -86,6 +86,10 @@ module Volcano
86
86
  @auth_state.store_if_current?(nil, generation, event: event, notifications: notifications, lineage: lineage)
87
87
  end
88
88
 
89
+ def reject_refresh_if_current?(...) = @auth_state.reject_refresh_if_current?(...)
90
+
91
+ def refresh_rejected?(...) = @auth_state.refresh_rejected?(...)
92
+
89
93
  def subscribe_auth_state_change(...)
90
94
  @auth_state.subscribe(...)
91
95
  end
@@ -9,10 +9,14 @@ module Volcano
9
9
  case value
10
10
  when String then value.dup.freeze
11
11
  when Array then value.map { |item| capture(item) }.freeze
12
- when Hash then value.to_h { |key, item| [capture(key), capture(item)] }.freeze
12
+ when Hash then capture_hash(value)
13
13
  else value
14
14
  end
15
15
  end
16
+
17
+ def capture_hash(value)
18
+ value.to_h { |key, item| [capture(key), capture(item)] }.freeze
19
+ end
16
20
  end
17
21
  private_constant :ImmutableQueryValue
18
22
 
@@ -146,11 +150,17 @@ module Volcano
146
150
 
147
151
  def query_body
148
152
  { 'table' => @table }.tap do |body|
153
+ add_query_selection(body)
154
+ body['limit'] = @limit unless @limit.nil?
155
+ body['offset'] = @offset unless @offset.nil?
156
+ end
157
+ end
158
+
159
+ def add_query_selection(body)
160
+ body.tap do
149
161
  body['select'] = @columns unless @columns.empty? || @columns == ['*']
150
162
  body['filters'] = @filters unless @filters.empty?
151
163
  body['order'] = @order unless @order.empty?
152
- body['limit'] = @limit unless @limit.nil?
153
- body['offset'] = @offset unless @offset.nil?
154
164
  end
155
165
  end
156
166
  end
@@ -124,6 +124,10 @@ module Volcano
124
124
  def validate_paging(page, limit)
125
125
  raise ArgumentError, 'page must be a positive Integer' unless page.nil? || positive_integer?(page)
126
126
 
127
+ validate_page_limit(limit)
128
+ end
129
+
130
+ def validate_page_limit(limit)
127
131
  return if limit.nil? || (positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
128
132
 
129
133
  raise ArgumentError, "limit must be an Integer between 1 and #{MAX_PAGE_SIZE}"
@@ -51,12 +51,16 @@ module Volcano
51
51
  # `result` is the function's own JSON, so it is frozen all the way down.
52
52
  def immutable_value(value)
53
53
  case value
54
- when Hash then value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
54
+ when Hash then immutable_value_hash(value)
55
55
  when Array then value.map { |item| immutable_value(item) }.freeze
56
56
  when Time, String then value.dup.freeze
57
57
  else value
58
58
  end
59
59
  end
60
+
61
+ def immutable_value_hash(value)
62
+ value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
63
+ end
60
64
  end
61
65
 
62
66
  # One page of a durable function's executions, most recent first.
@@ -51,11 +51,10 @@ module Volcano
51
51
  # when the API itself is plaintext, so a resolve response cannot
52
52
  # downgrade a credential that is otherwise protected in transit.
53
53
  def valid_invoke_url(value, api_url)
54
- return nil unless value.is_a?(String) && !value.empty?
54
+ return nil unless nonempty_url?(value)
55
55
 
56
56
  uri = URI.parse(value)
57
- # URI accepts a port outside the range a connection can use.
58
- return nil if uri.host.to_s.empty? || !uri.port.to_i.between?(1, 65_535)
57
+ return nil unless usable_address?(uri)
59
58
 
60
59
  usable_scheme?(uri.scheme, api_url) ? value : nil
61
60
  rescue URI::InvalidURIError
@@ -107,6 +106,15 @@ module Volcano
107
106
 
108
107
  private
109
108
 
109
+ def nonempty_url?(value)
110
+ value.is_a?(String) && !value.empty?
111
+ end
112
+
113
+ def usable_address?(uri)
114
+ # URI accepts a port outside the range a connection can use.
115
+ !uri.host.to_s.empty? && uri.port.to_i.between?(1, 65_535)
116
+ end
117
+
110
118
  def usable_scheme?(scheme, api_url)
111
119
  case scheme&.downcase
112
120
  when 'https' then true
@@ -120,10 +128,14 @@ module Volcano
120
128
  @entries[key] = Entry.new(outcome: outcome, expires_at: now + ttl_seconds)
121
129
  next if @entries.size <= MAX_ENTRIES
122
130
 
123
- @entries.delete_if { |_, entry| entry.expires_at <= now }
124
- @entries.delete(@entries.min_by { |_, entry| entry.expires_at }&.first) while @entries.size > MAX_ENTRIES
131
+ evict_expired_entries
125
132
  end
126
133
  end
134
+
135
+ def evict_expired_entries
136
+ @entries.delete_if { |_, entry| entry.expires_at <= now }
137
+ @entries.delete(@entries.min_by { |_, entry| entry.expires_at }.first) while @entries.size > MAX_ENTRIES
138
+ end
127
139
  end
128
140
  end
129
141
  end
@@ -16,11 +16,15 @@ module Volcano
16
16
 
17
17
  def immutable_json(value)
18
18
  case value
19
- when Hash then value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
19
+ when Hash then immutable_json_hash(value)
20
20
  when Array then value.map { |item| immutable_json(item) }.freeze
21
21
  when String then value.dup.freeze
22
22
  else value
23
23
  end
24
24
  end
25
+
26
+ def immutable_json_hash(value)
27
+ value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
28
+ end
25
29
  end
26
30
  end
@@ -82,6 +82,10 @@ module Volcano
82
82
  return nil if text.empty?
83
83
  return text unless function_json?(text, headers)
84
84
 
85
+ parse_function_json(text)
86
+ end
87
+
88
+ def parse_function_json(text)
85
89
  parsed = JSON.parse(text)
86
90
  valid_function_encoding?(parsed) ? parsed : text
87
91
  rescue JSON::ParserError
@@ -90,13 +94,17 @@ module Volcano
90
94
 
91
95
  def valid_function_encoding?(value)
92
96
  case value
93
- when Hash then value.all? { |key, item| valid_function_encoding?(key) && valid_function_encoding?(item) }
97
+ when Hash then valid_function_hash_encoding?(value)
94
98
  when Array then value.all? { |item| valid_function_encoding?(item) }
95
99
  when String then value.valid_encoding?
96
100
  else true
97
101
  end
98
102
  end
99
103
 
104
+ def valid_function_hash_encoding?(value)
105
+ value.all? { |key, item| valid_function_encoding?(key) && valid_function_encoding?(item) }
106
+ end
107
+
100
108
  def function_json?(text, headers)
101
109
  return true if text.start_with?('{', '[')
102
110
 
@@ -10,7 +10,12 @@ module Volcano
10
10
  rescue NameError => e
11
11
  raise unless e.name == :Generated && e.message.include?('private constant Volcano::Generated')
12
12
 
13
- klass = Generated.const_get(type)
13
+ deserialize_private_model(Generated.const_get(type), value)
14
+ end
15
+
16
+ private
17
+
18
+ def deserialize_private_model(klass, value)
14
19
  if klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of)
15
20
  klass.build(value)
16
21
  else
@@ -139,12 +144,16 @@ module Volcano
139
144
  def plain_value(value)
140
145
  value = value.to_hash if value.respond_to?(:to_hash)
141
146
  case value
142
- when Hash then value.to_h { |key, item| [key.to_s, plain_value(item)] }
147
+ when Hash then plain_hash(value)
143
148
  when Array then value.map { |item| plain_value(item) }
144
149
  else value
145
150
  end
146
151
  end
147
152
 
153
+ def plain_hash(value)
154
+ value.to_h { |key, item| [key.to_s, plain_value(item)] }
155
+ end
156
+
148
157
  def deep_symbolize(value)
149
158
  case value
150
159
  when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
@@ -163,10 +172,14 @@ module Volcano
163
172
  end
164
173
 
165
174
  def prepare_stream(value)
166
- value.open if value.respond_to?(:closed?) && value.closed? && value.respond_to?(:open)
175
+ reopen_stream(value)
167
176
  value.binmode if value.respond_to?(:binmode)
168
177
  value.rewind if value.respond_to?(:rewind)
169
178
  end
179
+
180
+ def reopen_stream(value)
181
+ value.open if value.respond_to?(:closed?) && value.closed? && value.respond_to?(:open)
182
+ end
170
183
  end
171
184
 
172
185
  # Builds generated API wrappers with consistent authentication and timeouts.
@@ -7,12 +7,16 @@ module Volcano
7
7
 
8
8
  def immutable_json(value)
9
9
  case value
10
- when Hash then value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
10
+ when Hash then immutable_json_hash(value)
11
11
  when Array then value.map { |item| immutable_json(item) }.freeze
12
12
  when Time, String then value.dup.freeze
13
13
  else value
14
14
  end
15
15
  end
16
+
17
+ def immutable_json_hash(value)
18
+ value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
19
+ end
16
20
  end
17
21
  private_constant :ImmutableLogJSON
18
22
 
@@ -59,42 +59,6 @@ module Volcano
59
59
  end
60
60
  end
61
61
  EmailChangeResult = Data.define(:message, :new_email)
62
- USER_OPTIONAL_ATTRIBUTES = %i[
63
- project_id email_confirmed user_metadata app_metadata avatar_url banned_until
64
- last_sign_in_at created_at updated_at
65
- ].freeze
66
- private_constant :USER_OPTIONAL_ATTRIBUTES
67
-
68
- User = Data.define(
69
- :id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata,
70
- :avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at
71
- ) do
72
- def initialize(id:, email:, status:, **attributes)
73
- validate_optional_attributes(attributes)
74
- super(
75
- id: freeze_value(id),
76
- email: freeze_value(email),
77
- status: freeze_value(status),
78
- **USER_OPTIONAL_ATTRIBUTES.to_h { |name| [name, freeze_value(attributes[name])] }
79
- )
80
- end
81
-
82
- private
83
-
84
- def validate_optional_attributes(attributes)
85
- unknown = attributes.keys - USER_OPTIONAL_ATTRIBUTES
86
- raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
87
- end
88
-
89
- def freeze_value(value)
90
- case value
91
- when Hash then value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
92
- when Array then value.map { |item| freeze_value(item) }.freeze
93
- when String then value.dup.freeze
94
- else value.frozen? ? value : value.dup.freeze
95
- end
96
- end
97
- end
98
62
  LockLease = Data.define(:key, :token, :expires_at, :fencing_token) do
99
63
  def initialize(key:, token:, expires_at:, fencing_token:)
100
64
  super(
@@ -11,10 +11,10 @@ module Volcano
11
11
  )
12
12
  super(
13
13
  type: Immutable.call(type.to_s), schema: Immutable.call(schema.to_s),
14
- table: Immutable.call(table.to_s), record: record && Immutable.call(record),
15
- old_record: old_record && Immutable.call(old_record),
16
- columns: columns && Immutable.call(columns), timestamp: Immutable.call(timestamp.to_s),
17
- id: id && Immutable.call(id), mode: mode && Immutable.call(mode.to_s)
14
+ table: Immutable.call(table.to_s), record: Immutable.optional(record),
15
+ old_record: Immutable.optional(old_record),
16
+ columns: Immutable.optional(columns), timestamp: Immutable.call(timestamp.to_s),
17
+ id: Immutable.optional(id), mode: mode && Immutable.call(mode.to_s)
18
18
  )
19
19
  end
20
20
  end
@@ -11,7 +11,7 @@ module Volcano
11
11
  attr_reader :database_name
12
12
 
13
13
  def database_name=(name)
14
- unless name.nil? || (name.is_a?(String) && DATABASE_NAME.match?(name))
14
+ unless valid_database_name?(name)
15
15
  raise ArgumentError,
16
16
  'database name must match ^[a-z0-9_]+$ and contain at most 64 characters'
17
17
  end
@@ -20,6 +20,10 @@ module Volcano
20
20
 
21
21
  private
22
22
 
23
+ def valid_database_name?(name)
24
+ name.nil? || (name.is_a?(String) && DATABASE_NAME.match?(name))
25
+ end
26
+
23
27
  def initialize_postgres_database
24
28
  @database_name = nil
25
29
  @postgres_fetch_semaphore = nil
@@ -7,7 +7,7 @@ module Volcano
7
7
  private
8
8
 
9
9
  def expanded_postgres_changes(requests)
10
- return requests.map { |request| local_postgres_change(request.change) } unless requests.first.database_name
10
+ return local_postgres_changes(requests) unless requests.first.database_name
11
11
 
12
12
  records = fetch_postgres_records(requests)
13
13
  requests.map { |request| expanded_postgres_record(request, records) }
@@ -16,6 +16,10 @@ module Volcano
16
16
  requests.map(&:change)
17
17
  end
18
18
 
19
+ def local_postgres_changes(requests)
20
+ requests.map { |request| local_postgres_change(request.change) }
21
+ end
22
+
19
23
  def lightweight_delete?(change)
20
24
  change.mode == 'lightweight' && change.type == 'DELETE'
21
25
  end
@@ -48,9 +48,7 @@ module Volcano
48
48
  def self.subscribe(id:, channel:, recoverable: false, join_leave: false, recovery: nil)
49
49
  options = { 'channel' => channel }
50
50
  if recovery
51
- options.merge!('recover' => true, 'positioned' => true, 'recoverable' => true)
52
- options['epoch'] = recovery[:epoch] if recovery.key?(:epoch)
53
- options['offset'] = recovery[:offset] if recovery.key?(:offset)
51
+ add_recovery_options(options, recovery)
54
52
  elsif recoverable
55
53
  options['recoverable'] = true
56
54
  end
@@ -58,6 +56,13 @@ module Volcano
58
56
  { 'id' => id, 'subscribe' => options }
59
57
  end
60
58
 
59
+ def self.add_recovery_options(options, recovery)
60
+ options.merge!('recover' => true, 'positioned' => true, 'recoverable' => true)
61
+ options['epoch'] = recovery[:epoch] if recovery.key?(:epoch)
62
+ options['offset'] = recovery[:offset] if recovery.key?(:offset)
63
+ end
64
+ private_class_method :add_recovery_options
65
+
61
66
  def self.publish(id:, channel:, data:) = { 'id' => id, 'publish' => { 'channel' => channel, 'data' => data } }
62
67
 
63
68
  def self.unsubscribe(id:, channel:) = { 'id' => id, 'unsubscribe' => { 'channel' => channel } }
@@ -59,7 +59,10 @@ module Volcano
59
59
  result_position = recovery_position(result['epoch'], result['offset'])
60
60
  return [nil, nil] unless result_position
61
61
 
62
- publications = result.fetch('publications', [])
62
+ validate_recovery_publications(result.fetch('publications', []), result_position)
63
+ end
64
+
65
+ def validate_recovery_publications(publications, result_position)
63
66
  return [nil, nil] unless publications.is_a?(Array)
64
67
 
65
68
  validated = publications.map { |publication| validated_recovery_publication(publication, result_position) }
@@ -26,6 +26,11 @@ module Volcano
26
26
 
27
27
  completed = publication_position(publication, current)
28
28
  return unless completed
29
+
30
+ advance_publication_position(channel, completed, current)
31
+ end
32
+
33
+ def advance_publication_position(channel, completed, current)
29
34
  return mark_unknown_gap(channel) unless same_epoch?(completed, current)
30
35
  return if stale_position?(completed, current)
31
36
  return mark_unknown_gap(channel) unless next_position?(completed, current)
@@ -57,6 +62,10 @@ module Volcano
57
62
  return @position_gaps[channel] = true unless same_epoch?(gap, current)
58
63
  return if gap.fetch(:offset) <= current.fetch(:offset)
59
64
 
65
+ store_earliest_gap(channel, gap)
66
+ end
67
+
68
+ def store_earliest_gap(channel, gap)
60
69
  existing = @position_gaps[channel]
61
70
  return if existing == true
62
71
  return if existing && existing.fetch(:offset) <= gap.fetch(:offset)
@@ -34,14 +34,22 @@ module Volcano
34
34
  module Immutable
35
35
  module_function
36
36
 
37
+ def optional(value)
38
+ value && call(value)
39
+ end
40
+
37
41
  def call(value)
38
42
  case value
39
- when Hash then value.to_h { |key, child| [call(key), call(child)] }.freeze
43
+ when Hash then call_hash(value)
40
44
  when Array then value.map { |child| call(child) }.freeze
41
45
  when String then value.dup.freeze
42
46
  else value.freeze
43
47
  end
44
48
  end
49
+
50
+ def call_hash(value)
51
+ value.to_h { |key, item| [call(key), call(item)] }.freeze
52
+ end
45
53
  end
46
54
  private_constant :Immutable
47
55
 
@@ -37,15 +37,20 @@ module Volcano
37
37
  def self.validate_refresh(current, refreshed)
38
38
  return unless current
39
39
 
40
- expected = session_id(current.access_token)
41
- if expected && expected != session_id(refreshed&.access_token)
42
- raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
43
- end
40
+ validate_refresh_session_id(current, refreshed)
44
41
  return unless current.user_id && current.user_id != refreshed&.user_id
45
42
 
46
43
  raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
47
44
  end
48
45
 
46
+ def self.validate_refresh_session_id(current, refreshed)
47
+ expected = session_id(current.access_token)
48
+ return unless expected && expected != session_id(refreshed&.access_token)
49
+
50
+ raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
51
+ end
52
+ private_class_method :validate_refresh_session_id
53
+
49
54
  # Untrusted continuity constraint, never authenticated user identity.
50
55
  def self.session_id(access_token)
51
56
  payload = token_payload(access_token)
@@ -24,98 +24,26 @@ module Volcano
24
24
 
25
25
  def immutable_value(value)
26
26
  case value
27
- when Hash
28
- value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
27
+ when Hash then immutable_value_hash(value)
29
28
  when Array then value.map { |item| immutable_value(item) }.freeze
30
29
  when String then value.dup.freeze
31
- else value.frozen? ? value : value.dup.freeze
30
+ else immutable_value_scalar(value)
32
31
  end
33
32
  end
34
- end
35
-
36
- StoragePage = Data.define(:objects, :next_cursor) do
37
- def initialize(objects:, next_cursor: nil)
38
- cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
39
- super(objects: objects.to_a.dup.freeze, next_cursor: cursor)
40
- end
41
- end
42
-
43
- UploadSessionRequest = Data.define(:path, :content_type, :total_size, :part_size) do
44
- def initialize(path:, content_type:, total_size:, part_size: nil)
45
- super(
46
- path: path.dup.freeze,
47
- content_type: content_type.dup.freeze,
48
- total_size: total_size,
49
- part_size: part_size
50
- )
51
- end
52
- end
53
- private_constant :UploadSessionRequest
54
-
55
- UploadPartRequest = Data.define(:path, :session_id, :part_number, :data) do
56
- def initialize(path:, session_id:, part_number:, data:)
57
- super(
58
- path: path.dup.freeze,
59
- session_id: session_id.dup.freeze,
60
- part_number: part_number,
61
- data: data.dup.freeze
62
- )
63
- end
64
- end
65
- private_constant :UploadPartRequest
66
-
67
- UploadSessionReference = Data.define(:path, :session_id) do
68
- def initialize(path:, session_id:)
69
- super(path: path.dup.freeze, session_id: session_id.dup.freeze)
70
- end
71
- end
72
- private_constant :UploadSessionReference
73
33
 
74
- UploadSession = Data.define(:session_id, :part_size, :total_parts, :expires_at) do
75
- def initialize(session_id:, part_size:, total_parts:, expires_at:)
76
- super(
77
- session_id: session_id.dup.freeze,
78
- part_size: part_size,
79
- total_parts: total_parts,
80
- expires_at: expires_at.dup.freeze
81
- )
34
+ def immutable_value_scalar(value)
35
+ value.frozen? ? value : value.dup.freeze
82
36
  end
83
- end
84
37
 
85
- UploadPart = Data.define(:part_number, :etag, :size) do
86
- def initialize(part_number:, etag:, size:)
87
- super(part_number: part_number, etag: etag.dup.freeze, size: size)
38
+ def immutable_value_hash(value)
39
+ value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
88
40
  end
89
41
  end
90
42
 
91
- UPLOAD_SESSION_STATUS_ATTRIBUTES = %i[
92
- session_id status path content_type total_size part_size total_parts parts_uploaded
93
- bytes_uploaded parts expires_at created_at
94
- ].freeze
95
- private_constant :UPLOAD_SESSION_STATUS_ATTRIBUTES
96
-
97
- UploadSessionStatus = Data.define(*UPLOAD_SESSION_STATUS_ATTRIBUTES) do
98
- def initialize(**attributes)
99
- unknown = attributes.keys - UPLOAD_SESSION_STATUS_ATTRIBUTES
100
- raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
101
-
102
- missing = UPLOAD_SESSION_STATUS_ATTRIBUTES - attributes.keys
103
- raise ArgumentError, "missing keywords: #{missing.join(', ')}" unless missing.empty?
104
-
105
- values = UPLOAD_SESSION_STATUS_ATTRIBUTES.to_h do |name|
106
- [name, immutable_value(attributes.fetch(name))]
107
- end
108
- super(**values)
109
- end
110
-
111
- private
112
-
113
- def immutable_value(value)
114
- case value
115
- when Array then value.to_a.dup.freeze
116
- when String then value.dup.freeze
117
- else value.frozen? ? value : value.dup.freeze
118
- end
43
+ StoragePage = Data.define(:objects, :next_cursor) do
44
+ def initialize(objects:, next_cursor: nil)
45
+ cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
46
+ super(objects: objects.to_a.dup.freeze, next_cursor: cursor)
119
47
  end
120
48
  end
121
49
  end
@@ -54,13 +54,15 @@ module Volcano
54
54
 
55
55
  def storage_paths(paths)
56
56
  path_list = paths.is_a?(String) ? [paths] : paths.to_a
57
- if path_list.empty? || !path_list.all? { |path| path.is_a?(String) && !path.empty? }
58
- raise ArgumentError, 'storage paths must be non-empty strings'
59
- end
57
+ raise ArgumentError, 'storage paths must be non-empty strings' unless valid_storage_paths?(path_list)
60
58
 
61
59
  path_list.map { |path| path.dup.freeze }.freeze
62
60
  end
63
61
 
62
+ def valid_storage_paths?(paths)
63
+ !paths.empty? && paths.all? { |path| path.is_a?(String) && !path.empty? }
64
+ end
65
+
64
66
  def delete_path(path, binding)
65
67
  response = storage_request(binding: binding) do |token|
66
68
  @transport.delete_storage_object(
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ UploadSessionRequest = Data.define(:path, :content_type, :total_size, :part_size) do
5
+ def initialize(path:, content_type:, total_size:, part_size: nil)
6
+ super(
7
+ path: path.dup.freeze,
8
+ content_type: content_type.dup.freeze,
9
+ total_size: total_size,
10
+ part_size: part_size
11
+ )
12
+ end
13
+ end
14
+ private_constant :UploadSessionRequest
15
+
16
+ UploadPartRequest = Data.define(:path, :session_id, :part_number, :data) do
17
+ def initialize(path:, session_id:, part_number:, data:)
18
+ super(
19
+ path: path.dup.freeze,
20
+ session_id: session_id.dup.freeze,
21
+ part_number: part_number,
22
+ data: data.dup.freeze
23
+ )
24
+ end
25
+ end
26
+ private_constant :UploadPartRequest
27
+
28
+ UploadSessionReference = Data.define(:path, :session_id) do
29
+ def initialize(path:, session_id:)
30
+ super(path: path.dup.freeze, session_id: session_id.dup.freeze)
31
+ end
32
+ end
33
+ private_constant :UploadSessionReference
34
+
35
+ UploadSession = Data.define(:session_id, :part_size, :total_parts, :expires_at) do
36
+ def initialize(session_id:, part_size:, total_parts:, expires_at:)
37
+ super(
38
+ session_id: session_id.dup.freeze,
39
+ part_size: part_size,
40
+ total_parts: total_parts,
41
+ expires_at: expires_at.dup.freeze
42
+ )
43
+ end
44
+ end
45
+
46
+ UploadPart = Data.define(:part_number, :etag, :size) do
47
+ def initialize(part_number:, etag:, size:)
48
+ super(part_number: part_number, etag: etag.dup.freeze, size: size)
49
+ end
50
+ end
51
+
52
+ UPLOAD_SESSION_STATUS_ATTRIBUTES = %i[
53
+ session_id status path content_type total_size part_size total_parts parts_uploaded
54
+ bytes_uploaded parts expires_at created_at
55
+ ].freeze
56
+ private_constant :UPLOAD_SESSION_STATUS_ATTRIBUTES
57
+
58
+ UploadSessionStatus = Data.define(*UPLOAD_SESSION_STATUS_ATTRIBUTES) do
59
+ def initialize(**attributes)
60
+ unknown = attributes.keys - UPLOAD_SESSION_STATUS_ATTRIBUTES
61
+ raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
62
+
63
+ missing = UPLOAD_SESSION_STATUS_ATTRIBUTES - attributes.keys
64
+ raise ArgumentError, "missing keywords: #{missing.join(', ')}" unless missing.empty?
65
+
66
+ values = UPLOAD_SESSION_STATUS_ATTRIBUTES.to_h do |name|
67
+ [name, immutable_value(attributes.fetch(name))]
68
+ end
69
+ super(**values)
70
+ end
71
+
72
+ private
73
+
74
+ def immutable_value(value)
75
+ case value
76
+ when Array then value.to_a.dup.freeze
77
+ when String then value.dup.freeze
78
+ else value.frozen? ? value : value.dup.freeze
79
+ end
80
+ end
81
+ end
82
+ end
@@ -38,13 +38,17 @@ module Volcano
38
38
  payload = response.body.is_a?(Hash) ? response.body : {}
39
39
  retry_after = integer_header(response.headers, 'Retry-After') if response.status == 429
40
40
  error_type(response.status).new(
41
- payload['error'] || payload['message'] || 'Volcano request failed',
41
+ error_message(payload),
42
42
  status: response.status,
43
43
  code: payload['code']&.to_s,
44
44
  retry_after: retry_after
45
45
  )
46
46
  end
47
47
 
48
+ def error_message(payload)
49
+ payload['error'] || payload['message'] || 'Volcano request failed'
50
+ end
51
+
48
52
  def error_type(status)
49
53
  ERROR_TYPES.fetch(status) do
50
54
  status.between?(500, 599) ? Error::ServerError : Error::VolcanoError
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ USER_OPTIONAL_ATTRIBUTES = %i[
5
+ project_id email_confirmed user_metadata app_metadata avatar_url banned_until
6
+ last_sign_in_at created_at updated_at
7
+ ].freeze
8
+ private_constant :USER_OPTIONAL_ATTRIBUTES
9
+
10
+ User = Data.define(
11
+ :id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata,
12
+ :avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at
13
+ ) do
14
+ def initialize(id:, email:, status:, **attributes)
15
+ validate_optional_attributes(attributes)
16
+ super(
17
+ id: freeze_value(id),
18
+ email: freeze_value(email),
19
+ status: freeze_value(status),
20
+ **USER_OPTIONAL_ATTRIBUTES.to_h { |name| [name, freeze_value(attributes[name])] }
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def validate_optional_attributes(attributes)
27
+ unknown = attributes.keys - USER_OPTIONAL_ATTRIBUTES
28
+ raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
29
+ end
30
+
31
+ def freeze_value(value)
32
+ case value
33
+ when Hash then freeze_value_hash(value)
34
+ when Array then value.map { |item| freeze_value(item) }.freeze
35
+ when String then value.dup.freeze
36
+ else freeze_value_scalar(value)
37
+ end
38
+ end
39
+
40
+ def freeze_value_scalar(value)
41
+ value.frozen? ? value : value.dup.freeze
42
+ end
43
+
44
+ def freeze_value_hash(value)
45
+ value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.10.1'
4
+ VERSION = '0.10.2'
5
5
  end
data/lib/volcano.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  require_relative 'volcano/version'
4
4
  require_relative 'volcano/connection_string'
5
5
  require_relative 'volcano/models'
6
+ require_relative 'volcano/user'
6
7
  require_relative 'volcano/session'
7
8
  require_relative 'volcano/function_response'
8
9
  require_relative 'volcano/durable_models'
9
10
  require_relative 'volcano/log_responses'
10
11
  require_relative 'volcano/storage_models'
12
+ require_relative 'volcano/storage_upload_models'
11
13
  require_relative 'volcano/session_operations'
12
14
  require_relative 'volcano/auth_state'
13
15
  require_relative 'volcano/errors'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: volcano-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kong
@@ -148,6 +148,7 @@ files:
148
148
  - lib/volcano/auth_sign_out.rb
149
149
  - lib/volcano/auth_sign_up.rb
150
150
  - lib/volcano/auth_state.rb
151
+ - lib/volcano/auth_state_notifications.rb
151
152
  - lib/volcano/auth_update_user.rb
152
153
  - lib/volcano/client.rb
153
154
  - lib/volcano/connection_string.rb
@@ -597,8 +598,10 @@ files:
597
598
  - lib/volcano/storage_mutations.rb
598
599
  - lib/volcano/storage_public_url.rb
599
600
  - lib/volcano/storage_resumable_upload.rb
601
+ - lib/volcano/storage_upload_models.rb
600
602
  - lib/volcano/storage_upload_sessions.rb
601
603
  - lib/volcano/transport.rb
604
+ - lib/volcano/user.rb
602
605
  - lib/volcano/version.rb
603
606
  homepage: https://github.com/Kong/volcano-sdk-ruby
604
607
  licenses: