oso-oso 0.5.0 → 0.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '09ee09c3366406a34633c870ddfe4ff271971529'
4
- data.tar.gz: 764b0f3b440d567e19e45b71eff823b9000f35ef
3
+ metadata.gz: 2dbfedef3bc14832b47137d1c17b247af0cf8bf3
4
+ data.tar.gz: 3bad36d04f6744069299b72855be913bc4bd9562
5
5
  SHA512:
6
- metadata.gz: fdae19c2528c46a85004241f0a8e59639123e416724239670cb9ff2ffc1172a87bb116b481d575e466c74f634213abdced263211f991721f1ff990e1e085d884
7
- data.tar.gz: 309a196e2a0a3725e3bc1f45be8382ed85026dc7fc271662efaef8e207b0df2891f26da48ece702f1d9ce3e3731ea52c0a6cea30bf50af6acde54f08b44d79ec
6
+ metadata.gz: 13d18f6a934a266e82ec7e981187f7f8523c998b55158044bc09271c255fa56e40159246a55096447296971d88e9c5c68355684d37db99cea59b911810949cd9
7
+ data.tar.gz: 5cdae20c3b4c08752db940bcce466d65a787e9728ec935b837eb20eb98fffda4e88f812f63c1d75822f2bd25e6e741c1ae6df8620f2b587ca4b5db0f4901029b
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.5.0)
4
+ oso-oso (0.7.1)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
data/Makefile CHANGED
@@ -3,17 +3,17 @@
3
3
  rust:
4
4
  $(MAKE) -C ../.. rust-build
5
5
 
6
- install: rust
6
+ install:
7
7
  bundle install
8
8
 
9
- test: install
9
+ test: install rust
10
10
  bundle exec rake spec
11
11
 
12
- lint:
12
+ lint: install
13
13
  bundle exec rubocop
14
14
 
15
- typecheck:
15
+ typecheck: install
16
16
  bundle exec solargraph typecheck
17
17
 
18
- repl: install
18
+ repl: install rust
19
19
  bundle exec oso
Binary file
Binary file
@@ -27,36 +27,24 @@ module Oso
27
27
  class UnsupportedError < PolarRuntimeError; end
28
28
  class PolarTypeError < PolarRuntimeError; end
29
29
  class StackOverflowError < PolarRuntimeError; end
30
+ class FileLoadingError < PolarRuntimeError; end
30
31
 
31
32
  # Errors originating from this side of the FFI boundary.
32
33
 
33
34
  class UnregisteredClassError < PolarRuntimeError; end
34
- class MissingConstructorError < PolarRuntimeError; end
35
35
  class UnregisteredInstanceError < PolarRuntimeError; end
36
36
  class DuplicateInstanceRegistrationError < PolarRuntimeError; end
37
+
38
+ # TODO: I think this should probably have some arguments to say what the call is
37
39
  class InvalidCallError < PolarRuntimeError; end
38
40
  class InvalidConstructorError < PolarRuntimeError; end
39
41
  class InvalidQueryTypeError < PolarRuntimeError; end
40
- class InlineQueryFailedError < PolarRuntimeError; end
41
42
  class NullByteInPolarFileError < PolarRuntimeError; end
42
43
  class UnexpectedPolarTypeError < PolarRuntimeError; end
43
- class PolarFileAlreadyLoadedError < PolarRuntimeError # rubocop:disable Style/Documentation
44
- # @param file [String]
45
- def initialize(file)
46
- super("File #{file} has already been loaded.")
47
- end
48
- end
49
- class PolarFileContentsChangedError < PolarRuntimeError # rubocop:disable Style/Documentation
50
- # @param file [String]
51
- def initialize(file)
52
- super("A file with the name #{file}, but different contents, has already been loaded.")
53
- end
54
- end
55
- class PolarFileNameChangedError < PolarRuntimeError # rubocop:disable Style/Documentation
56
- # @param file [String]
57
- # @param existing [String]
58
- def initialize(file, existing)
59
- super("A file with the same contents as #{file} named #{existing} has already been loaded.")
44
+ class InlineQueryFailedError < PolarRuntimeError; # rubocop:disable Style/Documentation
45
+ # @param source [String]
46
+ def initialize(source)
47
+ super("Inline query failed: #{source}")
60
48
  end
61
49
  end
62
50
  class PolarFileExtensionError < PolarRuntimeError # rubocop:disable Style/Documentation
