rails_mcp_engine 0.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/README.md +134 -0
- data/Rakefile +10 -0
- data/app/controllers/rails_mcp_engine/application_controller.rb +5 -0
- data/app/controllers/rails_mcp_engine/chat_controller.rb +110 -0
- data/app/controllers/rails_mcp_engine/playground_controller.rb +183 -0
- data/app/lib/tool_meta.rb +91 -0
- data/app/lib/tool_schema/builder.rb +71 -0
- data/app/lib/tool_schema/fast_mcp_builder.rb +66 -0
- data/app/lib/tool_schema/fast_mcp_factory.rb +49 -0
- data/app/lib/tool_schema/ruby_llm_builder.rb +80 -0
- data/app/lib/tool_schema/ruby_llm_factory.rb +48 -0
- data/app/lib/tool_schema/sorbet_type_mapper.rb +137 -0
- data/app/services/tools/meta_tool_service.rb +165 -0
- data/app/views/rails_mcp_engine/chat/show.html.erb +478 -0
- data/app/views/rails_mcp_engine/playground/show.html.erb +138 -0
- data/config/routes.rb +9 -0
- data/lib/rails_mcp_engine/engine.rb +42 -0
- data/lib/rails_mcp_engine/version.rb +3 -0
- data/lib/rails_mcp_engine.rb +6 -0
- metadata +126 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'fast_mcp_builder'
|
|
6
|
+
|
|
7
|
+
module ToolSchema
|
|
8
|
+
class FastMcpFactory
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig do
|
|
12
|
+
params(
|
|
13
|
+
service_class: T.class_of(Object),
|
|
14
|
+
schema: T::Hash[Symbol, T.untyped],
|
|
15
|
+
before_call: T.nilable(Proc),
|
|
16
|
+
after_call: T.nilable(Proc)
|
|
17
|
+
).returns(T.class_of(Object))
|
|
18
|
+
end
|
|
19
|
+
def self.build(service_class, schema, before_call: nil, after_call: nil)
|
|
20
|
+
tool_constant = tool_class_name(service_class)
|
|
21
|
+
parent = Mcp
|
|
22
|
+
parent.send(:remove_const, tool_constant) if parent.const_defined?(tool_constant, false)
|
|
23
|
+
|
|
24
|
+
klass = Class.new(ApplicationTool) do
|
|
25
|
+
description(schema[:description])
|
|
26
|
+
tool_name(schema[:name])
|
|
27
|
+
arguments(&FastMcpBuilder.arguments_block(schema[:params]))
|
|
28
|
+
|
|
29
|
+
define_method(:call) do |**kwargs|
|
|
30
|
+
before_call&.call(kwargs)
|
|
31
|
+
result = service_class.new.public_send(schema[:entrypoint], **kwargs)
|
|
32
|
+
after_call&.call(result)
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
parent.const_set(tool_constant, klass)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { params(service_class: T.class_of(Object)).returns(String) }
|
|
41
|
+
def self.tool_class_name(service_class)
|
|
42
|
+
base = service_class.name.split('::').last
|
|
43
|
+
base.gsub(/Service$/, '')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Mcp
|
|
49
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module ToolSchema
|
|
7
|
+
module RubyLlmBuilder
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { params(params_ast: T::Array[T::Hash[Symbol, T.untyped]]).returns(Proc) }
|
|
11
|
+
def self.params_block(params_ast)
|
|
12
|
+
proc do
|
|
13
|
+
params_ast.each do |param|
|
|
14
|
+
RubyLlmBuilder.build_param(self, param)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(ctx: BasicObject, param: T::Hash[Symbol, T.untyped]).void }
|
|
20
|
+
def self.build_param(ctx, param)
|
|
21
|
+
name = param[:name]
|
|
22
|
+
case param[:type]
|
|
23
|
+
when :object
|
|
24
|
+
ctx.object(name) do
|
|
25
|
+
(param[:children] || []).each do |child|
|
|
26
|
+
RubyLlmBuilder.build_param(self, child)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
when :array
|
|
30
|
+
item = param[:item_type]
|
|
31
|
+
if item && scalar_type?(item[:type])
|
|
32
|
+
ctx.array(name, of: scalar_symbol(item[:type]))
|
|
33
|
+
elsif item&.dig(:type) == :object
|
|
34
|
+
ctx.array(name) do
|
|
35
|
+
object :item do
|
|
36
|
+
(item[:children] || []).each do |child|
|
|
37
|
+
RubyLlmBuilder.build_param(self, child)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
ctx.array(name)
|
|
43
|
+
end
|
|
44
|
+
when :any
|
|
45
|
+
ctx.object(name, description: param[:description]) do
|
|
46
|
+
additional_properties true
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
method = scalar_method(param[:type])
|
|
50
|
+
ctx.public_send(method, name)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
|
55
|
+
def self.scalar_type?(type)
|
|
56
|
+
%i[string integer float boolean].include?(type)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(type: T.untyped).returns(Symbol) }
|
|
60
|
+
def self.scalar_symbol(type)
|
|
61
|
+
case type
|
|
62
|
+
when :string then :string
|
|
63
|
+
when :integer then :integer
|
|
64
|
+
when :float then :float
|
|
65
|
+
when :boolean then :boolean
|
|
66
|
+
else :any
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(type: T.untyped).returns(Symbol) }
|
|
71
|
+
def self.scalar_method(type)
|
|
72
|
+
case type
|
|
73
|
+
when :integer then :integer
|
|
74
|
+
when :float then :float
|
|
75
|
+
when :boolean then :boolean
|
|
76
|
+
else :string
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'ruby_llm_builder'
|
|
6
|
+
|
|
7
|
+
module ToolSchema
|
|
8
|
+
class RubyLlmFactory
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig do
|
|
12
|
+
params(
|
|
13
|
+
service_class: T.class_of(Object),
|
|
14
|
+
schema: T::Hash[Symbol, T.untyped],
|
|
15
|
+
before_call: T.nilable(Proc),
|
|
16
|
+
after_call: T.nilable(Proc)
|
|
17
|
+
).returns(T.class_of(Object))
|
|
18
|
+
end
|
|
19
|
+
def self.build(service_class, schema, before_call: nil, after_call: nil)
|
|
20
|
+
tool_constant = tool_class_name(service_class)
|
|
21
|
+
parent = Tools
|
|
22
|
+
parent.send(:remove_const, tool_constant) if parent.const_defined?(tool_constant, false)
|
|
23
|
+
|
|
24
|
+
klass = Class.new(RubyLLM::Tool) do
|
|
25
|
+
description(schema[:description])
|
|
26
|
+
params(&RubyLlmBuilder.params_block(schema[:params]))
|
|
27
|
+
|
|
28
|
+
define_method(:execute) do |**kwargs|
|
|
29
|
+
before_call&.call(kwargs)
|
|
30
|
+
result = service_class.new.public_send(schema[:entrypoint], **kwargs)
|
|
31
|
+
after_call&.call(result)
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
parent.const_set(tool_constant, klass)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(service_class: T.class_of(Object)).returns(String) }
|
|
40
|
+
def self.tool_class_name(service_class)
|
|
41
|
+
base = service_class.name.split('::').last
|
|
42
|
+
base.gsub(/Service$/, '')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module Tools
|
|
48
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module ToolSchema
|
|
7
|
+
module SorbetTypeMapper
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
TypeAst = T.type_alias do
|
|
11
|
+
T::Hash[Symbol, T.untyped]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { params(method: UnboundMethod).returns(T::Hash[Symbol, T.untyped]) }
|
|
15
|
+
def self.map_signature(method)
|
|
16
|
+
signature = T::Private::Methods.signature_for_method(method)
|
|
17
|
+
raise ToolMeta::MissingSignatureError, "Missing Sorbet signature for #{method.name}" if signature.nil?
|
|
18
|
+
|
|
19
|
+
params_ast = signature.arg_types.map do |name, type|
|
|
20
|
+
map_param(name, type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
signature.kwarg_types.each do |name, type|
|
|
24
|
+
params_ast << map_param(name, type)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
params: params_ast,
|
|
29
|
+
return_type: map_type(signature.return_type)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(name: T.any(String, Symbol), type: T.untyped).returns(TypeAst) }
|
|
34
|
+
def self.map_param(name, type)
|
|
35
|
+
details = map_type(type)
|
|
36
|
+
details.merge(name: name.to_sym)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(type: T.untyped).returns(TypeAst) }
|
|
40
|
+
def self.map_type(type)
|
|
41
|
+
nilable, inner_type = unwrap_nilable(type)
|
|
42
|
+
mapped = case inner_type
|
|
43
|
+
when T::Types::FixedHash
|
|
44
|
+
map_fixed_hash(inner_type)
|
|
45
|
+
when T::Types::TypedArray
|
|
46
|
+
map_array(inner_type)
|
|
47
|
+
when T::Types::TypedHash
|
|
48
|
+
map_hash(inner_type)
|
|
49
|
+
when T::Types::Simple
|
|
50
|
+
map_simple(inner_type)
|
|
51
|
+
when T::Types::Union
|
|
52
|
+
map_union(inner_type)
|
|
53
|
+
else
|
|
54
|
+
{ type: :any }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
mapped.merge(required: !nilable)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(type: T::Types::Union).returns(TypeAst) }
|
|
61
|
+
def self.map_union(type)
|
|
62
|
+
non_nil_types = type.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
63
|
+
|
|
64
|
+
# Check for Boolean (TrueClass | FalseClass)
|
|
65
|
+
is_boolean = non_nil_types.length == 2 && non_nil_types.all? do |t|
|
|
66
|
+
t.is_a?(T::Types::Simple) && [TrueClass, FalseClass].include?(t.raw_type)
|
|
67
|
+
end
|
|
68
|
+
return { type: :boolean } if is_boolean
|
|
69
|
+
|
|
70
|
+
return { type: :any } if non_nil_types.length != 1
|
|
71
|
+
|
|
72
|
+
map_type(non_nil_types.first).merge(required: true)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(type: T::Types::TypedArray).returns(TypeAst) }
|
|
76
|
+
def self.map_array(type)
|
|
77
|
+
{
|
|
78
|
+
type: :array,
|
|
79
|
+
item_type: map_type(type.type)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(type: T::Types::TypedHash).returns(TypeAst) }
|
|
84
|
+
def self.map_hash(type)
|
|
85
|
+
{
|
|
86
|
+
type: :object,
|
|
87
|
+
children: [],
|
|
88
|
+
key_type: map_type(type.keys),
|
|
89
|
+
value_type: map_type(type.values)
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
sig { params(type: T::Types::FixedHash).returns(TypeAst) }
|
|
94
|
+
def self.map_fixed_hash(type)
|
|
95
|
+
children = type.keys.map do |key, value|
|
|
96
|
+
mapped = map_type(value.type)
|
|
97
|
+
mapped.merge(name: key.to_sym, required: value.required?)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
type: :object,
|
|
102
|
+
children: children
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(type: T::Types::Simple).returns(TypeAst) }
|
|
107
|
+
def self.map_simple(type)
|
|
108
|
+
raw = type.raw_type
|
|
109
|
+
case raw.name
|
|
110
|
+
when 'String'
|
|
111
|
+
{ type: :string }
|
|
112
|
+
when 'Integer'
|
|
113
|
+
{ type: :integer }
|
|
114
|
+
when 'Float'
|
|
115
|
+
{ type: :float }
|
|
116
|
+
when 'TrueClass', 'FalseClass'
|
|
117
|
+
{ type: :boolean }
|
|
118
|
+
else
|
|
119
|
+
if raw == T::Boolean
|
|
120
|
+
{ type: :boolean }
|
|
121
|
+
else
|
|
122
|
+
{ type: :any }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig { params(type: T.untyped).returns([T::Boolean, T.untyped]) }
|
|
128
|
+
def self.unwrap_nilable(type)
|
|
129
|
+
return [false, type] unless type.is_a?(T::Types::Union)
|
|
130
|
+
|
|
131
|
+
nilable = type.types.any? { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
132
|
+
non_nil = type.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
133
|
+
target = non_nil.length == 1 ? non_nil.first : type
|
|
134
|
+
[nilable, target]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require 'tool_meta'
|
|
6
|
+
require 'tool_schema/builder'
|
|
7
|
+
require 'tool_schema/ruby_llm_factory'
|
|
8
|
+
require 'tool_schema/fast_mcp_factory'
|
|
9
|
+
|
|
10
|
+
module Tools
|
|
11
|
+
class MetaToolService
|
|
12
|
+
extend T::Sig
|
|
13
|
+
extend ToolMeta
|
|
14
|
+
|
|
15
|
+
tool_name 'meta_tool'
|
|
16
|
+
tool_description 'Inspect and run registered tools via a single meta interface.'
|
|
17
|
+
tool_param :action, description: 'Operation to perform', enum: %w[search list list_summary get run]
|
|
18
|
+
tool_param :tool_name, description: 'Tool name to target (for get/run)', required: false
|
|
19
|
+
tool_param :query, description: 'Search string to match against tool names/descriptions', required: false
|
|
20
|
+
tool_param :arguments, description: 'Arguments to pass when running a tool', required: false
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
action: String,
|
|
25
|
+
tool_name: T.nilable(String),
|
|
26
|
+
query: T.nilable(String),
|
|
27
|
+
arguments: T.nilable(T::Hash[T.untyped, T.untyped])
|
|
28
|
+
).returns(T::Hash[Symbol, T.untyped])
|
|
29
|
+
end
|
|
30
|
+
def call(action:, tool_name: nil, query: nil, arguments: nil)
|
|
31
|
+
case action
|
|
32
|
+
when 'search'
|
|
33
|
+
search_tools(query)
|
|
34
|
+
when 'list'
|
|
35
|
+
{ tools: list_tools }
|
|
36
|
+
when 'list_summary'
|
|
37
|
+
{ tools: list_summaries }
|
|
38
|
+
when 'get'
|
|
39
|
+
get_tool(tool_name)
|
|
40
|
+
when 'run'
|
|
41
|
+
run_tool(tool_name, arguments || {})
|
|
42
|
+
else
|
|
43
|
+
{ error: "Unknown action: #{action}" }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(class_name: T.nilable(String), before_call: T.nilable(Proc), after_call: T.nilable(Proc)).returns(T::Hash[Symbol, T.untyped]) }
|
|
48
|
+
def register_tool(class_name, before_call: nil, after_call: nil)
|
|
49
|
+
return { error: 'class_name is required for register' } if class_name.nil? || class_name.empty?
|
|
50
|
+
|
|
51
|
+
service_class = constantize(class_name)
|
|
52
|
+
return { error: "Could not find #{class_name}" } if service_class.nil?
|
|
53
|
+
return { error: "#{class_name} must extend ToolMeta" } unless service_class.respond_to?(:tool_metadata)
|
|
54
|
+
|
|
55
|
+
ToolMeta.registry << service_class unless ToolMeta.registry.include?(service_class)
|
|
56
|
+
|
|
57
|
+
schema = ToolSchema::Builder.build(service_class)
|
|
58
|
+
ToolSchema::RubyLlmFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
|
|
59
|
+
ToolSchema::FastMcpFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
|
|
60
|
+
|
|
61
|
+
{ status: 'registered', tool: summary_payload(schema) }
|
|
62
|
+
rescue ToolMeta::MissingSignatureError => e
|
|
63
|
+
{ error: e.message }
|
|
64
|
+
rescue NameError => e
|
|
65
|
+
{ error: "Could not find #{class_name}: #{e.message}" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
sig { params(query: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
|
|
71
|
+
def search_tools(query)
|
|
72
|
+
return { tools: list_tools } if query.nil? || query.empty?
|
|
73
|
+
|
|
74
|
+
normalized = query.downcase
|
|
75
|
+
tools = list_tools.select do |tool|
|
|
76
|
+
tool[:name].downcase.include?(normalized) || tool[:description].downcase.include?(normalized)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
{ tools: tools }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
83
|
+
def list_tools
|
|
84
|
+
schemas.map { |schema| detailed_payload(schema) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
88
|
+
def list_summaries
|
|
89
|
+
schemas.map { |schema| summary_payload(schema) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { params(tool_name: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
|
|
93
|
+
def get_tool(tool_name)
|
|
94
|
+
schema = find_schema(tool_name)
|
|
95
|
+
return { error: "Tool not found: #{tool_name}" } unless schema
|
|
96
|
+
|
|
97
|
+
{ tool: detailed_payload(schema) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(tool_name: T.nilable(String), arguments: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
101
|
+
def run_tool(tool_name, arguments)
|
|
102
|
+
schema = find_schema(tool_name)
|
|
103
|
+
return { error: "Tool not found: #{tool_name}" } unless schema
|
|
104
|
+
|
|
105
|
+
service_class = schema[:service_class]
|
|
106
|
+
result = service_class.new.public_send(schema[:entrypoint], **deep_symbolize(arguments))
|
|
107
|
+
|
|
108
|
+
{ tool: summary_payload(schema), result: result }
|
|
109
|
+
rescue ArgumentError => e
|
|
110
|
+
{ error: e.message }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
114
|
+
def schemas
|
|
115
|
+
ToolMeta.registry.map { |service_class| ToolSchema::Builder.build(service_class) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { params(tool_name: T.nilable(String)).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
119
|
+
def find_schema(tool_name)
|
|
120
|
+
return nil if tool_name.nil? || tool_name.empty?
|
|
121
|
+
|
|
122
|
+
schemas.find { |schema| schema[:name] == tool_name }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
126
|
+
def summary_payload(schema)
|
|
127
|
+
{
|
|
128
|
+
name: schema[:name],
|
|
129
|
+
description: schema[:description],
|
|
130
|
+
usage: usage_string(schema)
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
135
|
+
def detailed_payload(schema)
|
|
136
|
+
summary_payload(schema).merge(params: schema[:params], return_type: schema[:return_type])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { params(schema: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
140
|
+
def usage_string(schema)
|
|
141
|
+
param_list = schema[:params].map { |param| param[:name].to_s }.join(', ')
|
|
142
|
+
"#{schema[:name]}(#{param_list})"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
sig { params(name: String).returns(T.class_of(Object)) }
|
|
146
|
+
def constantize(name)
|
|
147
|
+
Object.const_get(name)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sig { params(value: T.untyped).returns(T.untyped) }
|
|
151
|
+
def deep_symbolize(value)
|
|
152
|
+
case value
|
|
153
|
+
when Hash
|
|
154
|
+
value.each_with_object({}) do |(key, nested), result|
|
|
155
|
+
symbol_key = key.is_a?(String) ? key.to_sym : key
|
|
156
|
+
result[symbol_key] = deep_symbolize(nested)
|
|
157
|
+
end
|
|
158
|
+
when Array
|
|
159
|
+
value.map { |element| deep_symbolize(element) }
|
|
160
|
+
else
|
|
161
|
+
value
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|