oso-oso 0.2.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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