restate-sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/restate/serde.rb CHANGED
@@ -4,23 +4,22 @@
4
4
  require 'json'
5
5
 
6
6
  module Restate
7
+ EMPTY_BYTES = ''.b.freeze
8
+ private_constant :EMPTY_BYTES
9
+
7
10
  # JSON serializer/deserializer (default).
8
11
  # Converts Ruby objects to JSON byte strings and back.
9
12
  module JsonSerde
10
- extend T::Sig
11
-
12
13
  module_function
13
14
 
14
15
  # Serialize a Ruby object to a JSON byte string. Returns empty bytes for nil.
15
- sig { params(obj: T.untyped).returns(String) }
16
16
  def serialize(obj)
17
- return ''.b if obj.nil?
17
+ return EMPTY_BYTES if obj.nil?
18
18
 
19
19
  JSON.generate(obj).b
20
20
  end
21
21
 
22
22
  # Deserialize a JSON byte string to a Ruby object. Returns nil for nil or empty input.
23
- sig { params(buf: T.nilable(String)).returns(T.untyped) }
24
23
  def deserialize(buf)
25
24
  return nil if buf.nil? || buf.empty?
26
25
 
@@ -28,7 +27,6 @@ module Restate
28
27
  end
29
28
 
30
29
  # Return the JSON Schema for this serde, or nil if unspecified.
31
- sig { returns(T.nilable(T::Hash[String, T.untyped])) }
32
30
  def json_schema
33
31
  nil
34
32
  end
@@ -40,26 +38,21 @@ module Restate
40
38
  # Pass-through bytes serializer/deserializer.
41
39
  # Passes binary data through without transformation.
42
40
  module BytesSerde
43
- extend T::Sig
44
-
45
41
  module_function
46
42
 
47
43
  # Serialize an object by returning its binary encoding. Returns empty bytes for nil.
48
- sig { params(obj: T.untyped).returns(String) }
49
44
  def serialize(obj)
50
- return ''.b if obj.nil?
45
+ return EMPTY_BYTES if obj.nil?
51
46
 
52
47
  obj.b
53
48
  end
54
49
 
55
50
  # Deserialize by returning the raw buffer unchanged.
56
- sig { params(buf: T.nilable(String)).returns(T.nilable(String)) }
57
51
  def deserialize(buf)
58
52
  buf
59
53
  end
60
54
 
61
55
  # Return the JSON Schema for this serde, or nil if unspecified.
62
- sig { returns(T.nilable(T::Hash[String, T.untyped])) }
63
56
  def json_schema
64
57
  nil
65
58
  end
@@ -67,34 +60,29 @@ module Restate
67
60
 
68
61
  # Maps Ruby primitive types to JSON Schema snippets for discovery.
69
62
  PRIMITIVE_SCHEMAS = {
70
- String => { 'type' => 'string' },
71
- Integer => { 'type' => 'integer' },
72
- Float => { 'type' => 'number' },
73
- TrueClass => { 'type' => 'boolean' },
74
- FalseClass => { 'type' => 'boolean' },
75
- Array => { 'type' => 'array' },
76
- Hash => { 'type' => 'object' },
77
- NilClass => { 'type' => 'null' }
63
+ String => { 'type' => 'string' }.freeze,
64
+ Integer => { 'type' => 'integer' }.freeze,
65
+ Float => { 'type' => 'number' }.freeze,
66
+ TrueClass => { 'type' => 'boolean' }.freeze,
67
+ FalseClass => { 'type' => 'boolean' }.freeze,
68
+ Array => { 'type' => 'array' }.freeze,
69
+ Hash => { 'type' => 'object' }.freeze,
70
+ NilClass => { 'type' => 'null' }.freeze
78
71
  }.freeze
79
72
 
80
73
  # Serde resolution utilities: converts a type or serde into a serde object.
81
74
  module Serde
82
- extend T::Sig
83
-
84
75
  module_function
85
76
 
86
77
  # Check if an object quacks like a serde (has serialize + deserialize).
87
- sig { params(obj: T.untyped).returns(T::Boolean) }
88
78
  def serde?(obj)
89
79
  obj.respond_to?(:serialize) && obj.respond_to?(:deserialize)
90
80
  end
91
81
 
92
82
  # Resolve a type or serde into a serde object with serialize/deserialize/json_schema.
93
- sig { params(type_or_serde: T.untyped).returns(T.untyped) }
94
83
  def resolve(type_or_serde)
95
84
  return JsonSerde if type_or_serde.nil?
96
85
  return type_or_serde if serde?(type_or_serde)
97
- return TStructSerde.new(type_or_serde) if t_struct?(type_or_serde)
98
86
  return DryStructSerde.new(type_or_serde) if dry_struct?(type_or_serde)
