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.
- checksums.yaml +4 -4
- data/.yardopts +6 -1
- data/CHANGELOG.md +15 -0
- data/README.md +19 -18
- data/jsi.gemspec +2 -3
- data/lib/jsi/base/mutability.rb +44 -0
- data/lib/jsi/base/node.rb +199 -34
- data/lib/jsi/base.rb +412 -228
- data/lib/jsi/jsi_coder.rb +18 -16
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +57 -23
- data/lib/jsi/metaschema_node.rb +138 -107
- data/lib/jsi/ptr.rb +59 -37
- data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
- data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
- data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
- data/lib/jsi/schema/application/child_application.rb +0 -25
- data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/ref.rb +1 -1
- data/lib/jsi/schema/application/inplace_application/someof.rb +1 -1
- data/lib/jsi/schema/application/inplace_application.rb +0 -27
- data/lib/jsi/schema/draft04.rb +0 -1
- data/lib/jsi/schema/draft06.rb +0 -1
- data/lib/jsi/schema/draft07.rb +0 -1
- data/lib/jsi/schema/ref.rb +44 -18
- data/lib/jsi/schema/schema_ancestor_node.rb +65 -56
- data/lib/jsi/schema/validation/contains.rb +1 -1
- data/lib/jsi/schema/validation/draft04/minmax.rb +2 -0
- data/lib/jsi/schema/validation/draft04.rb +0 -2
- data/lib/jsi/schema/validation/draft06.rb +0 -2
- data/lib/jsi/schema/validation/draft07.rb +0 -2
- data/lib/jsi/schema/validation/items.rb +3 -3
- data/lib/jsi/schema/validation/pattern.rb +1 -1
- data/lib/jsi/schema/validation/properties.rb +4 -4
- data/lib/jsi/schema/validation/ref.rb +1 -1
- data/lib/jsi/schema/validation.rb +0 -2
- data/lib/jsi/schema.rb +405 -194
- data/lib/jsi/schema_classes.rb +196 -127
- data/lib/jsi/schema_registry.rb +66 -17
- data/lib/jsi/schema_set.rb +76 -30
- data/lib/jsi/simple_wrap.rb +2 -7
- data/lib/jsi/util/private/attr_struct.rb +28 -14
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +73 -92
- data/lib/jsi/util/typelike.rb +28 -28
- data/lib/jsi/util.rb +120 -36
- data/lib/jsi/validation/error.rb +4 -0
- data/lib/jsi/validation/result.rb +18 -32
- data/lib/jsi/version.rb +1 -1
- data/lib/jsi.rb +67 -25
- data/lib/schemas/json-schema.org/draft-04/schema.rb +159 -4
- data/lib/schemas/json-schema.org/draft-06/schema.rb +161 -4
- data/lib/schemas/json-schema.org/draft-07/schema.rb +188 -4
- metadata +19 -5
- data/lib/jsi/metaschema.rb +0 -6
- data/lib/jsi/schema/validation/core.rb +0 -39
data/lib/jsi/schema_set.rb
CHANGED
@@ -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
|
-
#
|
9
|
+
# Builds a SchemaSet, yielding a yielder to be called with each schema of the SchemaSet.
|
10
10
|
#
|
11
|
-
# @yield [
|
11
|
+
# @yield [Enumerator::Yielder]
|
12
12
|
# @return [SchemaSet]
|
13
|
-
def build
|
14
|
-
|
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
|
-
#
|
68
|
-
#
|
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
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# in the
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
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
|
139
|
+
# (see {Schema#inplace_applicator_schemas})
|
95
140
|
#
|
96
|
-
# @param instance (see Schema
|
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
|
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
|
164
|
+
# (see {Schema#child_applicator_schemas})
|
120
165
|
#
|
121
|
-
# @param instance (see Schema
|
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
|
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
|
-
|
149
|
-
|
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
|
-
|
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.
|
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 ']'
|
data/lib/jsi/simple_wrap.rb
CHANGED
@@ -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 =
|
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
|
-
|
20
|
-
unless
|
21
|
-
raise
|
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|
|
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
|
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.
|
96
|
+
-"\#<#{self.class.name}#{@attributes.map { |k, v| " #{k}: #{v.inspect}" }.join(',')}>"
|
91
97
|
end
|
92
98
|
|
93
|
-
|
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.
|
101
|
-
|
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
|
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
|
data/lib/jsi/util/private.rb
CHANGED
@@ -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
|
43
|
-
return false if name =~
|
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.
|
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
|
-
|
107
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
182
|
-
class InstantiationError < StandardError
|
183
|
-
end
|
173
|
+
alias_method :eql?, :==
|
184
174
|
|
185
|
-
|
186
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|