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.
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Oso
4
4
  module Polar
5
5
  # Base error type for Oso::Polar.
6
- class Error < ::RuntimeError
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 as [String]
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.
@@ -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 'RolesValidation'
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::RolesValidationError.new(msg, details: details)
149
+ ::Oso::Polar::ValidationError.new(msg, details: details)
150
150
  end
151
151
  end
152
152
  end
@@ -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 :enable_roles, :polar_enable_roles, [FFI::Polar], :int32
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
- # @raise [FFI::Error] if the FFI call returns an error.
41
- def enable_roles
42
- result = Rust.enable_roles(self)
43
- process_messages
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 result.zero?
50
+ handle_error if plan.nil?
51
+ # TODO(gw) more error checking?
52
+ JSON.parse plan
52
53
  end
53
54
 
54
- # @param src [String]
55
- # @param filename [String]
55
+ # @param sources [Array<Source>]
56
56
  # @raise [FFI::Error] if the FFI call returns an error.
57
- def load(src, filename: nil)
58
- loaded = Rust.load(self, src, filename)
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
@@ -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 result [Boolean]
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
@@ -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
- @classes = {}
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
- @classes = other.classes.dup
80
+ @types = other.types.dup
64
81
  @instances = other.instances.dup
65
82
  end
66
83
 
67
- # Fetch a Ruby class from the {#classes} cache.
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 classes.key? name
90
+ raise UnregisteredClassError, name unless types.key? name
74
91
 
75
- classes[name].get
92
+ types[name].klass.get
76
93
  end
77
94
 
78
- # Store a Ruby class in the {#classes} cache.
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 classes.key? name
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
- classes[name] = PolarClass.new(cls)
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 op [String] operation to perform.
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
- { 'ExternalInstance' => { 'instance_id' => cache_instance(value), 'repr' => nil } }
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