oso-oso 0.14.2 → 0.20.1.pre.beta
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 +3 -3
- data/Gemfile.lock +50 -27
- 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 +176 -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 +138 -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.rb +4 -3
- data/oso-oso.gemspec +2 -0
- metadata +34 -4
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Oso
|
4
|
+
module Polar
|
5
|
+
# Data filtering interface for Ruby
|
6
|
+
module DataFiltering
|
7
|
+
# Represents a set of filter sequences that should allow the host
|
8
|
+
# to obtain the records satisfying a query.
|
9
|
+
class FilterPlan
|
10
|
+
attr_reader :result_sets
|
11
|
+
|
12
|
+
def self.parse(polar, partials, class_name)
|
13
|
+
types = polar.host.serialize_types
|
14
|
+
parsed_json = polar.ffi.build_filter_plan(types, partials, 'resource', class_name)
|
15
|
+
result_sets = parsed_json['result_sets'].map do |rset|
|
16
|
+
ResultSet.parse polar, rset
|
17
|
+
end
|
18
|
+
|
19
|
+
new polar: polar, result_sets: result_sets
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(polar:, result_sets:)
|
23
|
+
@polar = polar
|
24
|
+
@result_sets = result_sets
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_query # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
28
|
+
combine = nil
|
29
|
+
result_sets.each_with_object([]) do |rs, qb|
|
30
|
+
rs.resolve_order.each_with_object({}) do |i, set_results|
|
31
|
+
req = rs.requests[i]
|
32
|
+
cs = req.constraints.each { |c| c.ground set_results }
|
33
|
+
typ = @polar.host.types[req.class_tag]
|
34
|
+
q = typ.build_query[cs]
|
35
|
+
if i != rs.result_id
|
36
|
+
set_results[i] = typ.exec_query[q]
|
37
|
+
else
|
38
|
+
combine = typ.combine_query
|
39
|
+
qb.push q
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end.reduce(&combine)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Represents a sequence of filters for one set of results
|
46
|
+
class ResultSet
|
47
|
+
attr_reader :requests, :resolve_order, :result_id
|
48
|
+
|
49
|
+
def self.parse(polar, parsed_json)
|
50
|
+
resolve_order = parsed_json['resolve_order']
|
51
|
+
result_id = parsed_json['result_id']
|
52
|
+
requests = parsed_json['requests'].each_with_object({}) do |req, reqs|
|
53
|
+
reqs[req[0].to_i] = Request.parse(polar, req[1])
|
54
|
+
end
|
55
|
+
|
56
|
+
new resolve_order: resolve_order, result_id: result_id, requests: requests
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(requests:, resolve_order:, result_id:)
|
60
|
+
@resolve_order = resolve_order
|
61
|
+
@requests = requests
|
62
|
+
@result_id = result_id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Represents a filter for a result set
|
67
|
+
class Request
|
68
|
+
attr_reader :constraints, :class_tag
|
69
|
+
|
70
|
+
def self.parse(polar, parsed_json)
|
71
|
+
constraints = parsed_json['constraints'].map do |con|
|
72
|
+
Filter.parse polar, con
|
73
|
+
end
|
74
|
+
class_tag = parsed_json['class_tag']
|
75
|
+
|
76
|
+
new(constraints: constraints, class_tag: class_tag)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(constraints:, class_tag:)
|
80
|
+
@constraints = constraints
|
81
|
+
@class_tag = class_tag
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Represents relationships between resources, eg. one-one or one-many
|
87
|
+
class Relation
|
88
|
+
attr_reader :kind, :other_type, :my_field, :other_field
|
89
|
+
|
90
|
+
def initialize(kind:, other_type:, my_field:, other_field:)
|
91
|
+
@kind = kind
|
92
|
+
@other_type = other_type
|
93
|
+
@my_field = my_field
|
94
|
+
@other_field = other_field
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Represents field-field relationships on one resource.
|
99
|
+
class Field
|
100
|
+
attr_reader :field
|
101
|
+
|
102
|
+
def initialize(field:)
|
103
|
+
@field = field
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Represents field-field relationships on different resources.
|
108
|
+
class Ref
|
109
|
+
attr_reader :field, :result_id
|
110
|
+
|
111
|
+
def initialize(field:, result_id:)
|
112
|
+
@field = field
|
113
|
+
@result_id = result_id
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Represents a condition that must hold on a resource.
|
118
|
+
class Filter
|
119
|
+
attr_reader :kind, :field, :value
|
120
|
+
|
121
|
+
CHECKS = {
|
122
|
+
'Eq' => ->(a, b) { a == b },
|
123
|
+
'In' => ->(a, b) { b.include? a },
|
124
|
+
'Neq' => ->(a, b) { a != b },
|
125
|
+
'Contains' => ->(a, b) { a.include? b }
|
126
|
+
}.freeze
|
127
|
+
|
128
|
+
def initialize(kind:, field:, value:)
|
129
|
+
@kind = kind
|
130
|
+
@field = field
|
131
|
+
@value = value
|
132
|
+
@check = CHECKS[kind]
|
133
|
+
raise "Unknown constraint kind `#{kind}`" if @check.nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
def ground(results)
|
137
|
+
return unless value.is_a? Ref
|
138
|
+
|
139
|
+
ref = value
|
140
|
+
@value = results[ref.result_id]
|
141
|
+
@value = value.map { |v| v.send ref.field } unless ref.field.nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
def check(item)
|
145
|
+
val = value.is_a?(Field) ? item.send(value.field) : value
|
146
|
+
item = field.nil? ? item : item.send(field)
|
147
|
+
@check[item, val]
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.parse(polar, constraint) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
151
|
+
kind = constraint['kind']
|
152
|
+
field = constraint['field']
|
153
|
+
value = constraint['value']
|
154
|
+
|
155
|
+
value_kind = value.keys.first
|
156
|
+
value = value[value_kind]
|
157
|
+
|
158
|
+
case value_kind
|
159
|
+
when 'Term'
|
160
|
+
value = polar.host.to_ruby value
|
161
|
+
when 'Ref'
|
162
|
+
child_field = value['field']
|
163
|
+
result_id = value['result_id']
|
164
|
+
value = Ref.new field: child_field, result_id: result_id
|
165
|
+
when 'Field'
|
166
|
+
value = Field.new field: value
|
167
|
+
else
|
168
|
+
raise "Unknown value kind `#{value_kind}`"
|
169
|
+
end
|
170
|
+
|
171
|
+
new kind: kind, field: field, value: value
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/oso/polar/errors.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Oso
|
4
4
|
module Polar
|
5
5
|
# Base error type for Oso::Polar.
|
6
|
-
class Error < ::
|
6
|
+
class Error < ::Oso::Error
|
7
7
|
attr_reader :stack_trace
|
8
8
|
|
9
9
|
# @param message [String]
|
@@ -68,7 +68,7 @@ module Oso
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
class DuplicateClassAliasError < PolarRuntimeError # rubocop:disable Style/Documentation
|
71
|
-
# @param
|
71
|
+
# @param name [String]
|
72
72
|
# @param old [Class]
|
73
73
|
# @param new [Class]
|
74
74
|
def initialize(name:, old:, new:)
|
@@ -101,7 +101,6 @@ module Oso
|
|
101
101
|
class ParameterError < ApiError; end
|
102
102
|
|
103
103
|
class ValidationError < Error; end
|
104
|
-
class RolesValidationError < Error; end
|
105
104
|
|
106
105
|
UNEXPECTED_EXPRESSION_MESSAGE = <<~MSG
|
107
106
|
Received Expression from Polar VM. The Expression type is not yet supported in this language.
|
data/lib/oso/polar/ffi/error.rb
CHANGED
@@ -56,7 +56,7 @@ module Oso
|
|
56
56
|
operational_error(subkind, msg: msg, details: details)
|
57
57
|
when 'Parameter'
|
58
58
|
api_error(subkind, msg: msg, details: details)
|
59
|
-
when '
|
59
|
+
when 'Validation'
|
60
60
|
validation_error(msg, details: details)
|
61
61
|
end
|
62
62
|
end
|
@@ -146,7 +146,7 @@ module Oso
|
|
146
146
|
# @return [::Oso::Polar::ValidationError] the object converted into the expected format.
|
147
147
|
private_class_method def self.validation_error(msg, details:)
|
148
148
|
# This is currently the only type of validation error.
|
149
|
-
::Oso::Polar::
|
149
|
+
::Oso::Polar::ValidationError.new(msg, details: details)
|
150
150
|
end
|
151
151
|
end
|
152
152
|
end
|
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
|