• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

notEthan / jsi / 21843029527

09 Feb 2026 10:17PM UTC coverage: 98.943%. Remained the same
21843029527

push

github

notEthan
Merge branches 'dynamic_root', 'base.conf.attrs', 'metaschema_modules_property_readers', 'misc825' and 'doc', remote-tracking branch 'origin/dependabot/submodules/{resources}/test/JSON-Schema-Test-Suite-75995a1' into HEAD

51 of 51 new or added lines in 11 files covered. (100.0%)

26 existing lines in 8 files now uncovered.

7771 of 7854 relevant lines covered (98.94%)

192498.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.27
/lib/jsi/base/node.rb
1
# frozen_string_literal: true
2

3
module JSI
72✔
4
  # Included on {Base} subclasses for instances that are Hash or
5
  # [#to_hash](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html#label-Hash-Convertible+Objects).
6
  #
7
  # Dynamically defines most methods of Hash to make the JSI duck-type like a Hash.
8
  module Base::HashNode
72✔
9
    # instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas.
10
    #
11
    # @yield [JSI::Base]
12
    # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
13
    def jsi_each_propertyName
72✔
14
      return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block_given?
738✔
15

16
      property_schemas = jsi_schemas.each_yield_set do |s, y|
558✔
17
        s.dialect_invoke_each(:propertyNames, &y)
576✔
18
      end
19
      jsi_node_content_hash_pubsend(:each_key) do |key|
558✔
20
        yield property_schemas.new_jsi(key)
1,062✔
21
      end
22

23
      nil
360✔
24
    end
25

26
    # See {Base#jsi_hash?}. Always true for HashNode.
27
    def jsi_hash?
72✔
28
      true
648✔
29
    end
30

31
    # Yields each key - see {Base#jsi_each_child_token}
32
    def jsi_each_child_token(&block)
72✔
33
      return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block
67,968✔
34
      jsi_node_content_hash_pubsend(:each_key, &block)
67,968✔
35
      nil
45,312✔
36
    end
37

38
    # See {Base#jsi_child_token_present?}
39
    def jsi_child_token_present?(token)
72✔
40
      jsi_node_content_hash_pubsend(:key?, token)
568,272✔
41
    end
42

43
    # See {Base#jsi_node_content_child}
44
    def jsi_node_content_child(token)
72✔
45
      # I could check token_present? and return nil here (as ArrayNode does).
46
      # without that check, if the instance defines Hash#default or #default_proc, that result is returned.
47
      # the preferred mechanism for a JSI's default value should be its schema.
48
      # but there's no compelling reason not to support both, so I'll return what #[] returns.
49
      jsi_node_content_hash_pubsend(:[], token)
539,850✔
50
    end
51

52
    # See {Base#[]}
53
    def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
72✔
54
      raise(BlockGivenError) if block_given?
611,146✔
55
      token = token.jsi_node_content if token.is_a?(Schema::SchemaAncestorNode)
611,146✔
56
      if jsi_node_content_hash_pubsend(:key?, token)
611,146✔
57
        jsi_child(token, as_jsi: as_jsi)
606,394✔
58
      else
59
        if use_default
4,752✔
60
          jsi_default_child(token, as_jsi: as_jsi)
144✔
61
        else
62
          nil
3,072✔
63
        end
64
      end
65
    end
66

67
    # See [Hash#store](https://ruby-doc.org/current/Hash.html#method-i-store)
68
    def store(key, value)
72✔
69
      self[key] = value
14✔
70
    end
71

72
    # See {Base#jsi_as_child_default_as_jsi}. true for HashNode.
73
    def jsi_as_child_default_as_jsi
72✔
74
      true
281,418✔
75
    end
76

77
    # yields each Hash key (JSON object property name) and value of this node.
78
    #
79
    # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
80
    #
81
    # @param key_as_jsi (see #each_key)
82
    # @param kw keyword arguments are passed to {Base#[]}
83
    # @yield [Object, Object] each key and value of this hash node
84
    # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
85
    def each(key_as_jsi: false, **kw, &block)
72✔
86
      return to_enum(__method__, key_as_jsi: key_as_jsi, **kw) { jsi_node_content_hash_pubsend(:size) } unless block
76,572✔
87
      if block.arity > 1
76,536✔
88
        each_key(key_as_jsi: key_as_jsi) { |k| yield(k, self[k, **kw]) }
