jsi-dev 0.0.0.pre.kramdown
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 +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.md +613 -0
- data/README.md +303 -0
- data/docs/glossary.md +281 -0
- data/jsi.gemspec +30 -0
- data/lib/jsi/base/node.rb +373 -0
- data/lib/jsi/base.rb +738 -0
- data/lib/jsi/jsi_coder.rb +92 -0
- data/lib/jsi/metaschema.rb +6 -0
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
- data/lib/jsi/metaschema_node.rb +262 -0
- data/lib/jsi/ptr.rb +314 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +13 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +14 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +13 -0
- data/lib/jsi/schema/draft06.rb +13 -0
- data/lib/jsi/schema/draft07.rb +13 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +183 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +110 -0
- data/lib/jsi/schema/validation/draft06.rb +120 -0
- data/lib/jsi/schema/validation/draft07.rb +157 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +49 -0
- data/lib/jsi/schema.rb +792 -0
- data/lib/jsi/schema_classes.rb +357 -0
- data/lib/jsi/schema_registry.rb +190 -0
- data/lib/jsi/schema_set.rb +219 -0
- data/lib/jsi/simple_wrap.rb +26 -0
- data/lib/jsi/util/private/attr_struct.rb +130 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +202 -0
- data/lib/jsi/util/typelike.rb +225 -0
- data/lib/jsi/util.rb +227 -0
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +212 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +5 -0
- data/lib/jsi.rb +105 -0
- data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
- data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +155 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# a Set of JSI Schemas. always frozen.
|
5
|
+
#
|
6
|
+
# any schema instance is described by a set of schemas.
|
7
|
+
class SchemaSet < ::Set
|
8
|
+
class << self
|
9
|
+
# builds a SchemaSet from a mutable Set which is added to by the given block
|
10
|
+
#
|
11
|
+
# @yield [Set] a Set to which the block may add schemas
|
12
|
+
# @return [SchemaSet]
|
13
|
+
def build
|
14
|
+
mutable_set = Set.new
|
15
|
+
yield mutable_set
|
16
|
+
new(mutable_set)
|
17
|
+
end
|
18
|
+
|
19
|
+
# ensures the given param becomes a SchemaSet. returns the param if it is already SchemaSet, otherwise
|
20
|
+
# initializes a SchemaSet from it.
|
21
|
+
#
|
22
|
+
# @param schemas [SchemaSet, Enumerable] the object to ensure becomes a SchemaSet
|
23
|
+
# @return [SchemaSet] the given SchemaSet, or a SchemaSet initialized from the given Enumerable
|
24
|
+
# @raise [ArgumentError] when the schemas param is not an Enumerable
|
25
|
+
# @raise [Schema::NotASchemaError] when the schemas param contains objects which are not Schemas
|
26
|
+
def ensure_schema_set(schemas)
|
27
|
+
if schemas.is_a?(SchemaSet)
|
28
|
+
schemas
|
29
|
+
else
|
30
|
+
new(schemas)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# initializes a SchemaSet from the given enum and freezes it.
|
36
|
+
#
|
37
|
+
# if a block is given, each element of the enum is passed to it, and the result must be a Schema.
|
38
|
+
# if no block is given, the enum must contain only Schemas.
|
39
|
+
#
|
40
|
+
# @param enum [#each] the schemas to be included in the SchemaSet, or items to be passed to the block
|
41
|
+
# @yieldparam yields each element of enum for preprocessing into a Schema
|
42
|
+
# @yieldreturn [JSI::Schema]
|
43
|
+
# @raise [JSI::Schema::NotASchemaError]
|
44
|
+
def initialize(enum, &block)
|
45
|
+
if enum.is_a?(Schema)
|
46
|
+
raise(ArgumentError, [
|
47
|
+
"#{SchemaSet} initialized with a #{Schema}",
|
48
|
+
"you probably meant to pass that to #{SchemaSet}[]",
|
49
|
+
"or to wrap that schema in a Set or Array for #{SchemaSet}.new",
|
50
|
+
"given: #{enum.pretty_inspect.chomp}",
|
51
|
+
].join("\n"))
|
52
|
+
end
|
53
|
+
|
54
|
+
unless enum.is_a?(Enumerable)
|
55
|
+
raise(ArgumentError, "#{SchemaSet} initialized with non-Enumerable: #{enum.pretty_inspect.chomp}")
|
56
|
+
end
|
57
|
+
|
58
|
+
super
|
59
|
+
|
60
|
+
not_schemas = reject { |s| s.is_a?(Schema) }
|
61
|
+
if !not_schemas.empty?
|
62
|
+
raise(Schema::NotASchemaError, [
|
63
|
+
"#{SchemaSet} initialized with non-schema objects:",
|
64
|
+
*not_schemas.map { |ns| ns.pretty_inspect.chomp },
|
65
|
+
].join("\n"))
|
66
|
+
end
|
67
|
+
|
68
|
+
freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
# Instantiates a new JSI whose content comes from the given `instance` param.
|
72
|
+
# This SchemaSet indicates the schemas of the JSI - its schemas are inplace
|
73
|
+
# applicators of this set's schemas which apply to the given instance.
|
74
|
+
#
|
75
|
+
# @param instance [Object] the instance to be represented as a JSI
|
76
|
+
# @param uri [#to_str, Addressable::URI] The retrieval URI of the instance.
|
77
|
+
#
|
78
|
+
# It is rare that this needs to be specified, and only useful for instances which contain schemas.
|
79
|
+
# See {Schema::DescribesSchema#new_schema}'s `uri` param documentation.
|
80
|
+
# @param register [Boolean] Whether schema resources in the instantiated JSI will be registered
|
81
|
+
# in the schema registry indicated by param `schema_registry`.
|
82
|
+
# This is only useful when the JSI is a schema or contains schemas.
|
83
|
+
# The JSI's root will be registered with the `uri` param, if specified, whether or not the
|
84
|
+
# root is a schema.
|
85
|
+
# @param schema_registry [SchemaRegistry, nil] The registry to use for references to other schemas and,
|
86
|
+
# depending on `register` and `uri` params, to register this JSI and/or any contained schemas with
|
87
|
+
# declared URIs.
|
88
|
+
# @param stringify_symbol_keys [Boolean] Whether the instance content will have any Symbol keys of Hashes
|
89
|
+
# replaced with Strings (recursively through the document).
|
90
|
+
# Replacement is done on a copy; the given instance is not modified.
|
91
|
+
# @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
|
92
|
+
# inplace applicators of the schemas in this set.
|
93
|
+
def new_jsi(instance,
|
94
|
+
uri: nil,
|
95
|
+
register: false,
|
96
|
+
schema_registry: JSI.schema_registry,
|
97
|
+
stringify_symbol_keys: false
|
98
|
+
)
|
99
|
+
if stringify_symbol_keys
|
100
|
+
instance = Util.deep_stringify_symbol_keys(instance)
|
101
|
+
end
|
102
|
+
|
103
|
+
applied_schemas = inplace_applicator_schemas(instance)
|
104
|
+
|
105
|
+
if uri
|
106
|
+
unless uri.respond_to?(:to_str)
|
107
|
+
raise(TypeError, "uri must be string or Addressable::URI; got: #{uri.inspect}")
|
108
|
+
end
|
109
|
+
uri = Util.uri(uri)
|
110
|
+
unless uri.absolute? && !uri.fragment
|
111
|
+
raise(ArgumentError, "uri must be an absolute URI with no fragment; got: #{uri.inspect}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
|
116
|
+
includes: SchemaClasses.includes_for(instance),
|
117
|
+
)
|
118
|
+
jsi = jsi_class.new(instance,
|
119
|
+
jsi_indicated_schemas: self,
|
120
|
+
jsi_schema_base_uri: uri,
|
121
|
+
jsi_schema_registry: schema_registry,
|
122
|
+
)
|
123
|
+
|
124
|
+
if register && schema_registry
|
125
|
+
schema_registry.register(jsi)
|
126
|
+
end
|
127
|
+
|
128
|
+
jsi
|
129
|
+
end
|
130
|
+
|
131
|
+
# a set of inplace applicator schemas of each schema in this set which apply to the given instance.
|
132
|
+
# (see {Schema#inplace_applicator_schemas})
|
133
|
+
#
|
134
|
+
# @param instance (see Schema#inplace_applicator_schemas)
|
135
|
+
# @return [JSI::SchemaSet]
|
136
|
+
def inplace_applicator_schemas(instance)
|
137
|
+
SchemaSet.new(each_inplace_applicator_schema(instance))
|
138
|
+
end
|
139
|
+
|
140
|
+
# yields each inplace applicator schema which applies to the given instance.
|
141
|
+
#
|
142
|
+
# @param instance (see Schema#inplace_applicator_schemas)
|
143
|
+
# @yield [JSI::Schema]
|
144
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
145
|
+
def each_inplace_applicator_schema(instance, &block)
|
146
|
+
return to_enum(__method__, instance) unless block
|
147
|
+
|
148
|
+
each do |schema|
|
149
|
+
schema.each_inplace_applicator_schema(instance, &block)
|
150
|
+
end
|
151
|
+
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
# a set of child applicator subschemas of each schema in this set which apply to the child
|
156
|
+
# of the given instance on the given token.
|
157
|
+
# (see {Schema#child_applicator_schemas})
|
158
|
+
#
|
159
|
+
# @param instance (see Schema#child_applicator_schemas)
|
160
|
+
# @return [JSI::SchemaSet]
|
161
|
+
def child_applicator_schemas(token, instance)
|
162
|
+
SchemaSet.new(each_child_applicator_schema(token, instance))
|
163
|
+
end
|
164
|
+
|
165
|
+
# yields each child applicator schema which applies to the child of
|
166
|
+
# the given instance on the given token.
|
167
|
+
#
|
168
|
+
# @param (see Schema#child_applicator_schemas)
|
169
|
+
# @yield [JSI::Schema]
|
170
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
171
|
+
def each_child_applicator_schema(token, instance, &block)
|
172
|
+
return to_enum(__method__, token, instance) unless block
|
173
|
+
|
174
|
+
each do |schema|
|
175
|
+
schema.each_child_applicator_schema(token, instance, &block)
|
176
|
+
end
|
177
|
+
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
# validates the given instance against our schemas
|
182
|
+
#
|
183
|
+
# @param instance [Object] the instance to validate against our schemas
|
184
|
+
# @return [JSI::Validation::Result]
|
185
|
+
def instance_validate(instance)
|
186
|
+
results = map { |schema| schema.instance_validate(instance) }
|
187
|
+
results.inject(Validation::FullResult.new, &:merge).freeze
|
188
|
+
end
|
189
|
+
|
190
|
+
# whether the given instance is valid against our schemas
|
191
|
+
# @param instance [Object] the instance to validate against our schemas
|
192
|
+
# @return [Boolean]
|
193
|
+
def instance_valid?(instance)
|
194
|
+
all? { |schema| schema.instance_valid?(instance) }
|
195
|
+
end
|
196
|
+
|
197
|
+
# @return [String]
|
198
|
+
def inspect
|
199
|
+
-"#{self.class}[#{map(&:inspect).join(", ")}]"
|
200
|
+
end
|
201
|
+
|
202
|
+
alias_method :to_s, :inspect
|
203
|
+
|
204
|
+
def pretty_print(q)
|
205
|
+
q.text self.class.to_s
|
206
|
+
q.text '['
|
207
|
+
q.group_sub {
|
208
|
+
q.nest(2) {
|
209
|
+
q.breakable('')
|
210
|
+
q.seplist(self, nil, :each) { |e|
|
211
|
+
q.pp e
|
212
|
+
}
|
213
|
+
}
|
214
|
+
}
|
215
|
+
q.breakable ''
|
216
|
+
q.text ']'
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
simple_wrap_implementation = Module.new do
|
5
|
+
def internal_child_applicate_keywords(token, instance)
|
6
|
+
yield self
|
7
|
+
end
|
8
|
+
|
9
|
+
def internal_inplace_applicate_keywords(instance, visited_refs)
|
10
|
+
yield self
|
11
|
+
end
|
12
|
+
|
13
|
+
def internal_validate_keywords(result_builder)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
simple_wrap_metaschema = JSI.new_metaschema(nil, schema_implementation_modules: [simple_wrap_implementation])
|
18
|
+
SimpleWrap = simple_wrap_metaschema.new_schema_module({})
|
19
|
+
|
20
|
+
# SimpleWrap is a JSI schema module which recursively wraps nested structures
|
21
|
+
module SimpleWrap
|
22
|
+
end
|
23
|
+
|
24
|
+
SimpleWrap::Implementation = simple_wrap_implementation
|
25
|
+
SimpleWrap::METASCHEMA = simple_wrap_metaschema
|
26
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
module Util::Private
|
5
|
+
# like a Struct, but stores all the attributes in one @attributes Hash, instead of individual instance
|
6
|
+
# variables for each attribute.
|
7
|
+
# this tends to be easier to work with and more flexible. keys which are symbols are converted to strings.
|
8
|
+
class AttrStruct
|
9
|
+
class AttrStructError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
class UndefinedAttributeKey < AttrStructError
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# creates a AttrStruct subclass with the given attribute keys.
|
17
|
+
# @param attribute_keys [Enumerable<String, Symbol>]
|
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(', ')}"
|
22
|
+
end
|
23
|
+
attribute_keys = attribute_keys.map { |key| convert_key(key) }
|
24
|
+
|
25
|
+
all_attribute_keys = (self.attribute_keys + attribute_keys).freeze
|
26
|
+
|
27
|
+
Class.new(self).tap do |klass|
|
28
|
+
klass.define_singleton_method(:attribute_keys) { all_attribute_keys }
|
29
|
+
|
30
|
+
attribute_keys.each do |attribute_key|
|
31
|
+
# reader
|
32
|
+
klass.send(:define_method, attribute_key) do
|
33
|
+
@attributes[attribute_key]
|
34
|
+
end
|
35
|
+
|
36
|
+
# writer
|
37
|
+
klass.send(:define_method, "#{attribute_key}=") do |value|
|
38
|
+
@attributes[attribute_key] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :[], :subclass
|
45
|
+
|
46
|
+
# the attribute keys defined for this class
|
47
|
+
# @return [Set<String>]
|
48
|
+
def attribute_keys
|
49
|
+
# empty for AttrStruct itself; redefined on each subclass
|
50
|
+
Util::Private::EMPTY_SET
|
51
|
+
end
|
52
|
+
|
53
|
+
# returns a frozen string, given a string or symbol.
|
54
|
+
# returns anything else as-is for the caller to handle.
|
55
|
+
# @api private
|
56
|
+
def convert_key(key)
|
57
|
+
# TODO use Symbol#name when available on supported rubies
|
58
|
+
key.is_a?(Symbol) ? key.to_s.freeze : key.frozen? ? key : key.is_a?(String) ? key.dup.freeze : key
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(attributes = {})
|
63
|
+
unless attributes.respond_to?(:to_hash)
|
64
|
+
raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
|
65
|
+
end
|
66
|
+
@attributes = {}
|
67
|
+
attributes.to_hash.each do |k, v|
|
68
|
+
@attributes[self.class.convert_key(k)] = v
|
69
|
+
end
|
70
|
+
bad = @attributes.keys.reject { |k| attribute_keys.include?(k) }
|
71
|
+
unless bad.empty?
|
72
|
+
raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](key)
|
77
|
+
@attributes[key.is_a?(Symbol) ? key.to_s : key]
|
78
|
+
end
|
79
|
+
|
80
|
+
def []=(key, value)
|
81
|
+
key = self.class.convert_key(key)
|
82
|
+
unless attribute_keys.include?(key)
|
83
|
+
raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
|
84
|
+
end
|
85
|
+
@attributes[key] = value
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [String]
|
89
|
+
def inspect
|
90
|
+
-"\#<#{self.class.name}#{@attributes.map { |k, v| " #{k}: #{v.inspect}" }.join(',')}>"
|
91
|
+
end
|
92
|
+
|
93
|
+
alias_method :to_s, :inspect
|
94
|
+
|
95
|
+
# pretty-prints a representation of self to the given printer
|
96
|
+
# @return [void]
|
97
|
+
def pretty_print(q)
|
98
|
+
q.text '#<'
|
99
|
+
q.text self.class.name
|
100
|
+
q.group_sub {
|
101
|
+
q.nest(2) {
|
102
|
+
q.breakable(@attributes.empty? ? '' : ' ')
|
103
|
+
q.seplist(@attributes, nil, :each_pair) { |k, v|
|
104
|
+
q.group {
|
105
|
+
q.text k
|
106
|
+
q.text ': '
|
107
|
+
q.pp v
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
}
|
112
|
+
q.breakable ''
|
113
|
+
q.text '>'
|
114
|
+
end
|
115
|
+
|
116
|
+
# (see AttrStruct.attribute_keys)
|
117
|
+
def attribute_keys
|
118
|
+
self.class.attribute_keys
|
119
|
+
end
|
120
|
+
|
121
|
+
include FingerprintHash
|
122
|
+
|
123
|
+
# see {Util::Private::FingerprintHash}
|
124
|
+
# @api private
|
125
|
+
def jsi_fingerprint
|
126
|
+
{class: self.class, attributes: @attributes}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
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
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# JSI::Util::Private classes, modules, constants, and methods are internal, and will be added and removed without warning.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Util::Private
|
8
|
+
autoload :AttrStruct, 'jsi/util/private/attr_struct'
|
9
|
+
autoload :MemoMap, 'jsi/util/private/memo_map'
|
10
|
+
|
11
|
+
extend self
|
12
|
+
|
13
|
+
EMPTY_ARY = [].freeze
|
14
|
+
|
15
|
+
EMPTY_SET = Set[].freeze
|
16
|
+
|
17
|
+
CLASSES_ALWAYS_FROZEN = Set[TrueClass, FalseClass, NilClass, Integer, Float, BigDecimal, Rational, Symbol].freeze
|
18
|
+
|
19
|
+
# is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
|
20
|
+
# a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
|
21
|
+
#
|
22
|
+
# TODO remove eventually (keyword argument compatibility)
|
23
|
+
LAST_ARGUMENT_AS_KEYWORD_PARAMETERS = begin
|
24
|
+
if Object.const_defined?(:Warning)
|
25
|
+
warn = ::Warning.instance_method(:warn)
|
26
|
+
::Warning.send(:remove_method, :warn)
|
27
|
+
::Warning.send(:define_method, :warn) { |*, **| }
|
28
|
+
end
|
29
|
+
|
30
|
+
-> (k: ) { k }[{k: nil}]
|
31
|
+
true
|
32
|
+
rescue ArgumentError
|
33
|
+
false
|
34
|
+
ensure
|
35
|
+
if Object.const_defined?(:Warning)
|
36
|
+
::Warning.send(:remove_method, :warn)
|
37
|
+
::Warning.send(:define_method, :warn, warn)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# we won't use #to_json on classes where it is defined by
|
42
|
+
# JSON::Ext::Generator::GeneratorMethods / JSON::Pure::Generator::GeneratorMethods
|
43
|
+
# this is a bit of a kluge and disregards any singleton class to_json, but it will do.
|
44
|
+
USE_TO_JSON_METHOD = Hash.new do |h, klass|
|
45
|
+
h[klass] = klass.method_defined?(:to_json) &&
|
46
|
+
klass.instance_method(:to_json).owner.name !~ /\AJSON:.*:GeneratorMethods\b/
|
47
|
+
end
|
48
|
+
|
49
|
+
RUBY_REJECT_NAME_CODEPOINTS = [
|
50
|
+
0..31, # C0 control chars
|
51
|
+
%q( !"#$%&'()*+,-./:;<=>?@[\\]^`{|}~).each_codepoint, # printable special chars (note: "_" not included)
|
52
|
+
127..159, # C1 control chars
|
53
|
+
].inject(Set[], &:merge).freeze
|
54
|
+
|
55
|
+
RUBY_REJECT_NAME_RE = Regexp.new('[' + Regexp.escape(RUBY_REJECT_NAME_CODEPOINTS.to_a.pack('U*')) + ']+').freeze
|
56
|
+
|
57
|
+
# is the given name ok to use as a ruby method name?
|
58
|
+
def ok_ruby_method_name?(name)
|
59
|
+
# must be a string
|
60
|
+
return false unless name.respond_to?(:to_str)
|
61
|
+
# must not begin with a digit
|
62
|
+
return false if name =~ /\A[0-9]/
|
63
|
+
# must not contain special or control characters
|
64
|
+
return false if name =~ RUBY_REJECT_NAME_RE
|
65
|
+
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
def const_name_from_parts(parts, join: '')
|
70
|
+
parts = parts.map do |part|
|
71
|
+
part = part.dup
|
72
|
+
part[/\A[^a-zA-Z]*/] = ''
|
73
|
+
part[0] = part[0].upcase if part[0]
|
74
|
+
part.gsub!(RUBY_REJECT_NAME_RE, '_')
|
75
|
+
part
|
76
|
+
end
|
77
|
+
if !parts.all?(&:empty?)
|
78
|
+
parts.reject(&:empty?).join(join).freeze
|
79
|
+
else
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# string or URI → frozen URI
|
85
|
+
# @return [Addressable::URI]
|
86
|
+
def uri(uri)
|
87
|
+
if uri.is_a?(Addressable::URI)
|
88
|
+
if uri.frozen?
|
89
|
+
uri
|
90
|
+
else
|
91
|
+
uri.dup.freeze
|
92
|
+
end
|
93
|
+
else
|
94
|
+
Addressable::URI.parse(uri).freeze
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
|
99
|
+
# to define a recursive function to return the length of an array:
|
100
|
+
#
|
101
|
+
# length = ycomb do |len|
|
102
|
+
# proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# length.call([0])
|
106
|
+
# # => 1
|
107
|
+
#
|
108
|
+
# see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
|
109
|
+
# and chapter 9 of the little schemer, available as the sample chapter at
|
110
|
+
# https://felleisen.org/matthias/BTLS-index.html
|
111
|
+
def ycomb
|
112
|
+
proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
|
113
|
+
end
|
114
|
+
|
115
|
+
def require_jmespath
|
116
|
+
return if instance_variable_defined?(:@jmespath_required)
|
117
|
+
begin
|
118
|
+
require 'jmespath'
|
119
|
+
rescue ::LoadError => e
|
120
|
+
# :nocov:
|
121
|
+
msg = [
|
122
|
+
"please install and/or add to your Gemfile the `jmespath` gem to use this. jmespath is not a dependency of JSI.",
|
123
|
+
"original error message:",
|
124
|
+
e.message,
|
125
|
+
].join("\n")
|
126
|
+
raise(e.class, msg, e.backtrace)
|
127
|
+
# :nocov:
|
128
|
+
end
|
129
|
+
hashlike = JSI::SchemaSet[].new_jsi({'test' => 0})
|
130
|
+
unless JMESPath.search('test', hashlike) == 0
|
131
|
+
# :nocov:
|
132
|
+
raise(::LoadError, [
|
133
|
+
"the loaded version of jmespath cannot be used with JSI.",
|
134
|
+
"jmespath is compatible with JSI objects as of version 1.5.0",
|
135
|
+
].join("\n"))
|
136
|
+
# :nocov:
|
137
|
+
end
|
138
|
+
@jmespath_required = true
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
# Defines equality methods and #hash (for Hash / Set), based on a method #jsi_fingerprint
|
143
|
+
# implemented by the includer. #jsi_fingerprint is to include the class and any properties
|
144
|
+
# of the instance which constitute its identity.
|
145
|
+
module FingerprintHash
|
146
|
+
# overrides BasicObject#==
|
147
|
+
def ==(other)
|
148
|
+
__id__ == other.__id__ || (other.is_a?(FingerprintHash) && jsi_fingerprint == other.jsi_fingerprint)
|
149
|
+
end
|
150
|
+
|
151
|
+
alias_method :eql?, :==
|
152
|
+
|
153
|
+
# overrides Kernel#hash
|
154
|
+
def hash
|
155
|
+
jsi_fingerprint.hash
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
module FingerprintHash::Immutable
|
160
|
+
include FingerprintHash
|
161
|
+
|
162
|
+
def ==(other)
|
163
|
+
return true if __id__ == other.__id__
|
164
|
+
return false unless other.is_a?(FingerprintHash)
|
165
|
+
# FingerprintHash::Immutable#hash being memoized, comparing that is basically free.
|
166
|
+
# not done with FingerprintHash, its #hash can be expensive.
|
167
|
+
return false if other.is_a?(FingerprintHash::Immutable) && hash != other.hash
|
168
|
+
jsi_fingerprint == other.jsi_fingerprint
|
169
|
+
end
|
170
|
+
|
171
|
+
alias_method :eql?, :==
|
172
|
+
|
173
|
+
def hash
|
174
|
+
@jsi_fingerprint_hash ||= jsi_fingerprint.hash
|
175
|
+
end
|
176
|
+
|
177
|
+
def freeze
|
178
|
+
hash
|
179
|
+
super
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
module Virtual
|
184
|
+
class InstantiationError < StandardError
|
185
|
+
end
|
186
|
+
|
187
|
+
# this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
|
188
|
+
def initialize
|
189
|
+
# :nocov:
|
190
|
+
raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
|
191
|
+
# :nocov:
|
192
|
+
end
|
193
|
+
|
194
|
+
# virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
|
195
|
+
def virtual_method
|
196
|
+
# :nocov:
|
197
|
+
raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
|
198
|
+
# :nocov:
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|