jsi 0.6.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 +33 -0
- data/LICENSE.md +1 -1
- data/README.md +29 -23
- data/jsi.gemspec +29 -0
- data/lib/jsi/base/mutability.rb +44 -0
- data/lib/jsi/base/node.rb +348 -0
- data/lib/jsi/base.rb +497 -339
- data/lib/jsi/jsi_coder.rb +19 -17
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +61 -26
- data/lib/jsi/metaschema_node.rb +161 -133
- data/lib/jsi/ptr.rb +80 -47
- data/lib/jsi/schema/application/child_application/contains.rb +11 -2
- 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/items.rb +3 -3
- data/lib/jsi/schema/application/child_application/properties.rb +3 -3
- data/lib/jsi/schema/application/child_application.rb +0 -27
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
- 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/ifthenelse.rb +3 -3
- data/lib/jsi/schema/application/inplace_application/ref.rb +2 -2
- data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
- data/lib/jsi/schema/application/inplace_application.rb +0 -32
- 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 +46 -19
- data/lib/jsi/schema/schema_ancestor_node.rb +69 -66
- data/lib/jsi/schema/validation/array.rb +3 -3
- data/lib/jsi/schema/validation/const.rb +1 -1
- data/lib/jsi/schema/validation/contains.rb +2 -2
- data/lib/jsi/schema/validation/dependencies.rb +1 -1
- data/lib/jsi/schema/validation/draft04/minmax.rb +8 -6
- 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/enum.rb +1 -1
- data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
- data/lib/jsi/schema/validation/items.rb +7 -7
- data/lib/jsi/schema/validation/not.rb +1 -1
- data/lib/jsi/schema/validation/numeric.rb +5 -5
- data/lib/jsi/schema/validation/object.rb +2 -2
- data/lib/jsi/schema/validation/pattern.rb +2 -2
- data/lib/jsi/schema/validation/properties.rb +7 -7
- data/lib/jsi/schema/validation/property_names.rb +1 -1
- data/lib/jsi/schema/validation/ref.rb +2 -2
- data/lib/jsi/schema/validation/required.rb +1 -1
- data/lib/jsi/schema/validation/someof.rb +3 -3
- data/lib/jsi/schema/validation/string.rb +2 -2
- data/lib/jsi/schema/validation/type.rb +1 -1
- data/lib/jsi/schema/validation.rb +1 -3
- data/lib/jsi/schema.rb +443 -226
- data/lib/jsi/schema_classes.rb +241 -147
- data/lib/jsi/schema_registry.rb +78 -19
- data/lib/jsi/schema_set.rb +114 -28
- data/lib/jsi/simple_wrap.rb +18 -4
- data/lib/jsi/util/private/attr_struct.rb +141 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +185 -0
- data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +79 -105
- data/lib/jsi/util.rb +157 -153
- 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 +65 -39
- data/lib/schemas/json-schema.org/draft-04/schema.rb +160 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +162 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +189 -3
- metadata +27 -11
- data/lib/jsi/metaschema.rb +0 -7
- data/lib/jsi/pathed_node.rb +0 -116
- data/lib/jsi/schema/validation/core.rb +0 -39
- data/lib/jsi/util/attr_struct.rb +0 -106
data/lib/jsi/schema_registry.rb
CHANGED
@@ -14,6 +14,7 @@ module JSI
|
|
14
14
|
|
15
15
|
# an exception raised when a URI we are looking for has not been registered
|
16
16
|
class ResourceNotFound < StandardError
|
17
|
+
attr_accessor :uri
|
17
18
|
end
|
18
19
|
|
19
20
|
def initialize
|
@@ -24,8 +25,8 @@ module JSI
|
|
24
25
|
|
25
26
|
# registers the given resource and/or schema resources it contains in the registry.
|
26
27
|
#
|
27
|
-
# each
|
28
|
-
# has an absolute URI (generally defined by the '$id' keyword).
|
28
|
+
# each descendent node of the resource (including the resource itself) is registered if it is a schema
|
29
|
+
# that has an absolute URI (generally defined by the '$id' keyword).
|
29
30
|
#
|
30
31
|
# the given resource itself will be registered, whether or not it is a schema, if it is the root
|
31
32
|
# of its document and was instantiated with the option `uri` specified.
|
@@ -45,12 +46,12 @@ module JSI
|
|
45
46
|
# allow for registration of resources at the root of a document whether or not they are schemas.
|
46
47
|
# jsi_schema_base_uri at the root comes from the `uri` parameter to new_jsi / new_schema.
|
47
48
|
if resource.jsi_schema_base_uri && resource.jsi_ptr.root?
|
48
|
-
|
49
|
+
internal_store(resource.jsi_schema_base_uri, resource)
|
49
50
|
end
|
50
51
|
|
51
|
-
resource.
|
52
|
+
resource.jsi_each_descendent_node do |node|
|
52
53
|
if node.is_a?(JSI::Schema) && node.schema_absolute_uri
|
53
|
-
|
54
|
+
internal_store(node.schema_absolute_uri, node)
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
@@ -72,12 +73,18 @@ module JSI
|
|
72
73
|
#
|
73
74
|
# the block would normally load JSON from the filesystem or similar.
|
74
75
|
#
|
75
|
-
# @param uri [
|
76
|
+
# @param uri [#to_str]
|
76
77
|
# @yieldreturn [JSI::Base] a JSI instance containing the resource identified by the given uri
|
77
78
|
# @return [void]
|
78
79
|
def autoload_uri(uri, &block)
|
79
|
-
uri =
|
80
|
-
|
80
|
+
uri = registration_uri(uri)
|
81
|
+
mutating
|
82
|
+
unless block
|
83
|
+
raise(ArgumentError, ["#{SchemaRegistry}#autoload_uri must be invoked with a block", "URI: #{uri}"].join("\n"))
|
84
|
+
end
|
85
|
+
if @autoload_uris.key?(uri)
|
86
|
+
raise(Collision, ["already registered URI for autoload", "URI: #{uri}", "loader: #{@autoload_uris[uri]}"].join("\n"))
|
87
|
+
end
|
81
88
|
@autoload_uris[uri] = block
|
82
89
|
nil
|
83
90
|
end
|
@@ -86,22 +93,50 @@ module JSI
|
|
86
93
|
# @return [JSI::Base]
|
87
94
|
# @raise [JSI::SchemaRegistry::ResourceNotFound]
|
88
95
|
def find(uri)
|
89
|
-
uri =
|
90
|
-
|
91
|
-
|
92
|
-
register(
|
96
|
+
uri = registration_uri(uri)
|
97
|
+
if @autoload_uris.key?(uri)
|
98
|
+
autoloaded = @autoload_uris[uri].call
|
99
|
+
register(autoloaded)
|
100
|
+
@autoload_uris.delete(uri)
|
93
101
|
end
|
94
|
-
|
95
|
-
|
96
|
-
|
102
|
+
if !@resources.key?(uri)
|
103
|
+
if autoloaded
|
104
|
+
msg = [
|
105
|
+
"URI #{uri} was registered with autoload_uri but the result did not contain a resource with that URI.",
|
106
|
+
"the resource resulting from autoload_uri was:",
|
107
|
+
autoloaded.pretty_inspect.chomp,
|
108
|
+
]
|
109
|
+
else
|
110
|
+
msg = ["URI #{uri} is not registered. registered URIs:", *(@resources.keys | @autoload_uris.keys)]
|
111
|
+
end
|
112
|
+
raise(ResourceNotFound.new(msg.join("\n")).tap { |e| e.uri = uri })
|
97
113
|
end
|
98
114
|
@resources[uri]
|
99
115
|
end
|
100
116
|
|
117
|
+
def inspect
|
118
|
+
[
|
119
|
+
"#<#{self.class}",
|
120
|
+
*[['resources', @resources.keys], ['autoload', @autoload_uris.keys]].map do |label, uris|
|
121
|
+
[
|
122
|
+
" #{label} (#{uris.size})#{uris.empty? ? "" : ":"}",
|
123
|
+
*uris.map do |uri|
|
124
|
+
" #{uri}"
|
125
|
+
end,
|
126
|
+
]
|
127
|
+
end.inject([], &:+),
|
128
|
+
'>',
|
129
|
+
].join("\n").freeze
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_s
|
133
|
+
inspect
|
134
|
+
end
|
135
|
+
|
101
136
|
def dup
|
102
137
|
self.class.new.tap do |reg|
|
103
138
|
@resources.each do |uri, resource|
|
104
|
-
reg.
|
139
|
+
reg.internal_store(uri, resource)
|
105
140
|
end
|
106
141
|
@autoload_uris.each do |uri, autoload|
|
107
142
|
reg.autoload_uri(uri, &autoload)
|
@@ -109,13 +144,21 @@ module JSI
|
|
109
144
|
end
|
110
145
|
end
|
111
146
|
|
147
|
+
def freeze
|
148
|
+
@resources.freeze
|
149
|
+
@autoload_uris.freeze
|
150
|
+
@resources_mutex = nil
|
151
|
+
super
|
152
|
+
end
|
153
|
+
|
112
154
|
protected
|
113
155
|
# @param uri [Addressable::URI]
|
114
156
|
# @param resource [JSI::Base]
|
115
157
|
# @return [void]
|
116
|
-
def
|
158
|
+
def internal_store(uri, resource)
|
159
|
+
mutating
|
117
160
|
@resources_mutex.synchronize do
|
118
|
-
|
161
|
+
uri = registration_uri(uri)
|
119
162
|
if @resources.key?(uri)
|
120
163
|
if @resources[uri] != resource
|
121
164
|
raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
|
@@ -129,13 +172,29 @@ module JSI
|
|
129
172
|
|
130
173
|
private
|
131
174
|
|
132
|
-
|
175
|
+
# registration URIs are
|
176
|
+
# - absolute
|
177
|
+
# - without fragment
|
178
|
+
# - not relative
|
179
|
+
# - normalized
|
180
|
+
# - frozen
|
181
|
+
# @param uri [#to_str]
|
182
|
+
# @return [Addressable::URI]
|
183
|
+
def registration_uri(uri)
|
184
|
+
uri = Util.uri(uri)
|
133
185
|
if uri.fragment
|
134
186
|
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
|
135
187
|
end
|
136
188
|
if uri.relative?
|
137
189
|
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
|
138
190
|
end
|
191
|
+
uri.normalize.freeze
|
192
|
+
end
|
193
|
+
|
194
|
+
def mutating
|
195
|
+
if frozen?
|
196
|
+
raise(FrozenError, "cannot modify frozen #{self.class}")
|
197
|
+
end
|
139
198
|
end
|
140
199
|
end
|
141
200
|
end
|
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
|
@@ -42,12 +40,25 @@ module JSI
|
|
42
40
|
# @yieldreturn [JSI::Schema]
|
43
41
|
# @raise [JSI::Schema::NotASchemaError]
|
44
42
|
def initialize(enum, &block)
|
43
|
+
if enum.is_a?(Schema)
|
44
|
+
raise(ArgumentError, [
|
45
|
+
"#{SchemaSet} initialized with a #{Schema}",
|
46
|
+
"you probably meant to pass that to #{SchemaSet}[]",
|
47
|
+
"or to wrap that schema in a Set or Array for #{SchemaSet}.new",
|
48
|
+
"given: #{enum.pretty_inspect.chomp}",
|
49
|
+
].join("\n"))
|
50
|
+
end
|
51
|
+
|
52
|
+
unless enum.is_a?(Enumerable)
|
53
|
+
raise(ArgumentError, "#{SchemaSet} initialized with non-Enumerable: #{enum.pretty_inspect.chomp}")
|
54
|
+
end
|
55
|
+
|
45
56
|
super
|
46
57
|
|
47
58
|
not_schemas = reject { |s| s.is_a?(Schema) }
|
48
59
|
if !not_schemas.empty?
|
49
60
|
raise(Schema::NotASchemaError, [
|
50
|
-
"
|
61
|
+
"#{SchemaSet} initialized with non-schema objects:",
|
51
62
|
*not_schemas.map { |ns| ns.pretty_inspect.chomp },
|
52
63
|
].join("\n"))
|
53
64
|
end
|
@@ -55,33 +66,79 @@ module JSI
|
|
55
66
|
freeze
|
56
67
|
end
|
57
68
|
|
58
|
-
#
|
59
|
-
#
|
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.
|
60
75
|
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# in the
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
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.
|
69
98
|
def new_jsi(instance,
|
70
|
-
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
|
71
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
|
+
|
72
110
|
applied_schemas = inplace_applicator_schemas(instance)
|
73
111
|
|
74
|
-
|
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
|
+
|
122
|
+
jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
|
123
|
+
includes: SchemaClasses.includes_for(instance),
|
124
|
+
mutable: mutable,
|
125
|
+
)
|
126
|
+
jsi = jsi_class.new(instance,
|
127
|
+
jsi_indicated_schemas: self,
|
75
128
|
jsi_schema_base_uri: uri,
|
129
|
+
jsi_schema_registry: schema_registry,
|
130
|
+
jsi_content_to_immutable: to_immutable,
|
76
131
|
)
|
77
132
|
|
133
|
+
schema_registry.register(jsi) if register && schema_registry
|
134
|
+
|
78
135
|
jsi
|
79
136
|
end
|
80
137
|
|
81
138
|
# a set of inplace applicator schemas of each schema in this set which apply to the given instance.
|
82
|
-
# (see {Schema
|
139
|
+
# (see {Schema#inplace_applicator_schemas})
|
83
140
|
#
|
84
|
-
# @param instance (see Schema
|
141
|
+
# @param instance (see Schema#inplace_applicator_schemas)
|
85
142
|
# @return [JSI::SchemaSet]
|
86
143
|
def inplace_applicator_schemas(instance)
|
87
144
|
SchemaSet.new(each_inplace_applicator_schema(instance))
|
@@ -89,7 +146,7 @@ module JSI
|
|
89
146
|
|
90
147
|
# yields each inplace applicator schema which applies to the given instance.
|
91
148
|
#
|
92
|
-
# @param instance (see Schema
|
149
|
+
# @param instance (see Schema#inplace_applicator_schemas)
|
93
150
|
# @yield [JSI::Schema]
|
94
151
|
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
95
152
|
def each_inplace_applicator_schema(instance, &block)
|
@@ -102,13 +159,40 @@ module JSI
|
|
102
159
|
nil
|
103
160
|
end
|
104
161
|
|
162
|
+
# a set of child applicator subschemas of each schema in this set which apply to the child
|
163
|
+
# of the given instance on the given token.
|
164
|
+
# (see {Schema#child_applicator_schemas})
|
165
|
+
#
|
166
|
+
# @param instance (see Schema#child_applicator_schemas)
|
167
|
+
# @return [JSI::SchemaSet]
|
168
|
+
def child_applicator_schemas(token, instance)
|
169
|
+
SchemaSet.new(each_child_applicator_schema(token, instance))
|
170
|
+
end
|
171
|
+
|
172
|
+
# yields each child applicator schema which applies to the child of
|
173
|
+
# the given instance on the given token.
|
174
|
+
#
|
175
|
+
# @param (see Schema#child_applicator_schemas)
|
176
|
+
# @yield [JSI::Schema]
|
177
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
178
|
+
def each_child_applicator_schema(token, instance, &block)
|
179
|
+
return to_enum(__method__, token, instance) unless block
|
180
|
+
|
181
|
+
each do |schema|
|
182
|
+
schema.each_child_applicator_schema(token, instance, &block)
|
183
|
+
end
|
184
|
+
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
|
105
188
|
# validates the given instance against our schemas
|
106
189
|
#
|
107
190
|
# @param instance [Object] the instance to validate against our schemas
|
108
191
|
# @return [JSI::Validation::Result]
|
109
192
|
def instance_validate(instance)
|
110
|
-
|
111
|
-
|
193
|
+
inject(Validation::FullResult.new) do |result, schema|
|
194
|
+
result.merge(schema.instance_validate(instance))
|
195
|
+
end.freeze
|
112
196
|
end
|
113
197
|
|
114
198
|
# whether the given instance is valid against our schemas
|
@@ -120,19 +204,21 @@ module JSI
|
|
120
204
|
|
121
205
|
# @return [String]
|
122
206
|
def inspect
|
123
|
-
"#{self.class}[#{map(&:inspect).join(", ")}]"
|
207
|
+
-"#{self.class}[#{map(&:inspect).join(", ")}]"
|
208
|
+
end
|
209
|
+
|
210
|
+
def to_s
|
211
|
+
inspect
|
124
212
|
end
|
125
213
|
|
126
214
|
def pretty_print(q)
|
127
215
|
q.text self.class.to_s
|
128
216
|
q.text '['
|
129
|
-
q.
|
130
|
-
q.nest(2) {
|
217
|
+
q.group(2) {
|
131
218
|
q.breakable('')
|
132
219
|
q.seplist(self, nil, :each) { |e|
|
133
220
|
q.pp e
|
134
221
|
}
|
135
|
-
}
|
136
222
|
}
|
137
223
|
q.breakable ''
|
138
224
|
q.text ']'
|
data/lib/jsi/simple_wrap.rb
CHANGED
@@ -1,12 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JSI
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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(Util::EMPTY_HASH)
|
8
19
|
|
9
20
|
# SimpleWrap is a JSI schema module which recursively wraps nested structures
|
10
21
|
module SimpleWrap
|
11
22
|
end
|
23
|
+
|
24
|
+
SimpleWrap::Implementation = simple_wrap_implementation
|
25
|
+
SimpleWrap::METASCHEMA = simple_wrap_metaschema
|
12
26
|
end
|
@@ -0,0 +1,141 @@
|
|
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_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
|
+
end
|
23
|
+
|
24
|
+
attribute_keys = attribute_keys.map { |key| convert_key(key) }
|
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
|
+
|
31
|
+
all_attribute_keys = (self.attribute_keys + attribute_keys).freeze
|
32
|
+
|
33
|
+
Class.new(self).tap do |klass|
|
34
|
+
klass.define_singleton_method(:attribute_keys) { all_attribute_keys }
|
35
|
+
|
36
|
+
attribute_keys.each do |attribute_key|
|
37
|
+
# reader
|
38
|
+
klass.send(:define_method, attribute_key) do
|
39
|
+
@attributes[attribute_key]
|
40
|
+
end
|
41
|
+
|
42
|
+
# writer
|
43
|
+
klass.send(:define_method, "#{attribute_key}=") do |value|
|
44
|
+
@attributes[attribute_key] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :[], :subclass
|
51
|
+
|
52
|
+
# the attribute keys defined for this class
|
53
|
+
# @return [Set<String>]
|
54
|
+
def attribute_keys
|
55
|
+
# empty for AttrStruct itself; redefined on each subclass
|
56
|
+
Util::Private::EMPTY_SET
|
57
|
+
end
|
58
|
+
|
59
|
+
# returns a frozen string, given a string or symbol.
|
60
|
+
# returns anything else as-is for the caller to handle.
|
61
|
+
# @api private
|
62
|
+
def convert_key(key)
|
63
|
+
# TODO use Symbol#name when available on supported rubies
|
64
|
+
key.is_a?(Symbol) ? key.to_s.freeze : key.frozen? ? key : key.is_a?(String) ? key.dup.freeze : key
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize(attributes = {})
|
69
|
+
unless attributes.respond_to?(:to_hash)
|
70
|
+
raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
|
71
|
+
end
|
72
|
+
@attributes = {}
|
73
|
+
attributes.to_hash.each do |k, v|
|
74
|
+
@attributes[self.class.convert_key(k)] = v
|
75
|
+
end
|
76
|
+
bad = @attributes.keys.reject { |k| class_attribute_keys.include?(k) }
|
77
|
+
unless bad.empty?
|
78
|
+
raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def [](key)
|
83
|
+
@attributes[key.is_a?(Symbol) ? key.to_s : key]
|
84
|
+
end
|
85
|
+
|
86
|
+
def []=(key, value)
|
87
|
+
key = self.class.convert_key(key)
|
88
|
+
unless class_attribute_keys.include?(key)
|
89
|
+
raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
|
90
|
+
end
|
91
|
+
@attributes[key] = value
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [String]
|
95
|
+
def inspect
|
96
|
+
-"\#<#{self.class.name}#{@attributes.map { |k, v| " #{k}: #{v.inspect}" }.join(',')}>"
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
inspect
|
101
|
+
end
|
102
|
+
|
103
|
+
# pretty-prints a representation of self to the given printer
|
104
|
+
# @return [void]
|
105
|
+
def pretty_print(q)
|
106
|
+
q.text '#<'
|
107
|
+
q.text self.class.name
|
108
|
+
q.group(2) {
|
109
|
+
q.breakable(' ') if !@attributes.empty?
|
110
|
+
q.seplist(@attributes, nil, :each_pair) { |k, v|
|
111
|
+
q.group {
|
112
|
+
q.text k
|
113
|
+
q.text ': '
|
114
|
+
q.pp v
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
q.breakable('') if !@attributes.empty?
|
119
|
+
q.text '>'
|
120
|
+
end
|
121
|
+
|
122
|
+
# (see AttrStruct.attribute_keys)
|
123
|
+
def class_attribute_keys
|
124
|
+
self.class.attribute_keys
|
125
|
+
end
|
126
|
+
|
127
|
+
def freeze
|
128
|
+
@attributes.freeze
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
include FingerprintHash
|
133
|
+
|
134
|
+
# see {Util::Private::FingerprintHash}
|
135
|
+
# @api private
|
136
|
+
def jsi_fingerprint
|
137
|
+
{class: self.class, attributes: @attributes}.freeze
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
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
|