oso-oso 0.2.5 → 0.2.6
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/Gemfile.lock +1 -1
- data/Makefile +3 -0
- data/bin/oso +7 -0
- data/ext/oso-oso/lib/libpolar.dylib +0 -0
- data/ext/oso-oso/lib/libpolar.so +0 -0
- data/ext/oso-oso/lib/polar.dll +0 -0
- data/lib/oso/oso.rb +5 -31
- data/lib/oso/polar.rb +1 -0
- data/lib/oso/polar/errors.rb +1 -0
- data/lib/oso/polar/ffi.rb +5 -3
- data/lib/oso/polar/ffi/polar.rb +0 -10
- data/lib/oso/polar/host.rb +235 -0
- data/lib/oso/polar/polar.rb +67 -286
- data/lib/oso/polar/query.rb +64 -26
- data/lib/oso/version.rb +1 -1
- data/oso-oso.gemspec +3 -3
- metadata +7 -6
- data/bin/console +0 -33
- data/bin/setup +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c00a51f40789d269beb2d63e29ad12b76565d94
|
4
|
+
data.tar.gz: 8f66dfdb31532125fd3593ddcfb7c3e76fd81df5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ac0a230d40ecab86a7896249eb875fa43a16705a0594c9b55acd9d21215bff572ce1abb18da0593455f1a3c749354453808b779bed3dbf14e171f8b26b7511c
|
7
|
+
data.tar.gz: 910cccfb441f515b20bcd1c21fc02a3f293b4445ed4526e2b8a2a921d97062df23156bc2726587269139a79edd687bd8d5b9fa79776bbc5910045f2205e58923
|
data/Gemfile.lock
CHANGED
data/Makefile
CHANGED
data/bin/oso
ADDED
Binary file
|
data/ext/oso-oso/lib/libpolar.so
CHANGED
Binary file
|
data/ext/oso-oso/lib/polar.dll
CHANGED
Binary file
|
data/lib/oso/oso.rb
CHANGED
@@ -1,47 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'polar/polar'
|
4
|
+
|
3
5
|
module Oso
|
4
6
|
# Oso authorization API.
|
5
|
-
class Oso
|
7
|
+
class Oso < Polar::Polar
|
6
8
|
def initialize
|
7
|
-
|
9
|
+
super
|
8
10
|
register_class(Http, name: 'Http')
|
9
11
|
register_class(PathMapper, name: 'PathMapper')
|
10
12
|
end
|
11
13
|
|
12
|
-
def load_file(file)
|
13
|
-
polar.load_file(file)
|
14
|
-
end
|
15
|
-
|
16
|
-
def load_str(str)
|
17
|
-
polar.load_str(str)
|
18
|
-
end
|
19
|
-
|
20
|
-
def register_class(cls, name: nil) # rubocop:disable Naming/MethodParameterName
|
21
|
-
if block_given?
|
22
|
-
polar.register_class(cls, name: name, from_polar: Proc.new)
|
23
|
-
else
|
24
|
-
polar.register_class(cls, name: name)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
14
|
def allow(actor:, action:, resource:)
|
29
|
-
|
15
|
+
query_predicate('allow', actor, action, resource).next
|
30
16
|
true
|
31
17
|
rescue StopIteration
|
32
18
|
false
|
33
19
|
end
|
34
|
-
|
35
|
-
def query_predicate(name, *args)
|
36
|
-
polar.query_pred(name, args: args)
|
37
|
-
end
|
38
|
-
|
39
|
-
def load_queued_files
|
40
|
-
polar.load_queued_files
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
attr_reader :polar
|
46
20
|
end
|
47
21
|
end
|
data/lib/oso/polar.rb
CHANGED
data/lib/oso/polar/errors.rb
CHANGED
@@ -40,6 +40,7 @@ module Oso
|
|
40
40
|
class DuplicateInstanceRegistrationError < PolarRuntimeError; end
|
41
41
|
class InvalidCallError < PolarRuntimeError; end
|
42
42
|
class InvalidConstructorError < PolarRuntimeError; end
|
43
|
+
class InvalidQueryTypeError < PolarRuntimeError; end
|
43
44
|
class InlineQueryFailedError < PolarRuntimeError; end
|
44
45
|
class NullByteInPolarFileError < PolarRuntimeError; end
|
45
46
|
class UnexpectedPolarTypeError < PolarRuntimeError; end
|
data/lib/oso/polar/ffi.rb
CHANGED
@@ -6,9 +6,11 @@ module Oso
|
|
6
6
|
module Polar
|
7
7
|
module FFI
|
8
8
|
LIB = ::FFI::Platform::LIBPREFIX + 'polar.' + ::FFI::Platform::LIBSUFFIX
|
9
|
-
|
10
|
-
|
11
|
-
#
|
9
|
+
RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
|
10
|
+
DEV_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
|
11
|
+
# If the lib exists in the ext/ dir, use it. Otherwise, fall back to
|
12
|
+
# checking the local Rust target dir.
|
13
|
+
LIB_PATH = File.file?(RELEASE_PATH) ? RELEASE_PATH : DEV_PATH
|
12
14
|
|
13
15
|
# Wrapper classes defined upfront to fix Ruby loading issues. Actual
|
14
16
|
# implementations live in the sibling `ffi/` directory and are `require`d
|
data/lib/oso/polar/ffi/polar.rb
CHANGED
@@ -15,7 +15,6 @@ module Oso
|
|
15
15
|
attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
|
16
16
|
attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string], FFI::Query
|
17
17
|
attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string], FFI::Query
|
18
|
-
attach_function :new_query_from_repl, :polar_query_from_repl, [FFI::Polar], FFI::Query
|
19
18
|
attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
|
20
19
|
attach_function :free, :polar_free, [FFI::Polar], :int32
|
21
20
|
end
|
@@ -76,15 +75,6 @@ module Oso
|
|
76
75
|
query
|
77
76
|
end
|
78
77
|
|
79
|
-
# @return [FFI::Query]
|
80
|
-
# @raise [FFI::Error] if the FFI call returns an error.
|
81
|
-
def new_query_from_repl
|
82
|
-
query = Rust.new_query_from_repl(self)
|
83
|
-
raise FFI::Error.get if query.null?
|
84
|
-
|
85
|
-
query
|
86
|
-
end
|
87
|
-
|
88
78
|
# @param name [String]
|
89
79
|
# @param value [Hash<String, Object>]
|
90
80
|
# @raise [FFI::Error] if the FFI call returns an error.
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Oso
|
4
|
+
module Polar
|
5
|
+
# Translate between Polar and the host language (Ruby).
|
6
|
+
class Host
|
7
|
+
protected
|
8
|
+
# @return [FFI::Polar]
|
9
|
+
attr_reader :ffi_polar
|
10
|
+
# @return [Hash<String, Class>]
|
11
|
+
attr_reader :classes
|
12
|
+
# @return [Hash<String, Object>]
|
13
|
+
attr_reader :constructors
|
14
|
+
# @return [Hash<Integer, Object>]
|
15
|
+
attr_reader :instances
|
16
|
+
|
17
|
+
public
|
18
|
+
def initialize(ffi_polar)
|
19
|
+
@ffi_polar = ffi_polar
|
20
|
+
@classes = {}
|
21
|
+
@constructors = {}
|
22
|
+
@instances = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize_copy(other)
|
26
|
+
@ffi_polar = other.ffi_polar
|
27
|
+
@classes = other.classes.dup
|
28
|
+
@constructors = other.constructors.dup
|
29
|
+
@instances = other.instances.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
# Fetch a Ruby class from the {#classes} cache.
|
33
|
+
#
|
34
|
+
# @param name [String]
|
35
|
+
# @return [Class]
|
36
|
+
# @raise [UnregisteredClassError] if the class has not been registered.
|
37
|
+
def get_class(name)
|
38
|
+
raise UnregisteredClassError, name unless classes.key? name
|
39
|
+
|
40
|
+
classes[name]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Store a Ruby class in the {#classes} cache.
|
44
|
+
#
|
45
|
+
# @param cls [Class] the class to cache
|
46
|
+
# @param name [String] the name to cache the class as. Defaults to the name of the class.
|
47
|
+
# @return [String] the name the class is cached as.
|
48
|
+
# @raise [UnregisteredClassError] if the class has not been registered.
|
49
|
+
def cache_class(cls, name:, constructor:)
|
50
|
+
name = cls.name if name.nil?
|
51
|
+
raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
|
52
|
+
|
53
|
+
classes[name] = cls
|
54
|
+
if constructor.nil?
|
55
|
+
constructors[name] = :new
|
56
|
+
elsif constructor.respond_to? :call
|
57
|
+
constructors[name] = constructor
|
58
|
+
else
|
59
|
+
raise InvalidConstructorError
|
60
|
+
end
|
61
|
+
name
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fetch a constructor from the {#constructors} cache.
|
65
|
+
#
|
66
|
+
# @param name [String]
|
67
|
+
# @return [Symbol] if constructor is the default of `:new`.
|
68
|
+
# @return [Proc] if a custom constructor was registered.
|
69
|
+
# @raise [UnregisteredConstructorError] if the constructor has not been registered.
|
70
|
+
def get_constructor(name)
|
71
|
+
raise MissingConstructorError, name unless constructors.key? name
|
72
|
+
|
73
|
+
constructors[name]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if an instance has been cached.
|
77
|
+
#
|
78
|
+
# @param id [Integer]
|
79
|
+
# @return [Boolean]
|
80
|
+
def instance?(id)
|
81
|
+
case id
|
82
|
+
when Integer
|
83
|
+
instances.key? id
|
84
|
+
else
|
85
|
+
instances.value? id
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Fetch a Ruby instance from the {#instances} cache.
|
90
|
+
#
|
91
|
+
# @param id [Integer]
|
92
|
+
# @return [Object]
|
93
|
+
# @raise [UnregisteredInstanceError] if the ID has not been registered.
|
94
|
+
def get_instance(id)
|
95
|
+
raise UnregisteredInstanceError, id unless instance? id
|
96
|
+
|
97
|
+
instances[id]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
|
101
|
+
#
|
102
|
+
# @param instance [Object]
|
103
|
+
# @param id [Integer]
|
104
|
+
# @return [Integer]
|
105
|
+
def cache_instance(instance, id: nil)
|
106
|
+
id = ffi_polar.new_id if id.nil?
|
107
|
+
instances[id] = instance
|
108
|
+
id
|
109
|
+
end
|
110
|
+
|
111
|
+
# Construct and cache a Ruby instance.
|
112
|
+
#
|
113
|
+
# @param cls_name [String]
|
114
|
+
# @param fields [Hash<String, Hash>]
|
115
|
+
# @param id [Integer]
|
116
|
+
# @raise [PolarRuntimeError] if instance construction fails.
|
117
|
+
def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
118
|
+
constructor = get_constructor(cls_name)
|
119
|
+
fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
|
120
|
+
instance = if constructor == :new
|
121
|
+
if fields.empty?
|
122
|
+
get_class(cls_name).__send__(:new)
|
123
|
+
else
|
124
|
+
get_class(cls_name).__send__(:new, **fields)
|
125
|
+
end
|
126
|
+
elsif fields.empty?
|
127
|
+
constructor.call
|
128
|
+
else
|
129
|
+
constructor.call(**fields)
|
130
|
+
end
|
131
|
+
cache_instance(instance, id: id)
|
132
|
+
rescue StandardError => e
|
133
|
+
raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check if the left class is more specific than the right class
|
137
|
+
# with respect to the given instance.
|
138
|
+
#
|
139
|
+
# @param instance_id [Integer]
|
140
|
+
# @param left_tag [String]
|
141
|
+
# @param right_tag [String]
|
142
|
+
# @return [Boolean]
|
143
|
+
def subspecializer?(instance_id, left_tag:, right_tag:)
|
144
|
+
mro = get_instance(instance_id).class.ancestors
|
145
|
+
mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
|
146
|
+
rescue StandardError
|
147
|
+
false
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check if instance is an instance of class.
|
151
|
+
#
|
152
|
+
# @param instance_id [Integer]
|
153
|
+
# @param class_tag [String]
|
154
|
+
# @return [Boolean]
|
155
|
+
def isa?(instance_id, class_tag:)
|
156
|
+
instance = get_instance(instance_id)
|
157
|
+
cls = get_class(class_tag)
|
158
|
+
instance.is_a? cls
|
159
|
+
rescue PolarRuntimeError
|
160
|
+
false
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check if two instances unify
|
164
|
+
#
|
165
|
+
# @param left_instance_id [Integer]
|
166
|
+
# @param right_instance_id [Integer]
|
167
|
+
# @return [Boolean]
|
168
|
+
def unify?(left_instance_id, right_instance_id)
|
169
|
+
left_instance = get_instance(left_instance_id)
|
170
|
+
right_instance = get_instance(right_instance_id)
|
171
|
+
left_instance == right_instance
|
172
|
+
rescue PolarRuntimeError
|
173
|
+
false
|
174
|
+
end
|
175
|
+
|
176
|
+
# Turn a Ruby value into a Polar term that's ready to be sent across the
|
177
|
+
# FFI boundary.
|
178
|
+
#
|
179
|
+
# @param value [Object]
|
180
|
+
# @return [Hash<String, Object>]
|
181
|
+
def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
182
|
+
value = case true # rubocop:disable Lint/LiteralAsCondition
|
183
|
+
when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
|
184
|
+
{ 'Boolean' => value }
|
185
|
+
when value.instance_of?(Integer)
|
186
|
+
{ 'Number' => { 'Integer' => value } }
|
187
|
+
when value.instance_of?(Float)
|
188
|
+
{ 'Number' => { 'Float' => value } }
|
189
|
+
when value.instance_of?(String)
|
190
|
+
{ 'String' => value }
|
191
|
+
when value.instance_of?(Array)
|
192
|
+
{ 'List' => value.map { |el| to_polar_term(el) } }
|
193
|
+
when value.instance_of?(Hash)
|
194
|
+
{ 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
|
195
|
+
when value.instance_of?(Predicate)
|
196
|
+
{ 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
|
197
|
+
when value.instance_of?(Variable)
|
198
|
+
# This is supported so that we can query for unbound variables
|
199
|
+
{ 'Variable' => value }
|
200
|
+
else
|
201
|
+
{ 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
|
202
|
+
end
|
203
|
+
{ 'value' => value }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Turn a Polar term passed across the FFI boundary into a Ruby value.
|
207
|
+
#
|
208
|
+
# @param data [Hash<String, Object>]
|
209
|
+
# @option data [Integer] :id
|
210
|
+
# @option data [Integer] :offset Character offset of the term in its source string.
|
211
|
+
# @option data [Hash<String, Object>] :value
|
212
|
+
# @return [Object]
|
213
|
+
# @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
|
214
|
+
def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
215
|
+
tag, value = data['value'].first
|
216
|
+
case tag
|
217
|
+
when 'String', 'Boolean'
|
218
|
+
value
|
219
|
+
when 'Number'
|
220
|
+
value.values.first
|
221
|
+
when 'List'
|
222
|
+
value.map { |el| to_ruby(el) }
|
223
|
+
when 'Dictionary'
|
224
|
+
value['fields'].transform_values { |v| to_ruby(v) }
|
225
|
+
when 'ExternalInstance'
|
226
|
+
get_instance(value['instance_id'])
|
227
|
+
when 'Call'
|
228
|
+
Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
|
229
|
+
else
|
230
|
+
raise UnexpectedPolarTypeError, tag
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/lib/oso/polar/polar.rb
CHANGED
@@ -1,21 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
+
require 'pp'
|
4
5
|
require 'set'
|
5
6
|
|
6
7
|
module Oso
|
7
8
|
module Polar
|
8
9
|
# Create and manage an instance of the Polar runtime.
|
9
|
-
class Polar
|
10
|
+
class Polar
|
11
|
+
# @return [Host]
|
12
|
+
attr_reader :host
|
13
|
+
|
10
14
|
def initialize
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@classes = {}
|
14
|
-
@constructors = {}
|
15
|
-
@instances = {}
|
15
|
+
@ffi_polar = FFI::Polar.create
|
16
|
+
@host = Host.new(ffi_polar)
|
16
17
|
@load_queue = Set.new
|
17
18
|
end
|
18
19
|
|
20
|
+
# Replace the current Polar instance but retain all registered classes and constructors.
|
21
|
+
def clear
|
22
|
+
load_queue.clear
|
23
|
+
@ffi_polar = FFI::Polar.create
|
24
|
+
end
|
25
|
+
|
19
26
|
# Enqueue a Polar policy file for loading into the KB.
|
20
27
|
#
|
21
28
|
# @param name [String]
|
@@ -38,23 +45,32 @@ module Oso
|
|
38
45
|
def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
|
39
46
|
raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
|
40
47
|
|
41
|
-
|
48
|
+
ffi_polar.load_str(str, filename: filename)
|
42
49
|
loop do
|
43
|
-
next_query =
|
50
|
+
next_query = ffi_polar.next_inline_query
|
44
51
|
break if next_query.nil?
|
45
52
|
|
46
53
|
begin
|
47
|
-
Query.new(next_query,
|
54
|
+
Query.new(next_query, host: host).results.next
|
48
55
|
rescue StopIteration
|
49
56
|
raise InlineQueryFailedError
|
50
57
|
end
|
51
58
|
end
|
52
59
|
end
|
53
60
|
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
# Query for a predicate, parsing it if necessary.
|
62
|
+
def query(query)
|
63
|
+
load_queued_files
|
64
|
+
new_host = host.dup
|
65
|
+
case query
|
66
|
+
when String
|
67
|
+
ffi_query = ffi_polar.new_query_from_str(query)
|
68
|
+
when Predicate
|
69
|
+
ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
|
70
|
+
else
|
71
|
+
raise InvalidQueryTypeError
|
72
|
+
end
|
73
|
+
Query.new(ffi_query, host: new_host).results
|
58
74
|
end
|
59
75
|
|
60
76
|
# Query for a predicate.
|
@@ -62,245 +78,69 @@ module Oso
|
|
62
78
|
# @param name [String]
|
63
79
|
# @param args [Array<Object>]
|
64
80
|
# @raise [Error] if the FFI call raises one.
|
65
|
-
def
|
66
|
-
|
67
|
-
load_queued_files
|
68
|
-
pred = Predicate.new(name, args: args)
|
69
|
-
query_ffi_instance = ffi_instance.new_query_from_term(to_polar_term(pred))
|
70
|
-
Query.new(query_ffi_instance, polar: self).results
|
81
|
+
def query_predicate(name, *args)
|
82
|
+
query(Predicate.new(name, args: args))
|
71
83
|
end
|
72
84
|
|
73
85
|
# Start a REPL session.
|
74
86
|
#
|
75
87
|
# @raise [Error] if the FFI call raises one.
|
76
|
-
def repl # rubocop:disable Metrics/MethodLength
|
77
|
-
|
88
|
+
def repl(load: false) # rubocop:disable Metrics/MethodLength
|
89
|
+
ARGV.map { |f| load_file(f) } if load
|
78
90
|
load_queued_files
|
91
|
+
|
79
92
|
loop do
|
80
|
-
|
81
|
-
|
93
|
+
print('> ')
|
94
|
+
begin
|
95
|
+
query = STDIN.readline.chomp.chomp(';')
|
96
|
+
rescue EOFError
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
ffi_query = ffi_polar.new_query_from_str(query)
|
102
|
+
rescue ParseError => e
|
103
|
+
puts("Parse error: " + e.to_s)
|
104
|
+
next
|
105
|
+
end
|
106
|
+
|
107
|
+
begin
|
108
|
+
results = Query.new(ffi_query, host: host).results.to_a
|
109
|
+
rescue PolarRuntimeError => e
|
110
|
+
puts(e.to_s)
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
82
114
|
if results.empty?
|
83
|
-
|
115
|
+
pp false
|
84
116
|
else
|
85
117
|
results.each do |result|
|
86
|
-
|
118
|
+
if result.empty?
|
119
|
+
pp true
|
120
|
+
else
|
121
|
+
pp result
|
122
|
+
end
|
87
123
|
end
|
88
124
|
end
|
89
125
|
end
|
90
126
|
end
|
91
127
|
|
92
|
-
# Get a unique ID from Polar.
|
93
|
-
#
|
94
|
-
# @return [Integer]
|
95
|
-
# @raise [Error] if the FFI call raises one.
|
96
|
-
def new_id
|
97
|
-
ffi_instance.new_id
|
98
|
-
end
|
99
|
-
|
100
128
|
# Register a Ruby class with Polar.
|
101
129
|
#
|
102
130
|
# @param cls [Class]
|
103
131
|
# @param name [String]
|
104
132
|
# @param from_polar [Proc]
|
105
133
|
# @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
|
106
|
-
def register_class(cls, name: nil, from_polar: nil)
|
107
|
-
|
108
|
-
|
109
|
-
# (Option<Symbol>) that defaults to :new?
|
110
|
-
name = cls.name if name.nil?
|
111
|
-
raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
|
112
|
-
|
113
|
-
classes[name] = cls
|
114
|
-
if from_polar.nil?
|
115
|
-
constructors[name] = :new
|
116
|
-
elsif from_polar.respond_to? :call
|
117
|
-
constructors[name] = from_polar
|
118
|
-
else
|
119
|
-
raise InvalidConstructorError
|
134
|
+
def register_class(cls, name: nil, from_polar: nil)
|
135
|
+
if block_given?
|
136
|
+
from_polar = Proc.new
|
120
137
|
end
|
121
|
-
|
138
|
+
name = host.cache_class(cls, name: name, constructor: from_polar)
|
122
139
|
register_constant(name, value: cls)
|
123
140
|
end
|
124
141
|
|
125
142
|
def register_constant(name, value:)
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
# Register a Ruby method call, wrapping the call result in a generator if
|
130
|
-
# it isn't already one.
|
131
|
-
#
|
132
|
-
# @param method [#to_sym]
|
133
|
-
# @param call_id [Integer]
|
134
|
-
# @param instance [Hash]
|
135
|
-
# @param args [Array<Hash>]
|
136
|
-
# @raise [InvalidCallError] if the method doesn't exist on the instance or
|
137
|
-
# the args passed to the method are invalid.
|
138
|
-
def register_call(method, call_id:, instance:, args:)
|
139
|
-
return if calls.key?(call_id)
|
140
|
-
|
141
|
-
args = args.map { |a| to_ruby(a) }
|
142
|
-
if instance["value"].has_key? "ExternalInstance"
|
143
|
-
instance_id = instance["value"]["ExternalInstance"]["instance_id"]
|
144
|
-
instance = get_instance(instance_id)
|
145
|
-
else
|
146
|
-
instance = to_ruby(instance)
|
147
|
-
end
|
148
|
-
result = instance.__send__(method, *args)
|
149
|
-
result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
|
150
|
-
calls[call_id] = result.lazy
|
151
|
-
rescue ArgumentError, NoMethodError
|
152
|
-
raise InvalidCallError
|
153
|
-
end
|
154
|
-
|
155
|
-
# Retrieve the next result from a registered call and pass it to {#to_polar_term}.
|
156
|
-
#
|
157
|
-
# @param id [Integer]
|
158
|
-
# @return [Hash]
|
159
|
-
# @raise [StopIteration] if the call has been exhausted.
|
160
|
-
def next_call_result(id)
|
161
|
-
to_polar_term(calls[id].next)
|
162
|
-
end
|
163
|
-
|
164
|
-
# Construct and cache a Ruby instance.
|
165
|
-
#
|
166
|
-
# @param cls_name [String]
|
167
|
-
# @param fields [Hash<String, Hash>]
|
168
|
-
# @param id [Integer]
|
169
|
-
# @raise [PolarRuntimeError] if instance construction fails.
|
170
|
-
def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
171
|
-
constructor = get_constructor(cls_name)
|
172
|
-
fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
|
173
|
-
instance = if constructor == :new
|
174
|
-
if fields.empty?
|
175
|
-
get_class(cls_name).__send__(:new)
|
176
|
-
else
|
177
|
-
get_class(cls_name).__send__(:new, **fields)
|
178
|
-
end
|
179
|
-
elsif fields.empty?
|
180
|
-
constructor.call
|
181
|
-
else
|
182
|
-
constructor.call(**fields)
|
183
|
-
end
|
184
|
-
cache_instance(instance, id: id)
|
185
|
-
rescue StandardError => e
|
186
|
-
raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
|
187
|
-
end
|
188
|
-
|
189
|
-
# Check if an instance has been cached.
|
190
|
-
#
|
191
|
-
# @param id [Integer]
|
192
|
-
# @return [Boolean]
|
193
|
-
def instance?(id)
|
194
|
-
instances.key? id
|
195
|
-
end
|
196
|
-
|
197
|
-
# Fetch a Ruby instance from the {#instances} cache.
|
198
|
-
#
|
199
|
-
# @param id [Integer]
|
200
|
-
# @return [Object]
|
201
|
-
# @raise [UnregisteredInstanceError] if the ID has not been registered.
|
202
|
-
def get_instance(id)
|
203
|
-
raise UnregisteredInstanceError, id unless instance? id
|
204
|
-
|
205
|
-
instances[id]
|
206
|
-
end
|
207
|
-
|
208
|
-
# Check if the left class is more specific than the right class for the
|
209
|
-
# given instance.
|
210
|
-
#
|
211
|
-
# @param instance_id [Integer]
|
212
|
-
# @param left_tag [String]
|
213
|
-
# @param right_tag [String]
|
214
|
-
# @return [Boolean]
|
215
|
-
def subspecializer?(instance_id, left_tag:, right_tag:)
|
216
|
-
mro = get_instance(instance_id).class.ancestors
|
217
|
-
mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
|
218
|
-
rescue StandardError
|
219
|
-
false
|
220
|
-
end
|
221
|
-
|
222
|
-
# Check if instance is an instance of class.
|
223
|
-
#
|
224
|
-
# @param instance_id [Integer]
|
225
|
-
# @param class_tag [String]
|
226
|
-
# @return [Boolean]
|
227
|
-
def isa?(instance_id, class_tag:)
|
228
|
-
instance = get_instance(instance_id)
|
229
|
-
cls = get_class(class_tag)
|
230
|
-
instance.is_a? cls
|
231
|
-
rescue PolarRuntimeError
|
232
|
-
false
|
233
|
-
end
|
234
|
-
|
235
|
-
# Check if two instances unify
|
236
|
-
#
|
237
|
-
# @param left_instance_id [Integer]
|
238
|
-
# @param right_instance_id [Integer]
|
239
|
-
# @return [Boolean]
|
240
|
-
def unify?(left_instance_id, right_instance_id)
|
241
|
-
left_instance = get_instance(left_instance_id)
|
242
|
-
right_instance = get_instance(right_instance_id)
|
243
|
-
left_instance == right_instance
|
244
|
-
rescue PolarRuntimeError
|
245
|
-
false
|
246
|
-
end
|
247
|
-
|
248
|
-
# Turn a Ruby value into a Polar term that's ready to be sent across the
|
249
|
-
# FFI boundary.
|
250
|
-
#
|
251
|
-
# @param value [Object]
|
252
|
-
# @return [Hash<String, Object>]
|
253
|
-
def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
254
|
-
value = case true # rubocop:disable Lint/LiteralAsCondition
|
255
|
-
when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
|
256
|
-
{ 'Boolean' => value }
|
257
|
-
when value.instance_of?(Integer)
|
258
|
-
{ 'Number' => { 'Integer' => value } }
|
259
|
-
when value.instance_of?(Float)
|
260
|
-
{ 'Number' => { 'Float' => value } }
|
261
|
-
when value.instance_of?(String)
|
262
|
-
{ 'String' => value }
|
263
|
-
when value.instance_of?(Array)
|
264
|
-
{ 'List' => value.map { |el| to_polar_term(el) } }
|
265
|
-
when value.instance_of?(Hash)
|
266
|
-
{ 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
|
267
|
-
when value.instance_of?(Predicate)
|
268
|
-
{ 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
|
269
|
-
when value.instance_of?(Variable)
|
270
|
-
# This is supported so that we can query for unbound variables
|
271
|
-
{ 'Variable' => value }
|
272
|
-
else
|
273
|
-
{ 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
|
274
|
-
end
|
275
|
-
{ 'value' => value }
|
276
|
-
end
|
277
|
-
|
278
|
-
# Turn a Polar term passed across the FFI boundary into a Ruby value.
|
279
|
-
#
|
280
|
-
# @param data [Hash<String, Object>]
|
281
|
-
# @option data [Integer] :id
|
282
|
-
# @option data [Integer] :offset Character offset of the term in its source string.
|
283
|
-
# @option data [Hash<String, Object>] :value
|
284
|
-
# @return [Object]
|
285
|
-
# @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
|
286
|
-
def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
287
|
-
tag, value = data['value'].first
|
288
|
-
case tag
|
289
|
-
when 'String', 'Boolean'
|
290
|
-
value
|
291
|
-
when 'Number'
|
292
|
-
value.values.first
|
293
|
-
when 'List'
|
294
|
-
value.map { |el| to_ruby(el) }
|
295
|
-
when 'Dictionary'
|
296
|
-
value['fields'].transform_values { |v| to_ruby(v) }
|
297
|
-
when 'ExternalInstance'
|
298
|
-
get_instance(value['instance_id'])
|
299
|
-
when 'Call'
|
300
|
-
Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
|
301
|
-
else
|
302
|
-
raise UnexpectedPolarTypeError, tag
|
303
|
-
end
|
143
|
+
ffi_polar.register_constant(name, value: host.to_polar_term(value))
|
304
144
|
end
|
305
145
|
|
306
146
|
# Load all queued files, flushing the {#load_queue}.
|
@@ -313,69 +153,10 @@ module Oso
|
|
313
153
|
|
314
154
|
private
|
315
155
|
|
316
|
-
# @return [Hash<Integer, Enumerator>]
|
317
|
-
attr_reader :calls
|
318
|
-
# @return [Hash<String, Class>]
|
319
|
-
attr_reader :classes
|
320
|
-
# @return [Hash<String, Object>]
|
321
|
-
attr_reader :constructors
|
322
156
|
# @return [FFI::Polar]
|
323
|
-
attr_reader :
|
324
|
-
# @return [Hash<Integer, Object>]
|
325
|
-
attr_reader :instances
|
157
|
+
attr_reader :ffi_polar
|
326
158
|
# @return [Array<String>]
|
327
159
|
attr_reader :load_queue
|
328
|
-
|
329
|
-
# Clear the instance and call caches.
|
330
|
-
def clear_query_state
|
331
|
-
calls.clear
|
332
|
-
instances.clear
|
333
|
-
end
|
334
|
-
|
335
|
-
# Query for a Polar string.
|
336
|
-
#
|
337
|
-
# @param str [String]
|
338
|
-
# @return [Enumerator]
|
339
|
-
def query_str(str)
|
340
|
-
clear_query_state
|
341
|
-
load_queued_files
|
342
|
-
query_ffi_instance = ffi_instance.new_query_from_str(str)
|
343
|
-
Query.new(query_ffi_instance, polar: self).results
|
344
|
-
end
|
345
|
-
|
346
|
-
# Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
|
347
|
-
#
|
348
|
-
# @param instance [Object]
|
349
|
-
# @param id [Integer]
|
350
|
-
# @return [Integer]
|
351
|
-
def cache_instance(instance, id: nil)
|
352
|
-
id = new_id if id.nil?
|
353
|
-
instances[id] = instance
|
354
|
-
id
|
355
|
-
end
|
356
|
-
|
357
|
-
# Fetch a Ruby class from the {#classes} cache.
|
358
|
-
#
|
359
|
-
# @param name [String]
|
360
|
-
# @return [Class]
|
361
|
-
# @raise [UnregisteredClassError] if the class has not been registered.
|
362
|
-
def get_class(name)
|
363
|
-
raise UnregisteredClassError, name unless classes.key? name
|
364
|
-
|
365
|
-
classes[name]
|
366
|
-
end
|
367
|
-
|
368
|
-
# Fetch a constructor from the {#constructors} cache.
|
369
|
-
#
|
370
|
-
# @param name [String]
|
371
|
-
# @return [Symbol] if constructor is the default of `:new`.
|
372
|
-
# @return [Proc] if a custom constructor was registered.
|
373
|
-
# @raise [UnregisteredConstructorError] if the constructor has not been registered.
|
374
|
-
def get_constructor(name)
|
375
|
-
raise MissingConstructorError, name unless constructors.key? name
|
376
|
-
|
377
|
-
constructors[name]
|
378
|
-
end
|
379
160
|
end
|
380
161
|
end
|
381
162
|
end
|
data/lib/oso/polar/query.rb
CHANGED
@@ -6,20 +6,58 @@ module Oso
|
|
6
6
|
class Query
|
7
7
|
attr_reader :results
|
8
8
|
|
9
|
-
# @param
|
10
|
-
# @param
|
11
|
-
def initialize(
|
12
|
-
@
|
13
|
-
@
|
9
|
+
# @param ffi_query [FFI::Query]
|
10
|
+
# @param ffi_polar [FFI::Polar]
|
11
|
+
def initialize(ffi_query, host:)
|
12
|
+
@calls = {}
|
13
|
+
@ffi_query = ffi_query
|
14
|
+
@host = host
|
14
15
|
@results = start
|
15
16
|
end
|
16
17
|
|
17
18
|
private
|
18
19
|
|
20
|
+
# @return [Hash<Integer, Enumerator>]
|
21
|
+
attr_reader :calls
|
19
22
|
# @return [FFI::Query]
|
20
|
-
attr_reader :
|
21
|
-
# @return [
|
22
|
-
attr_reader :
|
23
|
+
attr_reader :ffi_query
|
24
|
+
# @return [Host]
|
25
|
+
attr_reader :host
|
26
|
+
|
27
|
+
# Send result of predicate check across FFI boundary.
|
28
|
+
#
|
29
|
+
# @param result [Boolean]
|
30
|
+
# @param call_id [Integer]
|
31
|
+
# @raise [Error] if the FFI call raises one.
|
32
|
+
def question_result(result, call_id:)
|
33
|
+
ffi_query.question_result(result, call_id: call_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Register a Ruby method call, wrapping the call result in a generator if
|
37
|
+
# it isn't already one.
|
38
|
+
#
|
39
|
+
# @param method [#to_sym]
|
40
|
+
# @param call_id [Integer]
|
41
|
+
# @param instance [Hash]
|
42
|
+
# @param args [Array<Hash>]
|
43
|
+
# @raise [InvalidCallError] if the method doesn't exist on the instance or
|
44
|
+
# the args passed to the method are invalid.
|
45
|
+
def register_call(method, call_id:, instance:, args:)
|
46
|
+
return if calls.key?(call_id)
|
47
|
+
|
48
|
+
args = args.map { |a| host.to_ruby(a) }
|
49
|
+
if instance['value'].key? 'ExternalInstance'
|
50
|
+
instance_id = instance['value']['ExternalInstance']['instance_id']
|
51
|
+
instance = host.get_instance(instance_id)
|
52
|
+
else
|
53
|
+
instance = host.to_ruby(instance)
|
54
|
+
end
|
55
|
+
result = instance.__send__(method, *args)
|
56
|
+
result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
|
57
|
+
calls[call_id] = result.lazy
|
58
|
+
rescue ArgumentError, NoMethodError
|
59
|
+
raise InvalidCallError
|
60
|
+
end
|
23
61
|
|
24
62
|
# Send next result of Ruby method call across FFI boundary.
|
25
63
|
#
|
@@ -27,16 +65,16 @@ module Oso
|
|
27
65
|
# @param call_id [Integer]
|
28
66
|
# @raise [Error] if the FFI call raises one.
|
29
67
|
def call_result(result, call_id:)
|
30
|
-
|
68
|
+
ffi_query.call_result(result, call_id: call_id)
|
31
69
|
end
|
32
70
|
|
33
|
-
#
|
71
|
+
# Retrieve the next result from a registered call and pass it to {#to_polar_term}.
|
34
72
|
#
|
35
|
-
# @param
|
36
|
-
# @
|
37
|
-
# @raise [
|
38
|
-
def
|
39
|
-
|
73
|
+
# @param id [Integer]
|
74
|
+
# @return [Hash]
|
75
|
+
# @raise [StopIteration] if the call has been exhausted.
|
76
|
+
def next_call_result(id)
|
77
|
+
host.to_polar_term(calls[id].next)
|
40
78
|
end
|
41
79
|
|
42
80
|
# Fetch the next result from calling a Ruby method and prepare it for
|
@@ -48,8 +86,8 @@ module Oso
|
|
48
86
|
# @param instance_id [Integer]
|
49
87
|
# @raise [Error] if the FFI call raises one.
|
50
88
|
def handle_call(method, call_id:, instance:, args:)
|
51
|
-
|
52
|
-
result = JSON.dump(
|
89
|
+
register_call(method, call_id: call_id, instance: instance, args: args)
|
90
|
+
result = JSON.dump(next_call_result(call_id))
|
53
91
|
call_result(result, call_id: call_id)
|
54
92
|
rescue InvalidCallError, StopIteration
|
55
93
|
call_result(nil, call_id: call_id)
|
@@ -65,19 +103,19 @@ module Oso
|
|
65
103
|
def start # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
66
104
|
Enumerator.new do |yielder| # rubocop:disable Metrics/BlockLength
|
67
105
|
loop do # rubocop:disable Metrics/BlockLength
|
68
|
-
event =
|
106
|
+
event = ffi_query.next_event
|
69
107
|
case event.kind
|
70
108
|
when 'Done'
|
71
109
|
break
|
72
110
|
when 'Result'
|
73
|
-
yielder << event.data['bindings'].transform_values { |v|
|
111
|
+
yielder << event.data['bindings'].transform_values { |v| host.to_ruby(v) }
|
74
112
|
when 'MakeExternal'
|
75
113
|
id = event.data['instance_id']
|
76
|
-
raise DuplicateInstanceRegistrationError, id if
|
114
|
+
raise DuplicateInstanceRegistrationError, id if host.instance? id
|
77
115
|
|
78
116
|
cls_name = event.data['instance']['tag']
|
79
117
|
fields = event.data['instance']['fields']['fields']
|
80
|
-
|
118
|
+
host.make_instance(cls_name, fields: fields, id: id)
|
81
119
|
when 'ExternalCall'
|
82
120
|
call_id = event.data['call_id']
|
83
121
|
instance = event.data['instance']
|
@@ -88,24 +126,24 @@ module Oso
|
|
88
126
|
instance_id = event.data['instance_id']
|
89
127
|
left_tag = event.data['left_class_tag']
|
90
128
|
right_tag = event.data['right_class_tag']
|
91
|
-
answer =
|
129
|
+
answer = host.subspecializer?(instance_id, left_tag: left_tag, right_tag: right_tag)
|
92
130
|
question_result(answer, call_id: event.data['call_id'])
|
93
131
|
when 'ExternalIsa'
|
94
132
|
instance_id = event.data['instance_id']
|
95
133
|
class_tag = event.data['class_tag']
|
96
|
-
answer =
|
134
|
+
answer = host.isa?(instance_id, class_tag: class_tag)
|
97
135
|
question_result(answer, call_id: event.data['call_id'])
|
98
136
|
when 'ExternalUnify'
|
99
137
|
left_instance_id = event.data['left_instance_id']
|
100
138
|
right_instance_id = event.data['right_instance_id']
|
101
|
-
answer =
|
139
|
+
answer = host.unify?(left_instance_id, right_instance_id)
|
102
140
|
question_result(answer, call_id: event.data['call_id'])
|
103
141
|
when 'Debug'
|
104
142
|
puts event.data['message'] if event.data['message']
|
105
143
|
print '> '
|
106
144
|
input = $stdin.gets.chomp!
|
107
|
-
command = JSON.dump(
|
108
|
-
|
145
|
+
command = JSON.dump(host.to_polar_term(input))
|
146
|
+
ffi_query.debug_command(command)
|
109
147
|
else
|
110
148
|
raise "Unhandled event: #{JSON.dump(event.inspect)}"
|
111
149
|
end
|
data/lib/oso/version.rb
CHANGED
data/oso-oso.gemspec
CHANGED
@@ -18,11 +18,11 @@ Gem::Specification.new do |spec|
|
|
18
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
19
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
20
20
|
files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
-
files
|
21
|
+
files + Dir['ext/oso-oso/lib/*']
|
22
22
|
end
|
23
23
|
|
24
|
-
spec.bindir = '
|
25
|
-
spec.executables = spec.files.grep(%r{^
|
24
|
+
spec.bindir = 'bin'
|
25
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
26
26
|
spec.require_paths = ['lib']
|
27
27
|
|
28
28
|
# Runtime dependencies
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oso-oso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oso Security
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-07-
|
11
|
+
date: 2020-07-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -97,7 +97,8 @@ dependencies:
|
|
97
97
|
description:
|
98
98
|
email:
|
99
99
|
- support@osohq.com
|
100
|
-
executables:
|
100
|
+
executables:
|
101
|
+
- oso
|
101
102
|
extensions: []
|
102
103
|
extra_rdoc_files: []
|
103
104
|
files:
|
@@ -109,8 +110,7 @@ files:
|
|
109
110
|
- Makefile
|
110
111
|
- README.md
|
111
112
|
- Rakefile
|
112
|
-
- bin/
|
113
|
-
- bin/setup
|
113
|
+
- bin/oso
|
114
114
|
- ext/oso-oso/lib/libpolar.dylib
|
115
115
|
- ext/oso-oso/lib/libpolar.so
|
116
116
|
- ext/oso-oso/lib/polar.dll
|
@@ -125,6 +125,7 @@ files:
|
|
125
125
|
- lib/oso/polar/ffi/polar.rb
|
126
126
|
- lib/oso/polar/ffi/query.rb
|
127
127
|
- lib/oso/polar/ffi/query_event.rb
|
128
|
+
- lib/oso/polar/host.rb
|
128
129
|
- lib/oso/polar/polar.rb
|
129
130
|
- lib/oso/polar/predicate.rb
|
130
131
|
- lib/oso/polar/query.rb
|
data/bin/console
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require 'bundler/setup'
|
5
|
-
require 'oso'
|
6
|
-
|
7
|
-
polar = Oso::Polar.new.tap do |p| # rubocop:disable Lint/UselessAssignment
|
8
|
-
p.load_str('f(1); f(2); g(1); g(2); h(2); k(x) := f(x), h(x), g(x);')
|
9
|
-
puts 'f(x)', p.send(:query_str, 'f(x)').to_a
|
10
|
-
puts 'k(x)', p.send(:query_str, 'k(x)').to_a
|
11
|
-
|
12
|
-
p.load_str('foo(1, 2); foo(3, 4); foo(5, 6);')
|
13
|
-
expected = [{ 'x' => 1, 'y' => 2 }, { 'x' => 3, 'y' => 4 }, { 'x' => 5, 'y' => 6 }]
|
14
|
-
raise 'AssertionError' if p.send(:query_str, 'foo(x, y)').to_a != expected
|
15
|
-
|
16
|
-
class TestClass # rubocop:disable Style/Documentation
|
17
|
-
def my_method
|
18
|
-
1
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
p.register_class(TestClass)
|
23
|
-
|
24
|
-
p.load_str('external(x, 3) := x = new TestClass{}.my_method;')
|
25
|
-
results = p.send(:query_str, 'external(1, x)')
|
26
|
-
p results.next
|
27
|
-
|
28
|
-
# p.load_str('testDebug() := debug(), foo(x, y), k(y);')
|
29
|
-
# p.send(:query_str, 'testDebug()').next
|
30
|
-
end
|
31
|
-
|
32
|
-
require 'pry'
|
33
|
-
Pry.start
|