ruby-mana 0.5.1 → 0.5.7
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 +70 -17
- data/LICENSE +1 -1
- data/README.md +189 -166
- data/lib/mana/backends/anthropic.rb +9 -27
- data/lib/mana/backends/base.rb +51 -4
- data/lib/mana/backends/openai.rb +17 -42
- data/lib/mana/compiler.rb +162 -46
- data/lib/mana/config.rb +94 -6
- data/lib/mana/engine.rb +628 -38
- data/lib/mana/introspect.rb +58 -19
- data/lib/mana/logger.rb +99 -0
- data/lib/mana/memory.rb +132 -39
- data/lib/mana/memory_store.rb +18 -8
- data/lib/mana/mixin.rb +2 -2
- data/lib/mana/mock.rb +40 -0
- data/lib/mana/security_policy.rb +195 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +7 -30
- metadata +12 -38
- data/data/lang-rules.yml +0 -196
- data/lib/mana/backends/registry.rb +0 -23
- data/lib/mana/context_window.rb +0 -28
- data/lib/mana/effect_registry.rb +0 -155
- data/lib/mana/engines/base.rb +0 -79
- data/lib/mana/engines/detect.rb +0 -93
- data/lib/mana/engines/javascript.rb +0 -314
- data/lib/mana/engines/llm.rb +0 -467
- data/lib/mana/engines/python.rb +0 -314
- data/lib/mana/engines/ruby_eval.rb +0 -11
- data/lib/mana/namespace.rb +0 -39
- data/lib/mana/object_registry.rb +0 -89
- data/lib/mana/remote_ref.rb +0 -85
- data/lib/mana/test.rb +0 -18
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mana
|
|
4
|
-
module Backends
|
|
5
|
-
ANTHROPIC_PATTERNS = /^(claude-)/i
|
|
6
|
-
OPENAI_PATTERNS = /^(gpt-|o1-|o3-|chatgpt-|dall-e|tts-|whisper-)/i
|
|
7
|
-
|
|
8
|
-
def self.for(config)
|
|
9
|
-
return config.backend if config.backend.is_a?(Base)
|
|
10
|
-
|
|
11
|
-
case config.backend&.to_s
|
|
12
|
-
when "openai" then OpenAI.new(config)
|
|
13
|
-
when "anthropic" then Anthropic.new(config)
|
|
14
|
-
else
|
|
15
|
-
# Auto-detect from model name
|
|
16
|
-
case config.model
|
|
17
|
-
when ANTHROPIC_PATTERNS then Anthropic.new(config)
|
|
18
|
-
else OpenAI.new(config) # Default to OpenAI (most compatible)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
data/lib/mana/context_window.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mana
|
|
4
|
-
module ContextWindow
|
|
5
|
-
SIZES = {
|
|
6
|
-
/claude-3-5-sonnet/ => 200_000,
|
|
7
|
-
/claude-sonnet-4/ => 200_000,
|
|
8
|
-
/claude-3-5-haiku/ => 200_000,
|
|
9
|
-
/claude-3-opus/ => 200_000,
|
|
10
|
-
/claude-opus-4/ => 200_000,
|
|
11
|
-
/gpt-4o/ => 128_000,
|
|
12
|
-
/gpt-4-turbo/ => 128_000,
|
|
13
|
-
/gpt-3\.5/ => 16_385
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
DEFAULT = 128_000
|
|
17
|
-
|
|
18
|
-
def self.detect(model_name)
|
|
19
|
-
return DEFAULT unless model_name
|
|
20
|
-
|
|
21
|
-
SIZES.each do |pattern, size|
|
|
22
|
-
return size if model_name.match?(pattern)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
DEFAULT
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
data/lib/mana/effect_registry.rb
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mana
|
|
4
|
-
# Registry for custom effect handlers.
|
|
5
|
-
#
|
|
6
|
-
# Users define effects that become LLM tools automatically:
|
|
7
|
-
#
|
|
8
|
-
# Mana.define_effect :query_db,
|
|
9
|
-
# description: "Execute a SQL query" do |sql:|
|
|
10
|
-
# DB.execute(sql)
|
|
11
|
-
# end
|
|
12
|
-
#
|
|
13
|
-
# The block's keyword parameters become the tool's input schema.
|
|
14
|
-
# The block's return value is serialized and sent back to the LLM.
|
|
15
|
-
module EffectRegistry
|
|
16
|
-
class EffectDefinition
|
|
17
|
-
attr_reader :name, :description, :handler, :params
|
|
18
|
-
|
|
19
|
-
def initialize(name, description: nil, &handler)
|
|
20
|
-
@name = name.to_s
|
|
21
|
-
@description = description || @name
|
|
22
|
-
@handler = handler
|
|
23
|
-
@params = extract_params(handler)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Convert to LLM tool definition
|
|
27
|
-
def to_tool
|
|
28
|
-
properties = {}
|
|
29
|
-
required = []
|
|
30
|
-
|
|
31
|
-
@params.each do |param|
|
|
32
|
-
properties[param[:name]] = {
|
|
33
|
-
type: infer_type(param[:default]),
|
|
34
|
-
description: param[:name]
|
|
35
|
-
}
|
|
36
|
-
required << param[:name] if param[:required]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
tool = {
|
|
40
|
-
name: @name,
|
|
41
|
-
description: @description,
|
|
42
|
-
input_schema: {
|
|
43
|
-
type: "object",
|
|
44
|
-
properties: properties
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
tool[:input_schema][:required] = required unless required.empty?
|
|
48
|
-
tool
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Call the handler with LLM-provided input
|
|
52
|
-
def call(input)
|
|
53
|
-
kwargs = {}
|
|
54
|
-
@params.each do |param|
|
|
55
|
-
key = param[:name]
|
|
56
|
-
if input.key?(key)
|
|
57
|
-
kwargs[key.to_sym] = input[key]
|
|
58
|
-
elsif param[:default] != :__mana_no_default__
|
|
59
|
-
# Use block's default
|
|
60
|
-
elsif param[:required]
|
|
61
|
-
raise Mana::Error, "missing required parameter: #{key}"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
if kwargs.empty? && @params.empty?
|
|
66
|
-
@handler.call
|
|
67
|
-
else
|
|
68
|
-
@handler.call(**kwargs)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def extract_params(block)
|
|
75
|
-
return [] unless block
|
|
76
|
-
|
|
77
|
-
block.parameters.map do |(type, name)|
|
|
78
|
-
case type
|
|
79
|
-
when :keyreq
|
|
80
|
-
{ name: name.to_s, required: true, default: :__mana_no_default__ }
|
|
81
|
-
when :key
|
|
82
|
-
# Try to get default value — not possible via reflection,
|
|
83
|
-
# so we mark it as optional with unknown default
|
|
84
|
-
{ name: name.to_s, required: false, default: nil }
|
|
85
|
-
when :keyrest
|
|
86
|
-
# **kwargs — skip, can't generate schema
|
|
87
|
-
nil
|
|
88
|
-
else
|
|
89
|
-
# Positional args — treat as required string params
|
|
90
|
-
{ name: name.to_s, required: true, default: :__mana_no_default__ } if name
|
|
91
|
-
end
|
|
92
|
-
end.compact
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def infer_type(default)
|
|
96
|
-
case default
|
|
97
|
-
when Integer then "integer"
|
|
98
|
-
when Float then "number"
|
|
99
|
-
when TrueClass, FalseClass then "boolean"
|
|
100
|
-
when Array then "array"
|
|
101
|
-
when Hash then "object"
|
|
102
|
-
else "string"
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
RESERVED_EFFECTS = %w[read_var write_var read_attr write_attr call_func done remember].freeze
|
|
108
|
-
|
|
109
|
-
class << self
|
|
110
|
-
def registry
|
|
111
|
-
@registry ||= {}
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def define(name, description: nil, &handler)
|
|
115
|
-
name_s = name.to_s
|
|
116
|
-
if RESERVED_EFFECTS.include?(name_s)
|
|
117
|
-
raise Mana::Error, "cannot override built-in effect: #{name_s}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
registry[name_s] = EffectDefinition.new(name, description: description, &handler)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def undefine(name)
|
|
124
|
-
registry.delete(name.to_s)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def defined?(name)
|
|
128
|
-
registry.key?(name.to_s)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def get(name)
|
|
132
|
-
registry[name.to_s]
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Generate tool definitions for all registered effects
|
|
136
|
-
def tool_definitions
|
|
137
|
-
registry.values.map(&:to_tool)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def clear!
|
|
141
|
-
@registry = {}
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Handle a tool call if it matches a registered effect
|
|
145
|
-
# Returns [handled, result] — handled is true if we processed it
|
|
146
|
-
def handle(name, input)
|
|
147
|
-
effect = get(name)
|
|
148
|
-
return [false, nil] unless effect
|
|
149
|
-
|
|
150
|
-
result = effect.call(input)
|
|
151
|
-
[true, result]
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
data/lib/mana/engines/base.rb
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mana
|
|
4
|
-
module Engines
|
|
5
|
-
class Base
|
|
6
|
-
attr_reader :config, :binding
|
|
7
|
-
|
|
8
|
-
def initialize(caller_binding, config = Mana.config)
|
|
9
|
-
@binding = caller_binding
|
|
10
|
-
@config = config
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# --- Capability queries ---
|
|
14
|
-
# Subclasses override to declare what they support.
|
|
15
|
-
|
|
16
|
-
# Is this an execution engine (Ruby/JS/Python) or a reasoning engine (LLM)?
|
|
17
|
-
# Execution engines can execute code, hold state, and be called bidirectionally.
|
|
18
|
-
# Reasoning engines process natural language but cannot hold references or state.
|
|
19
|
-
def execution_engine?
|
|
20
|
-
true
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Can this engine hold remote references to objects in other engines?
|
|
24
|
-
# Derived from execution_engine? — only execution engines support remote refs.
|
|
25
|
-
def supports_remote_ref?
|
|
26
|
-
execution_engine?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Can code in this engine call back into another engine (bidirectional)?
|
|
30
|
-
# Derived from execution_engine? — only execution engines support bidirectional calls.
|
|
31
|
-
def supports_bidirectional?
|
|
32
|
-
execution_engine?
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Does this engine maintain mutable state across calls?
|
|
36
|
-
# Derived from execution_engine? — only execution engines maintain state.
|
|
37
|
-
def supports_state?
|
|
38
|
-
execution_engine?
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Execute code/prompt in this engine, return the result
|
|
42
|
-
# Subclasses must implement this
|
|
43
|
-
def execute(code)
|
|
44
|
-
raise NotImplementedError, "#{self.class}#execute not implemented"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Read a variable from the Ruby binding
|
|
48
|
-
def read_var(name)
|
|
49
|
-
if @binding.local_variables.include?(name.to_sym)
|
|
50
|
-
@binding.local_variable_get(name.to_sym)
|
|
51
|
-
elsif @binding.receiver.respond_to?(name.to_sym, true)
|
|
52
|
-
@binding.receiver.send(name.to_sym)
|
|
53
|
-
else
|
|
54
|
-
raise NameError, "undefined variable: #{name}"
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Write a variable to the Ruby binding
|
|
59
|
-
def write_var(name, value)
|
|
60
|
-
@binding.local_variable_set(name.to_sym, value)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Serialize a Ruby value for cross-language transfer
|
|
64
|
-
# Simple types: copy. Complex objects: will be remote refs (future)
|
|
65
|
-
def serialize(value)
|
|
66
|
-
case value
|
|
67
|
-
when Numeric, String, Symbol, TrueClass, FalseClass, NilClass
|
|
68
|
-
value
|
|
69
|
-
when Array
|
|
70
|
-
value.map { |v| serialize(v) }
|
|
71
|
-
when Hash
|
|
72
|
-
value.transform_values { |v| serialize(v) }
|
|
73
|
-
else
|
|
74
|
-
value.to_s
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/mana/engines/detect.rb
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Mana
|
|
6
|
-
module Engines
|
|
7
|
-
RULES_PATH = File.join(__dir__, "..", "..", "..", "data", "lang-rules.yml")
|
|
8
|
-
|
|
9
|
-
class Detector
|
|
10
|
-
attr_reader :rules
|
|
11
|
-
|
|
12
|
-
def initialize(rules_path = RULES_PATH)
|
|
13
|
-
@rules = YAML.safe_load(File.read(rules_path))["languages"]
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Detect language, return engine class
|
|
17
|
-
# context: previous detection result (for context inference)
|
|
18
|
-
def detect(code, context: nil)
|
|
19
|
-
scores = {}
|
|
20
|
-
@rules.each do |lang, rule_set|
|
|
21
|
-
scores[lang] = score(code, rule_set)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Context inference: boost previous language slightly
|
|
25
|
-
# Only boost if there's already some evidence (score > 0)
|
|
26
|
-
if context && scores[context] && scores[context] > 0
|
|
27
|
-
scores[context] += 2
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
best = scores.max_by { |_, v| v }
|
|
31
|
-
|
|
32
|
-
# If best score is very low, default to natural_language (LLM)
|
|
33
|
-
if best[1] <= 0
|
|
34
|
-
return engine_for("natural_language")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
engine_for(best[0])
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def score(code, rule_set)
|
|
43
|
-
s = 0
|
|
44
|
-
# Strong signals: +3 each
|
|
45
|
-
(rule_set["strong"] || []).each { |token| s += 3 if code.include?(token) }
|
|
46
|
-
# Weak signals: +1 each
|
|
47
|
-
(rule_set["weak"] || []).each { |token| s += 1 if code.include?(token) }
|
|
48
|
-
# Anti signals: -5 each (strong negative)
|
|
49
|
-
(rule_set["anti"] || []).each { |token| s -= 5 if code.include?(token) }
|
|
50
|
-
# Pattern signals: +4 each
|
|
51
|
-
(rule_set["patterns"] || []).each do |pattern|
|
|
52
|
-
s += 4 if code.match?(Regexp.new(pattern))
|
|
53
|
-
end
|
|
54
|
-
s
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def engine_for(lang)
|
|
58
|
-
case lang
|
|
59
|
-
when "javascript" then load_js_engine
|
|
60
|
-
when "python" then load_py_engine
|
|
61
|
-
when "ruby" then Engines::Ruby
|
|
62
|
-
else Engines::LLM
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def load_js_engine
|
|
67
|
-
require_relative "javascript"
|
|
68
|
-
Engines::JavaScript
|
|
69
|
-
rescue LoadError => e
|
|
70
|
-
warn "Mana: JavaScript engine unavailable (#{e.message}), falling back to LLM"
|
|
71
|
-
Engines::LLM
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def load_py_engine
|
|
75
|
-
require_relative "python"
|
|
76
|
-
Engines::Python
|
|
77
|
-
rescue LoadError => e
|
|
78
|
-
warn "Mana: Python engine unavailable (#{e.message}), falling back to LLM"
|
|
79
|
-
Engines::LLM
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Module-level convenience method
|
|
84
|
-
def self.detect(code, context: nil)
|
|
85
|
-
@detector ||= Detector.new
|
|
86
|
-
@detector.detect(code, context: context)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def self.reset_detector!
|
|
90
|
-
@detector = nil
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
begin
|
|
4
|
-
require "mini_racer"
|
|
5
|
-
rescue LoadError
|
|
6
|
-
raise LoadError, "mini_racer gem is required for JavaScript support. Add `gem 'mini_racer'` to your Gemfile."
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
require "json"
|
|
10
|
-
require "set"
|
|
11
|
-
|
|
12
|
-
module Mana
|
|
13
|
-
module Engines
|
|
14
|
-
class JavaScript < Base
|
|
15
|
-
# JS helper code that creates Proxy wrappers for remote Ruby objects.
|
|
16
|
-
# Injected once per V8 context.
|
|
17
|
-
JS_PROXY_HELPER = <<~JS
|
|
18
|
-
(function() {
|
|
19
|
-
if (typeof __mana_create_proxy !== 'undefined') return;
|
|
20
|
-
|
|
21
|
-
// FinalizationRegistry for automatic GC of remote refs.
|
|
22
|
-
// When a JS proxy is garbage collected, notify Ruby to release the original object.
|
|
23
|
-
if (typeof FinalizationRegistry !== 'undefined') {
|
|
24
|
-
globalThis.__mana_ref_gc = new FinalizationRegistry(function(refId) {
|
|
25
|
-
try { ruby.__ref_release(refId); } catch(e) {}
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
globalThis.__mana_create_proxy = function(refId, typeName) {
|
|
30
|
-
var proxy = new Proxy({ __mana_ref: refId, __mana_type: typeName }, {
|
|
31
|
-
get: function(target, prop) {
|
|
32
|
-
if (prop === '__mana_ref') return target.__mana_ref;
|
|
33
|
-
if (prop === '__mana_type') return target.__mana_type;
|
|
34
|
-
if (prop === '__mana_alive') return ruby.__ref_alive(refId);
|
|
35
|
-
if (prop === 'release') return function() { ruby.__ref_release(refId); };
|
|
36
|
-
if (prop === 'toString' || prop === Symbol.toPrimitive) {
|
|
37
|
-
return function() { return ruby.__ref_to_s(refId); };
|
|
38
|
-
}
|
|
39
|
-
if (prop === 'inspect') {
|
|
40
|
-
return function() { return 'RemoteRef<' + typeName + '#' + refId + '>'; };
|
|
41
|
-
}
|
|
42
|
-
if (typeof prop === 'symbol') return undefined;
|
|
43
|
-
return function() {
|
|
44
|
-
var args = Array.prototype.slice.call(arguments);
|
|
45
|
-
return ruby.__ref_call(refId, prop, JSON.stringify(args));
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Register with FinalizationRegistry so GC triggers release
|
|
51
|
-
if (globalThis.__mana_ref_gc) {
|
|
52
|
-
globalThis.__mana_ref_gc.register(proxy, refId);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return proxy;
|
|
56
|
-
};
|
|
57
|
-
})();
|
|
58
|
-
JS
|
|
59
|
-
|
|
60
|
-
# Thread-local persistent V8 context (lazy-loaded, long-running)
|
|
61
|
-
def self.context
|
|
62
|
-
Thread.current[:mana_js_context] ||= create_context
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.create_context
|
|
66
|
-
MiniRacer::Context.new
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def self.reset!
|
|
70
|
-
ctx = Thread.current[:mana_js_context]
|
|
71
|
-
ctx&.dispose
|
|
72
|
-
Thread.current[:mana_js_context] = nil
|
|
73
|
-
Thread.current[:mana_js_callbacks_attached] = nil
|
|
74
|
-
ObjectRegistry.reset!
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def execute(code)
|
|
78
|
-
ctx = self.class.context
|
|
79
|
-
|
|
80
|
-
# 1. Attach Ruby callbacks (methods + effects + ref operations)
|
|
81
|
-
attach_ruby_callbacks(ctx)
|
|
82
|
-
|
|
83
|
-
# 2. Inject Ruby variables into JS scope
|
|
84
|
-
inject_ruby_vars(ctx, code)
|
|
85
|
-
|
|
86
|
-
# 3. Execute the JS code
|
|
87
|
-
result = ctx.eval(code)
|
|
88
|
-
|
|
89
|
-
# 4. Extract any new/modified variables back to Ruby binding
|
|
90
|
-
extract_js_vars(ctx, code)
|
|
91
|
-
|
|
92
|
-
result
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
def attach_ruby_callbacks(ctx)
|
|
98
|
-
attached = Thread.current[:mana_js_callbacks_attached] ||= Set.new
|
|
99
|
-
|
|
100
|
-
# Install the JS Proxy helper (once per context)
|
|
101
|
-
unless attached.include?("__proxy_helper")
|
|
102
|
-
ctx.eval(JS_PROXY_HELPER)
|
|
103
|
-
attached << "__proxy_helper"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# ruby.read / ruby.write -- variable bridge
|
|
107
|
-
unless attached.include?("ruby.read")
|
|
108
|
-
bnd = @binding
|
|
109
|
-
ctx.attach("ruby.read", proc { |name|
|
|
110
|
-
sym = name.to_sym
|
|
111
|
-
if bnd.local_variables.include?(sym)
|
|
112
|
-
val = bnd.local_variable_get(sym)
|
|
113
|
-
json_safe(val)
|
|
114
|
-
else
|
|
115
|
-
nil
|
|
116
|
-
end
|
|
117
|
-
})
|
|
118
|
-
attached << "ruby.read"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
unless attached.include?("ruby.write")
|
|
122
|
-
bnd = @binding
|
|
123
|
-
ctx.attach("ruby.write", proc { |name, value|
|
|
124
|
-
bnd.local_variable_set(name.to_sym, value)
|
|
125
|
-
value
|
|
126
|
-
})
|
|
127
|
-
attached << "ruby.write"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Remote reference operations
|
|
131
|
-
attach_ref_callbacks(ctx, attached)
|
|
132
|
-
|
|
133
|
-
# Attach methods from the caller's receiver
|
|
134
|
-
attach_receiver_methods(ctx, attached)
|
|
135
|
-
|
|
136
|
-
# Attach registered Mana effects
|
|
137
|
-
attach_effects(ctx, attached)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Attach callbacks for operating on remote Ruby object references from JS.
|
|
141
|
-
def attach_ref_callbacks(ctx, attached)
|
|
142
|
-
registry = ObjectRegistry.current
|
|
143
|
-
|
|
144
|
-
unless attached.include?("ruby.__ref_call")
|
|
145
|
-
ctx.attach("ruby.__ref_call", proc { |ref_id, method_name, args_json|
|
|
146
|
-
obj = registry.get(ref_id)
|
|
147
|
-
raise "Remote reference #{ref_id} has been released" unless obj
|
|
148
|
-
|
|
149
|
-
args = args_json ? JSON.parse(args_json) : []
|
|
150
|
-
result = obj.public_send(method_name.to_sym, *args)
|
|
151
|
-
json_safe(result)
|
|
152
|
-
})
|
|
153
|
-
attached << "ruby.__ref_call"
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
unless attached.include?("ruby.__ref_release")
|
|
157
|
-
ctx.attach("ruby.__ref_release", proc { |ref_id|
|
|
158
|
-
registry.release(ref_id)
|
|
159
|
-
nil
|
|
160
|
-
})
|
|
161
|
-
attached << "ruby.__ref_release"
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
unless attached.include?("ruby.__ref_alive")
|
|
165
|
-
ctx.attach("ruby.__ref_alive", proc { |ref_id|
|
|
166
|
-
registry.registered?(ref_id)
|
|
167
|
-
})
|
|
168
|
-
attached << "ruby.__ref_alive"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
unless attached.include?("ruby.__ref_to_s")
|
|
172
|
-
ctx.attach("ruby.__ref_to_s", proc { |ref_id|
|
|
173
|
-
obj = registry.get(ref_id)
|
|
174
|
-
obj ? obj.to_s : "released"
|
|
175
|
-
})
|
|
176
|
-
attached << "ruby.__ref_to_s"
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def attach_receiver_methods(ctx, attached)
|
|
181
|
-
receiver = @binding.receiver
|
|
182
|
-
user_methods = receiver.class.instance_methods(false) -
|
|
183
|
-
Object.instance_methods -
|
|
184
|
-
[:~@]
|
|
185
|
-
|
|
186
|
-
user_methods.each do |method_name|
|
|
187
|
-
key = "ruby.#{method_name}"
|
|
188
|
-
next if attached.include?(key)
|
|
189
|
-
|
|
190
|
-
recv = receiver
|
|
191
|
-
ctx.attach(key, proc { |*args| json_safe(recv.public_send(method_name, *args)) })
|
|
192
|
-
attached << key
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def attach_effects(ctx, attached)
|
|
197
|
-
Mana::EffectRegistry.registry.each do |name, effect|
|
|
198
|
-
key = "ruby.#{name}"
|
|
199
|
-
next if attached.include?(key)
|
|
200
|
-
|
|
201
|
-
eff = effect
|
|
202
|
-
ctx.attach(key, proc { |*args|
|
|
203
|
-
input = if args.length == 1 && args[0].is_a?(Hash)
|
|
204
|
-
args[0]
|
|
205
|
-
elsif eff.params.length == args.length
|
|
206
|
-
eff.params.zip(args).to_h { |p, v| [p[:name], v] }
|
|
207
|
-
else
|
|
208
|
-
{}
|
|
209
|
-
end
|
|
210
|
-
json_safe(eff.call(input))
|
|
211
|
-
})
|
|
212
|
-
attached << key
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def inject_ruby_vars(ctx, code)
|
|
217
|
-
@binding.local_variables.each do |var_name|
|
|
218
|
-
next unless code.match?(/\b#{Regexp.escape(var_name.to_s)}\b/)
|
|
219
|
-
|
|
220
|
-
value = @binding.local_variable_get(var_name)
|
|
221
|
-
inject_value(ctx, var_name.to_s, value)
|
|
222
|
-
rescue => e
|
|
223
|
-
next
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Inject a single value into the JS context.
|
|
228
|
-
# Simple types are serialized to JSON. Complex objects become remote ref proxies.
|
|
229
|
-
def inject_value(ctx, name, value)
|
|
230
|
-
case value
|
|
231
|
-
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
232
|
-
ctx.eval("var #{name} = #{JSON.generate(value)}")
|
|
233
|
-
when Symbol
|
|
234
|
-
ctx.eval("var #{name} = #{JSON.generate(value.to_s)}")
|
|
235
|
-
when Array
|
|
236
|
-
ctx.eval("var #{name} = #{JSON.generate(serialize_array(value))}")
|
|
237
|
-
when Hash
|
|
238
|
-
ctx.eval("var #{name} = #{JSON.generate(serialize_hash(value))}")
|
|
239
|
-
else
|
|
240
|
-
# Complex object → register and create JS Proxy
|
|
241
|
-
ref_id = ObjectRegistry.current.register(value)
|
|
242
|
-
ctx.eval("var #{name} = __mana_create_proxy(#{ref_id}, #{JSON.generate(value.class.name)})")
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def serialize_array(arr)
|
|
247
|
-
arr.map { |v| simple_value?(v) ? serialize_simple(v) : v.to_s }
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def serialize_hash(hash)
|
|
251
|
-
hash.transform_keys(&:to_s).transform_values { |v|
|
|
252
|
-
simple_value?(v) ? serialize_simple(v) : v.to_s
|
|
253
|
-
}
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def simple_value?(value)
|
|
257
|
-
case value
|
|
258
|
-
when Numeric, String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash
|
|
259
|
-
true
|
|
260
|
-
else
|
|
261
|
-
false
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def serialize_simple(value)
|
|
266
|
-
case value
|
|
267
|
-
when Symbol then value.to_s
|
|
268
|
-
when Array then serialize_array(value)
|
|
269
|
-
when Hash then serialize_hash(value)
|
|
270
|
-
else value
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def extract_js_vars(ctx, code)
|
|
275
|
-
declared_vars = extract_declared_vars(code)
|
|
276
|
-
declared_vars.each do |var_name|
|
|
277
|
-
begin
|
|
278
|
-
value = ctx.eval(var_name)
|
|
279
|
-
deserialized = deserialize(value)
|
|
280
|
-
write_var(var_name, deserialized)
|
|
281
|
-
rescue MiniRacer::RuntimeError
|
|
282
|
-
next
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def extract_declared_vars(code)
|
|
288
|
-
vars = []
|
|
289
|
-
code.scan(/\b(?:const|let|var)\s+(\w+)\s*=/).each { |m| vars << m[0] }
|
|
290
|
-
code.scan(/^(\w+)\s*=[^=>]/).each { |m| vars << m[0] }
|
|
291
|
-
vars.uniq
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def deserialize(value)
|
|
295
|
-
value
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def json_safe(value)
|
|
299
|
-
case value
|
|
300
|
-
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
301
|
-
value
|
|
302
|
-
when Symbol
|
|
303
|
-
value.to_s
|
|
304
|
-
when Array
|
|
305
|
-
value.map { |v| json_safe(v) }
|
|
306
|
-
when Hash
|
|
307
|
-
value.transform_keys(&:to_s).transform_values { |v| json_safe(v) }
|
|
308
|
-
else
|
|
309
|
-
value.to_s
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
end
|