26,122✔
89
      else
90
        each_key(key_as_jsi: key_as_jsi) { |k| yield([k, self[k, **kw]]) }
155,246✔
91
      end
92
      self
32,094✔
93
    end
94

95
    alias_method(:each_pair, :each)
72✔
96

97
    # Yields each key (property name)
98
    # @param key_as_jsi [Boolean] Yield each key as a JSI instance, per {#jsi_each_propertyName}
99
    # @yield [String, Base]
100
    def each_key(key_as_jsi: false, &block)
72✔
101
      return to_enum(__method__, key_as_jsi: key_as_jsi) { size } unless block
77,992✔
102
      if key_as_jsi
77,956✔
UNCOV
103
        jsi_each_propertyName(&block)
×
104
      else
105
        jsi_node_content_hash_pubsend(:each_key, &block)
77,956✔
106
      end
107
      self
33,496✔
108
    end
109

110
    # a hash in which each key is a key of the instance hash and each value is the result of {Base#[]}
111
    # @param kw keyword arguments are passed to {Base#[]}
112
    # @return [Hash]
113
    def to_hash(**kw)
72✔
114
      hash = {}
1,042✔
115
      each_key { |k| hash[k] = self[k, **kw] }
3,696✔
116
      hash.freeze
1,042✔
117
    end
118

119
    # See {Base#as_json}
120
    def as_json(options = {})
72✔
121
      hash = {}
288✔
122
      each_key do |k|
288✔
123
        ks = k.is_a?(String) ? k :
558✔
124
          k.is_a?(Symbol) ? k.to_s :
50✔
125
          k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
