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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: ec2b2b33a484c1770041164811415985e0e6c135a3fb03688d40813d0b332772
4
- data.tar.gz: 960bcf13f984e19b359da8da1ac6478702c48d68f1ed9f1150250d0d4d0d1df4
2
+ SHA1:
3
+ metadata.gz: 919322c6735cfeb3615da5dd85b4188c59e8716d
4
+ data.tar.gz: 1c881b365f4098e3bbdad3152168e82ab31df87f
5
5
  SHA512:
6
- metadata.gz: 9b49f6a116fb1e542674a19047c16484f8f08a54dda51f83918644abfca808e164e628c164fcfd6ad0b2497549c3192a5a194264b138b018c01adbbea8b38cd7
7
- data.tar.gz: 30d9fe7a3c625c1245140415c010066c21a388f79b5edce8b6376631712f33ca1bfc82f208e10244c6094b671588e6d1448e241bc11377e2b58c5a154b1afa66
6
+ metadata.gz: b898b3636cdb1cfa5fe909634292cf030bd60ccf768e964cb258c7e1c076d34b52e953bf8b37b27e2cc42926c44efdbd7b600dc21fea63e7144834870d80db1f
7
+ data.tar.gz: 1e38944c4a55b81b3dbecbf1864be035632bd840e0f709380c8b14bfaecf657a1abb273b74b171aae35df9210beddb7e42f4c3547af74e482d0db4b214ca987f
@@ -0,0 +1,7 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+ Exclude:
4
+ - '**/*~'
5
+ - 'bin/oso'
6
+ - 'vendor/**/*'
7
+ NewCops: enable
data/Gemfile CHANGED
@@ -3,4 +3,3 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
-
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.2.2)
4
+ oso-oso (0.4.0)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
@@ -12,18 +12,18 @@ GEM
12
12
  benchmark (0.1.0)
13
13
  byebug (11.1.3)
14
14
  coderay (1.1.3)
15
- diff-lcs (1.3)
15
+ diff-lcs (1.4.4)
16
16
  e2mmap (0.1.0)
17
17
  ffi (1.13.1)
18
18
  jaro_winkler (1.5.4)
19
19
  maruku (0.7.3)
20
20
  method_source (1.0.0)
21
21
  mini_portile2 (2.4.0)
22
- nokogiri (1.10.9)
22
+ nokogiri (1.10.10)
23
23
  mini_portile2 (~> 2.4.0)
24
- parallel (1.19.1)
25
- parser (2.7.1.3)
26
- ast (~> 2.4.0)
24
+ parallel (1.19.2)
25
+ parser (2.7.1.4)
26
+ ast (~> 2.4.1)
27
27
  pry (0.13.1)
28
28
  coderay (~> 1.1)
29
29
  method_source (~> 1.0)
@@ -49,19 +49,19 @@ GEM
49
49
  diff-lcs (>= 1.2.0, < 2.0)
50
50
  rspec-support (~> 3.9.0)
51
51
  rspec-support (3.9.3)
52
- rubocop (0.85.1)
52
+ rubocop (0.89.0)
53
53
  parallel (~> 1.10)
54
- parser (>= 2.7.0.1)
54
+ parser (>= 2.7.1.1)
55
55
  rainbow (>= 2.2.2, < 4.0)
56
56
  regexp_parser (>= 1.7)
57
57
  rexml
58
- rubocop-ast (>= 0.0.3)
58
+ rubocop-ast (>= 0.1.0, < 1.0)
59
59
  ruby-progressbar (~> 1.7)
60
60
  unicode-display_width (>= 1.4.0, < 2.0)
61
- rubocop-ast (0.0.3)
62
- parser (>= 2.7.0.1)
61
+ rubocop-ast (0.3.0)
62
+ parser (>= 2.7.1.4)
63
63
  ruby-progressbar (1.10.1)
64
- solargraph (0.39.8)
64
+ solargraph (0.39.13)
65
65
  backport (~> 1.1)
66
66
  benchmark
67
67
  bundler (>= 1.17.2)
@@ -88,6 +88,7 @@ DEPENDENCIES
88
88
  pry-byebug (~> 3.9.0)