99
87
  return TypeSerde.new(type_or_serde, PRIMITIVE_SCHEMAS[type_or_serde]) if PRIMITIVE_SCHEMAS.key?(type_or_serde)
100
88
  return TypeSerde.new(type_or_serde, type_or_serde.json_schema) if type_or_serde.respond_to?(:json_schema)
@@ -102,60 +90,12 @@ module Restate
102
90
  JsonSerde
103
91
  end
104
92
 
105
- # Check if a value is a T::Struct subclass.
106
- sig { params(val: T.untyped).returns(T::Boolean) }
107
- def t_struct?(val)
108
- !!(val.is_a?(Class) && val < T::Struct)
109
- end
110
-
111
93
  # Check if a value is a Dry::Struct subclass.
112
- sig { params(val: T.untyped).returns(T.nilable(T::Boolean)) }
113
94
  def dry_struct?(val)
114
95
  defined?(::Dry::Struct) && val.is_a?(Class) && val < ::Dry::Struct
115
96
  end
116
97
 
117
- # Generate a JSON Schema from a T::Struct class by introspecting its props.
118
- sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[String, T.untyped]) }
119
- def t_struct_to_json_schema(struct_class) # rubocop:disable Metrics
120
- properties = {}
121
- required = []
122
-
123
- T.unsafe(struct_class).props.each do |name, meta|
124
- prop_name = (meta[:serialized_form] || name).to_s
125
- properties[prop_name] = t_type_to_json_schema(meta[:type_object] || meta[:type])
126
- required << prop_name unless meta[:fully_optional] || meta[:_tnilable]
127
- end
128
-
129
- schema = { 'type' => 'object', 'properties' => properties }
130
- schema['required'] = required unless required.empty?
131
- schema
132
- end
133
-
134
- # Convert a Sorbet T::Types type object to a JSON Schema hash.
135
- sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) }
136
- def t_type_to_json_schema(type) # rubocop:disable Metrics
137
- case type
138
- when T::Types::Simple
139
- PRIMITIVE_SCHEMAS[type.raw_type] || {}
140
- when T::Types::Union
141
- schemas = type.types.map { |t| t_type_to_json_schema(t) }
142
- schemas.uniq!
143
- schemas.length == 1 ? schemas.first : { 'anyOf' => schemas }
144
- when T::Types::TypedArray
145
- { 'type' => 'array', 'items' => t_type_to_json_schema(type.type) }
146
- when T::Types::TypedHash
147
- { 'type' => 'object' }
148
- when Class
149
- return t_struct_to_json_schema(type) if type < T::Struct
150
-
151
- PRIMITIVE_SCHEMAS[type] || {}
152
- else
153
- {}
154
- end
155
- end
156
-
157
98
  # Generate a JSON Schema from a Dry::Struct class.
158
- sig { params(struct_class: T.untyped).returns(T::Hash[String, T.untyped]) }
159
99
  def dry_struct_to_json_schema(struct_class)
160
100
  properties = {}
161
101
  required = []
@@ -172,7 +112,6 @@ module Restate
172
112
  end
173
113
 
174
114
  # Convert a dry-types type to a JSON Schema hash.
175
- sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) }
176
115
  def dry_type_to_json_schema(type) # rubocop:disable Metrics
177
116
  type_class = type.class.name || ''
178
117
 
@@ -198,7 +137,6 @@ module Restate
198
137
  end
199
138
 
200
139
  # Convert a nominal dry-type (with .primitive) to JSON Schema.
201
- sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) }
202
140
  def nominal_to_json_schema(type)
203
141
  prim = type.primitive
204
142
  return dry_struct_to_json_schema(prim) if dry_struct?(prim)
@@ -210,32 +148,25 @@ module Restate
210
148
  # Serde wrapper for primitive types and classes with a .json_schema method.
211
149
  # Delegates serialize/deserialize to JsonSerde, adds schema.
212
150
  class TypeSerde
213
- extend T::Sig
214
-
215
- sig { returns(T.untyped) }
216
151
  attr_reader :type_class
217
152
 
218
153
  # Create a TypeSerde for the given type with a precomputed JSON Schema.
219
- sig { params(type: T.untyped, schema: T.nilable(T::Hash[String, T.untyped])).void }
220
154
  def initialize(type, schema)
221
155
  @type_class = type
222
156
  @schema = schema
223
157
  end
224
158
 
225
159
  # Serialize a Ruby object to JSON bytes via JsonSerde.
226
- sig { params(obj: T.untyped).returns(String) }
227
160
  def serialize(obj)
228
161
  JsonSerde.serialize(obj)
229
162
  end
230
163
 
231
164
  # Deserialize JSON bytes to a Ruby object via JsonSerde.
232
- sig { params(buf: T.nilable(String)).returns(T.untyped) }
233
165
  def deserialize(buf)
