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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +29 -23
  6. data/jsi.gemspec +29 -0
  7. data/lib/jsi/base/mutability.rb +44 -0
  8. data/lib/jsi/base/node.rb +348 -0
  9. data/lib/jsi/base.rb +497 -339
  10. data/lib/jsi/jsi_coder.rb +19 -17
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +61 -26
  12. data/lib/jsi/metaschema_node.rb +161 -133
  13. data/lib/jsi/ptr.rb +80 -47
  14. data/lib/jsi/schema/application/child_application/contains.rb +11 -2
  15. data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
  16. data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
  17. data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
  18. data/lib/jsi/schema/application/child_application/items.rb +3 -3
  19. data/lib/jsi/schema/application/child_application/properties.rb +3 -3
  20. data/lib/jsi/schema/application/child_application.rb +0 -27
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +2 -2
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
  28. data/lib/jsi/schema/application/inplace_application.rb +0 -32
  29. data/lib/jsi/schema/draft04.rb +0 -1
  30. data/lib/jsi/schema/draft06.rb +0 -1
  31. data/lib/jsi/schema/draft07.rb +0 -1
  32. data/lib/jsi/schema/ref.rb +46 -19
  33. data/lib/jsi/schema/schema_ancestor_node.rb +69 -66
  34. data/lib/jsi/schema/validation/array.rb +3 -3
  35. data/lib/jsi/schema/validation/const.rb +1 -1
  36. data/lib/jsi/schema/validation/contains.rb +2 -2
  37. data/lib/jsi/schema/validation/dependencies.rb +1 -1
  38. data/lib/jsi/schema/validation/draft04/minmax.rb +8 -6
  39. data/lib/jsi/schema/validation/draft04.rb +0 -2
  40. data/lib/jsi/schema/validation/draft06.rb +0 -2
  41. data/lib/jsi/schema/validation/draft07.rb +0 -2
  42. data/lib/jsi/schema/validation/enum.rb +1 -1
  43. data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
  44. data/lib/jsi/schema/validation/items.rb +7 -7
  45. data/lib/jsi/schema/validation/not.rb +1 -1
  46. data/lib/jsi/schema/validation/numeric.rb +5 -5
  47. data/lib/jsi/schema/validation/object.rb +2 -2
  48. data/lib/jsi/schema/validation/pattern.rb +2 -2
  49. data/lib/jsi/schema/validation/properties.rb +7 -7
  50. data/lib/jsi/schema/validation/property_names.rb +1 -1
  51. data/lib/jsi/schema/validation/ref.rb +2 -2
  52. data/lib/jsi/schema/validation/required.rb +1 -1
  53. data/lib/jsi/schema/validation/someof.rb +3 -3
  54. data/lib/jsi/schema/validation/string.rb +2 -2
  55. data/lib/jsi/schema/validation/type.rb +1 -1
  56. data/lib/jsi/schema/validation.rb +1 -3
  57. data/lib/jsi/schema.rb +443 -226
  58. data/lib/jsi/schema_classes.rb +241 -147
  59. data/lib/jsi/schema_registry.rb +78 -19
  60. data/lib/jsi/schema_set.rb +114 -28
  61. data/lib/jsi/simple_wrap.rb +18 -4
  62. data/lib/jsi/util/private/attr_struct.rb +141 -0
  63. data/lib/jsi/util/private/memo_map.rb +75 -0
  64. data/lib/jsi/util/private.rb +185 -0
  65. data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +79 -105
  66. data/lib/jsi/util.rb +157 -153
  67. data/lib/jsi/validation/error.rb +4 -0
  68. data/lib/jsi/validation/result.rb +18 -32
  69. data/lib/jsi/version.rb +1 -1
  70. data/lib/jsi.rb +65 -39
  71. data/lib/schemas/json-schema.org/draft-04/schema.rb +160 -3
  72. data/lib/schemas/json-schema.org/draft-06/schema.rb +162 -3
  73. data/lib/schemas/json-schema.org/draft-07/schema.rb +189 -3
  74. metadata +27 -11
  75. data/lib/jsi/metaschema.rb +0 -7
  76. data/lib/jsi/pathed_node.rb +0 -116
  77. data/lib/jsi/schema/validation/core.rb +0 -39
  78. data/lib/jsi/util/attr_struct.rb +0 -106
@@ -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 child of the resource (including the resource itself) is registered if it is a schema that
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
- register_single(resource.jsi_schema_base_uri, resource)
49
+ internal_store(resource.jsi_schema_base_uri, resource)
49
50
  end
50
51
 
51
- resource.jsi_each_child_node do |node|
52
+ resource.jsi_each_descendent_node do |node|
52
53
  if node.is_a?(JSI::Schema) && node.schema_absolute_uri
53
- register_single(node.schema_absolute_uri, node)
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 [Addressable::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 = Addressable::URI.parse(uri)
80
- ensure_uri_absolute(uri)
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 = Addressable::URI.parse(uri)
90
- ensure_uri_absolute(uri)
91
- if @autoload_uris.key?(uri) && !@resources.key?(uri)
92
- register(@autoload_uris[uri].call)
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
- registered_uris = @resources.keys
95
- if !registered_uris.include?(uri)
96
- raise(ResourceNotFound, "URI #{uri} is not registered. registered URIs:\n#{registered_uris.join("\n")}")
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.register_single(uri, resource)
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 register_single(uri, resource)
158
+ def internal_store(uri, resource)
159
+ mutating
117
160
  @resources_mutex.synchronize do
118
- ensure_uri_absolute(uri)
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
- def ensure_uri_absolute(uri)
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
@@ -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
@@ -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
- "JSI::SchemaSet initialized with non-schema objects:",
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
- # instantiates the given instance as a JSI. its schemas are inplace applicators matched from the schemas
59
- # 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.
60
75
  #
61
- # @param instance [Object] the JSON Schema instance to be represented as a JSI
62
- # @param uri [nil, #to_str, Addressable::URI] for an instance document containing schemas, this is
63
- # the URI of the document, whether or not the document is itself a schema.
64
- # in the normal case where the document does not contain any schemas, `uri` has no effect.
65
- # schemas within the document use this uri as the base URI to resolve relative URIs.
66
- # the resulting JSI may be registered with a {SchemaRegistry} (see {JSI.schema_registry}).
67
- # @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are inplace
68
- # 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.
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
- jsi = JSI::SchemaClasses.class_for_schemas(applied_schemas).new(instance,
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::Application::InplaceApplication#inplace_applicator_schemas})
139
+ # (see {Schema#inplace_applicator_schemas})
83
140
  #
84
- # @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
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::Application::InplaceApplication#inplace_applicator_schemas)
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
- results = map { |schema| schema.instance_validate(instance) }
111
- 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
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.group_sub {
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 ']'
@@ -1,12 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- SimpleWrap = JSI::JSONSchemaOrgDraft06.new_schema_module({
5
- "additionalProperties": {"$ref": "#"},
6
- "items": {"$ref": "#"}
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