89
89
  rake (~> 12.0)
90
90
  rspec (~> 3.0)
91
+ rubocop (~> 0.89.0)
91
92
  solargraph (~> 0.39.8)
92
93
  yard (~> 0.9.25)
93
94
 
data/Makefile CHANGED
@@ -1,16 +1,19 @@
1
- CARGO_FLAGS := $(shell [ -z $${RELEASE} ] && echo "" || echo "--all-features --release")
2
- export CARGO_FLAGS
3
- TARGET_DIR := $(shell [ -z $${RELEASE} ] && echo "debug" || echo "release")
4
- LIB_EXT := $(shell [ $$(uname) = "Linux" ] && echo "so" || echo "dylib")
1
+ .PHONY: rust install test lint typecheck repl
5
2
 
6
- .PHONY: install rust-build test
7
-
8
- rust-build:
3
+ rust:
9
4
  $(MAKE) -C ../.. rust-build
10
- cp ../../target/$(TARGET_DIR)/libpolar.$(LIB_EXT) ext/oso-oso/lib/libpolar.$(LIB_EXT)
11
5
 
12
- install: rust-build
6
+ install: rust
13
7
  bundle install
14
8
 
15
9
  test: install
16
10
  bundle exec rake spec
11
+
12
+ lint:
13
+ bundle exec rubocop
14
+
15
+ typecheck:
16
+ bundle exec solargraph typecheck
17
+
18
+ repl: install
19
+ bundle exec oso
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Oso::Oso
1
+ # oso-oso
2
2
 
3
3
  ## Installation
4
4
 
@@ -18,13 +18,10 @@ Or install it yourself as:
18
18
 
19
19
  ## Development
20
20
 
21
- After checking out the repo, run `bin/setup` to install dependencies. Then, run
22
- `rake spec` to run the tests. You can also run `bin/console` for an interactive
23
- prompt that will allow you to experiment.
21
+ After checking out the repo, run `bundle install` to install dependencies.
22
+ Then, run `bundle exec rake spec` to run the tests. You can also run `bundle
23
+ exec oso` for an interactive REPL that will allow you to experiment.
24
24
 
25
- To install this gem onto your local machine, run `bundle exec rake install`. To
26
- release a new version, update the version number in `version.rb`, and then run
27
- `bundle exec rake release`, which will create a git tag for the version, push
28
- git commits and tags, and push the `.gem` file to
29
- [rubygems.org](https://rubygems.org).
25
+ To install this gem onto your local machine, run `bundle exec rake install`.
30
26
 
27
+ New releases are minted and pushed to RubyGems via GitHub Actions workflows.
data/Rakefile CHANGED
@@ -6,4 +6,3 @@ require 'rspec/core/rake_task'
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
-
data/bin/oso ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ #-*-ruby-*-
3
+
4
+ require 'bundler/setup'
5
+ require 'oso'
6
+
7
+ Oso.new.repl(load: true)
Binary file
Binary file
@@ -3,7 +3,7 @@
3
3
  module Oso
4
4
  # An HTTP resource.
5
5
  class Http
6
- def initialize(hostname: nil, path: nil, query: nil)
6
+ def initialize(hostname, path, query)
7
7
  @hostname = hostname
8
8
  @path = path
9
9
  @query = query
@@ -1,47 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'polar/polar'
4
+
3
5
  module Oso
4
6
  # Oso authorization API.
5
- class Oso
7
+ class Oso < Polar::Polar
6
8
  def initialize
7
- @polar = ::Oso::Polar.new
9
+ super
8
10
  register_class(Http, name: 'Http')
9
11
  register_class(PathMapper, name: 'PathMapper')
10
12
  end
11
13
 
12
- def load_file(file)
13
- polar.load_file(file)
14
- end
15
-
16
- def load_str(str)
17
- polar.load_str(str)
18
- end
19
-
20
- def register_class(cls, name: nil) # rubocop:disable Naming/MethodParameterName
21
- if block_given?
22
- polar.register_class(cls, name: name, from_polar: Proc.new)
23
- else
24
- polar.register_class(cls, name: name)
25
- end
26
- end
27
-
28
- def allow(actor:, action:, resource:)
29
- polar.query_pred('allow', args: [actor, action, resource]).next
14
+ def allowed?(actor:, action:, resource:)
15
+ query_rule('allow', actor, action, resource).next
30
16
  true
31
17
  rescue StopIteration
32
18
  false
33
19
  end
34
-
35
- def query_predicate(name, *args)
36
- polar.query_pred(name, args: args)
37
- end
38
-
39
- def load_queued_files
40
- polar.load_queued_files
41
- end
42
-
43
- private
44
-
45
- attr_reader :polar
46
20
  end
47
21
  end
@@ -4,7 +4,7 @@ module Oso
4
4
  # Map from a template string with capture groups of the form
5
5
  # `{name}` to a dictionary of the form `{name: captured_value}`
6
6
  class PathMapper
7
- def initialize(template:)
7
+ def initialize(template)
8
8
  capture_group = /({([^}]+)})/