36✔
126
          raise(TypeError, "JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
10✔
127
        hash[ks] = jsi_child_node(k).as_json(**options)
420✔
128
      end
129
      hash
270✔
130
    end
131

132
    include Util::Hashlike
72✔
133

134
    if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
72✔
135
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
136
      # @param method_name [String, Symbol]
137
      # @param a positional arguments are passed to the invocation of method_name
138
      # @param b block is passed to the invocation of method_name
139
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
140
      def jsi_node_content_hash_pubsend(method_name, *a, &b)
16✔
141
        if jsi_node_content.respond_to?(method_name)
436,902✔
142
          jsi_node_content.public_send(method_name, *a, &b)
436,450✔
143
        else
144
          jsi_node_content.to_hash.public_send(method_name, *a, &b)
452✔
145
        end
146
      end
147
    else
148
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
149
      # @param method_name [String, Symbol]
150
      # @param a positional arguments are passed to the invocation of method_name
151
      # @param kw keyword arguments are passed to the invocation of method_name
152
      # @param b block is passed to the invocation of method_name
153
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
154
      def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
56✔
155
        if jsi_node_content.respond_to?(method_name)
1,531,214✔
156
          jsi_node_content.public_send(method_name, *a, **kw, &b)
1,529,632✔
157
        else
158
          jsi_node_content.to_hash.public_send(method_name, *a, **kw, &b)
1,582✔
159
        end
160
      end
161
    end
162

163
    # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
164
    SAFE_KEY_ONLY_METHODS.reject { |m| instance_method(m).owner == self }.each do |method_name|
720✔
165
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
576✔
166
        define_method(method_name) do |*a, &b|
128✔
167
          jsi_node_content_hash_pubsend(method_name, *a, &b)
22,748✔
168
        end
169
      else
170
        define_method(method_name) do |*a, **kw, &b|
448✔
171
          jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
79,618✔
172
        end
173
      end
174
    end
175
  end
176

177
  # Included on {Base} subclasses for instances that are Array or
178
  # [#to_ary](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html#label-Array-Convertible+Objects).
179
  #
180
  # Dynamically defines most methods of Array to make the JSI duck-type like an Array.
181
  module Base::ArrayNode
72✔
182
    # See {Base#jsi_array?}. Always true for ArrayNode.
183
    def jsi_array?
72✔
184
      true
540✔
185
    end
186

187
    # Yields each index - see {Base#jsi_each_child_token}
188
    def jsi_each_child_token(&block)
72✔
189
      return to_enum(__method__) { jsi_node_content_ary_pubsend(:size) } unless block
12,006✔
190
      jsi_node_content_ary_pubsend(:each_index, &block)
12,006✔
191
      nil
8,004✔
192
    end
193

194
    # See {Base#jsi_child_token_present?}
195
    def jsi_child_token_present?(token)
72✔
196
      token.is_a?(Integer) && token >= 0 && token < jsi_node_content_ary_pubsend(:size)
448,194✔
197
    end
198

199
    # See {Base#jsi_node_content_child}
200
    def jsi_node_content_child(token)
72✔
201
      # we check token_present? here (unlike HashNode) because we do not want to pass
202
      # negative indices, Ranges, or non-Integers to Array#[]
203
      if jsi_child_token_present?(token)
223,692✔
204
        jsi_node_content_ary_pubsend(:[], token)
223,638✔
205
      else
206
        nil
36✔
207
      end
208
    end
209

210
    # See {Base#[]}
211
    def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
72✔
212
      raise(BlockGivenError) if block_given?
187,278✔
213
      token = token.jsi_node_content if token.is_a?(Schema::SchemaAncestorNode)
187,278✔
214
      size = jsi_node_content_ary_pubsend(:size)
187,278✔
215
      if token.is_a?(Integer)
187,278✔
216
        if token < 0
179,628✔
217
          if token < -size
90✔
218
            nil
36✔
219
          else
220
            jsi_child(token + size, as_jsi: as_jsi)
36✔
221
          end
222
        else
223
          if token < size
179,538✔
224
            jsi_child(token, as_jsi: as_jsi)
179,448✔
225
          else
226
            if use_default
90✔
227
              jsi_default_child(token, as_jsi: as_jsi)
54✔
228
            else
229
              nil
24✔
230
            end
231
          end
232
        end
233
      elsif token.is_a?(Range)
7,648✔
234
        type_err = proc do
7,596✔
UNCOV
235
          raise(TypeError, [
×
236
            "given range does not contain Integers",
237
            "range: #{token.inspect}",
238
          ].join("\n"))
239
        end
240

241
        start_idx = token.begin
7,596✔
242
        if start_idx.is_a?(Integer)
7,596✔
243
          start_idx += size if start_idx < 0
6,828✔
244
          return Util::EMPTY_ARY if start_idx == size
6,828✔
245
          return nil if start_idx < 0 || start_idx > size
5,976✔
246
        elsif start_idx.nil?
766✔
247
          start_idx = 0
768✔
248
        else
UNCOV
249
          type_err.call
×
250
        end
251

252
        end_idx = token.end
4,608✔
253
        if end_idx.is_a?(Integer)
4,608✔
254
          end_idx += size if end_idx < 0
4,128✔
255
          end_idx += 1 unless token.exclude_end?
4,128✔
256
          end_idx = size if end_idx > size
4,128✔
257
          return Util::EMPTY_ARY if start_idx >= end_idx
4,128✔
258
        elsif end_idx.nil?
478✔
259
          end_idx = size
480✔
260
        else
UNCOV
261
          type_err.call
×
262
        end
263

264
        (start_idx...end_idx).map { |i| jsi_child(i, as_jsi: as_jsi) }.freeze
10,212✔
265
      else
266
        raise(TypeError, [
60✔
267
          "expected `token` param to be an Integer or Range",
268
          "token: #{token.inspect}",
12✔
269
        ].join("\n"))
270
      end
271
    end
272

273
    # See {Base#jsi_as_child_default_as_jsi}. true for ArrayNode.
274
    def jsi_as_child_default_as_jsi
72✔
275
      true
76,554✔
276
    end
277

278
    # yields each array element of this node.
279
    #
280
    # each yielded element is the result of {Base#[]} for each index of the instance array.
281
    #
282
    # @param kw keyword arguments are passed to {Base#[]}
283
    # @yield [Object] each element of this array node
284
    # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
285
    def each(**kw, &block)
72✔
286
      return to_enum(__method__, **kw) { jsi_node_content_ary_pubsend(:size) } unless block
44,608✔
287
      jsi_node_content_ary_pubsend(:each_index) { |i| yield(self[i, **kw]) }
200,558✔
288
      self
44,572✔
289
    end
290

291
    # an array, the same size as the instance array, in which the element at each index is the
292
    # result of {Base#[]}.
293
    # @param kw keyword arguments are passed to {Base#[]}
294
    # @return [Array]
295
    def to_ary(**kw)
72✔
296
      to_a(**kw)
1,356✔
297
    end
298

299
    # See {Base#as_json}
300
    def as_json(options = {})
72✔
301
      each_index.map { |i| jsi_child_node(i).as_json(**options) }
378✔
302
    end
303

304
    include Util::Arraylike
72✔
305

306
    if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
72✔
307
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
308
      # @param method_name [String, Symbol]
309
      # @param a positional arguments are passed to the invocation of method_name
310
      # @param b block is passed to the invocation of method_name
311
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
312
      def jsi_node_content_ary_pubsend(method_name, *a, &b)
16✔
313
        if jsi_node_content.respond_to?(method_name)
203,520✔
314
          jsi_node_content.public_send(method_name, *a, &b)
202,988✔
315
        else
316
          jsi_node_content.to_ary.public_send(method_name, *a, &b)
532✔
317
        end
318
      end
319
    else
320
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
321
      # @param method_name [String, Symbol]
322
      # @param a positional arguments are passed to the invocation of method_name
323
      # @param kw keyword arguments are passed to the invocation of method_name
324
      # @param b block is passed to the invocation of method_name
325
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
326
      def jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
56✔
327
        if jsi_node_content.respond_to?(method_name)
720,808✔
328
          jsi_node_content.public_send(method_name, *a, **kw, &b)
718,946✔
329
        else
330
          jsi_node_content.to_ary.public_send(method_name, *a, **kw, &b)
1,862✔
331
        end
332
      end
333
    end
334

335
    # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
336
    # we override these methods from Arraylike
337
    SAFE_INDEX_ONLY_METHODS.reject { |m| instance_method(m).owner == self }.each do |method_name|
360✔
338
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
288✔
339
        define_method(method_name) do |*a, &b|
64✔
340
          jsi_node_content_ary_pubsend(method_name, *a, &b)
1,912✔
341
        end
342
      else
343
        define_method(method_name) do |*a, **kw, &b|
224✔
344
          jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
6,692✔
345
        end
346
      end
347
    end
348
  end
349

350
  # Included on {Base} subclasses for instances that are String or
351
  # [#to_str](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html#label-String-Convertible+Objects).
352
  #
353
  # Dynamically defines most methods of String to make the JSI duck-type like a String.
354
  module Base::StringNode
72✔
355
    delegate_methods = %w(% * + << =~ [] []=
72✔
356
      ascii_only? b byteindex byterindex bytes bytesize byteslice bytesplice capitalize capitalize!
357
      casecmp casecmp? center chars chomp chomp! chop chop! chr clear codepoints concat count delete delete!
358
      delete_prefix delete_prefix! delete_suffix delete_suffix! downcase downcase!
359
      each_byte each_char each_codepoint each_grapheme_cluster each_line
360
      empty? encode encode! encoding end_with? force_encoding getbyte grapheme_clusters gsub gsub! hex
361
      include? index insert intern length lines ljust lstrip lstrip! match match? next next! oct ord
362
      partition prepend replace reverse reverse! rindex rjust rpartition rstrip rstrip! scan scrub scrub!
363
      setbyte size slice slice! split squeeze squeeze! start_with? strip strip! sub sub! succ succ! sum
364
      swapcase swapcase! to_c to_f to_i to_r to_s to_str to_sym tr tr! tr_s tr_s!
365
      unicode_normalize unicode_normalize! unicode_normalized? unpack unpack1 upcase upcase! upto valid_encoding?
366
    )
367
    delegate_methods.each do |method_name|
72✔
368
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
8,568✔
369
        define_method(method_name) do |*a, &b|
1,904✔
370
          if jsi_node_content.respond_to?(method_name)
4✔
371
            jsi_node_content.public_send(method_name, *a, &b)
4✔
372
          else
UNCOV
373
            jsi_node_content.to_str.public_send(method_name, *a, &b)
×
374
          end
375
        end
376
      else
377
        define_method(method_name) do |*a, **kw, &b|
6,664✔
378
          if jsi_node_content.respond_to?(method_name)
22✔
379
            jsi_node_content.public_send(method_name, *a, **kw, &b)
22✔
380
          else
381
            jsi_node_content.to_str.public_send(method_name, *a, **kw, &b)
×
382
          end
383
        end
384
      end
385
    end
386
  end
387
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc