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

notEthan / jsi / 13823364261

12 Mar 2025 11:14PM UTC coverage: 98.791% (+0.01%) from 98.777%
13823364261

push

github

notEthan
Merge branches 'child_node_as_jsi', 'each_immediate_inplace_applicator_schema', 'base.root_conf', 'bootstrap_schema_memoize', 'validation_result_instance_memoize', 'schema_set_by_identity', 'struct', 'registry', 'application_tests' and 'misc818' into HEAD

433 of 449 new or added lines in 35 files covered. (96.44%)

29 existing lines in 7 files now uncovered.

7270 of 7359 relevant lines covered (98.79%)

163698.0 hits per line

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

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

3
module JSI
56✔
4
  # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
5
  # is a Hash (or responds to `#to_hash`)
6
  module Base::HashNode
56✔
7
    # instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas.
8
    #
9
    # @yield [JSI::Base]
10
    # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
11
    def jsi_each_propertyName
56✔
12
      return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block_given?
574✔
13

14
      property_schemas = jsi_schemas.each_yield_set do |s, y|
434✔
15
        s.dialect_invoke_each(:propertyNames, &y)
448✔
16
      end
17
      jsi_node_content_hash_pubsend(:each_key) do |key|
434✔
18
        yield property_schemas.new_jsi(key)
826✔
19
      end
20

21
      nil
240✔
22
    end
23

24
    # See {Base#jsi_hash?}. Always true for HashNode.
25
    def jsi_hash?
56✔
26
      true
448✔
27
    end
28

29
    # Yields each key - see {Base#jsi_each_child_token}
30
    def jsi_each_child_token(&block)
56✔
31
      return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block
51,142✔
32
      jsi_node_content_hash_pubsend(:each_key, &block)
51,142✔
33
      nil
29,224✔
34
    end
35

36
    # See {Base#jsi_child_token_present?}
37
    def jsi_child_token_present?(token)
56✔
38
      jsi_node_content_hash_pubsend(:key?, token)
1,939,416✔
39
    end
40

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

50
    # See {Base#[]}
51
    def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
56✔
52
      raise(BlockGivenError) if block_given?
375,082✔
53
      token = token.jsi_node_content if token.is_a?(Schema::SchemaAncestorNode)
375,082✔
54
      if jsi_node_content_hash_pubsend(:key?, token)
375,082✔
55
        jsi_child(token, as_jsi: as_jsi)
371,652✔
56
      else
57
        if use_default
3,430✔
58
          jsi_default_child(token, as_jsi: as_jsi)
112✔
59
        else
60
          nil
1,896✔
61
        end
62
      end
63
    end
64

65
    # See {Base#jsi_as_child_default_as_jsi}. true for HashNode.
66
    def jsi_as_child_default_as_jsi
56✔
67
      true
169,426✔
68
    end
69

70
    # yields each Hash key (JSON object property name) and value of this node.
71
    #
72
    # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
73
    #
74
    # @param key_as_jsi (see #each_key)
75
    # @param kw keyword arguments are passed to {Base#[]}
76
    # @yield [Object, Object] each key and value of this hash node
77
    # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
78
    def each(key_as_jsi: false, **kw, &block)
56✔
79
      return to_enum(__method__, key_as_jsi: key_as_jsi, **kw) { jsi_node_content_hash_pubsend(:size) } unless block
54,810✔
80
      if block.arity > 1
54,782✔
81
        each_key(key_as_jsi: key_as_jsi) { |k| yield(k, self[k, **kw]) }
21,470✔
82
      else
83
        each_key(key_as_jsi: key_as_jsi) { |k| yield([k, self[k, **kw]]) }
110,130✔
84
      end
85
      self
22,610✔
86
    end
87

88
    alias_method(:each_pair, :each)
56✔
89

90
    # Yields each key (property name)
91
    # @param key_as_jsi [Boolean] Yield each key as a JSI instance, per {#jsi_each_propertyName}
92
    # @yield [String, Base]