@@ -79,6 +67,12 @@ module Oso
79
67
  end
80
68
  end
81
69
 
70
+ class UnimplementedOperationError < PolarRuntimeError # rubocop:disable Style/Documentation
71
+ def initialize(operation)
72
+ super("#{operation} are unimplemented in the oso Ruby library")
73
+ end
74
+ end
75
+
82
76
  # Generic operational exception.
83
77
  class OperationalError < Error; end
84
78
  class UnknownError < OperationalError; end
@@ -46,6 +46,13 @@ module Oso
46
46
  Rust.free(ptr) unless ptr.null?
47
47
  end
48
48
  end
49
+
50
+ # Wrapper class for Source FFI pointer.
51
+ class Source < ::FFI::AutoPointer
52
+ def self.release(ptr)
53
+ Rust.free(ptr) unless ptr.null?
54
+ end
55
+ end
49
56
  end
50
57
  private_constant :FFI
51
58
  end
@@ -56,3 +63,4 @@ require 'oso/polar/ffi/query'
56
63
  require 'oso/polar/ffi/query_event'
57
64
  require 'oso/polar/ffi/error'
58
65
  require 'oso/polar/ffi/message'
66
+ require 'oso/polar/ffi/source'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -83,6 +85,8 @@ module Oso
83
85
  ::Oso::Polar::PolarTypeError.new(msg, details: details)
84
86
  when 'StackOverflow'
85
87
  ::Oso::Polar::StackOverflowError.new(msg, details: details)
88
+ when 'FileLoading'
89
+ ::Oso::Polar::FileLoadingError.new(msg, details: details)
86
90
  else
87
91
  ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
88
92
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -26,7 +28,7 @@ module Oso
26
28
  when 'Print'
27
29
  puts(msg)
28
30
  when 'Warning'
29
- warn('[warning] %<msg>s')
31
+ warn(format('[warning] %<msg>s', msg: msg))
30
32
  end
31
33
  end
32
34
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -10,7 +12,8 @@ module Oso
10
12
  ffi_lib FFI::LIB_PATH
11
13
 
12
14
  attach_function :new, :polar_new, [], FFI::Polar
13
- attach_function :load_str, :polar_load, [FFI::Polar, :string, :string], :int32
15
+ attach_function :load, :polar_load, [FFI::Polar, :string, :string], :int32
16
+ attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
14
17
  attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
15
18
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
19
  attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
@@ -33,12 +36,19 @@ module Oso
33
36
  # @param src [String]
34
37
  # @param filename [String]
35
38
  # @raise [FFI::Error] if the FFI call returns an error.
36
- def load_str(src, filename: nil)
37
- loaded = Rust.load_str(self, src, filename)
39
+ def load(src, filename: nil)
40
+ loaded = Rust.load(self, src, filename)
38
41
  process_messages
39
42
  raise FFI::Error.get if loaded.zero?
40
43
  end
41
44
 
45
+ # @raise [FFI::Error] if the FFI call returns an error.
46
+ def clear_rules
47
+ cleared = Rust.clear_rules(self)
48
+ process_messages
49
+ raise FFI::Error.get if cleared.zero?
50
+ end
51
+
42
52
  # @return [FFI::Query] if there are remaining inline queries.
43
53
  # @return [nil] if there are no remaining inline queries.
44
54
  # @raise [FFI::Error] if the FFI call returns an error.
@@ -84,7 +94,7 @@ module Oso
84
94
  # @param name [String]
85
95
  # @param value [Hash<String, Object>]
86
96
  # @raise [FFI::Error] if the FFI call returns an error.
87
- def register_constant(name, value:)
97
+ def register_constant(value, name:)
88
98
  registered = Rust.register_constant(self, name, JSON.dump(value))
89
99
  raise FFI::Error.get if registered.zero?
90
100
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -15,6 +17,7 @@ module Oso
15
17
  attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
16
18
  attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
17
19
  attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
20
+ attach_function :source, :polar_query_source_info, [FFI::Query], FFI::Source
18
21
  attach_function :free, :query_free, [FFI::Query], :int32
19
22
  end
20
23
  private_constant :Rust
@@ -74,6 +77,15 @@ module Oso
74
77
  message.process
75
78
  end
76
79
  end
80
+
81
+ # @return [String]
82
+ # @raise [FFI::Error] if the FFI call returns an error.
83
+ def source
84
+ res = Rust.source(self)
85
+ raise FFI::Error.get if res.null?
86
+
87
+ res.to_s
88
+ end
77
89
  end
78
90
  end
79
91
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for Source FFI pointer.
7
+ class Source < ::FFI::AutoPointer
8
+ # @return [String]
9
+ def to_s
10
+ @to_s ||= read_string.force_encoding('UTF-8')
11
+ end
12
+
13
+ Rust = Module.new do
14
+ extend ::FFI::Library
15
+ ffi_lib FFI::LIB_PATH
16
+
17
+ attach_function :free, :string_free, [Message], :int32
18
+ end
19
+
20
+ private_constant :Rust
21
+ end
22
+ end
23
+ end
24
+ end
@@ -10,8 +10,6 @@ module Oso
10
10
  attr_reader :ffi_polar
11
11
  # @return [Hash<String, Class>]
12
12
  attr_reader :classes
13
- # @return [Hash<String, Object>]
14
- attr_reader :constructors
15
13
  # @return [Hash<Integer, Object>]
16
14
  attr_reader :instances
17
15
 
@@ -20,14 +18,12 @@ module Oso
20
18
  def initialize(ffi_polar)
21
19
  @ffi_polar = ffi_polar
22
20
  @classes = {}
23
- @constructors = {}
24
21
  @instances = {}
25
22
  end
26
23
 
27
24
  def initialize_copy(other)
28
25
  @ffi_polar = other.ffi_polar
29
26
  @classes = other.classes.dup
30
- @constructors = other.constructors.dup
31
27
  @instances = other.instances.dup
32
28
  end
33
29
 
@@ -44,38 +40,18 @@ module Oso
44
40
 
45
41
  # Store a Ruby class in the {#classes} cache.
46
42
  #
47
- # @param cls [Class] the class to cache
48
- # @param name [String] the name to cache the class as. Defaults to the name of the class.
49
- # @param constructor [Proc] optional custom constructor function. Defaults to the :new method.
43
+ # @param cls [Class] the class to cache.
44
+ # @param name [String] the name to cache the class as.
50
45
  # @return [String] the name the class is cached as.
51
- # @raise [UnregisteredClassError] if the class has not been registered.
52
- def cache_class(cls, name:, constructor:) # rubocop:disable Metrics/MethodLength
53
- name = cls.name if name.nil?
46
+ # @raise [DuplicateClassAliasError] if attempting to register a class
47
+ # under a previously-registered name.
48
+ def cache_class(cls, name:)
54
49
  raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
55
50
 
56
51
  classes[name] = cls
57
- if constructor.nil?
58
- constructors[name] = :new
59
- elsif constructor.respond_to? :call
60
- constructors[name] = constructor
61
- else
62
- raise InvalidConstructorError
63
- end
64
52
  name
65
53
  end
66
54
 
67
- # Fetch a constructor from the {#constructors} cache.
68
- #
69
- # @param name [String]
70
- # @return [Symbol] if constructor is the default of `:new`.
71
- # @return [Proc] if a custom constructor was registered.
72
- # @raise [UnregisteredConstructorError] if the constructor has not been registered.
73
- def get_constructor(name)
74
- raise MissingConstructorError, name unless constructors.key? name
75
-
76
- constructors[name]
77
- end
78
-
79
55
  # Check if an instance exists in the {#instances} cache.
80
56
  #
81
57
  # @param id [Integer]
@@ -100,12 +76,12 @@ module Oso
100
76
  instances[id]
101
77
  end
102
78
 
103
- # Cache a Ruby instance in the {#instances} cache, fetching a {#new_id}
104
- # if one isn't provided.
79
+ # Cache a Ruby instance in the {#instances} cache, fetching a new id if
80
+ # one isn't provided.
105
81
  #
106
82
  # @param instance [Object]
107
- # @param id [Integer]
108
- # @return [Integer]
83
+ # @param id [Integer] the instance ID. Generated via FFI if not provided.
84
+ # @return [Integer] the instance ID.
109
85
  def cache_instance(instance, id: nil)
110
86
  id = ffi_polar.new_id if id.nil?
111
87
  instances[id] = instance
@@ -114,30 +90,17 @@ module Oso
114
90
 
115
91
  # Construct and cache a Ruby instance.
116
92
  #
