ruby-utcp 1.1.2 → 1.1.3
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/CHANGELOG.md +7 -0
- data/examples/servers/webrtc_server.rb +3 -3
- data/lib/utcp/code_mode/client.rb +30 -0
- data/lib/utcp/code_mode/control_flow.rb +97 -0
- data/lib/utcp/code_mode/evaluator.rb +176 -0
- data/lib/utcp/code_mode/expressions.rb +217 -0
- data/lib/utcp/code_mode/interface_generator.rb +84 -0
- data/lib/utcp/code_mode/runtime.rb +70 -0
- data/lib/utcp/code_mode/value_limits.rb +93 -0
- data/lib/utcp/code_mode/value_methods.rb +204 -0
- data/lib/utcp/code_mode.rb +9 -913
- data/lib/utcp/protocols/webrtc.rb +18 -13
- data/lib/utcp/protocols/websocket.rb +3 -0
- data/lib/utcp/version.rb +1 -1
- metadata +9 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def invoke_runtime_api(method_name, arguments, block)
|
|
9
|
+
raise CodeModeSyntaxError, "codemode.#{method_name} does not accept a block" if block
|
|
10
|
+
|
|
11
|
+
case method_name
|
|
12
|
+
when "call_tool"
|
|
13
|
+
require_arity!("codemode.call_tool", arguments, 1..2)
|
|
14
|
+
safe_tool_value(@client.call_tool(arguments[0], tool_arguments(arguments[1])))
|
|
15
|
+
when "call_tool_stream", "call_tool_streaming"
|
|
16
|
+
require_arity!("codemode.#{method_name}", arguments, 1..2)
|
|
17
|
+
safe_tool_value(@client.call_tool_streaming(arguments[0], tool_arguments(arguments[1])).to_a)
|
|
18
|
+
when "search_tools"
|
|
19
|
+
require_arity!("codemode.search_tools", arguments, 1..2)
|
|
20
|
+
options = arguments[1].is_a?(Hash) ? arguments[1] : {}
|
|
21
|
+
limit = arguments[1].is_a?(Numeric) ? arguments[1] : options.fetch("limit", 10)
|
|
22
|
+
safe_tool_value(@client.search_tools(arguments[0].to_s, limit: Integer(limit)).map do |tool|
|
|
23
|
+
InterfaceGenerator.tool_descriptor(tool)
|
|
24
|
+
end)
|
|
25
|
+
when "get_tool_interface"
|
|
26
|
+
require_arity!("codemode.get_tool_interface", arguments, 1)
|
|
27
|
+
tool = @client.list_tools.find { |candidate| candidate.name == arguments.first.to_s }
|
|
28
|
+
raise ToolNotFoundError, "Tool not found: #{arguments.first}" unless tool
|
|
29
|
+
safe_tool_value(InterfaceGenerator.tool_descriptor(tool))
|
|
30
|
+
when "interfaces"
|
|
31
|
+
require_arity!("codemode.interfaces", arguments, 0)
|
|
32
|
+
@interfaces.dup
|
|
33
|
+
when "get"
|
|
34
|
+
runtime_get(arguments)
|
|
35
|
+
else
|
|
36
|
+
raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available on codemode"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def runtime_get(arguments)
|
|
41
|
+
require_arity!("codemode.get", arguments, 2..3)
|
|
42
|
+
receiver, key, default = arguments
|
|
43
|
+
return receiver.fetch(key, default) if receiver.is_a?(Hash)
|
|
44
|
+
return receiver.fetch(Integer(key), default) if receiver.is_a?(Array)
|
|
45
|
+
return receiver[key] || default if receiver.is_a?(String)
|
|
46
|
+
|
|
47
|
+
raise CodeModeExecutionError, "codemode.get requires a hash, array, or string"
|
|
48
|
+
rescue IndexError, TypeError
|
|
49
|
+
default
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def tool_arguments(value)
|
|
53
|
+
return {} if value.nil?
|
|
54
|
+
raise CodeModeSyntaxError, "Tool arguments must be a hash" unless value.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
safe_tool_value(value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def log(kind, values)
|
|
60
|
+
rendered = values.map { |value| kind == "p" ? value.inspect : value.to_s }.join(kind == "print" ? "" : " ")
|
|
61
|
+
rendered = "[WARN] #{rendered}" if kind == "warn"
|
|
62
|
+
bytes = rendered.bytesize
|
|
63
|
+
raise CodeModeLimitError, "Code Mode log limit exceeded" if @log_bytes + bytes > MAX_LOG_BYTES
|
|
64
|
+
|
|
65
|
+
@logs << rendered
|
|
66
|
+
@log_bytes += bytes
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def preflight_binary_size!(left, operator, right)
|
|
9
|
+
if operator == :** && right.is_a?(Integer)
|
|
10
|
+
bases = left.is_a?(Rational) ? [left.numerator, left.denominator] : [left]
|
|
11
|
+
bases.each { |base| preflight_integer_power!(base, right) if base.is_a?(Integer) }
|
|
12
|
+
elsif operator == :* && left.is_a?(Integer) && right.is_a?(Integer)
|
|
13
|
+
if !left.zero? && !right.zero? && left.abs.bit_length + right.abs.bit_length > MAX_INTEGER_BITS
|
|
14
|
+
raise CodeModeLimitError, "Integer product exceeds #{MAX_VALUE_BYTES} bytes"
|
|
15
|
+
end
|
|
16
|
+
elsif operator == :+ && left.is_a?(String) && right.is_a?(String)
|
|
17
|
+
raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if left.bytesize + right.bytesize > MAX_VALUE_BYTES
|
|
18
|
+
elsif operator == :+ && left.is_a?(Array) && right.is_a?(Array)
|
|
19
|
+
raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items" if left.length + right.length > MAX_VALUE_ITEMS
|
|
20
|
+
elsif operator == :* && right.is_a?(Integer) && (left.is_a?(String) || left.is_a?(Array))
|
|
21
|
+
size = left.is_a?(String) ? left.bytesize : left.length
|
|
22
|
+
limit = left.is_a?(String) ? MAX_VALUE_BYTES : MAX_VALUE_ITEMS
|
|
23
|
+
raise CodeModeLimitError, "Repeated value exceeds Code Mode limits" if right.positive? && size > limit / right
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def preflight_integer_power!(base, exponent)
|
|
28
|
+
magnitude = base.abs
|
|
29
|
+
return if magnitude <= 1 || exponent.zero?
|
|
30
|
+
|
|
31
|
+
# Use an upper bound without constructing the potentially enormous result.
|
|
32
|
+
bits = magnitude.bit_length
|
|
33
|
+
bits -= 1 if (magnitude & (magnitude - 1)).zero?
|
|
34
|
+
if exponent.abs > (MAX_INTEGER_BITS - 1) / bits
|
|
35
|
+
raise CodeModeLimitError, "Integer power exceeds #{MAX_VALUE_BYTES} bytes"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def numeric_bytes(value)
|
|
40
|
+
case value
|
|
41
|
+
when Integer then [(value.abs.bit_length + 7) / 8, 1].max
|
|
42
|
+
when Rational then numeric_bytes(value.numerator) + numeric_bytes(value.denominator)
|
|
43
|
+
when Complex then numeric_bytes(value.real) + numeric_bytes(value.imaginary)
|
|
44
|
+
when Float
|
|
45
|
+
raise CodeModeLimitError, "Numeric result must be finite" unless value.finite?
|
|
46
|
+
8
|
|
47
|
+
else value.to_s.bytesize
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bounded_value!(value)
|
|
52
|
+
if value.is_a?(Numeric) && numeric_bytes(value) > MAX_VALUE_BYTES
|
|
53
|
+
raise CodeModeLimitError, "Numeric result exceeds #{MAX_VALUE_BYTES} bytes"
|
|
54
|
+
end
|
|
55
|
+
if value.is_a?(String) && value.bytesize > MAX_VALUE_BYTES
|
|
56
|
+
raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes"
|
|
57
|
+
end
|
|
58
|
+
if (value.is_a?(Array) || value.is_a?(Hash)) && value.length > MAX_VALUE_ITEMS
|
|
59
|
+
raise CodeModeLimitError, "Collection result exceeds #{MAX_VALUE_ITEMS} items"
|
|
60
|
+
end
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def safe_tool_value(value, depth = 0, budget = { items: MAX_VALUE_ITEMS, bytes: MAX_VALUE_BYTES })
|
|
65
|
+
raise CodeModeLimitError, "Tool result nesting is too deep" if depth > 64
|
|
66
|
+
budget[:items] -= 1
|
|
67
|
+
raise CodeModeLimitError, "Tool result contains too many values" if budget[:items].negative?
|
|
68
|
+
|
|
69
|
+
case value
|
|
70
|
+
when String
|
|
71
|
+
budget[:bytes] -= value.bytesize
|
|
72
|
+
raise CodeModeLimitError, "Tool result contains too much string data" if budget[:bytes].negative?
|
|
73
|
+
value.dup
|
|
74
|
+
when Numeric
|
|
75
|
+
budget[:bytes] -= numeric_bytes(value)
|
|
76
|
+
raise CodeModeLimitError, "Tool result contains too much numeric data" if budget[:bytes].negative?
|
|
77
|
+
value
|
|
78
|
+
when nil, true, false
|
|
79
|
+
value
|
|
80
|
+
when Array
|
|
81
|
+
value.map { |item| safe_tool_value(item, depth + 1, budget) }
|
|
82
|
+
when Hash
|
|
83
|
+
value.each_with_object({}) do |(key, item), result|
|
|
84
|
+
safe_key = key.is_a?(Symbol) ? key : key.to_s
|
|
85
|
+
result[safe_key] = safe_tool_value(item, depth + 1, budget)
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
raise CodeModeExecutionError, "Tool returned unsupported #{value.class} value"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class CodeMode
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def invoke_method(receiver, method_name, arguments, block)
|
|
9
|
+
return invoke_runtime_api(method_name, arguments, block) if receiver.is_a?(RuntimeAPI)
|
|
10
|
+
return receiver[method_name] if receiver.is_a?(Hash) && arguments.empty? && !block && receiver.key?(method_name)
|
|
11
|
+
|
|
12
|
+
unless SAFE_ENUMERABLE_METHODS.include?(method_name) || SAFE_VALUE_METHODS.include?(method_name)
|
|
13
|
+
raise CodeModeSyntaxError, "Method #{method_name.inspect} is not available in Code Mode"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
bounded_value!(invoke_safe_value_method(receiver, method_name, arguments, block))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def invoke_safe_value_method(receiver, method_name, arguments, block)
|
|
20
|
+
return invoke_enumerable(receiver, method_name, arguments, block) if SAFE_ENUMERABLE_METHODS.include?(method_name)
|
|
21
|
+
|
|
22
|
+
case method_name
|
|
23
|
+
when "length", "size" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:length) ? receiver.length : invalid_receiver!(receiver, method_name) }
|
|
24
|
+
when "empty?" then require_arity!(method_name, arguments, 0).then { receiver.respond_to?(:empty?) ? receiver.empty? : invalid_receiver!(receiver, method_name) }
|
|
25
|
+
when "nil?" then require_arity!(method_name, arguments, 0).then { receiver.nil? }
|
|
26
|
+
when "to_s" then require_arity!(method_name, arguments, 0).then { receiver.to_s }
|
|
27
|
+
when "inspect" then require_arity!(method_name, arguments, 0).then { receiver.inspect }
|
|
28
|
+
when "to_i" then require_arity!(method_name, arguments, 0).then { receiver.to_i }
|
|
29
|
+
when "to_f" then require_arity!(method_name, arguments, 0).then { receiver.to_f }
|
|
30
|
+
when "to_a" then require_arity!(method_name, arguments, 0).then { safe_to_a(receiver) }
|
|
31
|
+
when "to_h" then require_arity!(method_name, arguments, 0).then { receiver.is_a?(Hash) ? receiver.dup : invalid_receiver!(receiver, method_name) }
|
|
32
|
+
when "first" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).first(*arguments) }
|
|
33
|
+
when "last" then require_arity!(method_name, arguments, 0..1).then { enumerable_value!(receiver, method_name).last(*arguments) }
|
|
34
|
+
when "take" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).take(Integer(arguments[0])) }
|
|
35
|
+
when "drop" then require_arity!(method_name, arguments, 1).then { enumerable_value!(receiver, method_name).drop(Integer(arguments[0])) }
|
|
36
|
+
when "reverse", "sort", "uniq", "compact", "flatten", "keys", "values", "min", "max", "sum"
|
|
37
|
+
require_arity!(method_name, arguments, 0)
|
|
38
|
+
safe_zero_argument_method(receiver, method_name)
|
|
39
|
+
when "join" then safe_join(require_array!(receiver, method_name), arguments)
|
|
40
|
+
when "split" then bounded_value!(require_string!(receiver, method_name).split(*arguments))
|
|
41
|
+
when "strip", "downcase", "upcase" then require_string!(receiver, method_name).public_send(method_name)
|
|
42
|
+
when "include?" then collection_value!(receiver, method_name).include?(*arguments)
|
|
43
|
+
when "start_with?", "end_with?" then require_string!(receiver, method_name).public_send(method_name, *arguments)
|
|
44
|
+
when "key?", "has_key?" then require_hash!(receiver, method_name).key?(*arguments)
|
|
45
|
+
when "fetch" then indexable_value!(receiver, method_name).fetch(*arguments)
|
|
46
|
+
when "dig" then require_hash!(receiver, method_name).dig(*arguments)
|
|
47
|
+
when "slice" then indexable_value!(receiver, method_name).slice(*arguments)
|
|
48
|
+
when "merge" then bounded_value!(require_hash!(receiver, method_name).merge(require_hash!(arguments.fetch(0), method_name)))
|
|
49
|
+
when "abs", "ceil", "floor", "round", "zero?", "positive?", "negative?", "even?", "odd?"
|
|
50
|
+
require_numeric!(receiver, method_name).public_send(method_name, *arguments)
|
|
51
|
+
else
|
|
52
|
+
invalid_receiver!(receiver, method_name)
|
|
53
|
+
end
|
|
54
|
+
rescue NoMethodError, ArgumentError, TypeError, IndexError, KeyError => error
|
|
55
|
+
raise CodeModeExecutionError, "Invalid #{method_name} call: #{error.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def invoke_enumerable(receiver, method_name, arguments, block)
|
|
59
|
+
enumerable = enumerable_value!(receiver, method_name)
|
|
60
|
+
case method_name
|
|
61
|
+
when "map", "collect" then require_block!(method_name, block).then { enumerable.map { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
|
|
62
|
+
when "select", "filter" then require_block!(method_name, block).then { enumerable.select { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
63
|
+
when "reject" then require_block!(method_name, block).then { enumerable.reject { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
64
|
+
when "each"
|
|
65
|
+
require_block!(method_name, block)
|
|
66
|
+
enumerable.each { |*items| tick!; block.call(*block_arguments(enumerable, items)) }
|
|
67
|
+
receiver
|
|
68
|
+
when "find" then require_block!(method_name, block).then { enumerable.find { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
69
|
+
when "any?" then require_block!(method_name, block).then { enumerable.any? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
70
|
+
when "all?" then require_block!(method_name, block).then { enumerable.all? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
71
|
+
when "none?" then require_block!(method_name, block).then { enumerable.none? { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } }
|
|
72
|
+
when "count"
|
|
73
|
+
block ? enumerable.count { |*items| tick!; truthy?(block.call(*block_arguments(enumerable, items))) } : enumerable.count(*arguments)
|
|
74
|
+
when "sort_by" then require_block!(method_name, block).then { enumerable.sort_by { |*items| tick!; block.call(*block_arguments(enumerable, items)) } }
|
|
75
|
+
when "reduce", "inject"
|
|
76
|
+
reduce_enumerable(enumerable, arguments, require_block!(method_name, block))
|
|
77
|
+
else invalid_receiver!(receiver, method_name)
|
|
78
|
+
end
|
|
79
|
+
rescue BreakSignal => signal
|
|
80
|
+
signal.value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def block_arguments(enumerable, items)
|
|
84
|
+
return items.first if enumerable.is_a?(Hash) && items.length == 1 && items.first.is_a?(Array)
|
|
85
|
+
|
|
86
|
+
items
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reduce_enumerable(enumerable, arguments, block)
|
|
90
|
+
values = enumerable.to_a
|
|
91
|
+
if arguments.empty?
|
|
92
|
+
return nil if values.empty?
|
|
93
|
+
accumulator = values.shift
|
|
94
|
+
else
|
|
95
|
+
require_arity!("reduce", arguments, 1)
|
|
96
|
+
accumulator = arguments.first
|
|
97
|
+
end
|
|
98
|
+
values.each { |item| tick!; accumulator = block.call(accumulator, item) }
|
|
99
|
+
accumulator
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def safe_zero_argument_method(receiver, method_name)
|
|
103
|
+
allowed = case receiver
|
|
104
|
+
when Array then %w[reverse sort uniq compact flatten min max sum]
|
|
105
|
+
when Hash then %w[keys values]
|
|
106
|
+
when Range then %w[min max sum]
|
|
107
|
+
else []
|
|
108
|
+
end
|
|
109
|
+
invalid_receiver!(receiver, method_name) unless allowed.include?(method_name)
|
|
110
|
+
bounded_value!(receiver.public_send(method_name))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def safe_to_a(receiver)
|
|
114
|
+
return receiver.dup if receiver.is_a?(Array)
|
|
115
|
+
if receiver.is_a?(Range)
|
|
116
|
+
if receiver.begin.is_a?(Integer) && receiver.end.is_a?(Integer)
|
|
117
|
+
size = receiver.end - receiver.begin + (receiver.exclude_end? ? 0 : 1)
|
|
118
|
+
raise CodeModeLimitError, "Range contains too many values" if size > MAX_VALUE_ITEMS
|
|
119
|
+
end
|
|
120
|
+
return bounded_value!(receiver.to_a)
|
|
121
|
+
end
|
|
122
|
+
return receiver.to_a if receiver.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
invalid_receiver!(receiver, "to_a")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def safe_index(receiver, arguments)
|
|
128
|
+
raise CodeModeSyntaxError, "Indexing requires one or two arguments" unless (1..2).cover?(arguments.length)
|
|
129
|
+
raise CodeModeExecutionError, "#{receiver.class} values cannot be indexed" unless receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(String)
|
|
130
|
+
|
|
131
|
+
receiver[*arguments]
|
|
132
|
+
rescue TypeError, IndexError => error
|
|
133
|
+
raise CodeModeExecutionError, "Invalid index: #{error.message}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def enumerable_value!(receiver, method_name)
|
|
137
|
+
return receiver if receiver.is_a?(Array) || receiver.is_a?(Hash) || receiver.is_a?(Range)
|
|
138
|
+
|
|
139
|
+
invalid_receiver!(receiver, method_name)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def require_string!(value, method_name)
|
|
143
|
+
return value if value.is_a?(String)
|
|
144
|
+
|
|
145
|
+
invalid_receiver!(value, method_name)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def require_hash!(value, method_name)
|
|
149
|
+
return value if value.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
invalid_receiver!(value, method_name)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def require_array!(value, method_name)
|
|
155
|
+
return value if value.is_a?(Array)
|
|
156
|
+
|
|
157
|
+
invalid_receiver!(value, method_name)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def collection_value!(value, method_name)
|
|
161
|
+
return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String) || value.is_a?(Range)
|
|
162
|
+
|
|
163
|
+
invalid_receiver!(value, method_name)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def indexable_value!(value, method_name)
|
|
167
|
+
return value if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
|
|
168
|
+
|
|
169
|
+
invalid_receiver!(value, method_name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def require_numeric!(value, method_name)
|
|
173
|
+
return value if value.is_a?(Numeric)
|
|
174
|
+
|
|
175
|
+
invalid_receiver!(value, method_name)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def safe_join(receiver, arguments)
|
|
179
|
+
separator = arguments.empty? ? "" : arguments.first.to_s
|
|
180
|
+
estimated = receiver.sum { |value| value.to_s.bytesize }
|
|
181
|
+
estimated += separator.bytesize * [receiver.length - 1, 0].max
|
|
182
|
+
raise CodeModeLimitError, "String result exceeds #{MAX_VALUE_BYTES} bytes" if estimated > MAX_VALUE_BYTES
|
|
183
|
+
|
|
184
|
+
receiver.join(*arguments)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def invalid_receiver!(receiver, method_name)
|
|
188
|
+
raise CodeModeExecutionError, "#{method_name} is not supported for #{receiver.class} values"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def require_block!(method_name, block)
|
|
192
|
+
raise CodeModeSyntaxError, "#{method_name} requires a block" unless block
|
|
193
|
+
block
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def require_arity!(name, arguments, expected)
|
|
197
|
+
valid = expected.is_a?(Range) ? expected.cover?(arguments.length) : arguments.length == expected
|
|
198
|
+
return true if valid
|
|
199
|
+
|
|
200
|
+
raise CodeModeSyntaxError, "#{name} received #{arguments.length} arguments"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|