oso-oso 0.2.2 → 0.4.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 +5 -5
- data/.rubocop.yml +7 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +13 -12
- data/Makefile +12 -9
- data/README.md +6 -9
- data/Rakefile +0 -1
- 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/http.rb +1 -1
- data/lib/oso/oso.rb +6 -32
- data/lib/oso/path_mapper.rb +1 -1
- data/lib/oso/polar.rb +1 -0
- data/lib/oso/polar/errors.rb +25 -9
- data/lib/oso/polar/ffi.rb +6 -2
- data/lib/oso/polar/ffi/error.rb +2 -2
- data/lib/oso/polar/ffi/polar.rb +6 -16
- data/lib/oso/polar/ffi/query.rb +9 -0
- data/lib/oso/polar/host.rb +246 -0
- data/lib/oso/polar/polar.rb +117 -304
- data/lib/oso/polar/query.rb +92 -36
- data/lib/oso/version.rb +1 -1
- data/oso-oso.gemspec +11 -6
- metadata +30 -10
- data/bin/console +0 -33
- data/bin/setup +0 -6
data/lib/oso/polar/polar.rb
CHANGED
@@ -1,19 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
+
require 'pp'
|
4
5
|
require 'set'
|
6
|
+
require 'digest/md5'
|
7
|
+
|
8
|
+
# Missing Ruby type.
|
9
|
+
module PolarBoolean; end
|
10
|
+
# Monkey-patch Ruby true type.
|
11
|
+
class TrueClass; include PolarBoolean; end
|
12
|
+
# Monkey-patch Ruby false type.
|
13
|
+
class FalseClass; include PolarBoolean; end
|
5
14
|
|
6
15
|
module Oso
|
7
16
|
module Polar
|
8
17
|
# Create and manage an instance of the Polar runtime.
|
9
18
|
class Polar # rubocop:disable Metrics/ClassLength
|
19
|
+
# @return [Host]
|
20
|
+
attr_reader :host
|
21
|
+
|
10
22
|
def initialize
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
|
16
|
-
|
23
|
+
@ffi_polar = FFI::Polar.create
|
24
|
+
@host = Host.new(ffi_polar)
|
25
|
+
@loaded_names = {}
|
26
|
+
@loaded_contents = {}
|
27
|
+
|
28
|
+
# Register built-in classes.
|
29
|
+
register_class PolarBoolean, name: 'Boolean'
|
30
|
+
register_class Integer
|
31
|
+
register_class Float
|
32
|
+
register_class Array, name: 'List'
|
33
|
+
register_class Hash, name: 'Dictionary'
|
34
|
+
register_class String
|
35
|
+
end
|
36
|
+
|
37
|
+
# Replace the current Polar instance but retain all registered classes and constructors.
|
38
|
+
def clear
|
39
|
+
loaded_names.clear
|
40
|
+
loaded_contents.clear
|
41
|
+
@ffi_polar = FFI::Polar.create
|
17
42
|
end
|
18
43
|
|
19
44
|
# Enqueue a Polar policy file for loading into the KB.
|
@@ -21,11 +46,25 @@ module Oso
|
|
21
46
|
# @param name [String]
|
22
47
|
# @raise [PolarFileExtensionError] if provided filename has invalid extension.
|
23
48
|
# @raise [PolarFileNotFoundError] if provided filename does not exist.
|
24
|
-
def load_file(name)
|
25
|
-
raise PolarFileExtensionError unless
|
26
|
-
raise PolarFileNotFoundError, name unless File.file?(name)
|
49
|
+
def load_file(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
50
|
+
raise PolarFileExtensionError, name unless File.extname(name) == '.polar'
|
27
51
|
|
28
|
-
|
52
|
+
file_data = File.open(name, &:read)
|
53
|
+
hash = Digest::MD5.hexdigest(file_data)
|
54
|
+
|
55
|
+
if loaded_names.key?(name)
|
56
|
+
raise PolarFileAlreadyLoadedError, name if loaded_names[name] == hash
|
57
|
+
|
58
|
+
raise PolarFileContentsChangedError, name
|
59
|
+
elsif loaded_contents.key?(hash)
|
60
|
+
raise PolarFileNameChangedError, name, loaded_contents[hash]
|
61
|
+
else
|
62
|
+
load_str(file_data, filename: name)
|
63
|
+
loaded_names[name] = hash
|
64
|
+
loaded_contents[hash] = name
|
65
|
+
end
|
66
|
+
rescue Errno::ENOENT
|
67
|
+
raise PolarFileNotFoundError, name
|
29
68
|
end
|
30
69
|
|
31
70
|
# Load a Polar string into the KB.
|
@@ -38,344 +77,118 @@ module Oso
|
|
38
77
|
def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
|
39
78
|
raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
|
40
79
|
|
41
|
-
|
80
|
+
ffi_polar.load_str(str, filename: filename)
|
42
81
|
loop do
|
43
|
-
next_query =
|
82
|
+
next_query = ffi_polar.next_inline_query
|
44
83
|
break if next_query.nil?
|
45
84
|
|
46
85
|
begin
|
47
|
-
Query.new(next_query,
|
86
|
+
Query.new(next_query, host: host).results.next
|
48
87
|
rescue StopIteration
|
49
88
|
raise InlineQueryFailedError
|
50
89
|
end
|
51
90
|
end
|
52
91
|
end
|
53
92
|
|
54
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
93
|
+
# Query for a predicate, parsing it if necessary.
|
94
|
+
#
|
95
|
+
# @overload query(query)
|
96
|
+
# @param query [String]
|
97
|
+
# @return [Enumerator] of resulting bindings
|
98
|
+
# @raise [Error] if the FFI call raises one.
|
99
|
+
# @overload query(query)
|
100
|
+
# @param query [Predicate]
|
101
|
+
# @return [Enumerator] of resulting bindings
|
102
|
+
# @raise [Error] if the FFI call raises one.
|
103
|
+
def query(query)
|
104
|
+
new_host = host.dup
|
105
|
+
case query
|
106
|
+
when String
|
107
|
+
ffi_query = ffi_polar.new_query_from_str(query)
|
108
|
+
when Predicate
|
109
|
+
ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
|
110
|
+
else
|
111
|
+
raise InvalidQueryTypeError
|
112
|
+
end
|
113
|
+
Query.new(ffi_query, host: new_host).results
|
58
114
|
end
|
59
115
|
|
60
|
-
# Query for a
|
116
|
+
# Query for a rule.
|
61
117
|
#
|
62
118
|
# @param name [String]
|
63
119
|
# @param args [Array<Object>]
|
120
|
+
# @return [Enumerator] of resulting bindings
|
64
121
|
# @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
|
122
|
+
def query_rule(name, *args)
|
123
|
+
query(Predicate.new(name, args: args))
|
71
124
|
end
|
72
125
|
|
73
126
|
# Start a REPL session.
|
74
127
|
#
|
75
128
|
# @raise [Error] if the FFI call raises one.
|
76
|
-
def repl # rubocop:disable Metrics/MethodLength
|
77
|
-
|
78
|
-
|
79
|
-
loop do
|
80
|
-
query
|
81
|
-
|
129
|
+
def repl(load: false) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
130
|
+
ARGV.map { |f| load_file(f) } if load
|
131
|
+
|
132
|
+
loop do # rubocop:disable Metrics/BlockLength
|
133
|
+
print 'query> '
|
134
|
+
begin
|
135
|
+
query = $stdin.readline.chomp.chomp(';')
|
136
|
+
rescue EOFError
|
137
|
+
return
|
138
|
+
end
|
139
|
+
|
140
|
+
begin
|
141
|
+
ffi_query = ffi_polar.new_query_from_str(query)
|
142
|
+
rescue ParseError => e
|
143
|
+
puts "Parse error: #{e}"
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
begin
|
148
|
+
results = Query.new(ffi_query, host: host).results.to_a
|
149
|
+
rescue PolarRuntimeError => e
|
150
|
+
puts e
|
151
|
+
next
|
152
|
+
end
|
153
|
+
|
82
154
|
if results.empty?
|
83
|
-
|
155
|
+
pp false
|
84
156
|
else
|
85
157
|
results.each do |result|
|
86
|
-
|
158
|
+
if result.empty?
|
159
|
+
pp true
|
160
|
+
else
|
161
|
+
pp result
|
162
|
+
end
|
87
163
|
end
|
88
164
|
end
|
89
165
|
end
|
90
166
|
end
|
91
167
|
|
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
168
|
# Register a Ruby class with Polar.
|
101
169
|
#
|
102
170
|
# @param cls [Class]
|
103
171
|
# @param name [String]
|
104
172
|
# @param from_polar [Proc]
|
105
173
|
# @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
|
120
|
-
end
|
121
|
-
|
174
|
+
def register_class(cls, name: nil, from_polar: nil)
|
175
|
+
from_polar = Proc.new if block_given?
|
176
|
+
name = host.cache_class(cls, name: name, constructor: from_polar)
|
122
177
|
register_constant(name, value: cls)
|
123
178
|
end
|
124
179
|
|
125
180
|
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
|
304
|
-
end
|
305
|
-
|
306
|
-
# Load all queued files, flushing the {#load_queue}.
|
307
|
-
def load_queued_files
|
308
|
-
load_queue.reject! do |filename|
|
309
|
-
File.open(filename) { |file| load_str(file.read, filename: filename) }
|
310
|
-
true
|
311
|
-
end
|
181
|
+
ffi_polar.register_constant(name, value: host.to_polar_term(value))
|
312
182
|
end
|
313
183
|
|
314
184
|
private
|
315
185
|
|
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
186
|
# @return [FFI::Polar]
|
323
|
-
attr_reader :
|
324
|
-
# @return [Hash<
|
325
|
-
attr_reader :
|
326
|
-
# @return [
|
327
|
-
attr_reader :
|
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
|
187
|
+
attr_reader :ffi_polar
|
188
|
+
# @return [Hash<String, String>]
|
189
|
+
attr_reader :loaded_names
|
190
|
+
# @return [Hash<String, String>]
|
191
|
+
attr_reader :loaded_contents
|
379
192
|
end
|
380
193
|
end
|
381
194
|
end
|