117
- # @param cls_name [String]
118
- # @param initargs [Hash<String, Hash>]
119
- # @param id [Integer]
93
+ # @param cls_name [String] name of the instance's class.
94
+ # @param args [Array<Object>] positional args to the constructor.
95
+ # @param kwargs [Hash<String, Object>] keyword args to the constructor.
96
+ # @param id [Integer] the instance ID.
120
97
  # @raise [PolarRuntimeError] if instance construction fails.
121
- def make_instance(cls_name, initargs:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
122
- constructor = get_constructor(cls_name)
123
- instance = if constructor == :new
124
- if initargs.empty?
125
- get_class(cls_name).__send__(:new)
126
- elsif initargs.is_a? Array
127
- get_class(cls_name).__send__(:new, *initargs)
128
- elsif initargs.is_a? Hash
129
- get_class(cls_name).__send__(:new, **initargs)
130
- else
131
- raise PolarRuntimeError, "Bad initargs: #{initargs}"
132
- end
133
- elsif initargs.empty?
134
- constructor.call
135
- elsif initargs.is_a? Array
136
- constructor.call(*initargs)
137
- elsif initargs.is_a? Hash
138
- constructor.call(**initargs)
98
+ # @return [Integer] the instance ID.
99
+ def make_instance(cls_name, args:, kwargs:, id:)
100
+ instance = if kwargs.empty? # This check is for Ruby < 2.7.
101
+ get_class(cls_name).__send__(:new, *args)
139
102
  else
140
- raise PolarRuntimeError, "Bad initargs: #{initargs}"
103
+ get_class(cls_name).__send__(:new, *args, **kwargs)
141
104
  end
142
105
  cache_instance(instance, id: id)
143
106
  rescue StandardError => e
@@ -153,9 +116,9 @@ module Oso
153
116
  # @return [Boolean]
154
117
  def subspecializer?(instance_id, left_tag:, right_tag:)
155
118
  mro = get_instance(instance_id).class.ancestors
156
- mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
157
- rescue StandardError
158
- false
119
+ left_index = mro.index(get_class(left_tag))
120
+ right_index = mro.index(get_class(right_tag))
121
+ left_index && right_index && left_index < right_index
159
122
  end
160
123
 
161
124
  # Check if instance is an instance of class.
@@ -167,8 +130,6 @@ module Oso
167
130
  instance = to_ruby(instance)
168
131
  cls = get_class(class_tag)
169
132
  instance.is_a? cls
170
- rescue PolarRuntimeError
171
- false
172
133
  end
173
134
 
174
135
  # Check if two instances unify
@@ -180,8 +141,6 @@ module Oso
180
141
  left_instance = get_instance(left_instance_id)
181
142
  right_instance = get_instance(right_instance_id)
182
143
  left_instance == right_instance
183
- rescue PolarRuntimeError
184
- false
185
144
  end
186
145
 
187
146
  # Turn a Ruby value into a Polar term that's ready to be sent across the
@@ -189,22 +148,29 @@ module Oso
189
148
  #
190
149
  # @param value [Object]
191
150
  # @return [Hash<String, Object>]
192
- def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
151
+ def to_polar(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
193
152
  value = case true # rubocop:disable Lint/LiteralAsCondition
194
153
  when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
195
154
  { 'Boolean' => value }
196
155
  when value.instance_of?(Integer)
197
156
  { 'Number' => { 'Integer' => value } }
198
157
  when value.instance_of?(Float)
158
+ if value == Float::INFINITY
159
+ value = 'Infinity'
160
+ elsif value == -Float::INFINITY
161
+ value = '-Infinity'
162
+ elsif value.nan?
163
+ value = 'NaN'
164
+ end
199
165
  { 'Number' => { 'Float' => value } }
200
166
  when value.instance_of?(String)
201
167
  { 'String' => value }
202
168
  when value.instance_of?(Array)
203
- { 'List' => value.map { |el| to_polar_term(el) } }
169
+ { 'List' => value.map { |el| to_polar(el) } }
204
170
  when value.instance_of?(Hash)
205
- { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
171
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar(v) } } }
206
172
  when value.instance_of?(Predicate)
207
- { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
173
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar(el) } } }
208
174
  when value.instance_of?(Variable)
209
175
  # This is supported so that we can query for unbound variables
210
176
  { 'Variable' => value }
@@ -222,13 +188,28 @@ module Oso
222
188
  # @option data [Hash<String, Object>] :value
223
189
  # @return [Object]
