jsi 0.7.0 → 0.8.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +15 -0
  4. data/README.md +19 -18
  5. data/jsi.gemspec +2 -3
  6. data/lib/jsi/base/mutability.rb +44 -0
  7. data/lib/jsi/base/node.rb +199 -34
  8. data/lib/jsi/base.rb +412 -228
  9. data/lib/jsi/jsi_coder.rb +18 -16
  10. data/lib/jsi/metaschema_node/bootstrap_schema.rb +57 -23
  11. data/lib/jsi/metaschema_node.rb +138 -107
  12. data/lib/jsi/ptr.rb +59 -37
  13. data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
  14. data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
  15. data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
  16. data/lib/jsi/schema/application/child_application.rb +0 -25
  17. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
  18. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
  19. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
  20. data/lib/jsi/schema/application/inplace_application/ref.rb +1 -1
  21. data/lib/jsi/schema/application/inplace_application/someof.rb +1 -1
  22. data/lib/jsi/schema/application/inplace_application.rb +0 -27
  23. data/lib/jsi/schema/draft04.rb +0 -1
  24. data/lib/jsi/schema/draft06.rb +0 -1
  25. data/lib/jsi/schema/draft07.rb +0 -1
  26. data/lib/jsi/schema/ref.rb +44 -18
  27. data/lib/jsi/schema/schema_ancestor_node.rb +65 -56
  28. data/lib/jsi/schema/validation/contains.rb +1 -1
  29. data/lib/jsi/schema/validation/draft04/minmax.rb +2 -0
  30. data/lib/jsi/schema/validation/draft04.rb +0 -2
  31. data/lib/jsi/schema/validation/draft06.rb +0 -2
  32. data/lib/jsi/schema/validation/draft07.rb +0 -2
  33. data/lib/jsi/schema/validation/items.rb +3 -3
  34. data/lib/jsi/schema/validation/pattern.rb +1 -1
  35. data/lib/jsi/schema/validation/properties.rb +4 -4
  36. data/lib/jsi/schema/validation/ref.rb +1 -1
  37. data/lib/jsi/schema/validation.rb +0 -2
  38. data/lib/jsi/schema.rb +405 -194
  39. data/lib/jsi/schema_classes.rb +196 -127
  40. data/lib/jsi/schema_registry.rb +66 -17
  41. data/lib/jsi/schema_set.rb +76 -30
  42. data/lib/jsi/simple_wrap.rb +2 -7
  43. data/lib/jsi/util/private/attr_struct.rb +28 -14
  44. data/lib/jsi/util/private/memo_map.rb +75 -0
  45. data/lib/jsi/util/private.rb +73 -92
  46. data/lib/jsi/util/typelike.rb +28 -28
  47. data/lib/jsi/util.rb +120 -36
  48. data/lib/jsi/validation/error.rb +4 -0
  49. data/lib/jsi/validation/result.rb +18 -32
  50. data/lib/jsi/version.rb +1 -1
  51. data/lib/jsi.rb +67 -25
  52. data/lib/schemas/json-schema.org/draft-04/schema.rb +159 -4
  53. data/lib/schemas/json-schema.org/draft-06/schema.rb +161 -4
  54. data/lib/schemas/json-schema.org/draft-07/schema.rb +188 -4
  55. metadata +19 -5
  56. data/lib/jsi/metaschema.rb +0 -6
  57. data/lib/jsi/schema/validation/core.rb +0 -39
@@ -6,14 +6,12 @@ module JSI
6
6
  # any schema instance is described by a set of schemas.
7
7
  class SchemaSet < ::Set
8
8
  class << self
9
- # builds a SchemaSet from a mutable Set which is added to by the given block
9
+ # Builds a SchemaSet, yielding a yielder to be called with each schema of the SchemaSet.
10
10
  #
11
- # @yield [Set] a Set to which the block may add schemas
11
+ # @yield [Enumerator::Yielder]
12
12
  # @return [SchemaSet]
13
- def build
14
- mutable_set = Set.new
15
- yield mutable_set
16
- new(mutable_set)
13
+ def build(&block)
14
+ new(Enumerator.new(&block))
17
15
  end
18
16
 
19
17
  # ensures the given param becomes a SchemaSet. returns the param if it is already SchemaSet, otherwise
@@ -51,6 +49,10 @@ module JSI
51
49
  ].join("\n"))
52
50
  end
53
51
 