9
9
 
10
10
  template = template.dup
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'oso/polar/errors'
4
4
  require 'oso/polar/ffi'
5
+ require 'oso/polar/host'
5
6
  require 'oso/polar/polar'
6
7
  require 'oso/polar/predicate'
7
8
  require 'oso/polar/query'
@@ -5,16 +5,12 @@ module Oso
5
5
  # Base error type for Oso::Polar.
6
6
  class Error < ::RuntimeError
7
7
  attr_reader :stack_trace
8
-
8
+
9
9
  # @param message [String]
10
10
  # @param details [Hash]
11
11
  def initialize(message = nil, details: nil)
12
12
  @details = details
13
- if details and details.key?("stack_trace")
14
- @stack_trace = details['stack_trace']
15
- else
16
- @stack_trace = nil
17
- end
13
+ @stack_trace = details&.fetch('stack_trace', nil)
18
14
  super(message)
19
15
  end
20
16
  end
@@ -40,12 +36,32 @@ module Oso
40
36
  class DuplicateInstanceRegistrationError < PolarRuntimeError; end
41
37
  class InvalidCallError < PolarRuntimeError; end
42
38
  class InvalidConstructorError < PolarRuntimeError; end
39
+ class InvalidQueryTypeError < PolarRuntimeError; end
43
40
  class InlineQueryFailedError < PolarRuntimeError; end
44
41
  class NullByteInPolarFileError < PolarRuntimeError; end
45
42
  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.")
60
+ end
61
+ end
46
62
  class PolarFileExtensionError < PolarRuntimeError # rubocop:disable Style/Documentation
47
- def initialize
48
- super('Polar files must have .pol or .polar extension.')
63
+ def initialize(file)
64
+ super("Polar files must have .polar extension. Offending file: #{file}")
49
65
  end
50
66
  end
51
67
  class PolarFileNotFoundError < PolarRuntimeError # rubocop:disable Style/Documentation
@@ -58,7 +74,7 @@ module Oso
58
74
  # @param as [String]
59
75
  # @param old [Class]
60
76
  # @param new [Class]
61
- def initialize(name:, old:, new:) # rubocop:disable Naming/MethodParameterName
77
+ def initialize(name:, old:, new:)
62
78
  super("Attempted to alias #{new} as '#{name}', but #{old} already has that alias.")
63
79
  end
64
80
  end
@@ -5,8 +5,12 @@ require 'ffi'
5
5
  module Oso
6
6
  module Polar
7
7
  module FFI
8
- LIB = ::FFI::Platform::LIBPREFIX + 'polar.' + ::FFI::Platform::LIBSUFFIX
9
- LIB_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
8
+ LIB = "#{::FFI::Platform::LIBPREFIX}polar.#{::FFI::Platform::LIBSUFFIX}"
9
+ RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
+ DEV_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
11
+ # If the lib exists in the ext/ dir, use it. Otherwise, fall back to
12
+ # checking the local Rust target dir.
13
+ LIB_PATH = File.file?(RELEASE_PATH) ? RELEASE_PATH : DEV_PATH
10
14
 
11
15
  # Wrapper classes defined upfront to fix Ruby loading issues. Actual
12
16
  # implementations live in the sibling `ffi/` directory and are `require`d