224
190
  # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
225
- def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
191
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
226
192
  tag, value = data['value'].first
227
193
  case tag
228
194
  when 'String', 'Boolean'
229
195
  value
230
196
  when 'Number'
231
- value.values.first
197
+ num = value.values.first
198
+ if value.key? 'Float'
199
+ case num
200
+ when 'Infinity'
201
+ return Float::INFINITY
202
+ when '-Infinity'
203
+ return -Float::INFINITY
204
+ when 'NaN'
205
+ return Float::NAN
206
+ else
207
+ unless value['Float'].is_a? Float # rubocop:disable Metrics/BlockNesting
208
+ raise PolarRuntimeError, "Expected a floating point number, got \"#{value['Float']}\""
209
+ end
210
+ end
211
+ end
212
+ num
232
213
  when 'List'
233
214
  value.map { |el| to_ruby(el) }
234
215
  when 'Dictionary'
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'pp'
5
- require 'set'
6
- require 'digest/md5'
7
-
8
3
  # Missing Ruby type.
9
4
  module PolarBoolean; end
10
5
  # Monkey-patch Ruby true type.
@@ -42,8 +37,6 @@ module Oso
42
37
  def initialize
43
38
  @ffi_polar = FFI::Polar.create
44
39
  @host = Host.new(ffi_polar)
45
- @loaded_names = {}
46
- @loaded_contents = {}
47
40
 
48
41
  # Register built-in classes.
49
42
  register_class PolarBoolean, name: 'Boolean'
@@ -54,11 +47,12 @@ module Oso
54
47
  register_class String
55
48
  end
56
49
 
57
- # Replace the current Polar instance but retain all registered classes and constructors.
58
- def clear
59
- loaded_names.clear
60
- loaded_contents.clear
61
- @ffi_polar = FFI::Polar.create
50
+ # Clear all rules and rule sources from the current Polar instance
51
+ #
52
+ # @return [self] for chaining.
53
+ def clear_rules
54
+ ffi_polar.clear_rules
55
+ self
62
56
  end
63
57
 
64
58
  # Load a Polar policy file.
@@ -66,23 +60,12 @@ module Oso
66
60
  # @param name [String]
67
61
  # @raise [PolarFileExtensionError] if provided filename has invalid extension.
68
62
  # @raise [PolarFileNotFoundError] if provided filename does not exist.
69
- def load_file(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
63
+ # @return [self] for chaining.
64
+ def load_file(name)
70
65
  raise PolarFileExtensionError, name unless File.extname(name) == '.polar'
71
66
 
72
67
  file_data = File.open(name, &:read)
73
- hash = Digest::MD5.hexdigest(file_data)
74
-
75
- if loaded_names.key?(name)
76
- raise PolarFileAlreadyLoadedError, name if loaded_names[name] == hash
77
-
78
- raise PolarFileContentsChangedError, name
79
- elsif loaded_contents.key?(hash)
80
- raise PolarFileNameChangedError, name, loaded_contents[hash]
81
- else
82
- load_str(file_data, filename: name)
83
- loaded_names[name] = hash
84
- loaded_contents[hash] = name
85
- end
68
+ load_str(file_data, filename: name)
86
69
  rescue Errno::ENOENT
87
70
  raise PolarFileNotFoundError, name
88
71
  end
@@ -94,10 +77,11 @@ module Oso
94
77
  # @raise [NullByteInPolarFileError] if str includes a non-terminating null byte.
95
78
  # @raise [InlineQueryFailedError] on the first failed inline query.
96
79
  # @raise [Error] if any of the FFI calls raise one.
80
+ # @return [self] for chaining.
97
81
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
98
82
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
99
83
 
100
- ffi_polar.load_str(str, filename: filename)
84
+ ffi_polar.load(str, filename: filename)
101
85
  loop do
102
86
  next_query = ffi_polar.next_inline_query
103
87
  break if next_query.nil?
@@ -105,9 +89,10 @@ module Oso
105
89
  begin
106
90
  Query.new(next_query, host: host).results.next
107
91
  rescue StopIteration
108
- raise InlineQueryFailedError
92
+ raise InlineQueryFailedError, next_query.source
109
93
  end
110
94
  end
95
+ self
111
96
  end
112
97
 
113
98
  # Query for a Polar predicate or string.
@@ -126,7 +111,7 @@ module Oso
126
111
  when String
127
112
  ffi_query = ffi_polar.new_query_from_str(query)
128
113
  when Predicate
129
- ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
114
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar(query))
130
115
  else
