jsi 0.4.0 → 0.7.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. data/test/util_test.rb +0 -62
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- SimpleWrap = JSI::Schema.new({
5
- "additionalProperties": {"$ref": "#"},
6
- "items": {"$ref": "#"}
7
- }).jsi_schema_module
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
+ def internal_child_applicate_keywords(token, instance)
11
+ yield self
12
+ end
13
+
14
+ def internal_inplace_applicate_keywords(instance, visited_refs)
15
+ yield self
16
+ end
17
+
18
+ def internal_validate_keywords(result_builder)
19
+ end
20
+ end
21
+
22
+ simple_wrap_metaschema = MetaschemaNode.new(nil, schema_implementation_modules: [simple_wrap_implementation])
23
+ SimpleWrap = simple_wrap_metaschema.new_schema_module({})
8
24
 
9
25
  # SimpleWrap is a JSI schema module which recursively wraps nested structures
10
26
  module SimpleWrap
11
27
  end
28
+
29
+ SimpleWrap::Implementation = simple_wrap_implementation
30
+ SimpleWrap::METASCHEMA = simple_wrap_metaschema
12
31
  end
@@ -0,0 +1,127 @@
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.empty? ? '' : ' '}#{@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
+ def jsi_fingerprint
123
+ {class: self.class, attributes: @attributes}
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,204 @@
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
+
10
+ EMPTY_ARY = [].freeze
11
+
12
+ EMPTY_SET = Set[].freeze
13
+
14
+ # is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
15
+ # a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
16
+ #
17
+ # TODO remove eventually (keyword argument compatibility)
18
+ LAST_ARGUMENT_AS_KEYWORD_PARAMETERS = begin
19
+ if Object.const_defined?(:Warning)
20
+ warn = ::Warning.instance_method(:warn)
21
+ ::Warning.send(:remove_method, :warn)
22
+ ::Warning.send(:define_method, :warn) { |*, **| }
23
+ end
24
+
25
+ -> (k: ) { k }[{k: nil}]
26
+ true
27
+ rescue ArgumentError
28
+ false
29
+ ensure
30
+ if Object.const_defined?(:Warning)
31
+ ::Warning.send(:remove_method, :warn)
32
+ ::Warning.send(:define_method, :warn, warn)
33
+ end
34
+ end
35
+
36
+ # is the given name ok to use as a ruby method name?
37
+ def ok_ruby_method_name?(name)
38
+ # must be a string
39
+ return false unless name.respond_to?(:to_str)
40
+ # must not begin with a digit
41
+ return false if name =~ /\A[0-9]/
42
+ # must not contain characters special to ruby syntax
43
+ return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
44
+
45
+ return true
46
+ end
47
+
48
+ # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
49
+ # to define a recursive function to return the length of an array:
50
+ #
51
+ # length = ycomb do |len|
52
+ # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
53
+ # end
54
+ #
55
+ # length.call([0])
56
+ # # => 1
57
+ #
58
+ # see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
59
+ # and chapter 9 of the little schemer, available as the sample chapter at
60
+ # https://felleisen.org/matthias/BTLS-index.html
61
+ def ycomb
62
+ proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
63
+ end
64
+
65
+ def require_jmespath
66
+ return if instance_variable_defined?(:@jmespath_required)
67
+ begin
68
+ require 'jmespath'
69
+ rescue ::LoadError => e
70
+ # :nocov:
71
+ msg = [
72
+ "please install and/or add to your Gemfile the `jmespath` gem to use this. jmespath is not a dependency of JSI.",
73
+ "original error message:",
74
+ e.message,
75
+ ].join("\n")
76
+ raise(e.class, msg, e.backtrace)
77
+ # :nocov:
78
+ end
79
+ hashlike = JSI::SchemaSet[].new_jsi({'test' => 0})
80
+ unless JMESPath.search('test', hashlike) == 0
81
+ # :nocov:
82
+ raise(::LoadError, [
83
+ "the loaded version of jmespath cannot be used with JSI.",
84
+ "jmespath is compatible with JSI objects as of version 1.5.0",
85
+ ].join("\n"))
86
+ # :nocov:
87
+ end
88
+ @jmespath_required = true
89
+ nil
90
+ end
91
+
92
+ module FingerprintHash
93
+ # overrides BasicObject#==
94
+ def ==(other)
95
+ __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == jsi_fingerprint)
96
+ end
97
+
98
+ alias_method :eql?, :==
99
+
100
+ # overrides Kernel#hash
101
+ def hash
102
+ jsi_fingerprint.hash
103
+ end
104
+ end
105
+
106
+ class MemoMap
107
+ Result = AttrStruct[*%w(
108
+ value
109
+ inputs
110
+ inputs_hash
111
+ )]
112
+
113
+ class Result
114
+ end
115
+
116
+ def initialize(key_by: nil, &block)
117
+ @key_by = key_by
118
+ @block = block
119
+
120
+ # each result has its own mutex to update its memoized value thread-safely
121
+ @result_mutexes = {}
122
+ # another mutex to thread-safely initialize each result mutex
123
+ @result_mutexes_mutex = Mutex.new
124
+
125
+ @results = {}
126
+ end
127
+
128
+ def [](**inputs)
129
+ if @key_by
130
+ key = @key_by.call(**inputs)
131
+ else
132
+ key = inputs
133
+ end
134
+ result_mutex = @result_mutexes_mutex.synchronize do
135
+ @result_mutexes[key] ||= Mutex.new
136
+ end
137
+
138
+ result_mutex.synchronize do
139
+ inputs_hash = inputs.hash
140
+ if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
141
+ @results[key].value
142
+ else
143
+ value = @block.call(**inputs)
144
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
145
+ value
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ module Memoize
152
+ def self.extended(object)
153
+ object.send(:jsi_initialize_memos)
154
+ end
155
+
156
+ private
157
+
158
+ def jsi_initialize_memos
159
+ @jsi_memomaps_mutex = Mutex.new
160
+ @jsi_memomaps = {}
161
+ end
162
+
163
+ # @return [Util::MemoMap]
164
+ def jsi_memomap(name, **options, &block)
165
+ raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
166
+ unless @jsi_memomaps.key?(name)
167
+ @jsi_memomaps_mutex.synchronize do
168
+ # note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
169
+ # but that check is not thread safe. this check is.
170
+ @jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
171
+ end
172
+ end
173
+ @jsi_memomaps[name]
174
+ end
175
+
176
+ def jsi_memoize(name, **inputs, &block)
177
+ jsi_memomap(name, &block)[**inputs]
178
+ end
179
+ end
180
+
181
+ module Virtual
182
+ class InstantiationError < StandardError
183
+ end
184
+
185
+ # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
186
+ def initialize
187
+ # :nocov:
188
+ raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
189
+ # :nocov:
190
+ end
191
+
192
+ # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
193
+ def virtual_method
194
+ # :nocov:
195
+ raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
196
+ # :nocov:
197
+ end
198
+ end
199
+
200
+ public
201
+
202
+ extend self
203
+ end
204
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # @deprecated after v0.6
5
+ module Typelike
6
+ extend Util
7
+ end
8
+
9
+ # a module of methods for objects which behave like Hash but are not Hash.
10
+ #
11
+ # this module is intended to be internal to JSI. no guarantees or API promises
12
+ # are made for non-JSI classes including this module.
13
+ module Util::Hashlike
14
+ # safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
15
+ # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
16
+
17
+ # methods which do not need to access the value.
18
+ SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size)
19
+ SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at)
20
+ DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift)
21
+ # these return a modified copy
22
+ safe_modified_copy_methods = %w(compact)
23
+ # select and reject will return a modified copy but need the yielded block variable value from #[]
24
+ safe_kv_block_modified_copy_methods = %w(select reject)
25
+ SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
26
+ custom_methods = %w(merge) # defined below
27
+ safe_to_hash_methods = SAFE_METHODS -
28
+ safe_modified_copy_methods -
29
+ safe_kv_block_modified_copy_methods -
30
+ custom_methods
31
+ safe_to_hash_methods.each do |method_name|
32
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
33
+ define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
34
+ else
35
+ define_method(method_name) { |*a, **kw, &b| to_hash.public_send(method_name, *a, **kw, &b) }
36
+ end
37
+ end
38
+ safe_modified_copy_methods.each do |method_name|
39
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
40
+ define_method(method_name) do |*a, &b|
41
+ jsi_modified_copy do |object_to_modify|
42
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
43
+ responsive_object.public_send(method_name, *a, &b)
44
+ end
45
+ end
46
+ else
47
+ define_method(method_name) do |*a, **kw, &b|
48
+ jsi_modified_copy do |object_to_modify|
49
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
50
+ responsive_object.public_send(method_name, *a, **kw, &b)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ safe_kv_block_modified_copy_methods.each do |method_name|
56
+ define_method(method_name) do |**kw, &b|
57
+ jsi_modified_copy do |object_to_modify|
58
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
59
+ responsive_object.public_send(method_name) do |k, _v|
60
+ b.call(k, self[k, **kw])
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
67
+ # @param other [#to_hash] the other hash to update this hash from
68
+ # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
69
+ # is determined by calling the block with the key, its value in self and its value in other.
70
+ # @return self, updated with other
71
+ # @raise [TypeError] when `other` does not respond to #to_hash
72
+ def update(other, &block)
73
+ unless other.respond_to?(:to_hash)
74
+ raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
75
+ end
76
+ self_respondingto_key = respond_to?(:key?) ? self : to_hash
77
+ other.to_hash.each_pair do |key, value|
78
+ if block && self_respondingto_key.key?(key)
79
+ value = yield(key, self[key], value)
80
+ end
81
+ self[key] = value
82
+ end
83
+ self
84
+ end
85
+
86
+ alias_method :merge!, :update
87
+
88
+ # like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
89
+ # @param other [#to_hash] the other hash to merge into this
90
+ # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
91
+ # is determined by calling the block with the key, its value in self and its value in other.
92
+ # @return duplicate of this hash with the other hash merged in
93
+ # @raise [TypeError] when `other` does not respond to #to_hash
94
+ def merge(other, &block)
95
+ dup.update(other, &block)
96
+ end
97
+
98
+ # basically the same #inspect as Hash, but has the class name and, if responsive,
99
+ # self's #jsi_object_group_text
100
+ # @return [String]
101
+ def inspect
102
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
103
+ "\#{<#{object_group_str}>#{map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
104
+ end
105
+
106
+ alias_method :to_s, :inspect
107
+
108
+ # pretty-prints a representation of this hashlike to the given printer
109
+ # @return [void]
110
+ def pretty_print(q)
111
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
112
+ q.text "\#{<#{object_group_str}>"
113
+ q.group_sub {
114
+ q.nest(2) {
115
+ q.breakable(empty? ? '' : ' ')
116
+ q.seplist(self, nil, :each_pair) { |k, v|
117
+ q.group {
118
+ q.pp k
119
+ q.text ' => '
120
+ q.pp v
121
+ }
122
+ }
123
+ }
124
+ }
125
+ q.breakable ''
126
+ q.text '}'
127
+ end
128
+ end
129
+
130
+ # a module of methods for objects which behave like Array but are not Array.
131
+ #
132
+ # this module is intended to be internal to JSI. no guarantees or API promises
133
+ # are made for non-JSI classes including this module.
134
+ module Util::Arraylike
135
+ # safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
136
+ # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
137
+
138
+ # methods which do not need to access the element.
139
+ SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
140
+ # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
141
+ SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev at bsearch bsearch_index combination compact count cycle dig drop drop_while fetch find_index first include? index join last pack permutation product reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip)
142
+ DESTRUCTIVE_METHODS = %w(<< clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift)
143
+
144
+ # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
145
+ safe_modified_copy_methods = %w(compact)
146
+
147
+ # methods that return a modified copy and do need handling of block variables
148
+ safe_el_block_methods = %w(reject select)
149
+
150
+ SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
151
+ safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
152
+ safe_to_ary_methods.each do |method_name|
153
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
154
+ define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
155
+ else
156
+ define_method(method_name) { |*a, **kw, &b| to_ary.public_send(method_name, *a, **kw, &b) }
157
+ end
158
+ end
159
+ safe_modified_copy_methods.each do |method_name|
160
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
161
+ define_method(method_name) do |*a, &b|
162
+ jsi_modified_copy do |object_to_modify|
163
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
164
+ responsive_object.public_send(method_name, *a, &b)
165
+ end
166
+ end
167
+ else
168
+ define_method(method_name) do |*a, **kw, &b|
169
+ jsi_modified_copy do |object_to_modify|
170
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
171
+ responsive_object.public_send(method_name, *a, **kw, &b)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ safe_el_block_methods.each do |method_name|
177
+ define_method(method_name) do |**kw, &b|
178
+ jsi_modified_copy do |object_to_modify|
179
+ i = 0
180
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
181
+ responsive_object.public_send(method_name) do |_e|
182
+ b.call(self[i, **kw]).tap { i += 1 }
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
189
+ def assoc(obj)
190
+ # note: assoc implemented here (instead of delegated) due to inconsistencies in whether
191
+ # other implementations expect each element to be an Array or to respond to #to_ary
192
+ detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
193
+ end
194
+
195
+ # see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
196
+ def rassoc(obj)
197
+ # note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
198
+ # other implementations expect each element to be an Array or to respond to #to_ary
199
+ detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
200
+ end
201
+
202
+ # basically the same #inspect as Array, but has the class name and, if responsive,
203
+ # self's #jsi_object_group_text
204
+ # @return [String]
205
+ def inspect
206
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
207
+ "\#[<#{object_group_str}>#{map { |e| ' ' + e.inspect }.join(',')}]"
208
+ end
209
+
210
+ alias_method :to_s, :inspect
211
+
212
+ # pretty-prints a representation of this arraylike to the given printer
213
+ # @return [void]
214
+ def pretty_print(q)
215
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
216
+ q.text "\#[<#{object_group_str}>"
217
+ q.group_sub {
218
+ q.nest(2) {
219
+ q.breakable(empty? ? '' : ' ')
220
+ q.seplist(self, nil, :each) { |e|
221
+ q.pp e
222
+ }
223
+ }
224
+ }
225
+ q.breakable ''
226
+ q.text ']'
227
+ end
228
+ end
229
+ end