# frozen_string_literal: true module JSI module Base::Enumerable include ::Enumerable # an Array containing each item in this JSI. # # @param kw keyword arguments are passed to {Base#[]} - see its keyword params # @return [Array] def to_a(**kw) # TODO remove eventually (keyword argument compatibility) # discard when all supported ruby versions Enumerable#to_a delegate keywords to #each (3.0.1 breaks; 2.7.x warns) # https://bugs.ruby-lang.org/issues/18289 ary = [] each(**kw) do |e| ary << e end ary.freeze end alias_method :entries, :to_a end # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content}) # is a Hash (or responds to `#to_hash`) module Base::HashNode include Base::Enumerable # instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas. # # @yield [JSI::Base] # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil def jsi_each_propertyName return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block_given? property_schemas = SchemaSet.build do |schemas| jsi_schemas.each do |s| if s.keyword?('propertyNames') && s['propertyNames'].is_a?(Schema) schemas << s['propertyNames'] end end end jsi_node_content_hash_pubsend(:each_key) do |key| yield property_schemas.new_jsi(key) end nil end # See {Base#jsi_hash?}. Always true for HashNode. def jsi_hash? true end # Yields each key - see {Base#jsi_each_child_token} def jsi_each_child_token(&block) return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block jsi_node_content_hash_pubsend(:each_key, &block) nil end # See {Base#jsi_child_token_in_range?} def jsi_child_token_in_range?(token) jsi_node_content_hash_pubsend(:key?, token) end # See {Base#jsi_node_content_child} def jsi_node_content_child(token) # I could check token_in_range? and return nil here (as ArrayNode does). # without that check, if the instance defines Hash#default or #default_proc, that result is returned. # the preferred mechanism for a JSI's default value should be its schema. # but there's no compelling reason not to support both, so I'll return what #[] returns. jsi_node_content_hash_pubsend(:[], token) end # See {Base#[]} def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default) if jsi_node_content_hash_pubsend(:key?, token) jsi_child(token, as_jsi: as_jsi) else if use_default jsi_default_child(token, as_jsi: as_jsi) else nil end end end # yields each hash key and value of this node. # # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}. # # @param kw keyword arguments are passed to {Base#[]} # @yield [Object, Object] each key and value of this hash node # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self def each(**kw, &block) return to_enum(__method__, **kw) { jsi_node_content_hash_pubsend(:size) } unless block if block.arity > 1 jsi_node_content_hash_pubsend(:each_key) { |k| yield k, self[k, **kw] } else jsi_node_content_hash_pubsend(:each_key) { |k| yield [k, self[k, **kw]] } end self end # a hash in which each key is a key of the instance hash and each value is the result of {Base#[]} # @param kw keyword arguments are passed to {Base#[]} # @return [Hash] def to_hash(**kw) hash = {} jsi_node_content_hash_pubsend(:each_key) { |k| hash[k] = self[k, **kw] } hash.freeze end # See {Base#as_json} def as_json(options = {}) hash = {} each_key do |k| ks = k.is_a?(String) ? k : k.is_a?(Symbol) ? k.to_s : k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr : raise(TypeError, "JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}") hash[ks] = jsi_child(k, as_jsi: true).as_json(**options) end hash end include Util::Hashlike if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash # @param method_name [String, Symbol] # @param a positional arguments are passed to the invocation of method_name # @param b block is passed to the invocation of method_name # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash def jsi_node_content_hash_pubsend(method_name, *a, &b) if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, &b) else jsi_node_content.to_hash.public_send(method_name, *a, &b) end end else # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash # @param method_name [String, Symbol] # @param a positional arguments are passed to the invocation of method_name # @param kw keyword arguments are passed to the invocation of method_name # @param b block is passed to the invocation of method_name # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b) if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, **kw, &b) else jsi_node_content.to_hash.public_send(method_name, *a, **kw, &b) end end end # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash) SAFE_KEY_ONLY_METHODS.each do |method_name| if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS define_method(method_name) do |*a, &b| jsi_node_content_hash_pubsend(method_name, *a, &b) end else define_method(method_name) do |*a, **kw, &b| jsi_node_content_hash_pubsend(method_name, *a, **kw, &b) end end end end # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content}) # is an Array (or responds to `#to_ary`) module Base::ArrayNode include Base::Enumerable # See {Base#jsi_array?}. Always true for ArrayNode. def jsi_array? true end # Yields each index - see {Base#jsi_each_child_token} def jsi_each_child_token(&block) return to_enum(__method__) { jsi_node_content_ary_pubsend(:size) } unless block jsi_node_content_ary_pubsend(:each_index, &block) nil end # See {Base#jsi_child_token_in_range?} def jsi_child_token_in_range?(token) token.is_a?(Integer) && token >= 0 && token < jsi_node_content_ary_pubsend(:size) end # See {Base#jsi_node_content_child} def jsi_node_content_child(token) # we check token_in_range? here (unlike HashNode) because we do not want to pass # negative indices, Ranges, or non-Integers to Array#[] if jsi_child_token_in_range?(token) jsi_node_content_ary_pubsend(:[], token) else nil end end # See {Base#[]} def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default) size = jsi_node_content_ary_pubsend(:size) if token.is_a?(Integer) if token < 0 if token < -size nil else jsi_child(token + size, as_jsi: as_jsi) end else if token < size jsi_child(token, as_jsi: as_jsi) else if use_default jsi_default_child(token, as_jsi: as_jsi) else nil end end end elsif token.is_a?(Range) type_err = proc do raise(TypeError, [ "given range does not contain Integers", "range: #{token.inspect}", ].join("\n")) end start_idx = token.begin if start_idx.is_a?(Integer) start_idx += size if start_idx < 0 return Util::EMPTY_ARY if start_idx == size return nil if start_idx < 0 || start_idx > size elsif start_idx.nil? start_idx = 0 else type_err.call end end_idx = token.end if end_idx.is_a?(Integer) end_idx += size if end_idx < 0 end_idx += 1 unless token.exclude_end? end_idx = size if end_idx > size return Util::EMPTY_ARY if start_idx >= end_idx elsif end_idx.nil? end_idx = size else type_err.call end (start_idx...end_idx).map { |i| jsi_child(i, as_jsi: as_jsi) }.freeze else raise(TypeError, [ "expected `token` param to be an Integer or Range", "token: #{token.inspect}", ].join("\n")) end end # yields each array element of this node. # # each yielded element is the result of {Base#[]} for each index of the instance array. # # @param kw keyword arguments are passed to {Base#[]} # @yield [Object] each element of this array node # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self def each(**kw, &block) return to_enum(__method__, **kw) { jsi_node_content_ary_pubsend(:size) } unless block jsi_node_content_ary_pubsend(:each_index) { |i| yield(self[i, **kw]) } self end # an array, the same size as the instance array, in which the element at each index is the # result of {Base#[]}. # @param kw keyword arguments are passed to {Base#[]} # @return [Array] def to_ary(**kw) to_a(**kw) end # See {Base#as_json} def as_json(options = {}) each_index.map { |i| jsi_child(i, as_jsi: true).as_json(**options) } end include Util::Arraylike if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary # @param method_name [String, Symbol] # @param a positional arguments are passed to the invocation of method_name # @param b block is passed to the invocation of method_name # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary def jsi_node_content_ary_pubsend(method_name, *a, &b) if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, &b) else jsi_node_content.to_ary.public_send(method_name, *a, &b) end end else # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary # @param method_name [String, Symbol] # @param a positional arguments are passed to the invocation of method_name # @param kw keyword arguments are passed to the invocation of method_name # @param b block is passed to the invocation of method_name # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary def jsi_node_content_ary_pubsend(method_name, *a, **kw, &b) if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, **kw, &b) else jsi_node_content.to_ary.public_send(method_name, *a, **kw, &b) end end end # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a). # we override these methods from Arraylike SAFE_INDEX_ONLY_METHODS.each do |method_name| if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS define_method(method_name) do |*a, &b| jsi_node_content_ary_pubsend(method_name, *a, &b) end else define_method(method_name) do |*a, **kw, &b| jsi_node_content_ary_pubsend(method_name, *a, **kw, &b) end end end end module Base::StringNode delegate_methods = %w(% * + << =~ [] []= ascii_only? b byteindex byterindex bytes bytesize byteslice bytesplice capitalize capitalize! casecmp casecmp? center chars chomp chomp! chop chop! chr clear codepoints concat count delete delete! delete_prefix delete_prefix! delete_suffix delete_suffix! downcase downcase! each_byte each_char each_codepoint each_grapheme_cluster each_line empty? encode encode! encoding end_with? force_encoding getbyte grapheme_clusters gsub gsub! hex include? index insert intern length lines ljust lstrip lstrip! match match? next next! oct ord partition prepend replace reverse reverse! rindex rjust rpartition rstrip rstrip! scan scrub scrub! setbyte size slice slice! split squeeze squeeze! start_with? strip strip! sub sub! succ succ! sum swapcase swapcase! to_c to_f to_i to_r to_s to_str to_sym tr tr! tr_s tr_s! unicode_normalize unicode_normalize! unicode_normalized? unpack unpack1 upcase upcase! upto valid_encoding? ) delegate_methods.each do |method_name| if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS define_method(method_name) do |*a, &b| if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, &b) else jsi_node_content.to_str.public_send(method_name, *a, &b) end end else define_method(method_name) do |*a, **kw, &b| if jsi_node_content.respond_to?(method_name) jsi_node_content.public_send(method_name, *a, **kw, &b) else jsi_node_content.to_str.public_send(method_name, *a, **kw, &b) end end end end end end