@@ -22,7 +22,7 @@ module Oso
22
22
  #
23
23
  # @return [::Oso::Polar::Error] if there's an FFI error.
24
24
  # @return [::Oso::Polar::FFIErrorNotFound] if there isn't one.
25
- def self.get # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
25
+ def self.get # rubocop:disable Metrics/MethodLength
26
26
  error = Rust.get
27
27
  return ::Oso::Polar::FFIErrorNotFound if error.null?
28
28
 
@@ -48,7 +48,7 @@ module Oso
48
48
  # @param msg [String]
49
49
  # @param details [Hash<String, Object>]
50
50
  # @return [::Oso::Polar::ParseError] the object converted into the expected format.
51
- private_class_method def self.parse_error(kind, msg:, details:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
51
+ private_class_method def self.parse_error(kind, msg:, details:) # rubocop:disable Metrics/MethodLength
52
52
  case kind
53
53
  when 'ExtraToken'
54
54
  ::Oso::Polar::ParseError::ExtraToken.new(msg, details: details)
@@ -11,11 +11,10 @@ module Oso
11
11
 
12
12
  attach_function :new, :polar_new, [], FFI::Polar
13
13
  attach_function :load_str, :polar_load, [FFI::Polar, :string, :string], :int32
14
- attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar], FFI::Query
14
+ attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
15
15
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
- attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string], FFI::Query
17
- attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string], FFI::Query
18
- attach_function :new_query_from_repl, :polar_query_from_repl, [FFI::Polar], FFI::Query
16
+ attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
17
+ attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
19
18
  attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
20
19
  attach_function :free, :polar_free, [FFI::Polar], :int32
21
20
  end
@@ -41,7 +40,7 @@ module Oso
41
40
  # @return [nil] if there are no remaining inline queries.
42
41
  # @raise [FFI::Error] if the FFI call returns an error.
43
42
  def next_inline_query
44
- query = Rust.next_inline_query(self)
43
+ query = Rust.next_inline_query(self, 0)
45
44
  query.null? ? nil : query
46
45
  end
47
46
 
@@ -60,7 +59,7 @@ module Oso
60
59
  # @return [FFI::Query]
61
60
  # @raise [FFI::Error] if the FFI call returns an error.
62
61
  def new_query_from_str(str)
63
- query = Rust.new_query_from_str(self, str)
62
+ query = Rust.new_query_from_str(self, str, 0)
64
63
  raise FFI::Error.get if query.null?
65
64
 
66
65
  query
@@ -70,16 +69,7 @@ module Oso
70
69
  # @return [FFI::Query]
71
70
  # @raise [FFI::Error] if the FFI call returns an error.
72
71
  def new_query_from_term(term)
73
- query = Rust.new_query_from_term(self, JSON.dump(term))
74
- raise FFI::Error.get if query.null?
75
-
76
- query
77
- end
78
-
79
- # @return [FFI::Query]
80
- # @raise [FFI::Error] if the FFI call returns an error.
81
- def new_query_from_repl
82
- query = Rust.new_query_from_repl(self)
72
+ query = Rust.new_query_from_term(self, JSON.dump(term), 0)
83
73
  raise FFI::Error.get if query.null?
84
74
 
85
75
  query
@@ -12,6 +12,7 @@ module Oso
12
12
  attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], :int32
13
13
  attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], :int32
14
14
  attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
15
+ attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
15
16
  attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
16
17
  attach_function :free, :query_free, [FFI::Query], :int32
17
18
  end
@@ -41,6 +42,14 @@ module Oso
41
42
  raise FFI::Error.get if res.zero?
42
43
  end
43
44
 
45
+ # @param result [Boolean]
46
+ # @param call_id [Integer]
47
+ # @raise [FFI::Error] if the FFI call returns an error.
48
+ def application_error(message)
49
+ res = Rust.application_error(self, message)
50
+ raise FFI::Error.get if res.zero?
51
+ end
52
+
44
53
  # @return [::Oso::Polar::QueryEvent]
45
54
  # @raise [FFI::Error] if the FFI call returns an error.
