ruby-utcp 1.1.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 +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +22 -0
- data/Makefile +226 -0
- data/README.md +331 -0
- data/examples/basic.rb +33 -0
- data/examples/cli.rb +32 -0
- data/examples/generated/__init__.py +1 -0
- data/examples/generated/utcp_pb2.py +46 -0
- data/examples/generated/utcp_pb2_grpc.py +183 -0
- data/examples/graphql.rb +15 -0
- data/examples/grpc.rb +42 -0
- data/examples/grpc_python.py +52 -0
- data/examples/http.rb +17 -0
- data/examples/mcp.rb +28 -0
- data/examples/servers/graphql_server.rb +39 -0
- data/examples/servers/grpc_server.py +97 -0
- data/examples/servers/grpc_server.rb +62 -0
- data/examples/servers/http_helpers.rb +34 -0
- data/examples/servers/http_server.rb +28 -0
- data/examples/servers/mcp_stdio_server.rb +43 -0
- data/examples/servers/requirements-grpc.txt +2 -0
- data/examples/servers/sse_server.rb +36 -0
- data/examples/servers/streamable_http_server.rb +39 -0
- data/examples/servers/tcp_server.rb +58 -0
- data/examples/servers/udp_server.rb +33 -0
- data/examples/servers/webrtc_server.rb +78 -0
- data/examples/servers/websocket_server.rb +92 -0
- data/examples/sse.rb +16 -0
- data/examples/streamable_http.rb +17 -0
- data/examples/tcp.rb +20 -0
- data/examples/text.rb +23 -0
- data/examples/udp.rb +18 -0
- data/examples/webrtc.rb +19 -0
- data/examples/websocket.rb +17 -0
- data/lib/ruby-utcp.rb +4 -0
- data/lib/utcp/client.rb +217 -0
- data/lib/utcp/config.rb +79 -0
- data/lib/utcp/errors.rb +48 -0
- data/lib/utcp/migration.rb +88 -0
- data/lib/utcp/models.rb +794 -0
- data/lib/utcp/openapi_converter.rb +179 -0
- data/lib/utcp/protocols/base.rb +97 -0
- data/lib/utcp/protocols/cli.rb +186 -0
- data/lib/utcp/protocols/file.rb +52 -0
- data/lib/utcp/protocols/graphql.rb +277 -0
- data/lib/utcp/protocols/grpc.rb +207 -0
- data/lib/utcp/protocols/http.rb +340 -0
- data/lib/utcp/protocols/http_stream_support.rb +122 -0
- data/lib/utcp/protocols/mcp.rb +339 -0
- data/lib/utcp/protocols/socket_support.rb +51 -0
- data/lib/utcp/protocols/sse.rb +107 -0
- data/lib/utcp/protocols/streamable_http.rb +78 -0
- data/lib/utcp/protocols/tcp.rb +143 -0
- data/lib/utcp/protocols/text.rb +44 -0
- data/lib/utcp/protocols/udp.rb +61 -0
- data/lib/utcp/protocols/webrtc.rb +217 -0
- data/lib/utcp/protocols/websocket.rb +350 -0
- data/lib/utcp/registry.rb +67 -0
- data/lib/utcp/repository.rb +137 -0
- data/lib/utcp/serializer.rb +71 -0
- data/lib/utcp/utils.rb +118 -0
- data/lib/utcp/variables.rb +170 -0
- data/lib/utcp/version.rb +6 -0
- data/lib/utcp.rb +70 -0
- data/proto/utcp.proto +31 -0
- metadata +148 -0
data/lib/utcp/models.rb
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
|
|
8
|
+
module UTCP
|
|
9
|
+
class JsonSchema
|
|
10
|
+
include ModelSerialization
|
|
11
|
+
COMMON_FIELDS = %w[
|
|
12
|
+
$schema $id title description type properties items required enum const default
|
|
13
|
+
examples format additionalProperties pattern minimum maximum minLength maxLength
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def self.from_h(value)
|
|
17
|
+
return value if value.is_a?(self)
|
|
18
|
+
|
|
19
|
+
new(Utils.hash!(value || {}, "JSON Schema"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(value = nil, **keywords)
|
|
23
|
+
source = value || keywords
|
|
24
|
+
@data = Utils.stringify_keys(Utils.hash!(source, "JSON Schema"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def [](key)
|
|
28
|
+
@data[schema_key(key)]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def []=(key, value)
|
|
32
|
+
@data[schema_key(key)] = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
Utils.deep_copy(@data)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def method_missing(name, *arguments)
|
|
40
|
+
key = name.to_s
|
|
41
|
+
return @data[schema_key(key)] if arguments.empty? && schema_field?(key)
|
|
42
|
+
return @data[schema_key(key[0..-2])] = arguments.first if key.end_with?("=") && arguments.length == 1
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def respond_to_missing?(name, include_private = false)
|
|
48
|
+
key = name.to_s.sub(/=$/, "")
|
|
49
|
+
schema_field?(key) || super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def schema_key(key)
|
|
55
|
+
{ "schema_" => "$schema", "id_" => "$id" }.fetch(key.to_s, key.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def schema_field?(key)
|
|
59
|
+
actual = schema_key(key)
|
|
60
|
+
@data.key?(actual) || COMMON_FIELDS.include?(actual)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
JSONSchema = JsonSchema
|
|
64
|
+
|
|
65
|
+
class Auth
|
|
66
|
+
include ModelSerialization
|
|
67
|
+
attr_accessor :auth_type
|
|
68
|
+
|
|
69
|
+
def self.from_h(value)
|
|
70
|
+
return value if value.is_a?(Auth)
|
|
71
|
+
|
|
72
|
+
data = Utils.stringify_keys(Utils.hash!(value, "auth"))
|
|
73
|
+
type = Utils.required_string!(data["auth_type"], "auth.auth_type")
|
|
74
|
+
klass = UTCP.auth_class(type)
|
|
75
|
+
raise ValidationError.new("unsupported authentication type #{type.inspect}", path: "auth.auth_type") unless klass
|
|
76
|
+
|
|
77
|
+
klass.new(**Utils.symbolize_keys(data))
|
|
78
|
+
rescue ValidationError
|
|
79
|
+
raise
|
|
80
|
+
rescue StandardError => error
|
|
81
|
+
raise SerializerValidationError, "Invalid auth: #{error.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def initialize(auth_type:, **extra)
|
|
85
|
+
@auth_type = Utils.required_string!(auth_type, "auth_type")
|
|
86
|
+
@extra = Utils.stringify_keys(extra)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_h
|
|
90
|
+
{ "auth_type" => auth_type }.merge(Utils.deep_copy(@extra))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def [](key)
|
|
94
|
+
@extra[key.to_s]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class ApiKeyAuth < Auth
|
|
99
|
+
LOCATIONS = %w[header query cookie].freeze
|
|
100
|
+
attr_accessor :api_key, :var_name, :location
|
|
101
|
+
|
|
102
|
+
def initialize(api_key:, auth_type: "api_key", var_name: "X-Api-Key", location: "header", **extra)
|
|
103
|
+
super(auth_type: auth_type, **extra)
|
|
104
|
+
@api_key = Utils.required_string!(api_key, "api_key")
|
|
105
|
+
@var_name = Utils.required_string!(var_name, "var_name")
|
|
106
|
+
@location = location.to_s
|
|
107
|
+
raise ValidationError.new("must be header, query, or cookie", path: "location") unless LOCATIONS.include?(@location)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_h
|
|
111
|
+
super.merge("api_key" => api_key, "var_name" => var_name, "location" => location)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class BasicAuth < Auth
|
|
116
|
+
attr_accessor :username, :password
|
|
117
|
+
|
|
118
|
+
def initialize(username:, password:, auth_type: "basic", **extra)
|
|
119
|
+
super(auth_type: auth_type, **extra)
|
|
120
|
+
@username = Utils.required_string!(username, "username")
|
|
121
|
+
@password = Utils.required_string!(password, "password")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_h
|
|
125
|
+
super.merge("username" => username, "password" => password)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class OAuth2Auth < Auth
|
|
130
|
+
attr_accessor :token_url, :client_id, :client_secret, :scope
|
|
131
|
+
|
|
132
|
+
def initialize(token_url:, client_id:, client_secret:, auth_type: "oauth2", scope: nil, **extra)
|
|
133
|
+
super(auth_type: auth_type, **extra)
|
|
134
|
+
@token_url = Utils.required_string!(token_url, "token_url")
|
|
135
|
+
@client_id = Utils.required_string!(client_id, "client_id")
|
|
136
|
+
@client_secret = Utils.required_string!(client_secret, "client_secret")
|
|
137
|
+
@scope = Utils.optional_string!(scope, "scope")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def to_h
|
|
141
|
+
Utils.compact_hash(super.merge(
|
|
142
|
+
"token_url" => token_url,
|
|
143
|
+
"client_id" => client_id,
|
|
144
|
+
"client_secret" => client_secret,
|
|
145
|
+
"scope" => scope
|
|
146
|
+
))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class CallTemplate
|
|
151
|
+
include ModelSerialization
|
|
152
|
+
attr_accessor :name, :call_template_type, :auth, :allowed_communication_protocols
|
|
153
|
+
|
|
154
|
+
def self.from_h(value)
|
|
155
|
+
return value if value.is_a?(CallTemplate)
|
|
156
|
+
|
|
157
|
+
data = Utils.stringify_keys(Utils.hash!(value, "call template"))
|
|
158
|
+
type = Utils.required_string!(data["call_template_type"], "call_template_type")
|
|
159
|
+
klass = UTCP.call_template_class(type)
|
|
160
|
+
raise ValidationError.new("unsupported call template type #{type.inspect}", path: "call_template_type") unless klass
|
|
161
|
+
|
|
162
|
+
klass.new(**Utils.symbolize_keys(data))
|
|
163
|
+
rescue ValidationError
|
|
164
|
+
raise
|
|
165
|
+
rescue StandardError => error
|
|
166
|
+
raise SerializerValidationError, "Invalid call template: #{error.message}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def initialize(call_template_type:, name: nil, auth: nil, allowed_communication_protocols: nil, **extra)
|
|
170
|
+
@name = name.nil? || name.to_s.empty? ? SecureRandom.hex(16) : name.to_s
|
|
171
|
+
@call_template_type = Utils.required_string!(call_template_type.to_s, "call_template_type")
|
|
172
|
+
@auth = auth.nil? ? nil : Auth.from_h(auth)
|
|
173
|
+
@allowed_communication_protocols = normalize_protocols(allowed_communication_protocols)
|
|
174
|
+
@extra = Utils.stringify_keys(extra)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def allowed_protocols
|
|
178
|
+
protocols = allowed_communication_protocols
|
|
179
|
+
protocols.nil? || protocols.empty? ? [call_template_type] : protocols.dup
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def to_h
|
|
183
|
+
Utils.compact_hash({
|
|
184
|
+
"name" => name,
|
|
185
|
+
"call_template_type" => call_template_type,
|
|
186
|
+
"auth" => auth&.to_h,
|
|
187
|
+
"allowed_communication_protocols" => allowed_communication_protocols&.dup
|
|
188
|
+
}.merge(Utils.deep_copy(@extra)))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def [](key)
|
|
192
|
+
@extra[key.to_s]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def method_missing(name, *arguments)
|
|
196
|
+
key = name.to_s
|
|
197
|
+
return @extra[key] if arguments.empty? && @extra.key?(key)
|
|
198
|
+
return @extra[key[0..-2]] = arguments.first if key.end_with?("=") && arguments.length == 1
|
|
199
|
+
|
|
200
|
+
super
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def respond_to_missing?(name, include_private = false)
|
|
204
|
+
@extra.key?(name.to_s.sub(/=$/, "")) || super
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def normalize_protocols(value)
|
|
210
|
+
return nil if value.nil?
|
|
211
|
+
|
|
212
|
+
protocols = Utils.array!(value, "allowed_communication_protocols").map(&:to_s)
|
|
213
|
+
if protocols.any?(&:empty?)
|
|
214
|
+
raise ValidationError.new("must contain non-empty protocol names", path: "allowed_communication_protocols")
|
|
215
|
+
end
|
|
216
|
+
protocols.uniq
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class HttpCallTemplate < CallTemplate
|
|
221
|
+
METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS].freeze
|
|
222
|
+
attr_accessor :http_method, :url, :content_type, :auth_tools, :headers, :body_field,
|
|
223
|
+
:header_fields, :timeout
|
|
224
|
+
|
|
225
|
+
def initialize(url:, call_template_type: "http", http_method: "GET", content_type: "application/json",
|
|
226
|
+
auth_tools: nil, headers: nil, body_field: "body", header_fields: nil,
|
|
227
|
+
timeout: nil, **common)
|
|
228
|
+
super(call_template_type: call_template_type, **common)
|
|
229
|
+
@url = Utils.required_string!(url, "url")
|
|
230
|
+
@http_method = http_method.to_s.upcase
|
|
231
|
+
raise ValidationError.new("unsupported HTTP method #{@http_method}", path: "http_method") unless METHODS.include?(@http_method)
|
|
232
|
+
|
|
233
|
+
@content_type = Utils.required_string!(content_type, "content_type")
|
|
234
|
+
@auth_tools = auth_tools.nil? ? nil : Auth.from_h(auth_tools)
|
|
235
|
+
@headers = headers.nil? ? {} : Utils.stringify_keys(Utils.hash!(headers, "headers"))
|
|
236
|
+
@body_field = body_field.nil? ? nil : body_field.to_s
|
|
237
|
+
@header_fields = header_fields.nil? ? [] : Utils.array!(header_fields, "header_fields").map(&:to_s)
|
|
238
|
+
@timeout = timeout.nil? ? nil : Float(timeout)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def to_h
|
|
242
|
+
Utils.compact_hash(super.merge(
|
|
243
|
+
"http_method" => http_method,
|
|
244
|
+
"url" => url,
|
|
245
|
+
"content_type" => content_type,
|
|
246
|
+
"auth_tools" => auth_tools&.to_h,
|
|
247
|
+
"headers" => headers.empty? ? nil : Utils.deep_copy(headers),
|
|
248
|
+
"body_field" => body_field,
|
|
249
|
+
"header_fields" => header_fields.empty? ? nil : header_fields.dup,
|
|
250
|
+
"timeout" => timeout
|
|
251
|
+
))
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
class SseCallTemplate < CallTemplate
|
|
256
|
+
attr_accessor :url, :event_type, :reconnect, :retry_timeout, :headers, :body_field,
|
|
257
|
+
:header_fields, :timeout
|
|
258
|
+
|
|
259
|
+
def initialize(url:, call_template_type: "sse", event_type: nil, reconnect: true,
|
|
260
|
+
retry_timeout: 30_000, headers: nil, body_field: nil,
|
|
261
|
+
header_fields: nil, timeout: nil, **common)
|
|
262
|
+
super(call_template_type: call_template_type, **common)
|
|
263
|
+
@url = Utils.required_string!(url, "url")
|
|
264
|
+
@event_type = Utils.optional_string!(event_type, "event_type")
|
|
265
|
+
@reconnect = !!reconnect
|
|
266
|
+
@retry_timeout = Integer(retry_timeout)
|
|
267
|
+
@headers = headers.nil? ? {} : Utils.stringify_keys(Utils.hash!(headers, "headers"))
|
|
268
|
+
@body_field = Utils.optional_string!(body_field, "body_field")
|
|
269
|
+
@header_fields = header_fields.nil? ? [] : Utils.array!(header_fields, "header_fields").map(&:to_s)
|
|
270
|
+
@timeout = timeout.nil? ? nil : Float(timeout)
|
|
271
|
+
raise ValidationError.new("must not be negative", path: "retry_timeout") if @retry_timeout.negative?
|
|
272
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") if @timeout && !@timeout.positive?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def to_h
|
|
276
|
+
Utils.compact_hash(super.merge(
|
|
277
|
+
"url" => url,
|
|
278
|
+
"event_type" => event_type,
|
|
279
|
+
"reconnect" => reconnect,
|
|
280
|
+
"retry_timeout" => retry_timeout,
|
|
281
|
+
"headers" => headers.empty? ? nil : Utils.deep_copy(headers),
|
|
282
|
+
"body_field" => body_field,
|
|
283
|
+
"header_fields" => header_fields.empty? ? nil : header_fields.dup,
|
|
284
|
+
"timeout" => timeout
|
|
285
|
+
))
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
SSECallTemplate = SseCallTemplate
|
|
289
|
+
|
|
290
|
+
class StreamableHttpCallTemplate < CallTemplate
|
|
291
|
+
METHODS = %w[GET POST].freeze
|
|
292
|
+
attr_accessor :url, :http_method, :content_type, :chunk_size, :timeout, :headers,
|
|
293
|
+
:body_field, :header_fields
|
|
294
|
+
|
|
295
|
+
def initialize(url:, call_template_type: "streamable_http", http_method: "GET",
|
|
296
|
+
content_type: "application/octet-stream", chunk_size: 4096,
|
|
297
|
+
timeout: 60_000, headers: nil, body_field: nil,
|
|
298
|
+
header_fields: nil, **common)
|
|
299
|
+
super(call_template_type: call_template_type, **common)
|
|
300
|
+
@url = Utils.required_string!(url, "url")
|
|
301
|
+
@http_method = http_method.to_s.upcase
|
|
302
|
+
raise ValidationError.new("must be GET or POST", path: "http_method") unless METHODS.include?(@http_method)
|
|
303
|
+
|
|
304
|
+
@content_type = Utils.required_string!(content_type, "content_type")
|
|
305
|
+
@chunk_size = Integer(chunk_size)
|
|
306
|
+
@timeout = Integer(timeout)
|
|
307
|
+
@headers = headers.nil? ? {} : Utils.stringify_keys(Utils.hash!(headers, "headers"))
|
|
308
|
+
@body_field = Utils.optional_string!(body_field, "body_field")
|
|
309
|
+
@header_fields = header_fields.nil? ? [] : Utils.array!(header_fields, "header_fields").map(&:to_s)
|
|
310
|
+
raise ValidationError.new("must be greater than zero", path: "chunk_size") unless @chunk_size.positive?
|
|
311
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def to_h
|
|
315
|
+
Utils.compact_hash(super.merge(
|
|
316
|
+
"url" => url,
|
|
317
|
+
"http_method" => http_method,
|
|
318
|
+
"content_type" => content_type,
|
|
319
|
+
"chunk_size" => chunk_size,
|
|
320
|
+
"timeout" => timeout,
|
|
321
|
+
"headers" => headers.empty? ? nil : Utils.deep_copy(headers),
|
|
322
|
+
"body_field" => body_field,
|
|
323
|
+
"header_fields" => header_fields.empty? ? nil : header_fields.dup
|
|
324
|
+
))
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
StreamableHTTPCallTemplate = StreamableHttpCallTemplate
|
|
328
|
+
|
|
329
|
+
class WebSocketCallTemplate < CallTemplate
|
|
330
|
+
RESPONSE_FORMATS = %w[json text raw].freeze
|
|
331
|
+
attr_accessor :url, :message, :protocol, :keep_alive, :response_format, :timeout,
|
|
332
|
+
:headers, :header_fields
|
|
333
|
+
|
|
334
|
+
def initialize(url:, call_template_type: "websocket", message: nil, protocol: nil,
|
|
335
|
+
keep_alive: true, response_format: nil, timeout: 30,
|
|
336
|
+
headers: nil, header_fields: nil, **common)
|
|
337
|
+
super(call_template_type: call_template_type, **common)
|
|
338
|
+
@url = Utils.required_string!(url, "url")
|
|
339
|
+
@message = Utils.deep_copy(message)
|
|
340
|
+
@protocol = Utils.optional_string!(protocol, "protocol")
|
|
341
|
+
@keep_alive = !!keep_alive
|
|
342
|
+
@response_format = Utils.optional_string!(response_format, "response_format")
|
|
343
|
+
if @response_format && !RESPONSE_FORMATS.include?(@response_format)
|
|
344
|
+
raise ValidationError.new("must be json, text, or raw", path: "response_format")
|
|
345
|
+
end
|
|
346
|
+
@timeout = Float(timeout)
|
|
347
|
+
@headers = headers.nil? ? {} : Utils.stringify_keys(Utils.hash!(headers, "headers"))
|
|
348
|
+
@header_fields = header_fields.nil? ? [] : Utils.array!(header_fields, "header_fields").map(&:to_s)
|
|
349
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def to_h
|
|
353
|
+
Utils.compact_hash(super.merge(
|
|
354
|
+
"url" => url,
|
|
355
|
+
"message" => Utils.deep_copy(message),
|
|
356
|
+
"protocol" => protocol,
|
|
357
|
+
"keep_alive" => keep_alive,
|
|
358
|
+
"response_format" => response_format,
|
|
359
|
+
"timeout" => timeout,
|
|
360
|
+
"headers" => headers.empty? ? nil : Utils.deep_copy(headers),
|
|
361
|
+
"header_fields" => header_fields.empty? ? nil : header_fields.dup
|
|
362
|
+
))
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
WebsocketCallTemplate = WebSocketCallTemplate
|
|
366
|
+
|
|
367
|
+
class GrpcCallTemplate < CallTemplate
|
|
368
|
+
attr_accessor :host, :port, :service_name, :method_name, :target, :use_ssl,
|
|
369
|
+
:timeout, :metadata
|
|
370
|
+
|
|
371
|
+
def initialize(host:, port:, call_template_type: "grpc", service_name: "grpcpb.UTCPService",
|
|
372
|
+
method_name: nil, target: nil, use_ssl: true, timeout: 30,
|
|
373
|
+
metadata: nil, **common)
|
|
374
|
+
super(call_template_type: call_template_type, **common)
|
|
375
|
+
@host = Utils.required_string!(host, "host")
|
|
376
|
+
@port = Integer(port)
|
|
377
|
+
@service_name = Utils.required_string!(service_name, "service_name")
|
|
378
|
+
@method_name = Utils.optional_string!(method_name, "method_name")
|
|
379
|
+
@target = Utils.optional_string!(target, "target")
|
|
380
|
+
@use_ssl = !!use_ssl
|
|
381
|
+
@timeout = Float(timeout)
|
|
382
|
+
@metadata = metadata.nil? ? {} : Utils.stringify_keys(Utils.hash!(metadata, "metadata"))
|
|
383
|
+
validate_network_values!
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def to_h
|
|
387
|
+
Utils.compact_hash(super.merge(
|
|
388
|
+
"host" => host,
|
|
389
|
+
"port" => port,
|
|
390
|
+
"service_name" => service_name,
|
|
391
|
+
"method_name" => method_name,
|
|
392
|
+
"target" => target,
|
|
393
|
+
"use_ssl" => use_ssl,
|
|
394
|
+
"timeout" => timeout,
|
|
395
|
+
"metadata" => metadata.empty? ? nil : Utils.deep_copy(metadata)
|
|
396
|
+
))
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
private
|
|
400
|
+
|
|
401
|
+
def validate_network_values!
|
|
402
|
+
raise ValidationError.new("must be between 1 and 65535", path: "port") unless (1..65_535).cover?(@port)
|
|
403
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
GRPCCallTemplate = GrpcCallTemplate
|
|
407
|
+
|
|
408
|
+
class GraphQLCallTemplate < CallTemplate
|
|
409
|
+
OPERATION_TYPES = %w[query mutation subscription].freeze
|
|
410
|
+
attr_accessor :url, :operation_type, :operation_name, :headers, :header_fields,
|
|
411
|
+
:query, :variable_types, :selection_set, :timeout
|
|
412
|
+
|
|
413
|
+
def initialize(url:, call_template_type: "graphql", operation_type: "query",
|
|
414
|
+
operation_name: nil, headers: nil, header_fields: nil, query: nil,
|
|
415
|
+
variable_types: nil, selection_set: nil, timeout: 30, **common)
|
|
416
|
+
super(call_template_type: call_template_type, **common)
|
|
417
|
+
@url = Utils.required_string!(url, "url")
|
|
418
|
+
@operation_type = operation_type.to_s
|
|
419
|
+
unless OPERATION_TYPES.include?(@operation_type)
|
|
420
|
+
raise ValidationError.new("must be query, mutation, or subscription", path: "operation_type")
|
|
421
|
+
end
|
|
422
|
+
@operation_name = Utils.optional_string!(operation_name, "operation_name")
|
|
423
|
+
@headers = headers.nil? ? {} : Utils.stringify_keys(Utils.hash!(headers, "headers"))
|
|
424
|
+
@header_fields = header_fields.nil? ? [] : Utils.array!(header_fields, "header_fields").map(&:to_s)
|
|
425
|
+
@query = Utils.optional_string!(query, "query")
|
|
426
|
+
@variable_types = variable_types.nil? ? {} : Utils.stringify_keys(Utils.hash!(variable_types, "variable_types"))
|
|
427
|
+
@selection_set = Utils.optional_string!(selection_set, "selection_set")
|
|
428
|
+
@timeout = Float(timeout)
|
|
429
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def to_h
|
|
433
|
+
Utils.compact_hash(super.merge(
|
|
434
|
+
"url" => url,
|
|
435
|
+
"operation_type" => operation_type,
|
|
436
|
+
"operation_name" => operation_name,
|
|
437
|
+
"headers" => headers.empty? ? nil : Utils.deep_copy(headers),
|
|
438
|
+
"header_fields" => header_fields.empty? ? nil : header_fields.dup,
|
|
439
|
+
"query" => query,
|
|
440
|
+
"variable_types" => variable_types.empty? ? nil : Utils.deep_copy(variable_types),
|
|
441
|
+
"selection_set" => selection_set,
|
|
442
|
+
"timeout" => timeout
|
|
443
|
+
))
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
GraphqlCallTemplate = GraphQLCallTemplate
|
|
447
|
+
|
|
448
|
+
class TcpCallTemplate < CallTemplate
|
|
449
|
+
FORMATS = %w[json text].freeze
|
|
450
|
+
FRAMING = %w[length_prefix delimiter fixed_length stream].freeze
|
|
451
|
+
attr_accessor :host, :port, :request_data_format, :request_data_template,
|
|
452
|
+
:response_byte_format, :framing_strategy, :length_prefix_bytes,
|
|
453
|
+
:length_prefix_endian, :message_delimiter, :interpret_escape_sequences,
|
|
454
|
+
:fixed_message_length, :max_response_size, :timeout
|
|
455
|
+
|
|
456
|
+
def initialize(host:, port:, call_template_type: "tcp", request_data_format: "json",
|
|
457
|
+
request_data_template: nil, response_byte_format: "utf-8",
|
|
458
|
+
framing_strategy: "stream", length_prefix_bytes: 4,
|
|
459
|
+
length_prefix_endian: "big", message_delimiter: "\\x00",
|
|
460
|
+
interpret_escape_sequences: true, fixed_message_length: nil,
|
|
461
|
+
max_response_size: 65_536, timeout: 30_000, **common)
|
|
462
|
+
super(call_template_type: call_template_type, auth: nil, **common)
|
|
463
|
+
@host = Utils.required_string!(host, "host")
|
|
464
|
+
@port = Integer(port)
|
|
465
|
+
@request_data_format = request_data_format.to_s
|
|
466
|
+
@request_data_template = Utils.optional_string!(request_data_template, "request_data_template")
|
|
467
|
+
@response_byte_format = response_byte_format.nil? ? nil : response_byte_format.to_s
|
|
468
|
+
@framing_strategy = framing_strategy.to_s
|
|
469
|
+
@length_prefix_bytes = Integer(length_prefix_bytes)
|
|
470
|
+
@length_prefix_endian = length_prefix_endian.to_s
|
|
471
|
+
@message_delimiter = message_delimiter.to_s
|
|
472
|
+
@interpret_escape_sequences = !!interpret_escape_sequences
|
|
473
|
+
@fixed_message_length = fixed_message_length.nil? ? nil : Integer(fixed_message_length)
|
|
474
|
+
@max_response_size = Integer(max_response_size)
|
|
475
|
+
@timeout = Integer(timeout)
|
|
476
|
+
validate_socket_values!
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def to_h
|
|
480
|
+
Utils.compact_hash(super.merge(
|
|
481
|
+
"host" => host,
|
|
482
|
+
"port" => port,
|
|
483
|
+
"request_data_format" => request_data_format,
|
|
484
|
+
"request_data_template" => request_data_template,
|
|
485
|
+
"response_byte_format" => response_byte_format,
|
|
486
|
+
"framing_strategy" => framing_strategy,
|
|
487
|
+
"length_prefix_bytes" => length_prefix_bytes,
|
|
488
|
+
"length_prefix_endian" => length_prefix_endian,
|
|
489
|
+
"message_delimiter" => message_delimiter,
|
|
490
|
+
"interpret_escape_sequences" => interpret_escape_sequences,
|
|
491
|
+
"fixed_message_length" => fixed_message_length,
|
|
492
|
+
"max_response_size" => max_response_size,
|
|
493
|
+
"timeout" => timeout
|
|
494
|
+
))
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
private
|
|
498
|
+
|
|
499
|
+
def validate_socket_values!
|
|
500
|
+
raise ValidationError.new("must be between 1 and 65535", path: "port") unless (1..65_535).cover?(@port)
|
|
501
|
+
raise ValidationError.new("must be json or text", path: "request_data_format") unless FORMATS.include?(@request_data_format)
|
|
502
|
+
raise ValidationError.new("unsupported framing strategy", path: "framing_strategy") unless FRAMING.include?(@framing_strategy)
|
|
503
|
+
raise ValidationError.new("must be 1, 2, 4, or 8", path: "length_prefix_bytes") unless [1, 2, 4, 8].include?(@length_prefix_bytes)
|
|
504
|
+
raise ValidationError.new("must be big or little", path: "length_prefix_endian") unless %w[big little].include?(@length_prefix_endian)
|
|
505
|
+
if @framing_strategy == "fixed_length" && (!@fixed_message_length || !@fixed_message_length.positive?)
|
|
506
|
+
raise ValidationError.new("must be positive for fixed_length framing", path: "fixed_message_length")
|
|
507
|
+
end
|
|
508
|
+
raise ValidationError.new("must be greater than zero", path: "max_response_size") unless @max_response_size.positive?
|
|
509
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
TCPCallTemplate = TcpCallTemplate
|
|
513
|
+
|
|
514
|
+
class UdpCallTemplate < CallTemplate
|
|
515
|
+
FORMATS = %w[json text].freeze
|
|
516
|
+
attr_accessor :host, :port, :number_of_response_datagrams, :request_data_format,
|
|
517
|
+
:request_data_template, :response_byte_format, :timeout
|
|
518
|
+
|
|
519
|
+
def initialize(host:, port:, call_template_type: "udp", number_of_response_datagrams: 1,
|
|
520
|
+
request_data_format: "json", request_data_template: nil,
|
|
521
|
+
response_byte_format: "utf-8", timeout: 30_000, **common)
|
|
522
|
+
super(call_template_type: call_template_type, auth: nil, **common)
|
|
523
|
+
@host = Utils.required_string!(host, "host")
|
|
524
|
+
@port = Integer(port)
|
|
525
|
+
@number_of_response_datagrams = Integer(number_of_response_datagrams)
|
|
526
|
+
@request_data_format = request_data_format.to_s
|
|
527
|
+
@request_data_template = Utils.optional_string!(request_data_template, "request_data_template")
|
|
528
|
+
@response_byte_format = response_byte_format.nil? ? nil : response_byte_format.to_s
|
|
529
|
+
@timeout = Integer(timeout)
|
|
530
|
+
raise ValidationError.new("must be between 1 and 65535", path: "port") unless (1..65_535).cover?(@port)
|
|
531
|
+
raise ValidationError.new("must not be negative", path: "number_of_response_datagrams") if @number_of_response_datagrams.negative?
|
|
532
|
+
raise ValidationError.new("must be json or text", path: "request_data_format") unless FORMATS.include?(@request_data_format)
|
|
533
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def to_h
|
|
537
|
+
Utils.compact_hash(super.merge(
|
|
538
|
+
"host" => host,
|
|
539
|
+
"port" => port,
|
|
540
|
+
"number_of_response_datagrams" => number_of_response_datagrams,
|
|
541
|
+
"request_data_format" => request_data_format,
|
|
542
|
+
"request_data_template" => request_data_template,
|
|
543
|
+
"response_byte_format" => response_byte_format,
|
|
544
|
+
"timeout" => timeout
|
|
545
|
+
))
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
UDPCallTemplate = UdpCallTemplate
|
|
549
|
+
|
|
550
|
+
class WebRtcCallTemplate < CallTemplate
|
|
551
|
+
attr_accessor :signaling_server, :peer_id, :data_channel_name, :timeout, :ice_servers
|
|
552
|
+
|
|
553
|
+
def initialize(signaling_server:, peer_id:, data_channel_name:, call_template_type: "webrtc",
|
|
554
|
+
timeout: 30, ice_servers: nil, **common)
|
|
555
|
+
super(call_template_type: call_template_type, auth: nil, **common)
|
|
556
|
+
@signaling_server = Utils.required_string!(signaling_server, "signaling_server")
|
|
557
|
+
@peer_id = Utils.required_string!(peer_id, "peer_id")
|
|
558
|
+
@data_channel_name = Utils.required_string!(data_channel_name, "data_channel_name")
|
|
559
|
+
@timeout = Float(timeout)
|
|
560
|
+
@ice_servers = ice_servers.nil? ? [] : Utils.array!(ice_servers, "ice_servers").map do |server|
|
|
561
|
+
Utils.stringify_keys(Utils.hash!(server, "ice_server"))
|
|
562
|
+
end
|
|
563
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def to_h
|
|
567
|
+
Utils.compact_hash(super.merge(
|
|
568
|
+
"signaling_server" => signaling_server,
|
|
569
|
+
"peer_id" => peer_id,
|
|
570
|
+
"data_channel_name" => data_channel_name,
|
|
571
|
+
"timeout" => timeout,
|
|
572
|
+
"ice_servers" => ice_servers.empty? ? nil : Utils.deep_copy(ice_servers)
|
|
573
|
+
))
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
WebRTCCallTemplate = WebRtcCallTemplate
|
|
577
|
+
|
|
578
|
+
class McpCallTemplate < CallTemplate
|
|
579
|
+
attr_accessor :config, :register_resources_as_tools, :protocol_version, :timeout
|
|
580
|
+
|
|
581
|
+
def initialize(config:, call_template_type: "mcp", register_resources_as_tools: false,
|
|
582
|
+
protocol_version: "2025-06-18", timeout: 30, **common)
|
|
583
|
+
super(call_template_type: call_template_type, **common)
|
|
584
|
+
@config = Utils.stringify_keys(Utils.hash!(config, "config"))
|
|
585
|
+
servers = @config["mcpServers"]
|
|
586
|
+
unless servers.is_a?(Hash) && !servers.empty?
|
|
587
|
+
raise ValidationError.new("must contain a non-empty mcpServers object", path: "config")
|
|
588
|
+
end
|
|
589
|
+
@register_resources_as_tools = !!register_resources_as_tools
|
|
590
|
+
@protocol_version = Utils.required_string!(protocol_version, "protocol_version")
|
|
591
|
+
@timeout = Float(timeout)
|
|
592
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def servers
|
|
596
|
+
config["mcpServers"]
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def to_h
|
|
600
|
+
super.merge(
|
|
601
|
+
"config" => Utils.deep_copy(config),
|
|
602
|
+
"register_resources_as_tools" => register_resources_as_tools,
|
|
603
|
+
"protocol_version" => protocol_version,
|
|
604
|
+
"timeout" => timeout
|
|
605
|
+
)
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
MCPCallTemplate = McpCallTemplate
|
|
609
|
+
|
|
610
|
+
class CommandStep
|
|
611
|
+
include ModelSerialization
|
|
612
|
+
attr_accessor :command, :append_to_final_output
|
|
613
|
+
|
|
614
|
+
def self.from_h(value)
|
|
615
|
+
return value if value.is_a?(self)
|
|
616
|
+
|
|
617
|
+
new(**Utils.symbolize_keys(Utils.stringify_keys(Utils.hash!(value, "command step"))))
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def initialize(command:, append_to_final_output: nil, **_extra)
|
|
621
|
+
@command = Utils.required_string!(command, "command")
|
|
622
|
+
unless append_to_final_output.nil? || append_to_final_output == true || append_to_final_output == false
|
|
623
|
+
raise ValidationError.new("must be true, false, or null", path: "append_to_final_output")
|
|
624
|
+
end
|
|
625
|
+
@append_to_final_output = append_to_final_output
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def to_h
|
|
629
|
+
Utils.compact_hash("command" => command, "append_to_final_output" => append_to_final_output)
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
class CliCallTemplate < CallTemplate
|
|
634
|
+
attr_accessor :commands, :env_vars, :inherit_env_vars, :working_dir, :timeout
|
|
635
|
+
|
|
636
|
+
def initialize(commands:, call_template_type: "cli", env_vars: nil, inherit_env_vars: nil,
|
|
637
|
+
working_dir: nil, timeout: 120, **common)
|
|
638
|
+
super(call_template_type: call_template_type, **common)
|
|
639
|
+
@commands = Utils.array!(commands, "commands").map { |item| CommandStep.from_h(item) }
|
|
640
|
+
raise ValidationError.new("must contain at least one command", path: "commands") if @commands.empty?
|
|
641
|
+
|
|
642
|
+
@env_vars = env_vars.nil? ? {} : Utils.stringify_keys(Utils.hash!(env_vars, "env_vars"))
|
|
643
|
+
@inherit_env_vars = inherit_env_vars.nil? ? nil : Utils.array!(inherit_env_vars, "inherit_env_vars").map(&:to_s)
|
|
644
|
+
@working_dir = Utils.optional_string!(working_dir, "working_dir")
|
|
645
|
+
@timeout = Float(timeout)
|
|
646
|
+
raise ValidationError.new("must be greater than zero", path: "timeout") unless @timeout.positive?
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def to_h
|
|
650
|
+
Utils.compact_hash(super.merge(
|
|
651
|
+
"commands" => commands.map(&:to_h),
|
|
652
|
+
"env_vars" => env_vars.empty? ? nil : Utils.deep_copy(env_vars),
|
|
653
|
+
"inherit_env_vars" => inherit_env_vars&.dup,
|
|
654
|
+
"working_dir" => working_dir,
|
|
655
|
+
"timeout" => timeout
|
|
656
|
+
))
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
class TextCallTemplate < CallTemplate
|
|
661
|
+
attr_accessor :content, :base_url, :auth_tools
|
|
662
|
+
|
|
663
|
+
def initialize(content:, call_template_type: "text", base_url: nil, auth_tools: nil, **common)
|
|
664
|
+
super(call_template_type: call_template_type, auth: nil, **common)
|
|
665
|
+
@content = Utils.required_string!(content, "content")
|
|
666
|
+
@base_url = Utils.optional_string!(base_url, "base_url")
|
|
667
|
+
@auth_tools = auth_tools.nil? ? nil : Auth.from_h(auth_tools)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def to_h
|
|
671
|
+
Utils.compact_hash(super.merge(
|
|
672
|
+
"content" => content,
|
|
673
|
+
"base_url" => base_url,
|
|
674
|
+
"auth_tools" => auth_tools&.to_h
|
|
675
|
+
))
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
class FileCallTemplate < CallTemplate
|
|
680
|
+
attr_accessor :file_path, :auth_tools
|
|
681
|
+
|
|
682
|
+
def initialize(file_path:, call_template_type: "file", auth_tools: nil, **common)
|
|
683
|
+
super(call_template_type: call_template_type, auth: nil, **common)
|
|
684
|
+
@file_path = Utils.required_string!(file_path, "file_path")
|
|
685
|
+
@auth_tools = auth_tools.nil? ? nil : Auth.from_h(auth_tools)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def to_h
|
|
689
|
+
Utils.compact_hash(super.merge("file_path" => file_path, "auth_tools" => auth_tools&.to_h))
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
class Tool
|
|
694
|
+
include ModelSerialization
|
|
695
|
+
attr_accessor :name, :description, :inputs, :outputs, :tags, :average_response_size,
|
|
696
|
+
:tool_call_template
|
|
697
|
+
|
|
698
|
+
def self.from_h(value)
|
|
699
|
+
return value if value.is_a?(self)
|
|
700
|
+
|
|
701
|
+
data = Utils.stringify_keys(Utils.hash!(value, "tool"))
|
|
702
|
+
new(**Utils.symbolize_keys(data))
|
|
703
|
+
rescue ValidationError
|
|
704
|
+
raise
|
|
705
|
+
rescue StandardError => error
|
|
706
|
+
raise SerializerValidationError, "Invalid tool: #{error.message}"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def initialize(name:, tool_call_template:, description: "", inputs: nil, outputs: nil,
|
|
710
|
+
tags: nil, average_response_size: nil, **extra)
|
|
711
|
+
@name = Utils.required_string!(name, "tool.name")
|
|
712
|
+
@description = Utils.optional_string!(description, "tool.description") || ""
|
|
713
|
+
@inputs = JsonSchema.from_h(inputs || {})
|
|
714
|
+
@outputs = JsonSchema.from_h(outputs || {})
|
|
715
|
+
@tags = (tags || []).tap { |value| Utils.array!(value, "tool.tags") }.map(&:to_s)
|
|
716
|
+
@average_response_size = average_response_size.nil? ? nil : Integer(average_response_size)
|
|
717
|
+
@tool_call_template = CallTemplate.from_h(tool_call_template)
|
|
718
|
+
@extra = Utils.stringify_keys(extra)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def to_h
|
|
722
|
+
Utils.compact_hash({
|
|
723
|
+
"name" => name,
|
|
724
|
+
"description" => description,
|
|
725
|
+
"inputs" => inputs.to_h,
|
|
726
|
+
"outputs" => outputs.to_h,
|
|
727
|
+
"tags" => tags.dup,
|
|
728
|
+
"average_response_size" => average_response_size,
|
|
729
|
+
"tool_call_template" => tool_call_template.to_h
|
|
730
|
+
}.merge(Utils.deep_copy(@extra)))
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
class Manual
|
|
735
|
+
include ModelSerialization
|
|
736
|
+
attr_accessor :utcp_version, :manual_version, :info, :tools
|
|
737
|
+
|
|
738
|
+
def self.from_h(value)
|
|
739
|
+
return value if value.is_a?(self)
|
|
740
|
+
|
|
741
|
+
data = Utils.stringify_keys(Utils.hash!(value, "manual"))
|
|
742
|
+
new(**Utils.symbolize_keys(data))
|
|
743
|
+
rescue ValidationError
|
|
744
|
+
raise
|
|
745
|
+
rescue StandardError => error
|
|
746
|
+
raise SerializerValidationError, "Invalid UTCP manual: #{error.message}"
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def initialize(tools:, utcp_version: VERSION, manual_version: "1.0.0", info: nil, **extra)
|
|
750
|
+
@utcp_version = Utils.required_string!(utcp_version, "utcp_version")
|
|
751
|
+
@manual_version = Utils.required_string!(manual_version, "manual_version")
|
|
752
|
+
@info = info.nil? ? {} : Utils.stringify_keys(Utils.hash!(info, "info"))
|
|
753
|
+
@tools = Utils.array!(tools, "tools").map { |tool| Tool.from_h(tool) }
|
|
754
|
+
@extra = Utils.stringify_keys(extra)
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def to_h
|
|
758
|
+
values = {
|
|
759
|
+
"manual_version" => manual_version,
|
|
760
|
+
"utcp_version" => utcp_version,
|
|
761
|
+
"info" => info.empty? ? nil : Utils.deep_copy(info),
|
|
762
|
+
"tools" => tools.map(&:to_h)
|
|
763
|
+
}.merge(Utils.deep_copy(@extra))
|
|
764
|
+
Utils.compact_hash(values)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
end
|
|
768
|
+
UtcpManual = Manual
|
|
769
|
+
|
|
770
|
+
class RegisterManualResult
|
|
771
|
+
include ModelSerialization
|
|
772
|
+
attr_accessor :manual_call_template, :manual, :success, :errors
|
|
773
|
+
|
|
774
|
+
def initialize(manual_call_template:, manual:, success:, errors: [])
|
|
775
|
+
@manual_call_template = CallTemplate.from_h(manual_call_template)
|
|
776
|
+
@manual = Manual.from_h(manual)
|
|
777
|
+
@success = !!success
|
|
778
|
+
@errors = Array(errors).map(&:to_s)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def success?
|
|
782
|
+
success
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def to_h
|
|
786
|
+
{
|
|
787
|
+
"manual_call_template" => manual_call_template.to_h,
|
|
788
|
+
"manual" => manual.to_h,
|
|
789
|
+
"success" => success,
|
|
790
|
+
"errors" => errors.dup
|
|
791
|
+
}
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
end
|