52
+ unless enum.is_a?(Enumerable)
53
+ raise(ArgumentError, "#{SchemaSet} initialized with non-Enumerable: #{enum.pretty_inspect.chomp}")
54
+ end
55
+
54
56
  super
55
57
 
56
58
  not_schemas = reject { |s| s.is_a?(Schema) }
@@ -64,36 +66,79 @@ module JSI
64
66
  freeze
65
67
  end
66
68
 
67
- # instantiates the given instance as a JSI. its schemas are inplace applicators matched from the schemas
68
- # in this SchemaSet which apply to the given instance.
69
+ # Instantiates a new JSI whose content comes from the given `instance` param.
70
+ # This SchemaSet indicates the schemas of the JSI - its schemas are inplace
71
+ # applicators of this set's schemas which apply to the given instance.
72
+ #
73
+ # @param instance [Object] the instance to be represented as a JSI
74
+ # @param uri [#to_str, Addressable::URI] The retrieval URI of the instance.
69
75
  #
70
- # @param instance [Object] the JSON Schema instance to be represented as a JSI
71
- # @param uri [nil, #to_str, Addressable::URI] for an instance document containing schemas, this is
72
- # the URI of the document, whether or not the document is itself a schema.
73
- # in the normal case where the document does not contain any schemas, `uri` has no effect.
74
- # schemas within the document use this uri as the base URI to resolve relative URIs.
75
- # the resulting JSI may be registered with a {SchemaRegistry} (see {JSI.schema_registry}).
76
- # @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are inplace
77
- # applicators matched to the instance from the schemas in this set.
76
+ # It is rare that this needs to be specified, and only useful for instances which contain schemas.
77
+ # See {Schema::MetaSchema#new_schema}'s `uri` param documentation.
78
+ # @param register [Boolean] Whether schema resources in the instantiated JSI will be registered
79
+ # in the schema registry indicated by param `schema_registry`.
80
+ # This is only useful when the JSI is a schema or contains schemas.
81
+ # The JSI's root will be registered with the `uri` param, if specified, whether or not the
82
+ # root is a schema.
83
+ # @param schema_registry [SchemaRegistry, nil] The registry to use for references to other schemas and,
84
+ # depending on `register` and `uri` params, to register this JSI and/or any contained schemas with
85
+ # declared URIs.
86
+ # @param stringify_symbol_keys [Boolean] Whether the instance content will have any Symbol keys of Hashes
87
+ # replaced with Strings (recursively through the document).
88
+ # Replacement is done on a copy; the given instance is not modified.
89
+ # @param to_immutable [#call, nil] A proc/callable which takes given instance content
90
+ # and results in an immutable (i.e. deeply frozen) object equal to that.
91
+ # If the instantiated JSI will be mutable, this is not used.
92
+ # Though not recommended, this may be nil with immutable JSIs if the instance content is otherwise
93
+ # guaranteed to be immutable, as well as any modified copies of the instance.
94
+ # @param mutable [Boolean] Whether the instantiated JSI will be mutable.
95
+ # The instance content will be transformed with `to_immutable` if the JSI will be immutable.
96
+ # @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
97
+ # inplace applicators of the schemas in this set.
78
98
  def new_jsi(instance,
79
- uri: nil
99
+ uri: nil,
100
+ register: false,
101
+ schema_registry: JSI.schema_registry,
102
+ stringify_symbol_keys: false,
103
+ to_immutable: DEFAULT_CONTENT_TO_IMMUTABLE,
104
+ mutable: true
80
105
  )
106
+ instance = Util.deep_stringify_symbol_keys(instance) if stringify_symbol_keys
107
+
108
+ instance = to_immutable.call(instance) if !mutable && to_immutable
109
+
81
110
  applied_schemas = inplace_applicator_schemas(instance)
82
111
 
112
+ if uri
113
+ unless uri.respond_to?(:to_str)
114
+ raise(TypeError, "uri must be string or Addressable::URI; got: #{uri.inspect}")
115
+ end
116
+ uri = Util.uri(uri)
117
+ unless uri.absolute? && !uri.fragment
118
+ raise(ArgumentError, "uri must be an absolute URI with no fragment; got: #{uri.inspect}")
119
+ end
120
+ end
121
+
83
122
  jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
84
123
  includes: SchemaClasses.includes_for(instance),
124
+ mutable: mutable,
85
125
  )
86
126
  jsi = jsi_class.new(instance,
127
+ jsi_indicated_schemas: self,
87
128
  jsi_schema_base_uri: uri,
129
+ jsi_schema_registry: schema_registry,
130
+ jsi_content_to_immutable: to_immutable,
88
131
  )
