jsi 0.7.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +29 -20
  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 +408 -198
  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. data/readme.rb +1 -1
  56. metadata +19 -5
  57. data/lib/jsi/metaschema.rb +0 -6
  58. 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: false
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