93
    def each_key(key_as_jsi: false, &block)
56✔
94
      return to_enum(__method__, key_as_jsi: key_as_jsi) { size } unless block
55,858✔
95
      if key_as_jsi
55,830✔
UNCOV
96
        jsi_each_propertyName(&block)
×
97
      else
98
        jsi_node_content_hash_pubsend(:each_key, &block)
55,830✔
99
      end
100
      self
23,644✔
101
    end
102

103
    # a hash in which each key is a key of the instance hash and each value is the result of {Base#[]}
104
    # @param kw keyword arguments are passed to {Base#[]}
105
    # @return [Hash]
106
    def to_hash(**kw)
56✔
107
      hash = {}
754✔
108
      each_key { |k| hash[k] = self[k, **kw] }
2,528✔
109
      hash.freeze
754✔
110
    end
111

112
    # See {Base#as_json}
113
    def as_json(options = {})
56✔
114
      hash = {}
224✔
115
      each_key do |k|
224✔
116
        ks = k.is_a?(String) ? k :
434✔
117
          k.is_a?(Symbol) ? k.to_s :
38✔
118
          k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
28✔
119
          raise(TypeError, "JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
6✔
120
        hash[ks] = jsi_child_node(k).as_json(**options)
300✔
121
      end
122
      hash
210✔
123
    end
124

125
    include Util::Hashlike
56✔
126

127
    if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
56✔
128
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
129
      # @param method_name [String, Symbol]
130
      # @param a positional arguments are passed to the invocation of method_name
131
      # @param b block is passed to the invocation of method_name
132
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
133
      def jsi_node_content_hash_pubsend(method_name, *a, &b)
16✔
134
        if jsi_node_content.respond_to?(method_name)
1,259,800✔
135
          jsi_node_content.public_send(method_name, *a, &b)
1,259,264✔
136
        else
137
          jsi_node_content.to_hash.public_send(method_name, *a, &b)
536✔
138
        end
139
      end
140
    else
141
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
142
      # @param method_name [String, Symbol]
143
      # @param a positional arguments are passed to the invocation of method_name
144
      # @param kw keyword arguments are passed to the invocation of method_name
145
      # @param b block is passed to the invocation of method_name
146
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
147
      def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
40✔
148
        if jsi_node_content.respond_to?(method_name)
3,153,180✔
149
          jsi_node_content.public_send(method_name, *a, **kw, &b)
3,151,840✔
150
        else
151
          jsi_node_content.to_hash.public_send(method_name, *a, **kw, &b)
1,340✔
152
        end
153
      end
154
    end
155

156
    # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
157
    SAFE_KEY_ONLY_METHODS.reject { |m| instance_method(m).owner == self }.each do |method_name|
560✔
158
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
448✔
159
        define_method(method_name) do |*a, &b|
128✔
160
          jsi_node_content_hash_pubsend(method_name, *a, &b)
21,168✔
161
        end
162
      else
163
        define_method(method_name) do |*a, **kw, &b|
320✔
164
          jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
52,920✔
165
        end
166
      end
167
    end
168
  end
169

170
  # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
171
  # is an Array (or responds to `#to_ary`)
172
  module Base::ArrayNode
56✔
173
    # See {Base#jsi_array?}. Always true for ArrayNode.
174
    def jsi_array?
56✔
175
      true
420✔
176
    end
177

178
    # Yields each index - see {Base#jsi_each_child_token}
179
    def jsi_each_child_token(&block)
56✔
180
      return to_enum(__method__) { jsi_node_content_ary_pubsend(:size) } unless block
9,282✔
181
      jsi_node_content_ary_pubsend(:each_index, &block)
9,282✔
182
      nil
5,304✔
183
    end
184

185
    # See {Base#jsi_child_token_present?}
186
    def jsi_child_token_present?(token)
56✔
187
      token.is_a?(Integer) && token >= 0 && token < jsi_node_content_ary_pubsend(:size)
899,538✔
188
    end
189

190
    # See {Base#jsi_node_content_child}
191
    def jsi_node_content_child(token)
56✔
192
      # we check token_present? here (unlike HashNode) because we do not want to pass
193
      # negative indices, Ranges, or non-Integers to Array#[]
194
      if jsi_child_token_present?(token)
449,398✔
195
        jsi_node_content_ary_pubsend(:[], token)
449,356✔
196
      else
197
        nil
24✔
198
      end
199
    end
200

201
    # See {Base#[]}
202
    def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
56✔
203
      raise(BlockGivenError) if block_given?
139,968✔
204
      token = token.jsi_node_content if token.is_a?(Schema::SchemaAncestorNode)
139,968✔
205
      size = jsi_node_content_ary_pubsend(:size)
139,968✔
206
      if token.is_a?(Integer)
139,968✔
207
        if token < 0
134,058✔
208
          if token < -size
70✔
209
            nil
24✔
210
          else
211
            jsi_child(token + size, as_jsi: as_jsi)
28✔
212
          end
213
        else
214
          if token < size
133,988✔
215
            jsi_child(token, as_jsi: as_jsi)
133,918✔
216
          else
217
            if use_default
70✔
218
              jsi_default_child(token, as_jsi: as_jsi)
42✔
219
            else
220
              nil
16✔
221
            end
222
          end
223
        end
224
      elsif token.is_a?(Range)
5,908✔
225
        type_err = proc do
5,868✔
226
          raise(TypeError, [
×
227
            "given range does not contain Integers",
228
            "range: #{token.inspect}",
229
          ].join("\n"))
230
        end
231

232
        start_idx = token.begin
5,868✔
233
        if start_idx.is_a?(Integer)
5,868✔
234
          start_idx += size if start_idx < 0
5,292✔
235
          return Util::EMPTY_ARY if start_idx == size
5,292✔
236
          return nil if start_idx < 0 || start_idx > size
4,632✔
237
        elsif start_idx.nil?
574✔
238
          start_idx = 0
576✔
239
        else
240
          type_err.call
×
241
        end
242

243
        end_idx = token.end
3,552✔
244
        if end_idx.is_a?(Integer)
3,552✔
245
          end_idx += size if end_idx < 0
3,192✔
246
          end_idx += 1 unless token.exclude_end?
3,192✔
247
          end_idx = size if end_idx > size
3,192✔
248
          return Util::EMPTY_ARY if start_idx >= end_idx
3,192✔
249
        elsif end_idx.nil?
358✔
250
          end_idx = size
360✔
251
        else
252
          type_err.call
×
253
        end
254

255
        (start_idx...end_idx).map { |i| jsi_child(i, as_jsi: as_jsi) }.freeze
7,836✔
256
      else
257
        raise(TypeError, [
48✔
258
          "expected `token` param to be an Integer or Range",
259
          "token: #{token.inspect}",
12✔
260
        ].join("\n"))
261
      end
262
    end
263

264
    # See {Base#jsi_as_child_default_as_jsi}. true for ArrayNode.
265
    def jsi_as_child_default_as_jsi
56✔
266
      true
56,898✔
267
    end
268

269
    # yields each array element of this node.
270
    #
271
    # each yielded element is the result of {Base#[]} for each index of the instance array.
272
    #
273
    # @param kw keyword arguments are passed to {Base#[]}
274
    # @yield [Object] each element of this array node
275
    # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
276
    def each(**kw, &block)
56✔
277
      return to_enum(__method__, **kw) { jsi_node_content_ary_pubsend(:size) } unless block
34,192✔
278
      jsi_node_content_ary_pubsend(:each_index) { |i| yield(self[i, **kw]) }
151,138✔
279
      self
34,164✔
280
    end
281

282
    # an array, the same size as the instance array, in which the element at each index is the
283
    # result of {Base#[]}.
284
    # @param kw keyword arguments are passed to {Base#[]}
285
    # @return [Array]
286
    def to_ary(**kw)
56✔
287
      to_a(**kw)
984✔
288
    end
289

290
    # See {Base#as_json}
291
    def as_json(options = {})
56✔
292
      each_index.map { |i| jsi_child_node(i).as_json(**options) }
294✔
293
    end
294

295
    include Util::Arraylike
56✔
296

297
    if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
56✔
298
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
299
      # @param method_name [String, Symbol]
300
      # @param a positional arguments are passed to the invocation of method_name
301
      # @param b block is passed to the invocation of method_name
302
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
303
      def jsi_node_content_ary_pubsend(method_name, *a, &b)
16✔
304
        if jsi_node_content.respond_to?(method_name)
437,162✔
305
          jsi_node_content.public_send(method_name, *a, &b)
436,598✔
306
        else
307
          jsi_node_content.to_ary.public_send(method_name, *a, &b)
564✔
308
        end
309
      end
310
    else
311
      # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
312
      # @param method_name [String, Symbol]
313
      # @param a positional arguments are passed to the invocation of method_name
314
      # @param kw keyword arguments are passed to the invocation of method_name
315
      # @param b block is passed to the invocation of method_name
316
      # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
317
      def jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
40✔
318
        if jsi_node_content.respond_to?(method_name)
1,101,810✔
319
          jsi_node_content.public_send(method_name, *a, **kw, &b)
1,100,400✔
320
        else
321
          jsi_node_content.to_ary.public_send(method_name, *a, **kw, &b)
1,410✔
322
        end
323
      end
324
    end
325

326
    # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
327
    # we override these methods from Arraylike
328
    SAFE_INDEX_ONLY_METHODS.reject { |m| instance_method(m).owner == self }.each do |method_name|
280✔
329
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
224✔
330
        define_method(method_name) do |*a, &b|
64✔
331
          jsi_node_content_ary_pubsend(method_name, *a, &b)
1,896✔
332
        end
333
      else
334
        define_method(method_name) do |*a, **kw, &b|
160✔
335
          jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
4,740✔
336
        end
337
      end
338
    end
339
  end
340

341
  module Base::StringNode
56✔
342
    delegate_methods = %w(% * + << =~ [] []=
56✔
343
      ascii_only? b byteindex byterindex bytes bytesize byteslice bytesplice capitalize capitalize!
344
      casecmp casecmp? center chars chomp chomp! chop chop! chr clear codepoints concat count delete delete!
345
      delete_prefix delete_prefix! delete_suffix delete_suffix! downcase downcase!
346
      each_byte each_char each_codepoint each_grapheme_cluster each_line
347
      empty? encode encode! encoding end_with? force_encoding getbyte grapheme_clusters gsub gsub! hex
348
      include? index insert intern length lines ljust lstrip lstrip! match match? next next! oct ord
349
      partition prepend replace reverse reverse! rindex rjust rpartition rstrip rstrip! scan scrub scrub!
350
      setbyte size slice slice! split squeeze squeeze! start_with? strip strip! sub sub! succ succ! sum
351
      swapcase swapcase! to_c to_f to_i to_r to_s to_str to_sym tr tr! tr_s tr_s!
352
      unicode_normalize unicode_normalize! unicode_normalized? unpack unpack1 upcase upcase! upto valid_encoding?
353
    )
354
    delegate_methods.each do |method_name|
56✔
355
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
6,664✔
356
        define_method(method_name) do |*a, &b|
1,904✔
357
          if jsi_node_content.respond_to?(method_name)
4✔
358
            jsi_node_content.public_send(method_name, *a, &b)
4✔
359
          else
360
            jsi_node_content.to_str.public_send(method_name, *a, &b)
×
361
          end
362
        end
363
      else
364
        define_method(method_name) do |*a, **kw, &b|
4,760✔
365
          if jsi_node_content.respond_to?(method_name)
14✔
366
            jsi_node_content.public_send(method_name, *a, **kw, &b)
14✔
367
          else
368
            jsi_node_content.to_str.public_send(method_name, *a, **kw, &b)
×
369
          end
370
        end
371
      end
372
    end
373
  end
374
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