234
166
  JsonSerde.deserialize(buf)
235
167
  end
236
168
 
237
169
  # Return the JSON Schema for this type.
238
- sig { returns(T.nilable(T::Hash[String, T.untyped])) }
239
170
  def json_schema
240
171
  @schema
241
172
  end
@@ -244,28 +175,22 @@ module Restate
244
175
  # Serde for Dry::Struct types.
245
176
  # Deserializes JSON into struct instances, serializes structs to JSON.
246
177
  class DryStructSerde
247
- extend T::Sig
248
-
249
- sig { returns(T.untyped) }
250
178
  attr_reader :struct_class
251
179
 
252
180
  # Create a DryStructSerde for the given Dry::Struct class.
253
- sig { params(struct_class: T.untyped).void }
254
181
  def initialize(struct_class)
255
182
  @struct_class = struct_class
256
183
  end
257
184
 
258
185
  # Serialize a Dry::Struct (or hash-like object) to JSON bytes.
259
- sig { params(obj: T.untyped).returns(String) }
260
186
  def serialize(obj)
261
- return ''.b if obj.nil?
187
+ return EMPTY_BYTES if obj.nil?
262
188
 
263
189
  hash = obj.respond_to?(:to_h) ? obj.to_h : obj
264
190
  JSON.generate(hash).b
265
191
  end
266
192
 
267
193
  # Deserialize JSON bytes into a Dry::Struct instance.
268
- sig { params(buf: T.nilable(String)).returns(T.untyped) }
269
194
  def deserialize(buf)
270
195
  return nil if buf.nil? || buf.empty?
271
196
 
@@ -274,49 +199,8 @@ module Restate
274
199
  end
275
200
 
276
201
  # Return the JSON Schema derived from the Dry::Struct definition.
277
- sig { returns(T::Hash[String, T.untyped]) }
278
202
  def json_schema
279
203
  @json_schema ||= Serde.dry_struct_to_json_schema(@struct_class)
280
204
  end
281
205
  end
282
-
283
- # Serde for T::Struct types (Sorbet's native typed structs).
284
- # Uses T::Struct#serialize for output and T::Struct.from_hash for input.
285
- # Generates JSON Schema from T::Struct props introspection.
286
- class TStructSerde
287
- extend T::Sig
288
-
289
- sig { returns(T.class_of(T::Struct)) }
290
- attr_reader :struct_class
291
-
292
- # Create a TStructSerde for the given T::Struct subclass.
293
- sig { params(struct_class: T.class_of(T::Struct)).void }
294
- def initialize(struct_class)
295
- @struct_class = struct_class
296
- end
297
-
298
- # Serialize a T::Struct instance to JSON bytes.
299
- sig { params(obj: T.untyped).returns(String) }
300
- def serialize(obj)
301
- return ''.b if obj.nil?
302
-
303
- hash = obj.is_a?(T::Struct) ? obj.serialize : obj
304
- JSON.generate(hash).b
305
- end
306
-
307
- # Deserialize JSON bytes into a T::Struct instance.
308
- sig { params(buf: T.nilable(String)).returns(T.untyped) }
309
- def deserialize(buf)
310
- return nil if buf.nil? || buf.empty?
311
-
312
- hash = JSON.parse(buf)
313
- T.unsafe(@struct_class).from_hash(hash)
314
- end
315
-
316
- # Return the JSON Schema derived from the T::Struct props.
317
- sig { returns(T::Hash[String, T.untyped]) }
318
- def json_schema
319
- @json_schema ||= Serde.t_struct_to_json_schema(@struct_class)
320
- end
321
- end
322
206
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: ignore
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'async'
@@ -14,21 +14,17 @@ module Restate
14
14
  # GET /health → health check
15
15
  # POST /invoke/:service/:handler → handler invocation
16
16
  class Server
17
- extend T::Sig
17
+ SDK_VERSION = Internal::SDK_VERSION
18
+ X_RESTATE_SERVER = "restate-sdk-ruby/#{SDK_VERSION}".freeze
18
19
 
19
- SDK_VERSION = T.let(Internal::SDK_VERSION, String)
20
- X_RESTATE_SERVER = T.let("restate-sdk-ruby/#{SDK_VERSION}".freeze, String)
20
+ LOGGER = Logger.new($stdout, progname: 'Restate::Server')
21
21
 
22
- LOGGER = T.let(Logger.new($stdout, progname: 'Restate::Server'), Logger)
23
-
24
- sig { params(endpoint: Endpoint).void }
25
22
  def initialize(endpoint)
26
- @endpoint = T.let(endpoint, Endpoint)
27
- @identity_verifier = T.let(Internal::IdentityVerifier.new(endpoint.identity_keys), Internal::IdentityVerifier)
23
+ @endpoint = endpoint
24
+ @identity_verifier = Internal::IdentityVerifier.new(endpoint.identity_keys)
28
25
  end