46
55
  def next_event
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Translate between Polar and the host language (Ruby).
6
+ class Host # rubocop:disable Metrics/ClassLength
7
+ protected
8
+
9
+ # @return [FFI::Polar]
10
+ attr_reader :ffi_polar
11
+ # @return [Hash<String, Class>]
12
+ attr_reader :classes
13
+ # @return [Hash<String, Object>]
14
+ attr_reader :constructors
15
+ # @return [Hash<Integer, Object>]
16
+ attr_reader :instances
17
+
18
+ public
19
+
20
+ def initialize(ffi_polar)
21
+ @ffi_polar = ffi_polar
22
+ @classes = {}
23
+ @constructors = {}
24
+ @instances = {}
25
+ end
26
+
27
+ def initialize_copy(other)
28
+ @ffi_polar = other.ffi_polar
29
+ @classes = other.classes.dup
30
+ @constructors = other.constructors.dup
31
+ @instances = other.instances.dup
32
+ end
33
+
34
+ # Fetch a Ruby class from the {#classes} cache.
35
+ #
36
+ # @param name [String]
37
+ # @return [Class]
38
+ # @raise [UnregisteredClassError] if the class has not been registered.
39
+ def get_class(name)
40
+ raise UnregisteredClassError, name unless classes.key? name
41
+
42
+ classes[name]
43
+ end
44
+
45
+ # Store a Ruby class in the {#classes} cache.
46
+ #
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
+ # @return [String] the name the class is cached as.
50
+ # @raise [UnregisteredClassError] if the class has not been registered.
51
+ def cache_class(cls, name:, constructor:) # rubocop:disable Metrics/MethodLength
52
+ name = cls.name if name.nil?
53
+ raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
54
+
55
+ classes[name] = cls
56
+ if constructor.nil?
57
+ constructors[name] = :new
58
+ elsif constructor.respond_to? :call
59
+ constructors[name] = constructor
60
+ else
61
+ raise InvalidConstructorError
62
+ end
63
+ name
64
+ end
65
+
66
+ # Fetch a constructor from the {#constructors} cache.
67
+ #
68
+ # @param name [String]
69
+ # @return [Symbol] if constructor is the default of `:new`.
70
+ # @return [Proc] if a custom constructor was registered.
71
+ # @raise [UnregisteredConstructorError] if the constructor has not been registered.
72
+ def get_constructor(name)
73
+ raise MissingConstructorError, name unless constructors.key? name
74
+
75
+ constructors[name]
76
+ end
77
+
78
+ # Check if an instance has been cached.
79
+ #
80
+ # @param id [Integer]
81
+ # @return [Boolean]
82
+ def instance?(id)
83
+ case id
84
+ when Integer
85
+ instances.key? id
86
+ else
87
+ instances.value? id
88
+ end
89
+ end
90
+
91
+ # Fetch a Ruby instance from the {#instances} cache.
92
+ #
93
+ # @param id [Integer]
94
+ # @return [Object]
95
+ # @raise [UnregisteredInstanceError] if the ID has not been registered.
96
+ def get_instance(id)
97
+ raise UnregisteredInstanceError, id unless instance? id
98
+
99
+ instances[id]
100
+ end
101
+
102
+ # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
103
+ #
104
+ # @param instance [Object]
105
+ # @param id [Integer]
106
+ # @return [Integer]
107
+ def cache_instance(instance, id: nil)
108
+ id = ffi_polar.new_id if id.nil?
109
+ instances[id] = instance
110
+ id
111
+ end
112
+
113
+ # Construct and cache a Ruby instance.
114
+ #
115
+ # @param cls_name [String]
116
+ # @param initargs [Hash<String, Hash>]
117
+ # @param id [Integer]
118
+ # @raise [PolarRuntimeError] if instance construction fails.
119
+ def make_instance(cls_name, initargs:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
120
+ constructor = get_constructor(cls_name)
121
+ instance = if constructor == :new
122
+ if initargs.empty?
123
+ get_class(cls_name).__send__(:new)
124
+ elsif initargs.is_a? Array
125
+ get_class(cls_name).__send__(:new, *initargs)
126
+ elsif initargs.is_a? Hash
127
+ get_class(cls_name).__send__(:new, **initargs)
128
+ else
129
+ raise PolarRuntimeError, "Bad initargs: #{initargs}"
130
+ end
131
+ elsif initargs.empty?
132
+ constructor.call
133
+ elsif initargs.is_a? Array
134
+ constructor.call(*initargs)
135
+ elsif initargs.is_a? Hash
136
+ constructor.call(**initargs)
137
+ else
138
+ raise PolarRuntimeError, "Bad initargs: #{initargs}"
139
+ end
140
+ cache_instance(instance, id: id)
141
+ rescue StandardError => e
142
+ raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
143
+ end
144
+
145
+ # Check if the left class is more specific than the right class
146
+ # with respect to the given instance.
147
+ #
148
+ # @param instance_id [Integer]
149
+ # @param left_tag [String]
150
+ # @param right_tag [String]
151
+ # @return [Boolean]
152
+ def subspecializer?(instance_id, left_tag:, right_tag:)
153
+ mro = get_instance(instance_id).class.ancestors
154
+ mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
155
+ rescue StandardError
156
+ false
157
+ end
158
+
159
+ # Check if instance is an instance of class.
160
+ #
161
+ # @param instance [Hash<String, Object>]
162
+ # @param class_tag [String]
163
+ # @return [Boolean]
164
+ def isa?(instance, class_tag:)
165
+ instance = to_ruby(instance)
166
+ cls = get_class(class_tag)
167
+ instance.is_a? cls
168
+ rescue PolarRuntimeError
169
+ false
170
+ end
171
+
172
+ # Check if two instances unify
173
+ #
174
+ # @param left_instance_id [Integer]
175
+ # @param right_instance_id [Integer]
176
+ # @return [Boolean]
177
+ def unify?(left_instance_id, right_instance_id)
178
+ left_instance = get_instance(left_instance_id)
179
+ right_instance = get_instance(right_instance_id)
180
+ left_instance == right_instance
181
+ rescue PolarRuntimeError
182
+ false
183
+ end
184
+
185
+ # Turn a Ruby value into a Polar term that's ready to be sent across the
186
+ # FFI boundary.
187
+ #
188
+ # @param value [Object]
189
+ # @return [Hash<String, Object>]
190
+ def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
191
+ value = case true # rubocop:disable Lint/LiteralAsCondition
192
+ when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
193
+ { 'Boolean' => value }
194
+ when value.instance_of?(Integer)
195
+ { 'Number' => { 'Integer' => value } }
196
+ when value.instance_of?(Float)
197
+ { 'Number' => { 'Float' => value } }
198
+ when value.instance_of?(String)
199
+ { 'String' => value }
200
+ when value.instance_of?(Array)
201
+ { 'List' => value.map { |el| to_polar_term(el) } }
202
+ when value.instance_of?(Hash)
203
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
204
+ when value.instance_of?(Predicate)
205
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
206
+ when value.instance_of?(Variable)
207
+ # This is supported so that we can query for unbound variables
208
+ { 'Variable' => value }
209
+ else
210
+ { 'ExternalInstance' => { 'instance_id' => cache_instance(value), 'repr' => value.to_s } }
211
+ end
212
+ { 'value' => value }
213
+ end
214
+
215
+ # Turn a Polar term passed across the FFI boundary into a Ruby value.
216
+ #
217
+ # @param data [Hash<String, Object>]
218
+ # @option data [Integer] :id
219
+ # @option data [Integer] :offset Character offset of the term in its source string.
220
+ # @option data [Hash<String, Object>] :value
221
+ # @return [Object]
222
+ # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
223
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
224
+ tag, value = data['value'].first
225
+ case tag
226
+ when 'String', 'Boolean'
227
+ value
228
+ when 'Number'
229
+ value.values.first
230
+ when 'List'
231
+ value.map { |el| to_ruby(el) }
232
+ when 'Dictionary'
233
+ value['fields'].transform_values { |v| to_ruby(v) }
234
+ when 'ExternalInstance'
235
+ get_instance(value['instance_id'])
236
+ when 'Call'
237
+ Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
238
+ when 'Variable'
239
+ Variable.new(value['name'])
240
+ else
241
+ raise UnexpectedPolarTypeError, tag
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end