restate-sdk 0.6.0 → 0.8.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.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +31 -14
- data/ext/restate_internal/Cargo.toml +1 -1
- data/lib/restate/client.rb +157 -0
- data/lib/restate/config.rb +35 -0
- data/lib/restate/context.rb +13 -128
- data/lib/restate/discovery.rb +6 -15
- data/lib/restate/durable_future.rb +15 -39
- data/lib/restate/endpoint.rb +5 -22
- data/lib/restate/errors.rb +2 -15
- data/lib/restate/handler.rb +21 -20
- data/lib/restate/serde.rb +14 -130
- data/lib/restate/server.rb +10 -26
- data/lib/restate/server_context.rb +50 -248
- data/lib/restate/service.rb +24 -3
- data/lib/restate/service_dsl.rb +70 -6
- data/lib/restate/service_proxy.rb +73 -0
- data/lib/restate/testing.rb +1 -1
- data/lib/restate/version.rb +2 -2
- data/lib/restate/virtual_object.rb +27 -4
- data/lib/restate/vm.rb +9 -74
- data/lib/restate/workflow.rb +27 -4
- data/lib/restate.rb +222 -62
- data/sig/restate.rbs +196 -0
- metadata +6 -18
- data/lib/tapioca/dsl/compilers/restate.rb +0 -116
- data/rbi/restate-sdk.rbi +0 -307
|
@@ -5,24 +5,19 @@ module Restate
|
|
|
5
5
|
# A durable future wrapping a VM handle. Lazily resolves on first +await+ and caches the result.
|
|
6
6
|
# Returned by +ctx.run+ and +ctx.sleep+.
|
|
7
7
|
class DurableFuture
|
|
8
|
-
extend T::Sig
|
|
9
|
-
|
|
10
|
-
sig { returns(Integer) }
|
|
11
8
|
attr_reader :handle
|
|
12
9
|
|
|
13
|
-
sig { params(ctx: ServerContext, handle: Integer, serde: T.untyped).void }
|
|
14
10
|
def initialize(ctx, handle, serde: nil)
|
|
15
|
-
@ctx =
|
|
16
|
-
@handle =
|
|
17
|
-
@serde =
|
|
18
|
-
@resolved =
|
|
19
|
-
@value =
|
|
11
|
+
@ctx = ctx
|
|
12
|
+
@handle = handle
|
|
13
|
+
@serde = serde
|
|
14
|
+
@resolved = false
|
|
15
|
+
@value = nil
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
# Block until the result is available and return it. Caches across calls.
|
|
23
19
|
#
|
|
24
20
|
# @return [Object] the deserialized result
|
|
25
|
-
sig { returns(T.untyped) }
|
|
26
21
|
def await
|
|
27
22
|
unless @resolved
|
|
28
23
|
raw = @ctx.resolve_handle(@handle)
|
|
@@ -35,7 +30,6 @@ module Restate
|
|
|
35
30
|
# Check whether the future has completed (non-blocking).
|
|
36
31
|
#
|
|
37
32
|
# @return [Boolean]
|
|
38
|
-
sig { returns(T::Boolean) }
|
|
39
33
|
def completed?
|
|
40
34
|
@resolved || @ctx.completed?(@handle)
|
|
41
35
|
end
|
|
@@ -45,26 +39,15 @@ module Restate
|
|
|
45
39
|
# Adds +invocation_id+ and +cancel+ on top of DurableFuture.
|
|
46
40
|
# Returned by +ctx.service_call+, +ctx.object_call+, +ctx.workflow_call+.
|
|
47
41
|
class DurableCallFuture < DurableFuture
|
|
48
|
-
extend T::Sig
|
|
49
|
-
|
|
50
|
-
sig do
|
|
51
|
-
params(
|
|
52
|
-
ctx: ServerContext,
|
|
53
|
-
result_handle: Integer,
|
|
54
|
-
invocation_id_handle: Integer,
|
|
55
|
-
output_serde: T.untyped
|
|
56
|
-
).void
|
|
57
|
-
end
|
|
58
42
|
def initialize(ctx, result_handle, invocation_id_handle, output_serde:)
|
|
59
43
|
super(ctx, result_handle)
|
|
60
|
-
@invocation_id_handle =
|
|
61
|
-
@output_serde =
|
|
62
|
-
@invocation_id_resolved =
|
|
63
|
-
@invocation_id_value =
|
|
44
|
+
@invocation_id_handle = invocation_id_handle
|
|
45
|
+
@output_serde = output_serde
|
|
46
|
+
@invocation_id_resolved = false
|
|
47
|
+
@invocation_id_value = nil
|
|
64
48
|
end
|
|
65
49
|
|
|
66
50
|
# Block until the result is available and return it. Deserializes via +output_serde+.
|
|
67
|
-
sig { returns(T.untyped) }
|
|
68
51
|
def await
|
|
69
52
|
unless @resolved
|
|
70
53
|
raw = @ctx.resolve_handle(@handle)
|
|
@@ -81,17 +64,15 @@ module Restate
|
|
|
81
64
|
# Returns the invocation ID of the remote call. Lazily resolved.
|
|
82
65
|
#
|
|
83
66
|
# @return [String] the invocation ID
|
|
84
|
-
sig { returns(String) }
|
|
85
67
|
def invocation_id
|
|
86
68
|
unless @invocation_id_resolved
|
|
87
69
|
@invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
|
|
88
70
|
@invocation_id_resolved = true
|
|
89
71
|
end
|
|
90
|
-
|
|
72
|
+
@invocation_id_value
|
|
91
73
|
end
|
|
92
74
|
|
|
93
75
|
# Cancel the remote invocation.
|
|
94
|
-
sig { void }
|
|
95
76
|
def cancel
|
|
96
77
|
@ctx.cancel_invocation(invocation_id)
|
|
97
78
|
end
|
|
@@ -100,30 +81,25 @@ module Restate
|
|
|
100
81
|
# A handle for fire-and-forget send operations.
|
|
101
82
|
# Returned by +ctx.service_send+, +ctx.object_send+, +ctx.workflow_send+.
|
|
102
83
|
class SendHandle
|
|
103
|
-
extend T::Sig
|
|
104
|
-
|
|
105
|
-
sig { params(ctx: ServerContext, invocation_id_handle: Integer).void }
|
|
106
84
|
def initialize(ctx, invocation_id_handle)
|
|
107
|
-
@ctx =
|
|
108
|
-
@invocation_id_handle =
|
|
109
|
-
@invocation_id_resolved =
|
|
110
|
-
@invocation_id_value =
|
|
85
|
+
@ctx = ctx
|
|
86
|
+
@invocation_id_handle = invocation_id_handle
|
|
87
|
+
@invocation_id_resolved = false
|
|
88
|
+
@invocation_id_value = nil
|
|
111
89
|
end
|
|
112
90
|
|
|
113
91
|
# Returns the invocation ID of the sent call. Lazily resolved.
|
|
114
92
|
#
|
|
115
93
|
# @return [String] the invocation ID
|
|
116
|
-
sig { returns(String) }
|
|
117
94
|
def invocation_id
|
|
118
95
|
unless @invocation_id_resolved
|
|
119
96
|
@invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
|
|
120
97
|
@invocation_id_resolved = true
|
|
121
98
|
end
|
|
122
|
-
|
|
99
|
+
@invocation_id_value
|
|
123
100
|
end
|
|
124
101
|
|
|
125
102
|
# Cancel the remote invocation.
|
|
126
|
-
sig { void }
|
|
127
103
|
def cancel
|
|
128
104
|
@ctx.cancel_invocation(invocation_id)
|
|
129
105
|
end
|
data/lib/restate/endpoint.rb
CHANGED
|
@@ -4,26 +4,15 @@
|
|
|
4
4
|
module Restate
|
|
5
5
|
# Container for registered services. Bind services here, then create the Rack app.
|
|
6
6
|
class Endpoint
|
|
7
|
-
|
|
7
|
+
attr_reader :services, :identity_keys, :middleware
|
|
8
8
|
|
|
9
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
|
10
|
-
attr_reader :services
|
|
11
|
-
|
|
12
|
-
sig { returns(T::Array[String]) }
|
|
13
|
-
attr_reader :identity_keys
|
|
14
|
-
|
|
15
|
-
sig { returns(T.nilable(String)) }
|
|
16
9
|
attr_accessor :protocol
|
|
17
10
|
|
|
18
|
-
sig { returns(T::Array[T.untyped]) }
|
|
19
|
-
attr_reader :middleware
|
|
20
|
-
|
|
21
|
-
sig { void }
|
|
22
11
|
def initialize
|
|
23
|
-
@services =
|
|
24
|
-
@protocol =
|
|
25
|
-
@identity_keys =
|
|
26
|
-
@middleware =
|
|
12
|
+
@services = {}
|
|
13
|
+
@protocol = nil
|
|
14
|
+
@identity_keys = []
|
|
15
|
+
@middleware = []
|
|
27
16
|
end
|
|
28
17
|
|
|
29
18
|
# Bind one or more services to this endpoint.
|
|
@@ -31,7 +20,6 @@ module Restate
|
|
|
31
20
|
# @param svcs [Array<Class<Service>, Class<VirtualObject>, Class<Workflow>>] services to bind
|
|
32
21
|
# @return [self]
|
|
33
22
|
# @raise [ArgumentError] if a service with the same name is already bound
|
|
34
|
-
sig { params(svcs: T.untyped).returns(T.self_type) }
|
|
35
23
|
def bind(*svcs)
|
|
36
24
|
svcs.each do |svc|
|
|
37
25
|
svc_name = svc.service_name
|
|
@@ -43,21 +31,18 @@ module Restate
|
|
|
43
31
|
end
|
|
44
32
|
|
|
45
33
|
# Force bidirectional streaming protocol.
|
|
46
|
-
sig { returns(T.self_type) }
|
|
47
34
|
def streaming_protocol
|
|
48
35
|
@protocol = 'bidi'
|
|
49
36
|
self
|
|
50
37
|
end
|
|
51
38
|
|
|
52
39
|
# Force request/response protocol.
|
|
53
|
-
sig { returns(T.self_type) }
|
|
54
40
|
def request_response_protocol
|
|
55
41
|
@protocol = 'request_response'
|
|
56
42
|
self
|
|
57
43
|
end
|
|
58
44
|
|
|
59
45
|
# Add an identity key for request verification.
|
|
60
|
-
sig { params(key: String).returns(T.self_type) }
|
|
61
46
|
def identity_key(key)
|
|
62
47
|
@identity_keys << key
|
|
63
48
|
self
|
|
@@ -116,7 +101,6 @@ module Restate
|
|
|
116
101
|
# @param args [Array] positional arguments for the middleware constructor
|
|
117
102
|
# @param kwargs [Hash] keyword arguments for the middleware constructor
|
|
118
103
|
# @return [self]
|
|
119
|
-
sig { params(klass: T.untyped, args: T.untyped, kwargs: T.untyped).returns(T.self_type) }
|
|
120
104
|
def use(klass, *args, **kwargs)
|
|
121
105
|
instance = if kwargs.empty?
|
|
122
106
|
klass.new(*args)
|
|
@@ -128,7 +112,6 @@ module Restate
|
|
|
128
112
|
end
|
|
129
113
|
|
|
130
114
|
# Build and return the Rack-compatible application.
|
|
131
|
-
sig { returns(T.untyped) }
|
|
132
115
|
def app
|
|
133
116
|
require_relative 'server'
|
|
134
117
|
Server.new(self)
|
data/lib/restate/errors.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
module Restate
|
|
@@ -7,24 +7,17 @@ module Restate
|
|
|
7
7
|
# @example
|
|
8
8
|
# raise Restate::TerminalError.new('not found', status_code: 404)
|
|
9
9
|
class TerminalError < StandardError
|
|
10
|
-
extend T::Sig
|
|
11
|
-
|
|
12
|
-
sig { returns(Integer) }
|
|
13
10
|
attr_reader :status_code
|
|
14
11
|
|
|
15
|
-
sig { params(message: String, status_code: Integer).void }
|
|
16
12
|
def initialize(message = 'Internal Server Error', status_code: 500)
|
|
17
13
|
super(message)
|
|
18
|
-
@status_code =
|
|
14
|
+
@status_code = status_code
|
|
19
15
|
end
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
# Internal: raised when the VM suspends execution.
|
|
23
19
|
# User code should NOT catch this.
|
|
24
20
|
class SuspendedError < StandardError
|
|
25
|
-
extend T::Sig
|
|
26
|
-
|
|
27
|
-
sig { void }
|
|
28
21
|
def initialize
|
|
29
22
|
super(
|
|
30
23
|
"Invocation got suspended, Restate will resume this invocation when progress can be made.\n" \
|
|
@@ -37,9 +30,6 @@ module Restate
|
|
|
37
30
|
|
|
38
31
|
# Internal: raised when the VM encounters a retryable error.
|
|
39
32
|
class InternalError < StandardError
|
|
40
|
-
extend T::Sig
|
|
41
|
-
|
|
42
|
-
sig { void }
|
|
43
33
|
def initialize
|
|
44
34
|
super(
|
|
45
35
|
"Invocation attempt raised a retryable error.\n" \
|
|
@@ -50,9 +40,6 @@ module Restate
|
|
|
50
40
|
|
|
51
41
|
# Internal: raised when the HTTP connection is lost.
|
|
52
42
|
class DisconnectedError < StandardError
|
|
53
|
-
extend T::Sig
|
|
54
|
-
|
|
55
|
-
sig { void }
|
|
56
43
|
def initialize
|
|
57
44
|
super('Disconnected. The connection to the restate server was lost. Restate will retry the attempt.')
|
|
58
45
|
end
|
data/lib/restate/handler.rb
CHANGED
|
@@ -27,38 +27,39 @@ module Restate
|
|
|
27
27
|
keyword_init: true
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
extend T::Sig
|
|
31
|
-
|
|
32
30
|
module_function
|
|
33
31
|
|
|
34
32
|
# Invoke a handler with the context and raw input bytes.
|
|
35
33
|
# The context is passed as the first argument to every handler.
|
|
36
34
|
# Middleware (if any) wraps the handler call.
|
|
37
35
|
# Returns raw output bytes.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
def invoke_handler(handler:, ctx:, in_buffer:, middleware: [])
|
|
37
|
+
out_arg = if middleware.empty?
|
|
38
|
+
invoke_handler_direct(handler, in_buffer)
|
|
39
|
+
else
|
|
40
|
+
invoke_handler_with_middleware(handler, ctx, in_buffer, middleware)
|
|
41
|
+
end
|
|
42
|
+
handler.handler_io.output_serde.serialize(out_arg)
|
|
41
43
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
50
|
-
handler.callable.call(ctx, in_arg)
|
|
51
|
-
else
|
|
52
|
-
handler.callable.call(ctx)
|
|
44
|
+
|
|
45
|
+
def invoke_handler_direct(handler, in_buffer)
|
|
46
|
+
if handler.arity == 1
|
|
47
|
+
begin
|
|
48
|
+
in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
|
|
53
51
|
end
|
|
52
|
+
handler.callable.call(in_arg)
|
|
53
|
+
else
|
|
54
|
+
handler.callable.call
|
|
54
55
|
end
|
|
56
|
+
end
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
def invoke_handler_with_middleware(handler, ctx, in_buffer, middleware)
|
|
59
|
+
call_handler = Kernel.proc { invoke_handler_direct(handler, in_buffer) }
|
|
57
60
|
chain = middleware.reverse.reduce(call_handler) do |nxt, mw|
|
|
58
61
|
Kernel.proc { mw.call(handler, ctx, &nxt) }
|
|
59
62
|
end
|
|
60
|
-
|
|
61
|
-
out_arg = chain.call
|
|
62
|
-
handler.handler_io.output_serde.serialize(out_arg)
|
|
63
|
+
chain.call
|
|
63
64
|
end
|
|
64
65
|
end
|
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
|
|
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
|
|
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
|
|
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
|