lib/jsi/base/node.rb in jsi-0.7.0 vs lib/jsi/base/node.rb in jsi-0.8.0

- old
+ new

@@ -1,49 +1,75 @@ # frozen_string_literal: true module JSI - module Base::Enumerable - include ::Enumerable - - # an Array containing each item in this JSI. + # 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 + # instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas. # - # @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 + # @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 - ary + jsi_node_content_hash_pubsend(:each_key) do |key| + yield property_schemas.new_jsi(key) + end + + nil end - alias_method :entries, :to_a + # See {Base#jsi_hash?}. Always true for HashNode. + def jsi_hash? + true + end - # a jsonifiable representation of the node content - # @return [Object] - def as_json(*opt) - # include Enumerable (above) means, if ActiveSupport is loaded, its undesirable #as_json is included - # https://github.com/rails/rails/blob/v7.0.0/activesupport/lib/active_support/core_ext/object/json.rb#L139-L143 - # although Base#as_json does clobber activesupport's, I want as_json defined correctly on the module too. - Typelike.as_json(jsi_node_content, *opt) + # 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 - 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 + # See {Base#jsi_child_token_in_range?} + def jsi_child_token_in_range?(token) + jsi_node_content_hash_pubsend(:key?, token) + end - # yields each hash key and value of this node. + # 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 (JSON object property name) and value of this node. # # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}. # - # returns an Enumerator if no block is given. - # # @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 @@ -57,13 +83,28 @@ # 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) - {}.tap { |h| jsi_node_content_hash_pubsend(:each_key) { |k| h[k] = self[k, **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_node(k).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] @@ -108,18 +149,103 @@ 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. # - # returns an Enumerator if no block is given. - # # @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 @@ -133,10 +259,15 @@ # @return [Array] def to_ary(**kw) to_a(**kw) end + # See {Base#as_json} + def as_json(options = {}) + each_index.map { |i| jsi_child_node(i).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] @@ -174,9 +305,43 @@ 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