oso-oso 0.15.1 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -3
- data/Gemfile.lock +49 -26
- data/README.md +1 -3
- 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/errors.rb +44 -0
- data/lib/oso/oso.rb +202 -3
- data/lib/oso/polar/data_filtering.rb +185 -0
- data/lib/oso/polar/errors.rb +2 -3
- data/lib/oso/polar/ffi/error.rb +2 -2
- data/lib/oso/polar/ffi/polar.rb +26 -18
- data/lib/oso/polar/ffi/query.rb +7 -2
- data/lib/oso/polar/host.rb +80 -13
- data/lib/oso/polar/polar.rb +142 -70
- data/lib/oso/polar/query.rb +91 -5
- data/lib/oso/polar.rb +1 -0
- data/lib/oso/version.rb +1 -1
- data/lib/oso-oso.rb +3 -0
- data/lib/oso.rb +7 -3
- data/oso-oso.gemspec +2 -0
- metadata +33 -2
data/lib/oso/polar/ffi/polar.rb
CHANGED
@@ -14,17 +14,22 @@ module Oso
|
|
14
14
|
ffi_lib FFI::LIB_PATH
|
15
15
|
|
16
16
|
attach_function :new, :polar_new, [], FFI::Polar
|
17
|
-
attach_function :
|
18
|
-
attach_function :validate_roles_config, :polar_validate_roles_config, [FFI::Polar, :string], :int32
|
19
|
-
attach_function :load, :polar_load, [FFI::Polar, :string, :string], :int32
|
17
|
+
attach_function :load, :polar_load, [FFI::Polar, :string], :int32
|
20
18
|
attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
|
21
19
|
attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
|
22
20
|
attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
|
23
21
|
attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
|
24
22
|
attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
|
25
23
|
attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
|
24
|
+
attach_function :register_mro, :polar_register_mro, [FFI::Polar, :string, :string], :int32
|
26
25
|
attach_function :next_message, :polar_next_polar_message, [FFI::Polar], FFI::Message
|
27
26
|
attach_function :free, :polar_free, [FFI::Polar], :int32
|
27
|
+
attach_function(
|
28
|
+
:build_filter_plan,
|
29
|
+
:polar_build_filter_plan,
|
30
|
+
[FFI::Polar, :string, :string, :string, :string],
|
31
|
+
:string
|
32
|
+
)
|
28
33
|
end
|
29
34
|
private_constant :Rust
|
30
35
|
|
@@ -37,25 +42,20 @@ module Oso
|
|
37
42
|
polar
|
38
43
|
end
|
39
44
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
handle_error if result.zero?
|
45
|
-
end
|
46
|
-
|
47
|
-
# @raise [FFI::Error] if the FFI call returns an error.
|
48
|
-
def validate_roles_config(config)
|
49
|
-
result = Rust.validate_roles_config(self, JSON.dump(config))
|
45
|
+
def build_filter_plan(types, partials, variable, class_tag)
|
46
|
+
types = JSON.dump(types)
|
47
|
+
partials = JSON.dump(partials)
|
48
|
+
plan = Rust.build_filter_plan(self, types, partials, variable, class_tag)
|
50
49
|
process_messages
|
51
|
-
handle_error if
|
50
|
+
handle_error if plan.nil?
|
51
|
+
# TODO(gw) more error checking?
|
52
|
+
JSON.parse plan
|
52
53
|
end
|
53
54
|
|
54
|
-
# @param
|
55
|
-
# @param filename [String]
|
55
|
+
# @param sources [Array<Source>]
|
56
56
|
# @raise [FFI::Error] if the FFI call returns an error.
|
57
|
-
def load(
|
58
|
-
loaded = Rust.load(self,
|
57
|
+
def load(sources)
|
58
|
+
loaded = Rust.load(self, JSON.dump(sources))
|
59
59
|
process_messages
|
60
60
|
handle_error if loaded.zero?
|
61
61
|
end
|
@@ -117,6 +117,14 @@ module Oso
|
|
117
117
|
handle_error if registered.zero?
|
118
118
|
end
|
119
119
|
|
120
|
+
# @param name [String]
|
121
|
+
# @param mro [Array<Integer>]
|
122
|
+
# @raise [FFI::Error] if the FFI call returns an error.
|
123
|
+
def register_mro(name, mro)
|
124
|
+
registered = Rust.register_mro(self, name, JSON.dump(mro))
|
125
|
+
handle_error if registered.zero?
|
126
|
+
end
|
127
|
+
|
120
128
|
def next_message
|
121
129
|
Rust.next_message(self)
|
122
130
|
end
|
data/lib/oso/polar/ffi/query.rb
CHANGED
@@ -21,6 +21,7 @@ module Oso
|
|
21
21
|
attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
|
22
22
|
attach_function :source, :polar_query_source_info, [FFI::Query], FFI::Source
|
23
23
|
attach_function :free, :query_free, [FFI::Query], :int32
|
24
|
+
attach_function :bind, :polar_bind, [FFI::Query, :string, :string], :int32
|
24
25
|
end
|
25
26
|
private_constant :Rust
|
26
27
|
|
@@ -49,8 +50,7 @@ module Oso
|
|
49
50
|
handle_error if res.zero?
|
50
51
|
end
|
51
52
|
|
52
|
-
# @param
|
53
|
-
# @param call_id [Integer]
|
53
|
+
# @param message [String]
|
54
54
|
# @raise [FFI::Error] if the FFI call returns an error.
|
55
55
|
def application_error(message)
|
56
56
|
res = Rust.application_error(self, message)
|
@@ -67,6 +67,11 @@ module Oso
|
|
67
67
|
::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
|
68
68
|
end
|
69
69
|
|
70
|
+
def bind(name, value)
|
71
|
+
res = Rust.bind(self, name, JSON.dump(value))
|
72
|
+
handle_error if res.zero?
|
73
|
+
end
|
74
|
+
|
70
75
|
def next_message
|
71
76
|
Rust.next_message(self)
|
72
77
|
end
|
data/lib/oso/polar/host.rb
CHANGED
@@ -34,14 +34,31 @@ module Oso
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
# For holding type metadata: name, fields, etc.
|
38
|
+
class UserType
|
39
|
+
attr_reader :name, :klass, :id, :fields, :build_query, :combine_query, :exec_query
|
40
|
+
|
41
|
+
def initialize(name:, klass:, id:, fields:, build_query:, combine_query:, exec_query:) # rubocop:disable Metrics/ParameterLists
|
42
|
+
@name = name
|
43
|
+
@klass = klass
|
44
|
+
@id = id
|
45
|
+
# accept symbol keys
|
46
|
+
@fields = fields.each_with_object({}) { |kv, o| o[kv[0].to_s] = kv[1] }
|
47
|
+
@build_query = build_query
|
48
|
+
@combine_query = combine_query
|
49
|
+
@exec_query = exec_query
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
37
53
|
# Translate between Polar and the host language (Ruby).
|
38
54
|
class Host # rubocop:disable Metrics/ClassLength
|
55
|
+
# @return [Hash<String, UserType>]
|
56
|
+
attr_reader :types
|
57
|
+
|
39
58
|
protected
|
40
59
|
|
41
60
|
# @return [FFI::Polar]
|
42
61
|
attr_reader :ffi_polar
|
43
|
-
# @return [Hash<String, Class>]
|
44
|
-
attr_reader :classes
|
45
62
|
# @return [Hash<Integer, Object>]
|
46
63
|
attr_reader :instances
|
47
64
|
# @return [Boolean]
|
@@ -53,42 +70,60 @@ module Oso
|
|
53
70
|
|
54
71
|
def initialize(ffi_polar)
|
55
72
|
@ffi_polar = ffi_polar
|
56
|
-
@
|
73
|
+
@types = {}
|
57
74
|
@instances = {}
|
58
75
|
@accept_expression = false
|
59
76
|
end
|
60
77
|
|
61
78
|
def initialize_copy(other)
|
62
79
|
@ffi_polar = other.ffi_polar
|
63
|
-
@
|
80
|
+
@types = other.types.dup
|
64
81
|
@instances = other.instances.dup
|
65
82
|
end
|
66
83
|
|
67
|
-
# Fetch a Ruby class from the {#
|
84
|
+
# Fetch a Ruby class from the {#types} cache.
|
68
85
|
#
|
69
86
|
# @param name [String]
|
70
87
|
# @return [Class]
|
71
88
|
# @raise [UnregisteredClassError] if the class has not been registered.
|
72
89
|
def get_class(name)
|
73
|
-
raise UnregisteredClassError, name unless
|
90
|
+
raise UnregisteredClassError, name unless types.key? name
|
74
91
|
|
75
|
-
|
92
|
+
types[name].klass.get
|
76
93
|
end
|
77
94
|
|
78
|
-
# Store a Ruby class in the {#
|
95
|
+
# Store a Ruby class in the {#types} cache.
|
79
96
|
#
|
80
97
|
# @param cls [Class] the class to cache.
|
81
98
|
# @param name [String] the name to cache the class as.
|
82
99
|
# @return [String] the name the class is cached as.
|
83
100
|
# @raise [DuplicateClassAliasError] if attempting to register a class
|
84
101
|
# under a previously-registered name.
|
85
|
-
def cache_class(cls, name:)
|
86
|
-
raise DuplicateClassAliasError.new name: name, old: get_class(name), new: cls if
|
102
|
+
def cache_class(cls, name:, fields:, build_query:, combine_query:, exec_query:) # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
103
|
+
raise DuplicateClassAliasError.new name: name, old: get_class(name), new: cls if types.key? name
|
87
104
|
|
88
|
-
|
105
|
+
types[name] = types[cls] = UserType.new(
|
106
|
+
name: name,
|
107
|
+
klass: PolarClass.new(cls),
|
108
|
+
id: cache_instance(cls),
|
109
|
+
fields: fields || {},
|
110
|
+
combine_query: combine_query,
|
111
|
+
exec_query: exec_query,
|
112
|
+
build_query: build_query
|
113
|
+
)
|
89
114
|
name
|
90
115
|
end
|
91
116
|
|
117
|
+
def register_mros # rubocop:disable Metrics/AbcSize
|
118
|
+
types.values.uniq.each do |typ|
|
119
|
+
mro = []
|
120
|
+
typ.klass.get.ancestors.each do |a|
|
121
|
+
mro.push(types[a].id) if types.key?(a)
|
122
|
+
end
|
123
|
+
ffi_polar.register_mro(typ.name, mro)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
92
127
|
# Check if an instance exists in the {#instances} cache.
|
93
128
|
#
|
94
129
|
# @param id [Integer]
|
@@ -160,7 +195,7 @@ module Oso
|
|
160
195
|
|
161
196
|
# Compare two values
|
162
197
|
#
|
163
|
-
# @param
|
198
|
+
# @param operation [String] operation to perform.
|
164
199
|
# @param args [Array<Object>] left and right args to operation.
|
165
200
|
# @raise [PolarRuntimeError] if operation fails or is unsupported.
|
166
201
|
# @return [Boolean]
|
@@ -190,6 +225,10 @@ module Oso
|
|
190
225
|
left_index && right_index && left_index < right_index
|
191
226
|
end
|
192
227
|
|
228
|
+
def subclass?(left_tag:, right_tag:)
|
229
|
+
get_class(left_tag) <= get_class(right_tag)
|
230
|
+
end
|
231
|
+
|
193
232
|
# Check if instance is an instance of class.
|
194
233
|
#
|
195
234
|
# @param instance [Hash<String, Object>]
|
@@ -201,6 +240,32 @@ module Oso
|
|
201
240
|
instance.is_a? cls
|
202
241
|
end
|
203
242
|
|
243
|
+
def serialize_types # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
244
|
+
polar_types = {}
|
245
|
+
types.values.uniq.each do |typ|
|
246
|
+
tag = typ.name
|
247
|
+
fields = typ.fields
|
248
|
+
field_types = {}
|
249
|
+
fields.each do |k, v|
|
250
|
+
field_types[k] =
|
251
|
+
if v.is_a? ::Oso::Polar::DataFiltering::Relation
|
252
|
+
{
|
253
|
+
'Relation' => {
|
254
|
+
'kind' => v.kind,
|
255
|
+
'other_class_tag' => v.other_type,
|
256
|
+
'my_field' => v.my_field,
|
257
|
+
'other_field' => v.other_field
|
258
|
+
}
|
259
|
+
}
|
260
|
+
else
|
261
|
+
{ 'Base' => { 'class_tag' => types[v].name } }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
polar_types[tag] = field_types
|
265
|
+
end
|
266
|
+
polar_types
|
267
|
+
end
|
268
|
+
|
204
269
|
# Turn a Ruby value into a Polar term that's ready to be sent across the
|
205
270
|
# FFI boundary.
|
206
271
|
#
|
@@ -242,7 +307,9 @@ module Oso
|
|
242
307
|
{ 'Pattern' => { 'Instance' => { 'tag' => value.tag, 'fields' => dict['Dictionary'] } } }
|
243
308
|
end
|
244
309
|
else
|
245
|
-
|
310
|
+
instance_id = nil
|
311
|
+
instance_id = types[value].id if value.is_a?(Class) && types.key?(value)
|
312
|
+
{ 'ExternalInstance' => { 'instance_id' => cache_instance(value, id: instance_id), 'repr' => nil } }
|
246
313
|
end
|
247
314
|
{ 'value' => value }
|
248
315
|
end
|
data/lib/oso/polar/polar.rb
CHANGED
@@ -27,6 +27,35 @@ def print_error(error)
|
|
27
27
|
warn error.message
|
28
28
|
end
|
29
29
|
|
30
|
+
# Polar source string with optional filename.
|
31
|
+
class Source
|
32
|
+
# @return [String]
|
33
|
+
attr_reader :src, :filename
|
34
|
+
|
35
|
+
# @param src [String]
|
36
|
+
# @param filename [String]
|
37
|
+
def initialize(src, filename: nil)
|
38
|
+
@src = src
|
39
|
+
@filename = filename
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_json(*_args)
|
43
|
+
{ src: src, filename: filename }.to_json
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def filename_to_source(filename)
|
48
|
+
raise Oso::Polar::PolarFileExtensionError, filename unless File.extname(filename) == '.polar'
|
49
|
+
|
50
|
+
src = File.open(filename, &:read)
|
51
|
+
|
52
|
+
raise Oso::Polar::NullByteInPolarFileError if src.chomp("\0").include?("\0")
|
53
|
+
|
54
|
+
Source.new(src, filename: filename)
|
55
|
+
rescue Errno::ENOENT
|
56
|
+
raise Oso::Polar::PolarFileNotFoundError, filename
|
57
|
+
end
|
58
|
+
|
30
59
|
module Oso
|
31
60
|
module Polar
|
32
61
|
# Create and manage an instance of the Polar runtime.
|
@@ -34,11 +63,10 @@ module Oso
|
|
34
63
|
# @return [Host]
|
35
64
|
attr_reader :host
|
36
65
|
|
37
|
-
def initialize
|
66
|
+
def initialize
|
38
67
|
@ffi_polar = FFI::Polar.create
|
39
68
|
@host = Host.new(ffi_polar)
|
40
69
|
@ffi_polar.enrich_message = @host.method(:enrich_message)
|
41
|
-
@polar_roles_enabled = false
|
42
70
|
|
43
71
|
# Register global constants.
|
44
72
|
register_constant nil, name: 'nil'
|
@@ -52,40 +80,24 @@ module Oso
|
|
52
80
|
register_class String
|
53
81
|
end
|
54
82
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
roles_helper = Class.new do
|
59
|
-
def self.join(separator, left, right)
|
60
|
-
[left, right].join(separator)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
register_constant(roles_helper, name: '__oso_internal_roles_helpers__')
|
64
|
-
ffi_polar.enable_roles
|
65
|
-
self.polar_roles_enabled = true
|
66
|
-
|
67
|
-
# validate config
|
68
|
-
validation_query_results = []
|
69
|
-
loop do
|
70
|
-
query = ffi_polar.next_inline_query
|
71
|
-
break if query.nil?
|
72
|
-
|
73
|
-
new_host = host.dup
|
74
|
-
new_host.accept_expression = true
|
75
|
-
results = Query.new(query, host: new_host).to_a
|
76
|
-
raise InlineQueryFailedError, query.source if results.empty?
|
83
|
+
def ffi
|
84
|
+
@ffi_polar
|
85
|
+
end
|
77
86
|
|
78
|
-
|
79
|
-
|
87
|
+
# get the (maybe user-supplied) name of a class.
|
88
|
+
# kind of a hack because of class autoreloading.
|
89
|
+
def get_class_name(klass) # rubocop:disable Metrics/AbcSize
|
90
|
+
if host.types.key? klass
|
91
|
+
host.types[klass].name
|
92
|
+
elsif host.types.key? klass.name
|
93
|
+
host.types[klass.name].name
|
94
|
+
else
|
95
|
+
rec = host.types.values.find { |v| v.klass.get == klass }
|
96
|
+
raise "Unknown class `#{klass}`" if rec.nil?
|
80
97
|
|
81
|
-
|
82
|
-
|
83
|
-
results.map do |result|
|
84
|
-
{ 'bindings' => result.transform_values { |v| host.to_polar(v) } }
|
85
|
-
end
|
98
|
+
host.types[klass] = rec
|
99
|
+
rec.name
|
86
100
|
end
|
87
|
-
|
88
|
-
ffi_polar.validate_roles_config(validation_query_results)
|
89
101
|
end
|
90
102
|
|
91
103
|
# Clear all rules and rule sources from the current Polar instance
|
@@ -93,23 +105,47 @@ module Oso
|
|
93
105
|
# @return [self] for chaining.
|
94
106
|
def clear_rules
|
95
107
|
ffi_polar.clear_rules
|
96
|
-
ffi_polar.enable_roles if polar_roles_enabled
|
97
108
|
self
|
98
109
|
end
|
99
110
|
|
100
|
-
# Load
|
111
|
+
# Load Polar policy files.
|
101
112
|
#
|
102
|
-
# @param
|
103
|
-
# @raise [PolarFileExtensionError] if
|
104
|
-
# @raise [PolarFileNotFoundError] if
|
113
|
+
# @param filenames [Array<String>]
|
114
|
+
# @raise [PolarFileExtensionError] if any filename has an invalid extension.
|
115
|
+
# @raise [PolarFileNotFoundError] if any filename does not exist.
|
116
|
+
# @raise [NullByteInPolarFileError] if any file contains a non-terminating null byte.
|
117
|
+
# @raise [Error] if any of the FFI calls raise one.
|
118
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
105
119
|
# @return [self] for chaining.
|
106
|
-
def
|
107
|
-
|
120
|
+
def load_files(filenames = [])
|
121
|
+
return if filenames.empty?
|
108
122
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
123
|
+
sources = filenames.map { |f| filename_to_source f }
|
124
|
+
load_sources(sources)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Load a Polar policy file.
|
129
|
+
#
|
130
|
+
# @param filename [String]
|
131
|
+
# @raise [PolarFileExtensionError] if filename has an invalid extension.
|
132
|
+
# @raise [PolarFileNotFoundError] if filename does not exist.
|
133
|
+
# @raise [NullByteInPolarFileError] if file contains a non-terminating null byte.
|
134
|
+
# @raise [Error] if any of the FFI calls raise one.
|
135
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
136
|
+
# @return [self] for chaining.
|
137
|
+
#
|
138
|
+
# @deprecated {#load_file} has been deprecated in favor of {#load_files}
|
139
|
+
# as of the 0.20 release. Please see changelog for migration
|
140
|
+
# instructions:
|
141
|
+
# https://docs.osohq.com/project/changelogs/2021-09-15.html
|
142
|
+
def load_file(filename)
|
143
|
+
warn <<~WARNING
|
144
|
+
`Oso#load_file` has been deprecated in favor of `Oso#load_files` as of the 0.20 release.
|
145
|
+
|
146
|
+
Please see changelog for migration instructions: https://docs.osohq.com/project/changelogs/2021-09-15.html
|
147
|
+
WARNING
|
148
|
+
load_files([filename])
|
113
149
|
end
|
114
150
|
|
115
151
|
# Load a Polar string into the KB.
|
@@ -117,26 +153,13 @@ module Oso
|
|
117
153
|
# @param str [String] Polar string to load.
|
118
154
|
# @param filename [String] Name of Polar source file.
|
119
155
|
# @raise [NullByteInPolarFileError] if str includes a non-terminating null byte.
|
120
|
-
# @raise [InlineQueryFailedError] on the first failed inline query.
|
121
156
|
# @raise [Error] if any of the FFI calls raise one.
|
157
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
122
158
|
# @return [self] for chaining.
|
123
|
-
def load_str(str, filename: nil)
|
159
|
+
def load_str(str, filename: nil)
|
124
160
|
raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
|
125
161
|
|
126
|
-
|
127
|
-
loop do
|
128
|
-
next_query = ffi_polar.next_inline_query
|
129
|
-
break if next_query.nil?
|
130
|
-
|
131
|
-
raise InlineQueryFailedError, next_query.source if Query.new(next_query, host: host).first.nil?
|
132
|
-
end
|
133
|
-
|
134
|
-
# If roles are enabled, re-validate config when new rules are loaded.
|
135
|
-
if polar_roles_enabled
|
136
|
-
self.polar_roles_enabled = false
|
137
|
-
enable_roles
|
138
|
-
end
|
139
|
-
|
162
|
+
load_sources([Source.new(str, filename: filename)])
|
140
163
|
self
|
141
164
|
end
|
142
165
|
|
@@ -150,17 +173,16 @@ module Oso
|
|
150
173
|
# @param query [Predicate]
|
151
174
|
# @return [Enumerator] of resulting bindings
|
152
175
|
# @raise [Error] if the FFI call raises one.
|
153
|
-
def query(query)
|
154
|
-
new_host = host.dup
|
176
|
+
def query(query, host: self.host.dup, bindings: {})
|
155
177
|
case query
|
156
178
|
when String
|
157
179
|
ffi_query = ffi_polar.new_query_from_str(query)
|
158
180
|
when Predicate
|
159
|
-
ffi_query = ffi_polar.new_query_from_term(
|
181
|
+
ffi_query = ffi_polar.new_query_from_term(host.to_polar(query))
|
160
182
|
else
|
161
183
|
raise InvalidQueryTypeError
|
162
184
|
end
|
163
|
-
Query.new(ffi_query, host:
|
185
|
+
Query.new(ffi_query, host: host, bindings: bindings)
|
164
186
|
end
|
165
187
|
|
166
188
|
# Query for a rule.
|
@@ -169,20 +191,43 @@ module Oso
|
|
169
191
|
# @param args [Array<Object>]
|
170
192
|
# @return [Enumerator] of resulting bindings
|
171
193
|
# @raise [Error] if the FFI call raises one.
|
172
|
-
def query_rule(name, *args)
|
173
|
-
|
194
|
+
def query_rule(name, *args, accept_expression: false, bindings: {})
|
195
|
+
host = self.host.dup
|
196
|
+
host.accept_expression = accept_expression
|
197
|
+
query(Predicate.new(name, args: args), host: host, bindings: bindings)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Query for a rule, returning true if it has any results.
|
201
|
+
#
|
202
|
+
# @param name [String]
|
203
|
+
# @param args [Array<Object>]
|
204
|
+
# @return [Boolean] indicating whether the query found at least one result.
|
205
|
+
# @raise [Error] if the FFI call raises one.
|
206
|
+
def query_rule_once(name, *args)
|
207
|
+
query_rule(name, *args).any?
|
174
208
|
end
|
175
209
|
|
176
210
|
# Register a Ruby class with Polar.
|
177
211
|
#
|
178
212
|
# @param cls [Class] the class to register.
|
179
213
|
# @param name [String] the name to register the class as. Defaults to the name of the class.
|
214
|
+
# @param fields [Hash] a map from field names on instances of +cls+ to types, or Relation objects.
|
215
|
+
# @param build_query [Proc] a method to produce a query for +cls+ objects, given a list of Filters.
|
216
|
+
# @param exec_query [Proc] a method to execute a query produced by +build_query+
|
217
|
+
# @param combine_query [Proc] a method to merge two queries produced by +build_query+
|
180
218
|
# @raise [DuplicateClassAliasError] if attempting to register a class
|
181
219
|
# under a previously-registered name.
|
182
220
|
# @raise [FFI::Error] if the FFI call returns an error.
|
183
221
|
# @return [self] for chaining.
|
184
|
-
def register_class(cls, name: nil)
|
185
|
-
name = host.cache_class(
|
222
|
+
def register_class(cls, name: nil, fields: nil, combine_query: nil, build_query: nil, exec_query: nil) # rubocop:disable Metrics/ParameterLists
|
223
|
+
name = host.cache_class(
|
224
|
+
cls,
|
225
|
+
name: name || cls.name,
|
226
|
+
fields: fields,
|
227
|
+
build_query: build_query || maybe_mtd(cls, :build_query),
|
228
|
+
combine_query: combine_query || maybe_mtd(cls, :combine_query),
|
229
|
+
exec_query: exec_query || maybe_mtd(cls, :exec_query)
|
230
|
+
)
|
186
231
|
register_constant(cls, name: name)
|
187
232
|
end
|
188
233
|
|
@@ -202,7 +247,7 @@ module Oso
|
|
202
247
|
# @param files [Array<String>]
|
203
248
|
# @raise [Error] if the FFI call raises one.
|
204
249
|
def repl(files = [])
|
205
|
-
files
|
250
|
+
load_files(files)
|
206
251
|
prompt = "#{FG_BLUE}query>#{RESET} "
|
207
252
|
# Try loading the readline module from the Ruby stdlib. If we get a
|
208
253
|
# LoadError, fall back to the standard REPL with no readline support.
|
@@ -214,9 +259,36 @@ module Oso
|
|
214
259
|
|
215
260
|
private
|
216
261
|
|
262
|
+
def type_constraint(var, cls)
|
263
|
+
Expression.new(
|
264
|
+
'And',
|
265
|
+
[Expression.new('Isa', [var, Pattern.new(get_class_name(cls), {})])]
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
def maybe_mtd(cls, mtd)
|
270
|
+
cls.respond_to?(mtd) && cls.method(mtd) || nil
|
271
|
+
end
|
272
|
+
|
217
273
|
# @return [FFI::Polar]
|
218
274
|
attr_reader :ffi_polar
|
219
|
-
|
275
|
+
|
276
|
+
# Register MROs, load Polar code, and check inline queries.
|
277
|
+
# @param sources [Array<Source>] Polar sources to load.
|
278
|
+
def load_sources(sources)
|
279
|
+
host.register_mros
|
280
|
+
ffi_polar.load(sources)
|
281
|
+
check_inline_queries
|
282
|
+
end
|
283
|
+
|
284
|
+
def check_inline_queries
|
285
|
+
loop do
|
286
|
+
next_query = ffi_polar.next_inline_query
|
287
|
+
break if next_query.nil?
|
288
|
+
|
289
|
+
raise InlineQueryFailedError, next_query.source if Query.new(next_query, host: host).none?
|
290
|
+
end
|
291
|
+
end
|
220
292
|
|
221
293
|
# The R and L in REPL for systems where readline is available.
|
222
294
|
def repl_readline(prompt)
|