131
116
  raise InvalidQueryTypeError
132
117
  end
@@ -145,18 +130,26 @@ module Oso
145
130
 
146
131
  # Register a Ruby class with Polar.
147
132
  #
148
- # @param cls [Class]
149
- # @param name [String]
150
- # @param from_polar [Proc]
151
- # @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
152
- def register_class(cls, name: nil, from_polar: nil)
153
- from_polar = Proc.new if block_given?
154
- name = host.cache_class(cls, name: name, constructor: from_polar)
155
- register_constant(name, value: cls)
133
+ # @param cls [Class] the class to register.
134
+ # @param name [String] the name to register the class as. Defaults to the name of the class.
135
+ # @raise [DuplicateClassAliasError] if attempting to register a class
136
+ # under a previously-registered name.
137
+ # @raise [FFI::Error] if the FFI call returns an error.
138
+ # @return [self] for chaining.
139
+ def register_class(cls, name: nil)
140
+ name = host.cache_class(cls, name: name || cls.name)
141
+ register_constant(cls, name: name)
156
142
  end
157
143
 
158
- def register_constant(name, value:)
159
- ffi_polar.register_constant(name, value: host.to_polar_term(value))
144
+ # Register a Ruby object with Polar.
145
+ #
146
+ # @param value [Object] the object to register.
147
+ # @param name [String] the name to register the object as.
148
+ # @return [self] for chaining.
149
+ # @raise [FFI::Error] if the FFI call returns an error.
150
+ def register_constant(value, name:)
151
+ ffi_polar.register_constant(host.to_polar(value), name: name)
152
+ self
160
153
  end
161
154
 
162
155
  # Start a REPL session.
@@ -178,10 +171,6 @@ module Oso
178
171
 
179
172
  # @return [FFI::Polar]
180
173
  attr_reader :ffi_polar
181
- # @return [Hash<String, String>]
182
- attr_reader :loaded_names
183
- # @return [Hash<String, String>]
184
- attr_reader :loaded_contents
185
174
 
186
175
  # The R and L in REPL for systems where readline is available.
187
176
  def repl_readline(prompt)
@@ -236,7 +225,7 @@ module Oso
236
225
  puts true
237
226
  else
238
227
  result.each do |variable, value|
239
- puts "#{variable} => #{value.inspect}"
228
+ puts "#{variable} = #{value.inspect}"
240
229
  end
241
230
  end
242
231
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  # A single Polar query.
@@ -43,16 +45,18 @@ module Oso
43
45
  # @param args [Array<Hash>]
44
46
  # @raise [InvalidCallError] if the method doesn't exist on the instance or
45
47
  # the args passed to the method are invalid.