89
132
 
133
+ schema_registry.register(jsi) if register && schema_registry
134
+
90
135
  jsi
91
136
  end
92
137
 
93
138
  # a set of inplace applicator schemas of each schema in this set which apply to the given instance.
94
- # (see {Schema::Application::InplaceApplication#inplace_applicator_schemas})
139
+ # (see {Schema#inplace_applicator_schemas})
95
140
  #
96
- # @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
141
+ # @param instance (see Schema#inplace_applicator_schemas)
97
142
  # @return [JSI::SchemaSet]
98
143
  def inplace_applicator_schemas(instance)
99
144
  SchemaSet.new(each_inplace_applicator_schema(instance))
@@ -101,7 +146,7 @@ module JSI
101
146
 
102
147
  # yields each inplace applicator schema which applies to the given instance.
103
148
  #
104
- # @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
149
+ # @param instance (see Schema#inplace_applicator_schemas)
105
150
  # @yield [JSI::Schema]
106
151
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
107
152
  def each_inplace_applicator_schema(instance, &block)
@@ -116,9 +161,9 @@ module JSI
116
161
 
117
162
  # a set of child applicator subschemas of each schema in this set which apply to the child
118
163
  # of the given instance on the given token.
119
- # (see {Schema::Application::ChildApplication#child_applicator_schemas})
164
+ # (see {Schema#child_applicator_schemas})
120
165
  #
121
- # @param instance (see Schema::Application::ChildApplication#child_applicator_schemas)
166
+ # @param instance (see Schema#child_applicator_schemas)
122
167
  # @return [JSI::SchemaSet]
123
168
  def child_applicator_schemas(token, instance)
124
169
  SchemaSet.new(each_child_applicator_schema(token, instance))
@@ -127,7 +172,7 @@ module JSI
127
172
  # yields each child applicator schema which applies to the child of
128
173
  # the given instance on the given token.
129
174
  #
130
- # @param (see Schema::Application::ChildApplication#child_applicator_schemas)
175
+ # @param (see Schema#child_applicator_schemas)
131
176
  # @yield [JSI::Schema]
132
177
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
133
178
  def each_child_applicator_schema(token, instance, &block)
@@ -145,8 +190,9 @@ module JSI
145
190
  # @param instance [Object] the instance to validate against our schemas
146
191
  # @return [JSI::Validation::Result]
147
192
  def instance_validate(instance)
148
- results = map { |schema| schema.instance_validate(instance) }
149
- results.inject(Validation::FullResult.new, &:merge).freeze
193
+ inject(Validation::FullResult.new) do |result, schema|
194
+ result.merge(schema.instance_validate(instance))
195
+ end.freeze
150
196
  end
151
197
 
152
198
  # whether the given instance is valid against our schemas
@@ -158,21 +204,21 @@ module JSI
158
204
 
159
205
  # @return [String]
160
206
  def inspect
161
- "#{self.class}[#{map(&:inspect).join(", ")}]"
207
+ -"#{self.class}[#{map(&:inspect).join(", ")}]"
162
208
  end
163
209
 
164
- alias_method :to_s, :inspect
210
+ def to_s
211
+ inspect
212
+ end
165
213
 
166
214
  def pretty_print(q)
167
215
  q.text self.class.to_s
168
216
  q.text '['
169
- q.group_sub {
170
- q.nest(2) {
217
+ q.group(2) {
171
218
  q.breakable('')
172
219
  q.seplist(self, nil, :each) { |e|
173
220
  q.pp e
174
221
  }
175
- }
176
222
  }
177
223
  q.breakable ''
178
224
  q.text ']'
@@ -2,11 +2,6 @@
2
2
 
3
3
  module JSI
4
4
  simple_wrap_implementation = Module.new do
5
- include Schema
6
- include Schema::Application::ChildApplication
7
- include Schema::Application::InplaceApplication
8
- include Schema::Validation::Core
9
-
10
5
  def internal_child_applicate_keywords(token, instance)
11
6
  yield self
12
7
  end
@@ -19,8 +14,8 @@ module JSI
19
14
  end
20
15
  end
21
16
 
22
- simple_wrap_metaschema = MetaschemaNode.new(nil, schema_implementation_modules: [simple_wrap_implementation])
23
- SimpleWrap = simple_wrap_metaschema.new_schema_module({})
17
+ simple_wrap_metaschema = JSI.new_metaschema(nil, schema_implementation_modules: [simple_wrap_implementation])
18
+ SimpleWrap = simple_wrap_metaschema.new_schema_module(Util::EMPTY_HASH)
24
19
 
25
20
  # SimpleWrap is a JSI schema module which recursively wraps nested structures
26
21
  module SimpleWrap
@@ -16,12 +16,18 @@ module JSI
16
16
  # creates a AttrStruct subclass with the given attribute keys.
17
17
  # @param attribute_keys [Enumerable<String, Symbol>]
18
18
  def subclass(*attribute_keys)
19
- bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
20
- unless bad.empty?
21
- raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}"
19
+ bad_type = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
20
+ unless bad_type.empty?
21
+ raise(ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad_type.map(&:inspect).join(', ')}")
22
22
  end
23
+
23
24
  attribute_keys = attribute_keys.map { |key| convert_key(key) }
24
25
 
26
+ bad_name = attribute_keys.reject { |key| Util::Private.ok_ruby_method_name?(key) }
27
+ unless bad_name.empty?
28
+ raise(ArgumentError, "attribute keys must be valid ruby method names; got keys: #{bad_name.map(&:inspect).join(', ')}")
29
+ end
30
+
25
31
  all_attribute_keys = (self.attribute_keys + attribute_keys).freeze
26
32
 
27
33
  Class.new(self).tap do |klass|
@@ -67,7 +73,7 @@ module JSI
67
73
  attributes.to_hash.each do |k, v|
68
74
  @attributes[self.class.convert_key(k)] = v
69
75
  end
70
- bad = @attributes.keys.reject { |k| attribute_keys.include?(k) }
76
+ bad = @attributes.keys.reject { |k| class_attribute_keys.include?(k) }
71
77
  unless bad.empty?
72
78
  raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
73
79
  end
@@ -79,7 +85,7 @@ module JSI
79
85
 
80
86
  def []=(key, value)
81
87
  key = self.class.convert_key(key)
82
- unless attribute_keys.include?(key)
88
+ unless class_attribute_keys.include?(key)
83
89
  raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
84
90
  end
85
91
  @attributes[key] = value
@@ -87,19 +93,20 @@ module JSI
87
93
 
88
94
  # @return [String]
89
95
  def inspect
90
- "\#<#{self.class.name}#{@attributes.empty? ? '' : ' '}#{@attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
96
+ -"\#<#{self.class.name}#{@attributes.map { |k, v| " #{k}: #{v.inspect}" }.join(',')}>"
91
97
  end
92
98
 
93
- alias_method :to_s, :inspect
99
+ def to_s
100
+ inspect
101
+ end
94
102
 
95
103
  # pretty-prints a representation of self to the given printer
96
104
  # @return [void]
97
105
  def pretty_print(q)
98
106
  q.text '#<'
99
107
  q.text self.class.name
100
- q.group_sub {
101
- q.nest(2) {
102
- q.breakable(@attributes.empty? ? '' : ' ')
108
+ q.group(2) {
109
+ q.breakable(' ') if !@attributes.empty?
103
110
  q.seplist(@attributes, nil, :each_pair) { |k, v|
104
111
  q.group {
105
112
  q.text k
@@ -107,20 +114,27 @@ module JSI
107
114
  q.pp v
108
115
  }
109
116
  }
110
- }
111
117
  }
112
- q.breakable ''
118
+ q.breakable('') if !@attributes.empty?
113
119
  q.text '>'
114
120
  end
115
121
 
116
122
  # (see AttrStruct.attribute_keys)
117
- def attribute_keys
123
+ def class_attribute_keys
118
124
  self.class.attribute_keys
119
125
  end
120
126
 
127
+ def freeze
128
+ @attributes.freeze
129
+ super
130
+ end
131
+
121
132
  include FingerprintHash
133
+
134
+ # see {Util::Private::FingerprintHash}
135
+ # @api private
122
136
  def jsi_fingerprint
123
- {class: self.class, attributes: @attributes}
137
+ {class: self.class, attributes: @attributes}.freeze
124
138
  end
125
139
  end
126
140
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Util::Private
5
+ class MemoMap
6
+ def initialize(key_by: nil, &block)
7
+ @key_by = key_by
8
+ @block = block || raise(ArgumentError, "no block given")
9
+
10
+ # each result has its own mutex to update its memoized value thread-safely
11
+ @result_mutexes = {}
12
+ # another mutex to thread-safely initialize each result mutex
13
+ @result_mutexes_mutex = Mutex.new
14
+
15
+ @results = {}
16
+ end
17
+
18
+ def key_for(inputs)
19
+ if @key_by
20
+ @key_by.call(**inputs)
21
+ else
22
+ inputs
23
+ end
24
+ end
25
+ end
26
+
27
+ class MemoMap::Mutable < MemoMap
28
+ Result = AttrStruct[*%w(
29
+ value
30
+ inputs
31
+ inputs_hash
32
+ )]
33
+
34
+ class Result
35
+ end
36
+
37
+ def [](**inputs)
38
+ key = key_for(inputs)
39
+
40
+ result_mutex = @result_mutexes_mutex.synchronize do
41
+ @result_mutexes[key] ||= Mutex.new
42
+ end
43
+
44
+ result_mutex.synchronize do
45
+ inputs_hash = inputs.hash
46
+ if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
47
+ @results[key].value
48
+ else
49
+ value = @block.call(**inputs)
50
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
51
+ value
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ class MemoMap::Immutable < MemoMap
58
+ def [](**inputs)
59
+ key = key_for(inputs)
60
+
61
+ result_mutex = @result_mutexes_mutex.synchronize do
62
+ @result_mutexes[key] ||= Mutex.new
63
+ end
64
+
65
+ result_mutex.synchronize do
66
+ if @results.key?(key)
67
+ @results[key]
68
+ else
69
+ @results[key] = @block.call(**inputs)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -6,11 +6,18 @@ module JSI
6
6
  # @api private
7
7
  module Util::Private
8
8
  autoload :AttrStruct, 'jsi/util/private/attr_struct'
9
+ autoload :MemoMap, 'jsi/util/private/memo_map'
10
+
11
+ extend self
9
12
 
10
13
  EMPTY_ARY = [].freeze
11
14
 
15
+ EMPTY_HASH = {}.freeze
16
+
12
17
  EMPTY_SET = Set[].freeze
13
18
 
19
+ CLASSES_ALWAYS_FROZEN = Set[TrueClass, FalseClass, NilClass, Integer, Float, BigDecimal, Rational, Symbol].freeze
20
+
14
21
  # is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
15
22
  # a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
16
23
  #
@@ -33,18 +40,63 @@ module JSI
33
40
  end
34
41
  end
35
42
 
43
+ # we won't use #to_json on classes where it is defined by
44
+ # JSON::Ext::Generator::GeneratorMethods / JSON::Pure::Generator::GeneratorMethods
45
+ # this is a bit of a kluge and disregards any singleton class to_json, but it will do.
46
+ USE_TO_JSON_METHOD = Hash.new do |h, klass|
47
+ h[klass] = klass.method_defined?(:to_json) &&
48
+ klass.instance_method(:to_json).owner.name !~ /\AJSON:.*:GeneratorMethods\b/
49
+ end
50
+
51
+ RUBY_REJECT_NAME_CODEPOINTS = [
52
+ 0..31, # C0 control chars
53
+ %q( !"#$%&'()*+,-./:;<=>?@[\\]^`{|}~).each_codepoint, # printable special chars (note: "_" not included)
54
+ 127..159, # C1 control chars
55
+ ].inject(Set[], &:merge).freeze
56
+
57
+ RUBY_REJECT_NAME_RE = Regexp.new('[' + Regexp.escape(RUBY_REJECT_NAME_CODEPOINTS.to_a.pack('U*')) + ']+').freeze
58
+
36
59
  # is the given name ok to use as a ruby method name?
37
60
  def ok_ruby_method_name?(name)
38
61
  # must be a string
39
62
  return false unless name.respond_to?(:to_str)
40
63
  # must not begin with a digit
41
64
  return false if name =~ /\A[0-9]/
42
- # must not contain characters special to ruby syntax
43
- return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
65
+ # must not contain special or control characters
66
+ return false if name =~ RUBY_REJECT_NAME_RE
44
67
 
45
68
  return true
46
69
  end
47
70
 
71
+ def const_name_from_parts(parts, join: '')
72
+ parts = parts.map do |part|
73
+ part = part.dup
74
+ part[/\A[^a-zA-Z]*/] = ''
75
+ part[0] = part[0].upcase if part[0]
76
+ part.gsub!(RUBY_REJECT_NAME_RE, '_')
77
+ part
78
+ end
79
+ if !parts.all?(&:empty?)
80
+ parts.reject(&:empty?).join(join).freeze
81
+ else
82
+ nil
83
+ end
84
+ end
85
+
86
+ # string or URI → frozen URI
87
+ # @return [Addressable::URI]
88
+ def uri(uri)
89
+ if uri.is_a?(Addressable::URI)
90
+ if uri.frozen?
91
+ uri
92
+ else
93
+ uri.dup.freeze
94
+ end
95
+ else
96
+ Addressable::URI.parse(uri).freeze
97
+ end
98
+ end
99
+
48
100
  # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
49
101
  # to define a recursive function to return the length of an array:
50
102
  #
@@ -89,10 +141,13 @@ module JSI
89
141
  nil
90
142
  end
91
143
 
144
+ # Defines equality methods and #hash (for Hash / Set), based on a method #jsi_fingerprint
145
+ # implemented by the includer. #jsi_fingerprint is to include the class and any properties
146
+ # of the instance which constitute its identity.
92
147
  module FingerprintHash
93
148
  # overrides BasicObject#==
94
149
  def ==(other)
95
- __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == jsi_fingerprint)
150
+ __id__ == other.__id__ || (other.is_a?(FingerprintHash) && jsi_fingerprint == other.jsi_fingerprint)
96
151
  end
97
152
 
98
153
  alias_method :eql?, :==
@@ -103,102 +158,28 @@ module JSI
103
158
  end
104
159
  end
105
160
 
106
- class MemoMap
107
- Result = AttrStruct[*%w(
108
- value
109
- inputs
110
- inputs_hash
111
- )]
112
-
113
- class Result
114
- end
115
-
116
- def initialize(key_by: nil, &block)
117
- @key_by = key_by
118
- @block = block
161
+ module FingerprintHash::Immutable
162
+ include FingerprintHash
119
163
 
120
- # each result has its own mutex to update its memoized value thread-safely
121
- @result_mutexes = {}
122
- # another mutex to thread-safely initialize each result mutex
123
- @result_mutexes_mutex = Mutex.new
124
-
125
- @results = {}
126
- end
127
-
128
- def [](**inputs)
129
- if @key_by
130
- key = @key_by.call(**inputs)
131
- else
132
- key = inputs
133
- end
134
- result_mutex = @result_mutexes_mutex.synchronize do
135
- @result_mutexes[key] ||= Mutex.new
136
- end
137
-
138
- result_mutex.synchronize do
139
- inputs_hash = inputs.hash
140
- if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
141
- @results[key].value
142
- else
143
- value = @block.call(**inputs)
144
- @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
145
- value
146
- end
147
- end
148
- end
149
- end
150
-
151
- module Memoize
152
- def self.extended(object)
153
- object.send(:jsi_initialize_memos)
154
- end
155
-
156
- private
157
-
158
- def jsi_initialize_memos
159
- @jsi_memomaps_mutex = Mutex.new
160
- @jsi_memomaps = {}
161
- end
162
-
163
- # @return [Util::MemoMap]
164
- def jsi_memomap(name, **options, &block)
165
- raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
166
- unless @jsi_memomaps.key?(name)
167
- @jsi_memomaps_mutex.synchronize do
168
- # note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
169
- # but that check is not thread safe. this check is.
170
- @jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
171
- end
172
- end
173
- @jsi_memomaps[name]
174
- end
175
-
176
- def jsi_memoize(name, **inputs, &block)
177
- jsi_memomap(name, &block)[**inputs]
164
+ def ==(other)
165
+ return true if __id__ == other.__id__
166
+ return false unless other.is_a?(FingerprintHash)
167
+ # FingerprintHash::Immutable#hash being memoized, comparing that is basically free.
168
+ # not done with FingerprintHash, its #hash can be expensive.
169
+ return false if other.is_a?(FingerprintHash::Immutable) && hash != other.hash
170
+ jsi_fingerprint == other.jsi_fingerprint
178
171
  end
179
- end
180
172
 
181
- module Virtual
182
- class InstantiationError < StandardError
183
- end
173
+ alias_method :eql?, :==
184
174
 
185
- # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
186
- def initialize
187
- # :nocov:
188
- raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
189
- # :nocov:
175
+ def hash
176
+ @jsi_fingerprint_hash ||= jsi_fingerprint.hash
190
177
  end
191
178
 
192
- # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
193
- def virtual_method
194
- # :nocov:
195
- raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
196
- # :nocov:
179
+ def freeze
180
+ hash
181
+ super
197
182
  end
198
183
  end
199
-
200
- public
201
-
202
- extend self
203
184
  end
204
185
  end