29
26
 
30
27
  # Rack interface
31
- sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) }
32
28
  def call(env)
33
29
  path = env['PATH_INFO'] || '/'
34
30
  parsed = parse_path(path)
@@ -51,7 +47,6 @@ module Restate
51
47
 
52
48
  private
53
49
 
54
- sig { params(path: String).returns(T::Hash[Symbol, T.untyped]) }
55
50
  def parse_path(path)
56
51
  segments = path.split('/').reject(&:empty?)
57
52
 
@@ -77,22 +72,18 @@ module Restate
77
72
  end
78
73
  end
79
74
 
80
- sig { returns(T.untyped) }
81
75
  def health_response
82
76
  [200, { 'content-type' => 'application/json', 'x-restate-server' => X_RESTATE_SERVER }, ['{"status":"ok"}']]
83
77
  end
84
78
 
85
- sig { returns(T.untyped) }
86
79
  def not_found_response
87
80
  [404, { 'x-restate-server' => X_RESTATE_SERVER }, ['']]
88
81
  end
89
82
 
90
- sig { params(status: Integer, message: String).returns(T.untyped) }
91
83
  def error_response(status, message)
92
84
  [status, { 'content-type' => 'text/plain', 'x-restate-server' => X_RESTATE_SERVER }, [message]]
93
85
  end
94
86
 
95
- sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) }
96
87
  def handle_discover(env)
97
88
  # Detect HTTP version for protocol mode
98
89
  http_version = env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.1'
@@ -119,7 +110,6 @@ module Restate
119
110
  end
120
111
  end
121
112
 
122
- sig { params(accept: String).returns(T.nilable(Integer)) }
123
113
  def negotiate_version(accept)
124
114
  if accept.include?('application/vnd.restate.endpointmanifest.v4+json')
125
115
  4
@@ -132,7 +122,6 @@ module Restate
132
122
  end
133
123
  end
134
124
 
135
- sig { params(env: T::Hash[String, T.untyped], service_name: T.untyped, handler_name: T.untyped).returns(T.untyped) }
136
125
  def handle_invocation(env, service_name, handler_name)
137
126
  # Verify identity
138
127
  request_headers = extract_headers(env)
@@ -154,7 +143,6 @@ module Restate
154
143
  process_invocation(env, handler, request_headers)
155
144
  end
156
145
 
157
- sig { params(env: T::Hash[String, T.untyped], handler: T.untyped, request_headers: T.untyped).returns(T.untyped) }
158
146
  def process_invocation(env, handler, request_headers)
159
147
  vm = VMWrapper.new(request_headers)
160
148
  status, response_headers = vm.get_response_head
@@ -171,7 +159,7 @@ module Restate
171
159
  # Read request body chunks and feed to VM until ready to execute,
172
160
  # then continue feeding remaining chunks via the input queue.
173
161
  rack_input = env['rack.input']
174
- ready = T.let(false, T::Boolean)
162
+ ready = false
175
163
  if rack_input
176
164
  # Feed chunks until the VM has enough to start execution
177
165
  while (chunk = rack_input.read_partial(16_384))
@@ -205,7 +193,8 @@ module Restate
205
193
  invocation: invocation,
206
194
  send_output: send_output,
207
195
  input_queue: input_queue,
208
- middleware: @endpoint.middleware
196
+ middleware: @endpoint.middleware,
197
+ outbound_middleware: @endpoint.outbound_middleware
209
198
  )
210
199
 
211
200
  # Spawn the handler as an async task so the response body can stream
@@ -246,11 +235,8 @@ module Restate
246
235
  # Rack 3 streaming body that yields chunks from an Async::Queue.
247
236
  # Terminates when nil is dequeued.
248
237
  class StreamingBody
249
- extend T::Sig
250
-
251
- sig { params(queue: Async::Queue).void }
252
238
  def initialize(queue)
253
- @queue = T.let(queue, Async::Queue)
239
+ @queue = queue
254
240
  end
255
241
 
256
242
  def each
@@ -263,13 +249,12 @@ module Restate
263
249
  end
264
250
  end
265
251
 
266
- sig { params(env: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]]) }
267
252
  def extract_headers(env)
268
- headers = T.let([], T::Array[T::Array[String]])
253
+ headers = []
269
254
  env.each do |key, value|
270
255
  next unless key.start_with?('HTTP_')
271
256
 
272
- header_name = key.sub('HTTP_', '').tr('_', '-').downcase
257
+ header_name = key.byteslice(5..).tr('_', '-').downcase!
273
258
  headers << [header_name, value]
274
259
  end
275
260
  # Also include content-type and content-length if present