46
- def register_call(attribute, call_id:, instance:, args:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
48
+ def register_call(attribute, call_id:, instance:, args:, kwargs:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
47
49
  return if calls.key?(call_id)
48
50
 
49
51
  instance = host.to_ruby(instance)
50
- if args.nil?
51
- result = instance.__send__(attribute)
52
- else
53
- args = args.map { |a| host.to_ruby(a) }
54
- result = instance.__send__(attribute, *args)
55
- end
52
+ args = args.map { |a| host.to_ruby(a) }
53
+ kwargs = Hash[kwargs.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
54
+ # The kwargs.empty? check is for Ruby < 2.7.
55
+ result = if kwargs.empty?
56
+ instance.__send__(attribute, *args)
57
+ else
58
+ instance.__send__(attribute, *args, **kwargs)
59
+ end
56
60
  result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
57
61
  calls[call_id] = result.lazy
58
62
  rescue ArgumentError, NoMethodError
@@ -68,13 +72,13 @@ module Oso
68
72
  ffi_query.call_result(result, call_id: call_id)
69
73
  end
70
74
 
71
- # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
75
+ # Retrieve the next result from a registered call and pass it to {#to_polar}.
72
76
  #
73
77
  # @param id [Integer]
74
78
  # @return [Hash]
75
79
  # @raise [StopIteration] if the call has been exhausted.
76
80
  def next_call_result(id)
77
- host.to_polar_term(calls[id].next)
81
+ host.to_polar(calls[id].next)
78
82
  end
79
83
 
80
84
  # Send application error across FFI boundary.
@@ -93,8 +97,8 @@ module Oso
93
97
  # @param call_id [Integer]
94
98
  # @param instance [Hash<String, Object>]
95
99
  # @raise [Error] if the FFI call raises one.
96
- def handle_call(attribute, call_id:, instance:, args:)
97
- register_call(attribute, call_id: call_id, instance: instance, args: args)
100
+ def handle_call(attribute, call_id:, instance:, args:, kwargs:)
101
+ register_call(attribute, call_id: call_id, instance: instance, args: args, kwargs: kwargs)
98
102
  result = JSON.dump(next_call_result(call_id))
99
103
  call_result(result, call_id: call_id)
100
104
  rescue InvalidCallError => e
@@ -104,6 +108,20 @@ module Oso
104
108
  call_result(nil, call_id: call_id)
105
109
  end
106
110
 
111
+ def handle_make_external(data) # rubocop:disable Metrics/AbcSize
112
+ id = data['instance_id']
113
+ raise DuplicateInstanceRegistrationError, id if host.instance? id
114
+
115
+ constructor = data['constructor']['value']
116
+ raise InvalidConstructorError unless constructor.key? 'Call'
117
+
118
+ cls_name = constructor['Call']['name']
119
+ args = constructor['Call']['args'].map { |arg| host.to_ruby(arg) }
120
+ kwargs = constructor['Call']['kwargs'] || {}
121
+ kwargs = Hash[kwargs.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
122
+ host.make_instance(cls_name, args: args, kwargs: kwargs, id: id)
123
+ end
124
+
107
125
  # Create a generator that can be polled to advance the query loop.
108
126
  #
109
127
  # @yieldparam [Hash<String, Object>]
@@ -119,27 +137,14 @@ module Oso
119
137
  when 'Result'
120
138
  yielder << event.data['bindings'].transform_values { |v| host.to_ruby(v) }
121
139
  when 'MakeExternal'
122
- id = event.data['instance_id']
123
- raise DuplicateInstanceRegistrationError, id if host.instance? id
124
-
125
- constructor = event.data['constructor']['value']
126
- if constructor.key? 'InstanceLiteral'
127
- cls_name = constructor['InstanceLiteral']['tag']
128
- fields = constructor['InstanceLiteral']['fields']['fields']
129
- initargs = Hash[fields.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
130
- elsif constructor.key? 'Call'
131
- cls_name = constructor['Call']['name']
132
- initargs = constructor['Call']['args'].map { |arg| host.to_ruby(arg) }
133
- else
134
- raise InvalidConstructorError
135
- end
136
- host.make_instance(cls_name, initargs: initargs, id: id)
140
+ handle_make_external(event.data)
137
141
  when 'ExternalCall'
138
142
  call_id = event.data['call_id']
139
143
  instance = event.data['instance']
140
144
  attribute = event.data['attribute']
141
- args = event.data['args']
142
- handle_call(attribute, call_id: call_id, instance: instance, args: args)
145
+ args = event.data['args'] || []
146
+ kwargs = event.data['kwargs'] || {}
147
+ handle_call(attribute, call_id: call_id, instance: instance, args: args, kwargs: kwargs)
143
148
  when 'ExternalIsSubSpecializer'
144
149
  instance_id = event.data['instance_id']
145
150
  left_tag = event.data['left_class_tag']
@@ -164,8 +169,10 @@ module Oso
164
169
  rescue EOFError
165
170
  next
166
171
  end
167
- command = JSON.dump(host.to_polar_term(input))
172
+ command = JSON.dump(host.to_polar(input))
168
173
  ffi_query.debug_command(command)
174
+ when 'ExternalOp'
175
+ raise UnimplementedOperationError, 'comparison operators'
169
176
  else
170
177
  raise "Unhandled event: #{JSON.dump(event.inspect)}"
171
178
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.5.0'
4
+ VERSION = '0.7.1'
5
5
  end
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.5.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-25 00:00:00.000000000 Z
11
+ date: 2020-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -141,6 +141,7 @@ files:
141
141
  - lib/oso/polar/ffi/polar.rb
142
142
  - lib/oso/polar/ffi/query.rb
143
143
  - lib/oso/polar/ffi/query_event.rb
144
+ - lib/oso/polar/ffi/source.rb
144
145
  - lib/oso/polar/host.rb
145
146
  - lib/oso/polar/polar.rb
146
147
  - lib/oso/polar/predicate.rb