From 3b3a090bb4dad83f5ded5c27793c89fb1986e5fb Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 14:58:53 -0700 Subject: [PATCH 01/10] Disable the dependabot bundler package ecosystem for now --- .github/dependabot.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 69dccfc..d20ab56 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,9 @@ updates: directory: "/" schedule: interval: "quarterly" - - package-ecosystem: "bundler" - directory: "/" - schedule: - interval: "quarterly" + # TODO: the bundler dependency updater doesn't seem to handle the Gemfile + # using the gemspec? Figure this out and re-enable, if possible + # - package-ecosystem: "bundler" + # directory: "/" + # schedule: + # interval: "quarterly" From 66ea27658e9e20d93e5a56c149ebfcc6fe3db792 Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 15:05:14 -0700 Subject: [PATCH 02/10] Remove the old .travis.yml file --- .travis.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe7bdcb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -language: ruby -rvm: - # No longer supported (but test anyways) - - 2.0.0 - - 2.1.10 - - 2.2.10 - - jruby-19mode - - 2.3.8 - - 2.4.10 - # Current stable supported by Travis - - 2.5.8 - - 2.6.6 - - 2.7.2 - - 3.0.0 - - jruby-9.1.9.0 - # Future - - ruby-head - - jruby-head - - truffleruby-head -sudo: false -matrix: - allow_failures: - # No longer supported - - rvm: 2.0.0 - - rvm: 2.1.10 - - rvm: 2.2.10 - - rvm: 2.3.8 - - rvm: 2.4.10 - - rvm: jruby-19mode - # Future - - rvm: ruby-head - - rvm: jruby-head - - rvm: truffleruby-head From 2cd4a5c5ef65ae42c6803496dc61611fba098bbe Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 15:14:23 -0700 Subject: [PATCH 03/10] Add rubocop --- .rubocop.yml | 12 ++++++++++++ recursive-open-struct.gemspec | 1 + 2 files changed, 13 insertions(+) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..d102046 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,12 @@ +# The behavior of RuboCop can be controlled via the .rubocop.yml +# configuration file. It makes it possible to enable/disable +# certain cops (checks) and to alter their behavior if they accept +# any parameters. The file can be placed either in your home +# directory or in some project directory. +# +# RuboCop will start looking for the configuration file in the directory +# where the inspected file is and continue its way up to the root directory. +# +# See https://docs.rubocop.org/rubocop/configuration +Layout/LineLength: + Max: 120 diff --git a/recursive-open-struct.gemspec b/recursive-open-struct.gemspec index bc7928a..62b5296 100644 --- a/recursive-open-struct.gemspec +++ b/recursive-open-struct.gemspec @@ -40,6 +40,7 @@ Gem::Specification.new do |s| s.add_development_dependency('rake', [">= 0"]) s.add_development_dependency('rdoc', [">= 0"]) s.add_development_dependency('rspec', "~> 3.2") + s.add_development_dependency('rubocop', ['>= 0']) s.add_development_dependency('simplecov', [">= 0"]) s.add_dependency('ostruct') From 209b91ecd8b9edeaa13f55d21a974eb35636233a Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 15:17:48 -0700 Subject: [PATCH 04/10] Add rubocop rake task --- Rakefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 466148f..fdba3c4 100644 --- a/Rakefile +++ b/Rakefile @@ -23,6 +23,9 @@ namespace :spec do end end +require 'rubocop/rake_task' +RuboCop::RakeTask.new + require 'rdoc/task' Rake::RDocTask.new do |rdoc| version = File.exist?('VERSION') ? File.read('VERSION') : "" @@ -33,7 +36,7 @@ Rake::RDocTask.new do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -task :default => :spec +task :default => [:spec, :rubocop] task :fix_permissions do File.umask 0022 From cd5c22605033da1809e0c43fc288308b1e7d1166 Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 16:08:48 -0700 Subject: [PATCH 05/10] Apply base rubocop ssettings to the repo --- .rubocop.yml | 29 +++ Gemfile | 2 + Rakefile | 28 +-- lib/recursive-open-struct.rb | 2 + lib/recursive_open_struct.rb | 51 +++-- lib/recursive_open_struct/debug_inspect.rb | 66 +++--- lib/recursive_open_struct/deep_dup.rb | 57 ++--- lib/recursive_open_struct/dig.rb | 12 +- lib/recursive_open_struct/version.rb | 4 +- recursive-open-struct.gemspec | 49 ++--- .../debug_inspect_spec.rb | 80 +++---- .../indifferent_access_spec.rb | 35 ++- .../open_struct_behavior_spec.rb | 70 +++--- .../ostruct_2_0_0_spec.rb | 67 +++--- .../ostruct_2_3_0_spec.rb | 42 ++-- .../recursion_and_subclassing_spec.rb | 8 +- spec/recursive_open_struct/recursion_spec.rb | 199 +++++++++--------- spec/recursive_open_struct/wrapping_spec.rb | 8 +- spec/spec_helper.rb | 6 +- 19 files changed, 446 insertions(+), 369 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d102046..fc7d918 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -10,3 +10,32 @@ # See https://docs.rubocop.org/rubocop/configuration Layout/LineLength: Max: 120 + +Metrics/BlockLength: + Exclude: + - "spec/**/*.rb" + - "*.gemspec" + +Metrics/MethodLength: + Enabled: false + +Naming/FileName: + Exclude: + # This file is deliberately named with dashes, to match the package name + - 'lib/recursive-open-struct.rb' + +Style/CommentedKeyword: + Exclude: + - "spec/**/*.rb" + +Style/Documentation: + Enabled: false + +# Can't really disable this in the gemspec file (it is reported on line 1, but +# line 1 has the frozen string literal comment) +Gemspec/RequiredRubyVersion: + Enabled: false +# TODO: should I set a required ruby version? At the moment, the project policy +# is to officially support supported Ruby versions (and JRuby), but to not +# officially support no-longer-maintained Ruby versions. + diff --git a/Gemfile b/Gemfile index 851fabc..5f10ba8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,4 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Rakefile b/Rakefile index fdba3c4..bf7fa20 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,4 @@ -# encoding: utf-8 +# frozen_string_literal: true require 'rubygems' require 'bundler/gem_tasks' @@ -9,16 +9,16 @@ RSpec::Core::RakeTask.new(:spec) do |spec| end namespace :spec do if RUBY_VERSION =~ /^1\.8/ - desc "Rspec code coverage (1.8.7)" + desc 'Rspec code coverage (1.8.7)' RSpec::Core::RakeTask.new(:coverage) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.rcov = true end else - desc "Rspec code coverage (1.9+)" + desc 'Rspec code coverage (1.9+)' task :coverage do ENV['COVERAGE'] = 'true' - Rake::Task["spec"].execute + Rake::Task['spec'].execute end end end @@ -28,7 +28,7 @@ RuboCop::RakeTask.new require 'rdoc/task' Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : "" + version = File.exist?('VERSION') ? File.read('VERSION') : '' rdoc.rdoc_dir = 'rdoc' rdoc.title = "recursive-open-struct #{version}" @@ -36,28 +36,28 @@ Rake::RDocTask.new do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -task :default => [:spec, :rubocop] +task default: %i[spec rubocop] task :fix_permissions do - File.umask 0022 + File.umask 0o022 filelist = `git ls-files`.split("\n") - FileUtils.chmod 0644, filelist, :verbose => true - FileUtils.chmod 0755, ['lib','spec'], :verbose => true + FileUtils.chmod 0o644, filelist, verbose: true + FileUtils.chmod 0o755, %w[lib spec], verbose: true end -desc "Update the AUTHORS.txt file" +desc 'Update the AUTHORS.txt file' task :update_authors do authors = `git log --format="%aN <%aE>"|sort -f|uniq` File.open('AUTHORS.txt', 'w') do |f| f.write("Recursive-open-struct was written by these fine people:\n\n") - f.write(authors.split("\n").map { |a| "* #{a}" }.join( "\n" )) + f.write(authors.split("\n").map { |a| "* #{a}" }.join("\n")) f.write("\n") end end -task :build => [:update_authors, :fix_permissions] +task build: %i[update_authors fix_permissions] -desc "Run an interactive pry shell with ros required" +desc 'Run an interactive pry shell with ros required' task :pry do - sh "pry -I lib -r recursive-open-struct" + sh 'pry -I lib -r recursive-open-struct' end diff --git a/lib/recursive-open-struct.rb b/lib/recursive-open-struct.rb index 886fe8f..53f6ed6 100644 --- a/lib/recursive-open-struct.rb +++ b/lib/recursive-open-struct.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'recursive_open_struct' diff --git a/lib/recursive_open_struct.rb b/lib/recursive_open_struct.rb index a0d01ea..b20feea 100644 --- a/lib/recursive_open_struct.rb +++ b/lib/recursive_open_struct.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'ostruct' require 'recursive_open_struct/version' @@ -12,6 +14,7 @@ # TODO: `#*_as_a_hash` deprecated. Nested hashes can be referenced using # `#to_h`. +# rubocop:disable Metrics/ClassLength class RecursiveOpenStruct < OpenStruct include Dig if OpenStruct.public_instance_methods.include? :dig @@ -28,7 +31,9 @@ def self.default_options } end - def initialize(hash=nil, passed_options={}) + # rubocop:disable Lint/MissingSuper + # Intentionally doesn't call super and initializes +@table+ itself. + def initialize(hash = nil, passed_options = {}) hash = hash.to_h if [hash.is_a?(RecursiveOpenStruct), hash.is_a?(OpenStruct)].any? hash ||= {} @@ -40,6 +45,7 @@ def initialize(hash=nil, passed_options={}) @sub_elements = {} end + # rubocop:enable Lint/MissingSuper def marshal_load(attributes) hash, @options = attributes @@ -69,17 +75,17 @@ def to_h # TODO: deprecated, unsupported by OpenStruct. OpenStruct does not consider # itself to be a "kind of" Hash. - alias_method :to_hash, :to_h + alias to_hash to_h # Continue supporting older rubies -- JRuby 9.1.x.x is still considered # stable, but is based on Ruby # 2.3.x and so uses :modifiable instead of :modifiable?. Furthermore, if # :modifiable is private, then make :modifiable? private too. - if !OpenStruct.private_instance_methods.include?(:modifiable?) + unless OpenStruct.private_instance_methods.include?(:modifiable?) if OpenStruct.private_instance_methods.include?(:modifiable) - alias_method :modifiable?, :modifiable + alias modifiable? modifiable elsif OpenStruct.public_instance_methods.include?(:modifiable) - alias_method :modifiable?, :modifiable + alias modifiable? modifiable private :modifiable? end end @@ -89,7 +95,7 @@ def [](name) v = @table[key_name] if v.is_a?(Hash) @sub_elements[key_name] ||= _create_sub_element_(v, mutate_input_hash: true) - elsif v.is_a?(Array) and @options[:recurse_over_arrays] + elsif v.is_a?(Array) && @options[:recurse_over_arrays] @sub_elements[key_name] ||= recurse_over_array(v) @sub_elements[key_name] = recurse_over_array(@sub_elements[key_name]) else @@ -100,7 +106,7 @@ def [](name) if private_instance_methods.include?(:modifiable?) || public_instance_methods.include?(:modifiable?) def []=(name, value) key_name = _get_key_from_table_(name) - tbl = modifiable? # Ensure we are modifiable + tbl = modifiable? # Ensure we are modifiable @sub_elements.delete(key_name) tbl[key_name] = value end @@ -120,19 +126,20 @@ def respond_to_missing?(mid, include_private = false) # Adapted implementation of method_missing to accommodate the differences # between ROS and OS. + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity def method_missing(mid, *args) len = args.length if mid =~ /^(.*)=$/ - if len != 1 - raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1) - end + raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1) if len != 1 + # self[$1.to_sym] = args[0] # modifiable?[new_ostruct_member!($1.to_sym)] = args[0] - new_ostruct_member!($1.to_sym) + new_ostruct_member!(::Regexp.last_match(1).to_sym) public_send(mid, args[0]) - elsif len == 0 + elsif len.zero? key = mid - key = $1 if key =~ /^(.*)_as_a_hash$/ + key = ::Regexp.last_match(1) if key =~ /^(.*)_as_a_hash$/ if @table.key?(_get_key_from_table_(key)) new_ostruct_member!(key) public_send(mid) @@ -147,6 +154,8 @@ def method_missing(mid, *args) raise err end end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize def freeze @table.each_key do |key| @@ -160,7 +169,7 @@ def freeze # 2.4.0. def new_ostruct_member(name) key_name = _get_key_from_table_(name) - unless self.singleton_class.method_defined?(name.to_sym) + unless singleton_class.method_defined?(name.to_sym) class << self; self; end.class_eval do define_method(name) do self[key_name] @@ -185,7 +194,12 @@ class << self; self; end.class_eval do def delete_field(name) sym = _get_key_from_table_(name) - singleton_class.__send__(:remove_method, sym, "#{sym}=") rescue NoMethodError # ignore if methods not yet generated. + begin + singleton_class.__send__(:remove_method, sym, "#{sym}=") + rescue StandardError + # ignore if methods not yet generated. + NoMethodError + end @sub_elements.delete(sym) @table.delete(sym) end @@ -203,8 +217,9 @@ def initialize_dup(orig) end def _get_key_from_table_(name) - return name.to_s if @table.has_key?(name.to_s) - return name.to_sym if @table.has_key?(name.to_sym) + return name.to_s if @table.key?(name.to_s) + return name.to_sym if @table.key?(name.to_sym) + name end @@ -222,5 +237,5 @@ def recurse_over_array(array) end array end - end +# rubocop:enable Metrics/ClassLength diff --git a/lib/recursive_open_struct/debug_inspect.rb b/lib/recursive_open_struct/debug_inspect.rb index c64cfab..3b4f0be 100644 --- a/lib/recursive_open_struct/debug_inspect.rb +++ b/lib/recursive_open_struct/debug_inspect.rb @@ -1,38 +1,46 @@ -module RecursiveOpenStruct::DebugInspect - def debug_inspect(io = STDOUT, indent_level = 0, recursion_limit = 12) - display_recursive_open_struct(io, @table, indent_level, recursion_limit) - end +# frozen_string_literal: true - def display_recursive_open_struct(io, ostrct_or_hash, indent_level, recursion_limit) +# rubocop:disable Metrics/AbcSize +# rubocop:disable Metrics/CyclomaticComplexity +# rubocop:disable Metrics/PerceivedComplexity +# rubocop:disable Style/StringConcatenation +class RecursiveOpenStruct < OpenStruct + module DebugInspect + def debug_inspect(io = $stdout, indent_level = 0, recursion_limit = 12) + display_recursive_open_struct(io, @table, indent_level, recursion_limit) + end - if recursion_limit <= 0 then - # protection against recursive structure (like in the tests) - io.puts ' '*indent_level + '(recursion limit reached)' - else - #puts ostrct_or_hash.inspect - if ostrct_or_hash.is_a?(self.class) then - ostrct_or_hash = ostrct_or_hash.marshal_dump - end + def display_recursive_open_struct(io, ostrct_or_hash, indent_level, recursion_limit) + if recursion_limit <= 0 + # protection against recursive structure (like in the tests) + io.puts ' ' * indent_level + '(recursion limit reached)' + else + # puts ostrct_or_hash.inspect + ostrct_or_hash = ostrct_or_hash.marshal_dump if ostrct_or_hash.is_a?(self.class) - # We'll display the key values like this : key = value - # to align display, we look for the maximum key length of the data that will be displayed - # (everything except hashes) - data_indent = ostrct_or_hash \ - .reject { |k, v| v.is_a?(self.class) || v.is_a?(Hash) } \ - .max {|a,b| a[0].to_s.length <=> b[0].to_s.length}[0].to_s.length - # puts "max length = #{data_indent}" + # We'll display the key values like this : key = value + # to align display, we look for the maximum key length of the data that will be displayed + # (everything except hashes) + data_indent = ostrct_or_hash \ + .reject { |_k, v| v.is_a?(self.class) || v.is_a?(Hash) } \ + .max { |a, b| a[0].to_s.length <=> b[0].to_s.length }[0].to_s.length + # puts "max length = #{data_indent}" - ostrct_or_hash.each do |key, value| - if (value.is_a?(self.class) || value.is_a?(Hash)) then - io.puts ' '*indent_level + key.to_s + '.' - display_recursive_open_struct(io, value, indent_level + 1, recursion_limit - 1) - else - io.puts ' '*indent_level + key.to_s + ' '*(data_indent - key.to_s.length) + ' = ' + value.inspect + ostrct_or_hash.each do |key, value| + if value.is_a?(self.class) || value.is_a?(Hash) + io.puts ' ' * indent_level + key.to_s + '.' + display_recursive_open_struct(io, value, indent_level + 1, recursion_limit - 1) + else + io.puts ' ' * indent_level + key.to_s + ' ' * (data_indent - key.to_s.length) + ' = ' + value.inspect + end end end - end - true + true + end end - end +# rubocop:enable Style/StringConcatenation +# rubocop:enable Metrics/PerceivedComplexity +# rubocop:enable Metrics/CyclomaticComplexity +# rubocop:enable Metrics/AbcSize diff --git a/lib/recursive_open_struct/deep_dup.rb b/lib/recursive_open_struct/deep_dup.rb index 8af870b..8f2742d 100644 --- a/lib/recursive_open_struct/deep_dup.rb +++ b/lib/recursive_open_struct/deep_dup.rb @@ -1,33 +1,40 @@ -require 'set' -class RecursiveOpenStruct::DeepDup - def initialize(opts={}) - @recurse_over_arrays = opts.fetch(:recurse_over_arrays, false) - @preserve_original_keys = opts.fetch(:preserve_original_keys, false) - end +# frozen_string_literal: true - def call(obj) - deep_dup(obj) - end +class RecursiveOpenStruct < OpenStruct + class DeepDup + def initialize(opts = {}) + @recurse_over_arrays = opts.fetch(:recurse_over_arrays, false) + @preserve_original_keys = opts.fetch(:preserve_original_keys, false) + end + + def call(obj) + deep_dup(obj) + end - private + private - def deep_dup(obj, visited=Set.new) - if obj.is_a?(Hash) - obj.each_with_object({}) do |(key, value), h| - h[@preserve_original_keys ? key : key.to_sym] = value_or_deep_dup(value, visited) - end - elsif obj.is_a?(Array) && @recurse_over_arrays - obj.each_with_object([]) do |value, arr| - value = value.is_a?(RecursiveOpenStruct) ? value.to_h : value - arr << value_or_deep_dup(value, visited) + # rubocop:disable Metrics/PerceivedComplexity + # rubocop:disable Metrics/CyclomaticComplexity + def deep_dup(obj, visited = Set.new) + if obj.is_a?(Hash) + obj.each_with_object({}) do |(key, value), h| + h[@preserve_original_keys ? key : key.to_sym] = value_or_deep_dup(value, visited) + end + elsif obj.is_a?(Array) && @recurse_over_arrays + obj.each_with_object([]) do |value, arr| + value = value.is_a?(RecursiveOpenStruct) ? value.to_h : value + arr << value_or_deep_dup(value, visited) + end + else + obj end - else - obj end - end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity - def value_or_deep_dup(value, visited) - obj_id = value.object_id - visited.include?(obj_id) ? value : deep_dup(value, visited << obj_id) + def value_or_deep_dup(value, visited) + obj_id = value.object_id + visited.include?(obj_id) ? value : deep_dup(value, visited << obj_id) + end end end diff --git a/lib/recursive_open_struct/dig.rb b/lib/recursive_open_struct/dig.rb index 518e7b7..c47785c 100644 --- a/lib/recursive_open_struct/dig.rb +++ b/lib/recursive_open_struct/dig.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + class RecursiveOpenStruct < OpenStruct + # Replaces +OpenStruct#dig+ to properly support treating nested values as + # RecursiveOpenStructs instead of returning the nested Hashes. + # + # This module is only added in when +OpenStruct#dig+ exists (the OpenStruct + # included in older Ruby versions didn't implement it) module Dig - - # Replaces +OpenStruct#dig+ to properly support treating nested values as - # RecursiveOpenStructs instead of returning the nested Hashes. def dig(name, *names) begin name = name.to_sym @@ -12,7 +16,7 @@ def dig(name, *names) name_val = self[name] - if names.length > 0 && name_val.respond_to?(:dig) + if !names.empty? && name_val.respond_to?(:dig) name_val.dig(*names) else name_val diff --git a/lib/recursive_open_struct/version.rb b/lib/recursive_open_struct/version.rb index 679563a..725da18 100644 --- a/lib/recursive_open_struct/version.rb +++ b/lib/recursive_open_struct/version.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # Necessary since the top-level class/module is a class that inherits from # OpenStruct. require 'ostruct' class RecursiveOpenStruct < OpenStruct - VERSION = "2.1.0" + VERSION = '2.1.0' end diff --git a/recursive-open-struct.gemspec b/recursive-open-struct.gemspec index 62b5296..a62848e 100644 --- a/recursive-open-struct.gemspec +++ b/recursive-open-struct.gemspec @@ -1,20 +1,21 @@ -# -*- encoding: utf-8 -*- -name = "recursive-open-struct" -version = File.foreach(File.join(__dir__, "lib", "recursive_open_struct", "version.rb")) do |line| - /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 +# frozen_string_literal: true + +name = 'recursive-open-struct' +version = File.foreach(File.join(__dir__, 'lib', 'recursive_open_struct', 'version.rb')) do |line| + /^\s*VERSION\s*=\s*'(.*)'/ =~ line and break Regexp.last_match(1) end Gem::Specification.new do |s| s.name = name s.version = version - s.authors = ["William (B.J.) Snow Orvis"] - s.email = "aetherknight@gmail.com" - s.date = Time.now.utc.strftime("%Y-%m-%d") - s.homepage = "https://github.com/aetherknight/recursive-open-struct" - s.licenses = ["MIT"] - - s.summary = "OpenStruct subclass that returns nested hash attributes as RecursiveOpenStructs" - s.description = <<-QUOTE .gsub(/^ /,'') + s.authors = ['William (B.J.) Snow Orvis'] + s.email = 'aetherknight@gmail.com' + s.date = Time.now.utc.strftime('%Y-%m-%d') + s.homepage = 'https://github.com/aetherknight/recursive-open-struct' + s.licenses = ['MIT'] + + s.summary = 'OpenStruct subclass that returns nested hash attributes as RecursiveOpenStructs' + s.description = <<-QUOTE.gsub(/^ /, '') RecursiveOpenStruct is a subclass of OpenStruct. It differs from OpenStruct in that it allows nested hashes to be treated in a recursive fashion. For example: @@ -25,23 +26,23 @@ Gem::Specification.new do |s| Also, nested hashes can still be accessed as hashes: ros.a_as_a_hash # { :b => 'c' } - QUOTE + QUOTE - s.files = `git ls-files lib`.split("\n") + ["AUTHORS.txt" , "CHANGELOG.md", "LICENSE.txt", "README.md"] - s.require_paths = ["lib"] + s.files = `git ls-files lib`.split("\n") + ['AUTHORS.txt', 'CHANGELOG.md', 'LICENSE.txt', 'README.md'] + s.require_paths = ['lib'] s.extra_rdoc_files = [ - "CHANGELOG.md", - "LICENSE.txt", - "README.md" + 'CHANGELOG.md', + 'LICENSE.txt', + 'README.md' ] - s.add_development_dependency('bundler', [">= 0"]) - s.add_development_dependency('pry', [">= 0"]) - s.add_development_dependency('rake', [">= 0"]) - s.add_development_dependency('rdoc', [">= 0"]) - s.add_development_dependency('rspec', "~> 3.2") + s.add_development_dependency('bundler', ['>= 0']) + s.add_development_dependency('pry', ['>= 0']) + s.add_development_dependency('rake', ['>= 0']) + s.add_development_dependency('rdoc', ['>= 0']) + s.add_development_dependency('rspec', '~> 3.2') s.add_development_dependency('rubocop', ['>= 0']) - s.add_development_dependency('simplecov', [">= 0"]) + s.add_development_dependency('simplecov', ['>= 0']) s.add_dependency('ostruct') end diff --git a/spec/recursive_open_struct/debug_inspect_spec.rb b/spec/recursive_open_struct/debug_inspect_spec.rb index a8c2623..6fed06e 100644 --- a/spec/recursive_open_struct/debug_inspect_spec.rb +++ b/spec/recursive_open_struct/debug_inspect_spec.rb @@ -1,25 +1,19 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' describe RecursiveOpenStruct do - describe "#debug_inspect" do + describe '#debug_inspect' do before(:each) do - h1 = { :a => 'a'} - h2 = { :a => 'b', :h1 => h1 } + h1 = { a: 'a' } + h2 = { a: 'b', h1: h1 } h1[:h2] = h2 @ros = RecursiveOpenStruct.new(h2) end - it "should have a simple way of display" do - @output = <<-QUOTE -a = "b" -h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. + it 'should have a simple way of display' do + @output = <<~QUOTE a = "b" h1. a = "a" @@ -36,35 +30,43 @@ h1. a = "a" h2. - (recursion limit reached) + a = "b" + h1. + a = "a" + h2. + a = "b" + h1. + a = "a" + h2. + (recursion limit reached) QUOTE @io = StringIO.new @ros.debug_inspect(@io) - expect(@io.string).to match /^a = "b"$/ - expect(@io.string).to match /^h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ a = "b"$/ - expect(@io.string).to match /^ h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ a = "b"$/ - expect(@io.string).to match /^ h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ a = "b"$/ - expect(@io.string).to match /^ h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ a = "b"$/ - expect(@io.string).to match /^ h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ a = "b"$/ - expect(@io.string).to match /^ h1\.$/ - expect(@io.string).to match /^ a = "a"$/ - expect(@io.string).to match /^ h2\.$/ - expect(@io.string).to match /^ \(recursion limit reached\)$/ + expect(@io.string).to match(/^a = "b"$/) + expect(@io.string).to match(/^h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ a = "b"$/) + expect(@io.string).to match(/^ h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ a = "b"$/) + expect(@io.string).to match(/^ h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ a = "b"$/) + expect(@io.string).to match(/^ h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ a = "b"$/) + expect(@io.string).to match(/^ h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ a = "b"$/) + expect(@io.string).to match(/^ h1\.$/) + expect(@io.string).to match(/^ a = "a"$/) + expect(@io.string).to match(/^ h2\.$/) + expect(@io.string).to match(/^ \(recursion limit reached\)$/) end end end diff --git a/spec/recursive_open_struct/indifferent_access_spec.rb b/spec/recursive_open_struct/indifferent_access_spec.rb index c62400c..70acc1b 100644 --- a/spec/recursive_open_struct/indifferent_access_spec.rb +++ b/spec/recursive_open_struct/indifferent_access_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' @@ -8,7 +10,7 @@ let(:new_symbol) { :foo } describe 'indifferent access' do - let(:hash) { {:foo => value, 'bar' => symbol} } + let(:hash) { { :foo => value, 'bar' => symbol } } let(:hash_ros_opts) { {} } subject(:hash_ros) { RecursiveOpenStruct.new(hash, hash_ros_opts) } @@ -20,7 +22,6 @@ it('allows getting with method') { expect(subject.foo).to be value } it('allows getting with symbol') { expect(subject[:foo]).to be value } it('allows getting with string') { expect(subject['foo']).to be value } - end context 'setting value with symbol' do @@ -31,7 +32,6 @@ it('allows getting with method') { expect(subject.foo).to be value } it('allows getting with symbol') { expect(subject[:foo]).to be value } it('allows getting with string') { expect(subject['foo']).to be value } - end context 'setting value with string' do @@ -42,7 +42,6 @@ it('allows getting with method') { expect(subject.foo).to be value } it('allows getting with symbol') { expect(subject[:foo]).to be value } it('allows getting with string') { expect(subject['foo']).to be value } - end context 'overwriting values' do @@ -117,10 +116,10 @@ context 'when preserve_original_keys is not enabled' do context 'transforms original keys to symbols' do subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true) } - let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } } - let(:symbolized_recursive_hash) { {:foo => [ {:bar => [ { :foo => :bar} ] } ] } } - let(:symbolized_modified_hash) { {:foo => [ {:bar => [ { :foo => :foo} ] } ] } } - let(:symbolized_hash) { Hash[hash.map{|(k,v)| [k.to_sym,v]}] } + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } + let(:symbolized_recursive_hash) { { foo: [{ bar: [{ foo: :bar }] }] } } + let(:symbolized_modified_hash) { { foo: [{ bar: [{ foo: :foo }] }] } } + let(:symbolized_hash) { Hash[hash.map { |(k, v)| [k.to_sym, v] }] } specify 'after initialization' do expect(hash_ros.to_h).to eq symbolized_hash @@ -139,11 +138,13 @@ context 'when preserve_original_keys is enabled' do context 'preserves the original keys' do - subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true, preserve_original_keys: true) } - let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } } - let(:modified_hash) { {:foo => [ {'bar' => [ { 'foo' => :foo} ] } ] } } + subject(:recursive) do + RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true, preserve_original_keys: true) + end + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } + let(:modified_hash) { { foo: [{ 'bar' => [{ 'foo' => :foo }] }] } } - let(:hash_ros_opts) { { preserve_original_keys: true }} + let(:hash_ros_opts) { { preserve_original_keys: true } } specify 'after initialization' do expect(hash_ros.to_h).to eq hash @@ -163,19 +164,19 @@ context 'when undefined method' do context 'when raise_on_missing is enabled' do subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, raise_on_missing: true) } - let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } } + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } specify 'raises NoMethodError' do - expect { + expect do recursive.undefined_method - }.to raise_error(NoMethodError) + end.to raise_error(NoMethodError) end end context 'when raise_on_missing is disabled' do context 'preserves the original keys' do subject(:recursive) { RecursiveOpenStruct.new(recursive_hash) } - let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } } + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } specify 'returns nil' do expect(recursive.undefined_method).to be_nil @@ -183,8 +184,6 @@ end end end - end - end end diff --git a/spec/recursive_open_struct/open_struct_behavior_spec.rb b/spec/recursive_open_struct/open_struct_behavior_spec.rb index 1739171..fa5ff23 100644 --- a/spec/recursive_open_struct/open_struct_behavior_spec.rb +++ b/spec/recursive_open_struct/open_struct_behavior_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' @@ -5,12 +7,12 @@ let(:hash) { {} } subject(:ros) { RecursiveOpenStruct.new(hash) } - describe "behavior it inherits from OpenStruct" do + describe 'behavior it inherits from OpenStruct' do context 'when not initialized from anything' do subject(:ros) { RecursiveOpenStruct.new } - it "can represent arbitrary data objects" do - ros.blah = "John Smith" - expect(ros.blah).to eq "John Smith" + it 'can represent arbitrary data objects' do + ros.blah = 'John Smith' + expect(ros.blah).to eq 'John Smith' end it 'returns nil for missing attributes' do @@ -31,24 +33,24 @@ end end - context "when initialized from a hash" do - let(:hash) { { :asdf => 'John Smith' } } + context 'when initialized from a hash' do + let(:hash) { { asdf: 'John Smith' } } context 'that contains symbol keys' do - it "turns those symbol keys into method names" do - expect(ros.asdf).to eq "John Smith" + it 'turns those symbol keys into method names' do + expect(ros.asdf).to eq 'John Smith' end end - it "can modify an existing key" do - ros.asdf = "George Washington" - expect(ros.asdf).to eq "George Washington" + it 'can modify an existing key' do + ros.asdf = 'George Washington' + expect(ros.asdf).to eq 'George Washington' end context 'that contains string keys' do let(:hash) { { 'asdf' => 'John Smith' } } - it "turns those string keys into method names" do - expect(ros.asdf).to eq "John Smith" + it 'turns those string keys into method names' do + expect(ros.asdf).to eq 'John Smith' end end @@ -63,7 +65,6 @@ expect(ros.test).to eq :foo expect(ros.rand).to eq 'not a number' end - end context 'that contains keys that mirror existing public methods inherited from Object' do @@ -74,34 +75,35 @@ end end - if [/\A([0-9]+)\.([0-9]+)\.([0-9]+)\z/.match(RUBY_VERSION)].tap { |l| m = l[0] ; l[0] = (m[1].to_i >= 2 && m[2].to_i >= 4) }.first + if [/\A([0-9]+)\.([0-9]+)\.([0-9]+)\z/.match(RUBY_VERSION)].tap do |l| + m = l[0] + l[0] = (m[1].to_i >= 2 && m[2].to_i >= 4) + end.first context 'when Ruby 2.4.0 or newer' do specify 'new_ostruct_member! is private' do - expect { + expect do ros.new_ostruct_member!(:bonsoir) - }.to raise_error(NoMethodError) - # OpenStruct.new().new_ostruct_member!(:foo) + end.to raise_error(NoMethodError) + # OpenStruct.new().new_ostruct_member!(:foo) end end end - end - - describe "handling of arbitrary attributes" do + describe 'handling of arbitrary attributes' do subject { RecursiveOpenStruct.new } before(:each) do - subject.blah = "John Smith" + subject.blah = 'John Smith' end - describe "#respond?" do + describe '#respond?' do it { expect(subject).to respond_to :blah } it { expect(subject).to respond_to :blah= } it { expect(subject).to_not respond_to :asdf } it { expect(subject).to_not respond_to :asdf= } end # describe #respond? - describe "#methods" do + describe '#methods' do it { expect(subject.methods.map(&:to_sym)).to include :blah } it { expect(subject.methods.map(&:to_sym)).to include :blah= } it { expect(subject.methods.map(&:to_sym)).to_not include :asdf } @@ -109,37 +111,37 @@ end # describe #methods end # describe handling of arbitrary attributes - describe "handling of freezing" do - let(:hash) { { :asdf => 'John Smith' } } + describe 'handling of freezing' do + let(:hash) { { asdf: 'John Smith' } } before do ros.freeze end - it "can read existing keys" do + it 'can read existing keys' do expect(ros.asdf).to eq 'John Smith' end - it "cannot write new keys" do + it 'cannot write new keys' do expect { ros.new_key = 'new_value' }.to raise_error FrozenError end - it "cannot write existing keys" do + it 'cannot write existing keys' do expect { ros.asdf = 'new_value' }.to raise_error FrozenError end - context "with recursive structure" do - let(:hash) { { :key => { :subkey => 42 } } } + context 'with recursive structure' do + let(:hash) { { key: { subkey: 42 } } } - it "can read existing sub-elements" do + it 'can read existing sub-elements' do expect(ros.key.subkey).to eq 42 end - it "can write new sub-elements" do + it 'can write new sub-elements' do expect { ros.key.new_subkey = 43 }.not_to raise_error end - it "can write existing sub-elements" do + it 'can write existing sub-elements' do expect { ros.key.subkey = 43 }.not_to raise_error end end diff --git a/spec/recursive_open_struct/ostruct_2_0_0_spec.rb b/spec/recursive_open_struct/ostruct_2_0_0_spec.rb index ae038bc..b774452 100644 --- a/spec/recursive_open_struct/ostruct_2_0_0_spec.rb +++ b/spec/recursive_open_struct/ostruct_2_0_0_spec.rb @@ -1,111 +1,102 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' describe RecursiveOpenStruct do - - let(:hash) { {:foo => 'foo', 'bar' => :bar} } + let(:hash) { { :foo => 'foo', 'bar' => :bar } } subject(:ros) { RecursiveOpenStruct.new(hash) } - describe "OpenStruct 2.0+ methods" do - - context "Hash style setter" do - - it "method exists" do + describe 'OpenStruct 2.0+ methods' do + context 'Hash style setter' do + it 'method exists' do expect(ros.respond_to?('[]=')).to be_truthy end - it "changes the value" do + it 'changes the value' do ros[:foo] = :foo ros.foo = :foo end - end - context "delete_field" do - + context 'delete_field' do before(:each) { ros.delete_field :foo } - it "removes the value" do + it 'removes the value' do expect(ros.foo).to be_nil expect(ros.to_h).to_not include(:foo) end - it "removes the getter method" do + it 'removes the getter method' do is_expected.to_not respond_to :foo end - it "removes the setter method" do - expect(ros.respond_to? 'foo=').to be_falsey + it 'removes the setter method' do + expect(ros.respond_to?('foo=')).to be_falsey end - it "works with indifferent access" do - expect(ros.delete_field :bar).to eq :bar + it 'works with indifferent access' do + expect(ros.delete_field(:bar)).to eq :bar is_expected.to_not respond_to :bar is_expected.to_not respond_to 'bar=' expect(ros.to_h).to be_empty end - end - context "eql?" do + context 'eql?' do subject(:new_ros) { ros.dup } - context "with identical ROS" do + context 'with identical ROS' do subject { ros } it { is_expected.to be_eql ros } end - context "with similar ROS" do + context 'with similar ROS' do subject { RecursiveOpenStruct.new(hash) } it { is_expected.to be_eql ros } end - context "with same Hash" do + context 'with same Hash' do subject { RecursiveOpenStruct.new(hash, recurse_over_arrays: true) } it { is_expected.to be_eql ros } end - context "with duplicated ROS" do + context 'with duplicated ROS' do subject { ros.dup } - it "fails on different value" do + it 'fails on different value' do subject.foo = 'bar' is_expected.not_to be_eql ros end - it "fails on missing field" do + it 'fails on missing field' do subject.delete_field :bar is_expected.not_to be_eql ros end - it "fails on added field" do + it 'fails on added field' do subject.baz = :baz is_expected.not_to be_eql ros end - end - end - context "hash" do - it "calculates table hash" do + context 'hash' do + it 'calculates table hash' do expect(ros.hash).to eq(ros.instance_variable_get('@table').hash) end - end - context "each_pair" do - it "iterates over hash keys, with keys as symbol" do + context 'each_pair' do + it 'iterates over hash keys, with keys as symbol' do ros_pairs = [] - ros.each_pair {|k,v| ros_pairs << [k,v]} + ros.each_pair { |k, v| ros_pairs << [k, v] } hash_pairs = [] - {:foo => 'foo', :bar => :bar}.each_pair {|k,v| hash_pairs << [k,v]} + { foo: 'foo', bar: :bar }.each_pair { |k, v| hash_pairs << [k, v] } - expect(ros_pairs).to match (hash_pairs) + expect(ros_pairs).to match(hash_pairs) end end - end # describe OpenStruct 2.0+ methods - end diff --git a/spec/recursive_open_struct/ostruct_2_3_0_spec.rb b/spec/recursive_open_struct/ostruct_2_3_0_spec.rb index f05865e..cf6573f 100644 --- a/spec/recursive_open_struct/ostruct_2_3_0_spec.rb +++ b/spec/recursive_open_struct/ostruct_2_3_0_spec.rb @@ -1,49 +1,55 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' +# rubocop:disable Style/SingleArgumentDig describe RecursiveOpenStruct do - describe "OpenStruct 2.3.0+ methods" do - describe "#dig" do + describe 'OpenStruct 2.3.0+ methods' do + describe '#dig' do # We only care when Ruby supports `#dig`. if OpenStruct.public_instance_methods.include? :dig - context "recurse_over_arrays: false" do - subject { RecursiveOpenStruct.new(a: { b: 2, c: ["doo", "bee", { inner: "one"}]}) } + context 'recurse_over_arrays: false' do + subject { RecursiveOpenStruct.new(a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] }) } - describe "OpenStruct-like behavior" do + describe 'OpenStruct-like behavior' do it { expect(subject.dig(:a, :b)).to eq 2 } - it { expect(subject.dig(:a, :c, 0)).to eq "doo" } - it { expect(subject.dig(:a, :c, 2, :inner)).to eq "one" } + it { expect(subject.dig(:a, :c, 0)).to eq 'doo' } + it { expect(subject.dig(:a, :c, 2, :inner)).to eq 'one' } end - describe "recursive behavior" do + describe 'recursive behavior' do it { expect(subject.dig(:a)).to eq RecursiveOpenStruct.new( - { b: 2, c: ["doo", "bee", { inner: "one"}]} + { b: 2, c: ['doo', 'bee', { inner: 'one' }] } ) } - it { expect(subject.dig(:a, :c, 2)).to eq({inner: "one"}) } + it { expect(subject.dig(:a, :c, 2)).to eq({ inner: 'one' }) } end end - context "recurse_over_arrays: true" do - subject { RecursiveOpenStruct.new({a: { b: 2, c: ["doo", "bee", { inner: "one"}]}}, recurse_over_arrays: true) } + context 'recurse_over_arrays: true' do + subject do + RecursiveOpenStruct.new({ a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] } }, recurse_over_arrays: true) + end - describe "OpenStruct-like behavior" do + describe 'OpenStruct-like behavior' do it { expect(subject.dig(:a, :b)).to eq 2 } - it { expect(subject.dig(:a, :c, 0)).to eq "doo" } - it { expect(subject.dig(:a, :c, 2, :inner)).to eq "one" } + it { expect(subject.dig(:a, :c, 0)).to eq 'doo' } + it { expect(subject.dig(:a, :c, 2, :inner)).to eq 'one' } end - describe "recursive behavior" do + describe 'recursive behavior' do it { expect(subject.dig(:a)).to eq RecursiveOpenStruct.new( - { b: 2, c: ["doo", "bee", { inner: "one"}]} + { b: 2, c: ['doo', 'bee', { inner: 'one' }] } ) } - it { expect(subject.dig(:a, :c, 2)).to eq RecursiveOpenStruct.new(inner: "one") } + it { expect(subject.dig(:a, :c, 2)).to eq RecursiveOpenStruct.new(inner: 'one') } end end end end # describe #dig end # describe OpenStruct 2.3+ methods end +# rubocop:enable Style/SingleArgumentDig diff --git a/spec/recursive_open_struct/recursion_and_subclassing_spec.rb b/spec/recursive_open_struct/recursion_and_subclassing_spec.rb index ec40890..4662791 100644 --- a/spec/recursive_open_struct/recursion_and_subclassing_spec.rb +++ b/spec/recursive_open_struct/recursion_and_subclassing_spec.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' describe RecursiveOpenStruct do - describe "subclassing RecursiveOpenStruct" do + describe 'subclassing RecursiveOpenStruct' do let(:subclass) { Class.new(RecursiveOpenStruct) } - subject(:rossc) { subclass.new({ :one => [{:two => :three}] }, recurse_over_arrays: true) } + subject(:rossc) { subclass.new({ one: [{ two: :three }] }, recurse_over_arrays: true) } - specify "nested objects use the subclass of the parent" do + specify 'nested objects use the subclass of the parent' do expect(rossc.one.first.class).to eq subclass end end diff --git a/spec/recursive_open_struct/recursion_spec.rb b/spec/recursive_open_struct/recursion_spec.rb index 4b094d5..5e26979 100644 --- a/spec/recursive_open_struct/recursion_spec.rb +++ b/spec/recursive_open_struct/recursion_spec.rb @@ -1,34 +1,36 @@ +# frozen_string_literal: true + +# rubocop:disable Security/MarshalLoad require_relative '../spec_helper' require 'recursive_open_struct' describe RecursiveOpenStruct do - - describe "recursive behavior" do - let(:h) { { :blah => { :another => 'value' } } } + describe 'recursive behavior' do + let(:h) { { blah: { another: 'value' } } } subject(:ros) { RecursiveOpenStruct.new(h) } - it "can convert the entire hash tree back into a hash" do + it 'can convert the entire hash tree back into a hash' do blank_obj = Object.new - h = {:asdf => 'John Smith', :foo => [{:bar => blank_obj}, {:baz => nil}]} + h = { asdf: 'John Smith', foo: [{ bar: blank_obj }, { baz: nil }] } ros = RecursiveOpenStruct.new(h) expect(ros.to_h).to eq h expect(ros.to_hash).to eq h end - it "returns accessed hashes as RecursiveOpenStructs instead of hashes" do + it 'returns accessed hashes as RecursiveOpenStructs instead of hashes' do expect(subject.blah.another).to eq 'value' end - it "handles subscript notation the same way as dotted notation" do + it 'handles subscript notation the same way as dotted notation' do expect(subject.blah.another).to eq subject[:blah].another end - it "uses #key_as_a_hash to return key as a Hash" do - expect(subject.blah_as_a_hash).to eq({ :another => 'value' }) + it 'uses #key_as_a_hash to return key as a Hash' do + expect(subject.blah_as_a_hash).to eq({ another: 'value' }) end - it "handles sub-element replacement with dotted notation before member setup" do + it 'handles sub-element replacement with dotted notation before member setup' do expect(ros[:blah][:another]).to eql 'value' expect(ros.methods).not_to include(:blah) @@ -37,7 +39,7 @@ expect(ros.blah.changed).to eql 'backing' end - it "handles being dump then loaded by Marshal" do + it 'handles being dump then loaded by Marshal' do foo_struct = [RecursiveOpenStruct.new] bar_struct = RecursiveOpenStruct.new(foo: foo_struct) serialized = Marshal.dump(bar_struct) @@ -45,9 +47,9 @@ expect(Marshal.load(serialized).foo).to eq(foo_struct) end - describe "handling loops in the original Hashes" do - let(:h1) { { :a => 'a'} } - let(:h2) { { :a => 'b', :h1 => h1 } } + describe 'handling loops in the original Hashes' do + let(:h1) { { a: 'a' } } + let(:h2) { { a: 'b', h1: h1 } } before(:each) { h1[:h2] = h2 } subject { RecursiveOpenStruct.new(h2) } @@ -60,36 +62,36 @@ it { expect(subject.h1).to_not eq subject.h1.h2 } end # describe handling loops in the origin Hashes - it "can modify a key of a sub-element" do + it 'can modify a key of a sub-element' do h = { - :blah => { - :blargh => 'Brad' + blah: { + blargh: 'Brad' } } ros = RecursiveOpenStruct.new(h) - ros.blah.blargh = "Janet" + ros.blah.blargh = 'Janet' - expect(ros.blah.blargh).to eq "Janet" + expect(ros.blah.blargh).to eq 'Janet' end describe 'subscript mutation notation' do it 'handles the basic case' do - subject[:blah] = 12345 - expect(subject.blah).to eql 12345 + subject[:blah] = 12_345 + expect(subject.blah).to eql 12_345 end it 'recurses properly' do subject[:blah][:another] = 'abc' expect(subject.blah.another).to eql 'abc' - expect(subject.blah_as_a_hash).to eql({ :another => 'abc' }) + expect(subject.blah_as_a_hash).to eql({ another: 'abc' }) end - let(:diff){ { :different => 'thing' } } + let(:diff) { { different: 'thing' } } it 'can replace the entire hash' do expect(subject.to_h).to eql(h) subject[:blah] = diff - expect(subject.to_h).to eql({ :blah => diff }) + expect(subject.to_h).to eql({ blah: diff }) end it 'updates sub-element cache' do @@ -100,85 +102,86 @@ end end - context "after a sub-element has been modified" do + context 'after a sub-element has been modified' do let(:hash) do - { :blah => { :blargh => "Brad" }, :some_array => [ 1, 2, 3] } + { blah: { blargh: 'Brad' }, some_array: [1, 2, 3] } end let(:updated_hash) do - { :blah => { :blargh => "Janet" }, :some_array => [ 1, 2, 3] } + { blah: { blargh: 'Janet' }, some_array: [1, 2, 3] } end subject { RecursiveOpenStruct.new(hash) } - before(:each) { subject.blah.blargh = "Janet" } + before(:each) { subject.blah.blargh = 'Janet' } - describe ".to_h" do - it "returns a hash tree that contains those modifications" do + describe '.to_h' do + it 'returns a hash tree that contains those modifications' do expect(subject.to_h).to eq updated_hash end - specify "modifying the returned hash tree does not modify the ROS" do - subject.to_h[:blah][:blargh] = "Dr Scott" + specify 'modifying the returned hash tree does not modify the ROS' do + subject.to_h[:blah][:blargh] = 'Dr Scott' - expect(subject.blah.blargh).to eq "Janet" + expect(subject.blah.blargh).to eq 'Janet' end end - it "does not mutate the original hash tree passed to the constructor" do + it 'does not mutate the original hash tree passed to the constructor' do expect(hash[:blah][:blargh]).to eq 'Brad' end - it "limits the deep-copy to the initial hash tree" do + it 'limits the deep-copy to the initial hash tree' do subject.some_array[0] = 4 expect(hash[:some_array][0]).to eq 4 end - describe "#dup" do + describe '#dup' do let(:duped_subject) { subject.dup } - it "preserves sub-element modifications" do + it 'preserves sub-element modifications' do expect(duped_subject.blah.blargh).to eq subject.blah.blargh end it "allows the copy's sub-elements to be modified independently from the original's" do - expect(subject.blah.blargh).to eq "Janet" + expect(subject.blah.blargh).to eq 'Janet' - duped_subject.blah.blargh = "Dr. Scott" + duped_subject.blah.blargh = 'Dr. Scott' - expect(subject.blah.blargh).to eq "Janet" - expect(duped_subject.blah.blargh).to eq "Dr. Scott" + expect(subject.blah.blargh).to eq 'Janet' + expect(duped_subject.blah.blargh).to eq 'Dr. Scott' end end end - context "when memoizing and then modifying entire recursive structures" do + context 'when memoizing and then modifying entire recursive structures' do subject do RecursiveOpenStruct.new( - { :blah => original_blah }, :recurse_over_arrays => true) + { blah: original_blah }, recurse_over_arrays: true + ) end before(:each) { subject.blah } # enforce memoization - context "when modifying an entire Hash" do - let(:original_blah) { { :a => 'A', :b => 'B' } } - let(:new_blah) { { :something_new => "C" } } + context 'when modifying an entire Hash' do + let(:original_blah) { { a: 'A', b: 'B' } } + let(:new_blah) { { something_new: 'C' } } before(:each) { subject.blah = new_blah } - it "returns the modified value instead of the memoized one" do - expect(subject.blah.something_new).to eq "C" + it 'returns the modified value instead of the memoized one' do + expect(subject.blah.something_new).to eq 'C' end - specify "the old value no longer exists" do + specify 'the old value no longer exists' do expect(subject.blah.a).to be_nil end end - context "when modifying an entire Array" do + context 'when modifying an entire Array' do let(:original_blah) { [1, 2, 3] } - it "returns the modified value instead of the memoized one" do + it 'returns the modified value instead of the memoized one' do new_blah = [4, 5, 6] subject.blah = new_blah expect(subject.blah).to eq new_blah @@ -187,12 +190,12 @@ end describe 'recursing over arrays' do - let(:blah_list) { [ { :foo => '1' }, { :foo => '2' }, 'baz' ] } - let(:h) { { :blah => blah_list } } + let(:blah_list) { [{ foo: '1' }, { foo: '2' }, 'baz'] } + let(:h) { { blah: blah_list } } - context "when dump and loaded by Marshal" do - let(:test) { RecursiveOpenStruct.new(h, :recurse_over_arrays => true) } - subject { Marshal.load(Marshal.dump(test))} + context 'when dump and loaded by Marshal' do + let(:test) { RecursiveOpenStruct.new(h, recurse_over_arrays: true) } + subject { Marshal.load(Marshal.dump(test)) } it { expect(subject.blah.length).to eq 3 } it { expect(subject.blah[0].foo).to eq '1' } @@ -201,8 +204,8 @@ it { expect(subject.blah[2]).to eq 'baz' } end - context "when recursing over arrays is enabled" do - subject { RecursiveOpenStruct.new(h, :recurse_over_arrays => true) } + context 'when recursing over arrays is enabled' do + subject { RecursiveOpenStruct.new(h, recurse_over_arrays: true) } it { expect(subject.blah.length).to eq 3 } it { expect(subject.blah[0].foo).to eq '1' } @@ -210,85 +213,82 @@ it { expect(subject.blah_as_a_hash).to eq blah_list } it { expect(subject.blah[2]).to eq 'baz' } - context "when an inner value changes" do - let(:updated_blah_list) { [ { :foo => '1' }, { :foo => 'Dr Scott' }, 'baz' ] } - let(:updated_h) { { :blah => updated_blah_list } } + context 'when an inner value changes' do + let(:updated_blah_list) { [{ foo: '1' }, { foo: 'Dr Scott' }, 'baz'] } + let(:updated_h) { { blah: updated_blah_list } } - before(:each) { subject.blah[1].foo = "Dr Scott" } + before(:each) { subject.blah[1].foo = 'Dr Scott' } - it "Retains changes across Array lookups" do - expect(subject.blah[1].foo).to eq "Dr Scott" + it 'Retains changes across Array lookups' do + expect(subject.blah[1].foo).to eq 'Dr Scott' end - it "propagates the changes through to .to_h across Array lookups" do + it 'propagates the changes through to .to_h across Array lookups' do expect(subject.to_h).to eq({ - :blah => [ { :foo => '1' }, { :foo => "Dr Scott" }, 'baz' ] - }) + blah: [{ foo: '1' }, { foo: 'Dr Scott' }, 'baz'] + }) end - it "deep-copies hashes within Arrays" do - subject.to_h[:blah][1][:foo] = "Rocky" + it 'deep-copies hashes within Arrays' do + subject.to_h[:blah][1][:foo] = 'Rocky' - expect(subject.blah[1].foo).to eq "Dr Scott" + expect(subject.blah[1].foo).to eq 'Dr Scott' end - it "does not mutate the input hash passed to the constructor" do + it 'does not mutate the input hash passed to the constructor' do expect(h[:blah][1][:foo]).to eq '2' end - it "the deep copy recurses over Arrays as well" do + it 'the deep copy recurses over Arrays as well' do expect(h[:blah][1][:foo]).to eq '2' end - describe "#dup" do + describe '#dup' do let(:duped_subject) { subject.dup } - it "preserves sub-element modifications" do + it 'preserves sub-element modifications' do expect(duped_subject.blah[1].foo).to eq subject.blah[1].foo end it "allows the copy's sub-elements to be modified independently from the original's" do - duped_subject.blah[1].foo = "Rocky" + duped_subject.blah[1].foo = 'Rocky' - expect(duped_subject.blah[1].foo).to eq "Rocky" - expect(subject.blah[1].foo).to eq "Dr Scott" + expect(duped_subject.blah[1].foo).to eq 'Rocky' + expect(subject.blah[1].foo).to eq 'Dr Scott' end end end - context "when array is nested deeper" do - let(:deep_hash) { { :foo => { :blah => blah_list } } } - subject { RecursiveOpenStruct.new(deep_hash, :recurse_over_arrays => true) } + context 'when array is nested deeper' do + let(:deep_hash) { { foo: { blah: blah_list } } } + subject { RecursiveOpenStruct.new(deep_hash, recurse_over_arrays: true) } it { expect(subject.foo.blah.length).to eq 3 } - it "Retains changes across Array lookups" do - subject.foo.blah[1].foo = "Dr Scott" - expect(subject.foo.blah[1].foo).to eq "Dr Scott" + it 'Retains changes across Array lookups' do + subject.foo.blah[1].foo = 'Dr Scott' + expect(subject.foo.blah[1].foo).to eq 'Dr Scott' end - end - context "when array is in an array" do - let(:haah) { { :blah => [ blah_list ] } } - subject { RecursiveOpenStruct.new(haah, :recurse_over_arrays => true) } + context 'when array is in an array' do + let(:haah) { { blah: [blah_list] } } + subject { RecursiveOpenStruct.new(haah, recurse_over_arrays: true) } it { expect(subject.blah.length).to eq 1 } it { expect(subject.blah[0].length).to eq 3 } - it "Retains changes across Array lookups" do - subject.blah[0][1].foo = "Dr Scott" + it 'Retains changes across Array lookups' do + subject.blah[0][1].foo = 'Dr Scott' - expect(subject.blah[0][1].foo).to eq "Dr Scott" + expect(subject.blah[0][1].foo).to eq 'Dr Scott' end - end - end # when recursing over arrays is enabled - context "when recursing over arrays is disabled" do + context 'when recursing over arrays is disabled' do subject { RecursiveOpenStruct.new(h) } it { expect(subject.blah.length).to eq 3 } - it { expect(subject.blah[0]).to eq({ :foo => '1' }) } + it { expect(subject.blah[0]).to eq({ foo: '1' }) } it { expect(subject.blah[0][:foo]).to eq '1' } end # when recursing over arrays is disabled @@ -318,8 +318,8 @@ expect(subject.mystery.science[0].theatre).to eq 9000 end - specify "the changes show up in .to_h" do - expect(subject.to_h).to eq({ mystery: { science: [{theatre: 9000}]}}) + specify 'the changes show up in .to_h' do + expect(subject.to_h).to eq({ mystery: { science: [{ theatre: 9000 }] } }) end end @@ -330,7 +330,7 @@ subject.mystery.science[0] = {} end - it "can have new values be set" do + it 'can have new values be set' do expect do subject.mystery.science[0].theatre = 9000 end.to_not raise_error @@ -342,14 +342,15 @@ end # recursing over arrays describe 'nested nil values' do - let(:h) { { foo: { bar: nil }} } + let(:h) { { foo: { bar: nil } } } it 'returns nil' do expect(subject.foo.bar).to be_nil end it 'returns a hash with the key and a nil value' do - expect(subject.to_hash).to eq({ foo: { bar: nil }}) + expect(subject.to_hash).to eq({ foo: { bar: nil } }) end end # nested nil values end # recursive behavior end +# rubocop:enable Security/MarshalLoad diff --git a/spec/recursive_open_struct/wrapping_spec.rb b/spec/recursive_open_struct/wrapping_spec.rb index 7a6f2e4..4b9f2c2 100644 --- a/spec/recursive_open_struct/wrapping_spec.rb +++ b/spec/recursive_open_struct/wrapping_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require_relative '../spec_helper' require 'recursive_open_struct' describe RecursiveOpenStruct do describe 'wrapping RecursiveOpenStruct' do - let(:h) { { :blah => { :another => 'value' } } } + let(:h) { { blah: { another: 'value' } } } subject(:ros) { RecursiveOpenStruct.new(RecursiveOpenStruct.new(h)) } it 'can convert the entire hash tree back into a hash' do @@ -25,7 +27,7 @@ end describe 'wrapping OpenStruct' do - let(:h) { { :blah => { :another => 'value' } } } + let(:h) { { blah: { another: 'value' } } } subject(:ros) { RecursiveOpenStruct.new(OpenStruct.new(h)) } it 'can convert the entire hash tree back into a hash' do @@ -47,7 +49,7 @@ end describe 'wrapping a subclass' do - let(:h) { { :blah => { :another => 'value' } } } + let(:h) { { blah: { another: 'value' } } } let(:subclass) { Class.new(RecursiveOpenStruct) } subject(:ros) { subclass.new(subclass.new(h)) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f07057e..8fcab27 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) require 'rspec' @@ -10,10 +12,10 @@ # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } RSpec.configure do |config| config.run_all_when_everything_filtered = true config.filter_run :focus -# config.expect_with(:rspec) { |c| c.syntax = :should } + # config.expect_with(:rspec) { |c| c.syntax = :should } end From 8adc1cf3450fd0021e470ef0563c6355f9df5ee7 Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 16:12:29 -0700 Subject: [PATCH 06/10] Add rubocop-rake --- .rubocop.yml | 3 +++ Rakefile | 1 + recursive-open-struct.gemspec | 1 + 3 files changed, 5 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index fc7d918..7502ecf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,9 @@ # where the inspected file is and continue its way up to the root directory. # # See https://docs.rubocop.org/rubocop/configuration +plugins: + - 'rubocop-rake' + Layout/LineLength: Max: 120 diff --git a/Rakefile b/Rakefile index bf7fa20..1fa3517 100644 --- a/Rakefile +++ b/Rakefile @@ -38,6 +38,7 @@ end task default: %i[spec rubocop] +desc 'Ensure all files have appropriate permissions before building a package' task :fix_permissions do File.umask 0o022 filelist = `git ls-files`.split("\n") diff --git a/recursive-open-struct.gemspec b/recursive-open-struct.gemspec index a62848e..ffc2cb8 100644 --- a/recursive-open-struct.gemspec +++ b/recursive-open-struct.gemspec @@ -42,6 +42,7 @@ Gem::Specification.new do |s| s.add_development_dependency('rdoc', ['>= 0']) s.add_development_dependency('rspec', '~> 3.2') s.add_development_dependency('rubocop', ['>= 0']) + s.add_development_dependency('rubocop-rake', ['>= 0']) s.add_development_dependency('simplecov', ['>= 0']) s.add_dependency('ostruct') From cd3a3127e08c18b47fb7b5fd31c0232e2d144dff Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 17:11:16 -0700 Subject: [PATCH 07/10] Add rubocope-rspec, do a pass over all specs to clean some up --- .rubocop.yml | 12 + recursive-open-struct.gemspec | 1 + .../debug_inspect_spec.rb | 88 +++--- .../indifferent_access_spec.rb | 132 ++++----- .../open_struct_behavior_spec.rb | 40 +-- .../ostruct_2_0_0_spec.rb | 56 ++-- .../ostruct_2_3_0_spec.rb | 32 +-- .../recursion_and_subclassing_spec.rb | 4 +- spec/recursive_open_struct/recursion_spec.rb | 251 +++++++++--------- spec/recursive_open_struct/wrapping_spec.rb | 17 +- 10 files changed, 325 insertions(+), 308 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 7502ecf..39adbc9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -10,6 +10,7 @@ # See https://docs.rubocop.org/rubocop/configuration plugins: - 'rubocop-rake' + - 'rubocop-rspec' Layout/LineLength: Max: 120 @@ -42,3 +43,14 @@ Gemspec/RequiredRubyVersion: # is to officially support supported Ruby versions (and JRuby), but to not # officially support no-longer-maintained Ruby versions. +RSpec/ExampleLength: + Enabled: false +RSpec/NestedGroups: + Enabled: false +RSpec/MultipleExpectations: + Enabled: false + +# Not sure why this one still fails -- the specs are all in a +# recursive_open_struct directory. +RSpec/SpecFilePathFormat: + Enabled: false diff --git a/recursive-open-struct.gemspec b/recursive-open-struct.gemspec index ffc2cb8..db86b09 100644 --- a/recursive-open-struct.gemspec +++ b/recursive-open-struct.gemspec @@ -43,6 +43,7 @@ Gem::Specification.new do |s| s.add_development_dependency('rspec', '~> 3.2') s.add_development_dependency('rubocop', ['>= 0']) s.add_development_dependency('rubocop-rake', ['>= 0']) + s.add_development_dependency('rubocop-rspec', ['>= 0']) s.add_development_dependency('simplecov', ['>= 0']) s.add_dependency('ostruct') diff --git a/spec/recursive_open_struct/debug_inspect_spec.rb b/spec/recursive_open_struct/debug_inspect_spec.rb index 6fed06e..c1dd0f6 100644 --- a/spec/recursive_open_struct/debug_inspect_spec.rb +++ b/spec/recursive_open_struct/debug_inspect_spec.rb @@ -5,68 +5,42 @@ describe RecursiveOpenStruct do describe '#debug_inspect' do - before(:each) do + subject(:ros) do h1 = { a: 'a' } h2 = { a: 'b', h1: h1 } h1[:h2] = h2 - @ros = RecursiveOpenStruct.new(h2) + + described_class.new(h2) end - it 'should have a simple way of display' do - @output = <<~QUOTE - a = "b" - h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. - a = "b" - h1. - a = "a" - h2. - (recursion limit reached) - QUOTE - @io = StringIO.new - @ros.debug_inspect(@io) - expect(@io.string).to match(/^a = "b"$/) - expect(@io.string).to match(/^h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ a = "b"$/) - expect(@io.string).to match(/^ h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ a = "b"$/) - expect(@io.string).to match(/^ h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ a = "b"$/) - expect(@io.string).to match(/^ h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ a = "b"$/) - expect(@io.string).to match(/^ h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ a = "b"$/) - expect(@io.string).to match(/^ h1\.$/) - expect(@io.string).to match(/^ a = "a"$/) - expect(@io.string).to match(/^ h2\.$/) - expect(@io.string).to match(/^ \(recursion limit reached\)$/) + it 'has a simple way of display' do + io = StringIO.new + ros.debug_inspect(io) + expect(io.string).to match(/^a = "b"$/) + expect(io.string).to match(/^h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ a = "b"$/) + expect(io.string).to match(/^ h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ a = "b"$/) + expect(io.string).to match(/^ h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ a = "b"$/) + expect(io.string).to match(/^ h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ a = "b"$/) + expect(io.string).to match(/^ h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ a = "b"$/) + expect(io.string).to match(/^ h1\.$/) + expect(io.string).to match(/^ a = "a"$/) + expect(io.string).to match(/^ h2\.$/) + expect(io.string).to match(/^ \(recursion limit reached\)$/) end end end diff --git a/spec/recursive_open_struct/indifferent_access_spec.rb b/spec/recursive_open_struct/indifferent_access_spec.rb index 70acc1b..fdb60fd 100644 --- a/spec/recursive_open_struct/indifferent_access_spec.rb +++ b/spec/recursive_open_struct/indifferent_access_spec.rb @@ -4,105 +4,111 @@ require 'recursive_open_struct' describe RecursiveOpenStruct do - let(:value) { 'foo' } - let(:symbol) { :bar } - let(:new_value) { 'bar' } - let(:new_symbol) { :foo } - describe 'indifferent access' do - let(:hash) { { :foo => value, 'bar' => symbol } } + subject(:hash_ros) { described_class.new(hash, hash_ros_opts) } + + let(:hash) { { :foo => value, 'bar' => :bar } } let(:hash_ros_opts) { {} } - subject(:hash_ros) { RecursiveOpenStruct.new(hash, hash_ros_opts) } - context 'setting value with method' do - before(:each) do - subject.foo = value + describe 'setting value with method' do + let(:value) { 'foo' } + + before do + hash_ros.foo = value end - it('allows getting with method') { expect(subject.foo).to be value } - it('allows getting with symbol') { expect(subject[:foo]).to be value } - it('allows getting with string') { expect(subject['foo']).to be value } + it('allows getting with method') { expect(hash_ros.foo).to be value } + it('allows getting with symbol') { expect(hash_ros[:foo]).to be value } + it('allows getting with string') { expect(hash_ros['foo']).to be value } end - context 'setting value with symbol' do - before(:each) do - subject[:foo] = value + describe 'setting value with symbol' do + let(:value) { 'foo' } + + before do + hash_ros[:foo] = value end - it('allows getting with method') { expect(subject.foo).to be value } - it('allows getting with symbol') { expect(subject[:foo]).to be value } - it('allows getting with string') { expect(subject['foo']).to be value } + it('allows getting with method') { expect(hash_ros.foo).to be value } + it('allows getting with symbol') { expect(hash_ros[:foo]).to be value } + it('allows getting with string') { expect(hash_ros['foo']).to be value } end - context 'setting value with string' do - before(:each) do - subject['foo'] = value + describe 'setting value with string' do + let(:value) { 'foo' } + + before do + hash_ros['foo'] = value end - it('allows getting with method') { expect(subject.foo).to be value } - it('allows getting with symbol') { expect(subject[:foo]).to be value } - it('allows getting with string') { expect(subject['foo']).to be value } + it('allows getting with method') { expect(hash_ros.foo).to be value } + it('allows getting with symbol') { expect(hash_ros[:foo]).to be value } + it('allows getting with string') { expect(hash_ros['foo']).to be value } end - context 'overwriting values' do - context 'set with method' do - before(:each) do - subject.foo = value + describe 'overwriting values' do + let(:value) { 'foo' } + let(:new_value) { 'bar' } + + describe 'set with method' do + before do + hash_ros.foo = value end it('overrides with symbol') do - subject[:foo] = new_value - expect(subject.foo).to be new_value + hash_ros[:foo] = new_value + expect(hash_ros.foo).to be new_value end it('overrides with string') do - subject['foo'] = new_value - expect(subject.foo).to be new_value + hash_ros['foo'] = new_value + expect(hash_ros.foo).to be new_value end end - context 'set with symbol' do - before(:each) do - subject[:foo] = value + describe 'set with symbol' do + before do + hash_ros[:foo] = value end it('overrides with method') do - subject.foo = new_value - expect(subject[:foo]).to be new_value + hash_ros.foo = new_value + expect(hash_ros[:foo]).to be new_value end it('overrides with string') do - subject['foo'] = new_value - expect(subject[:foo]).to be new_value + hash_ros['foo'] = new_value + expect(hash_ros[:foo]).to be new_value end end - context 'set with string' do - before(:each) do - subject['foo'] = value + describe 'set with string' do + before do + hash_ros['foo'] = value end it('overrides with method') do - subject.foo = new_value - expect(subject['foo']).to be new_value + hash_ros.foo = new_value + expect(hash_ros['foo']).to be new_value end it('overrides with symbol') do - subject[:foo] = new_value - expect(subject['foo']).to be new_value + hash_ros[:foo] = new_value + expect(hash_ros['foo']).to be new_value end end - context 'set with hash' do + describe 'set with hash' do it('overrides with method') do hash_ros.foo = new_value expect(hash_ros[:foo]).to be new_value - + new_symbol = :foo hash_ros.bar = new_symbol expect(hash_ros['bar']).to be new_symbol end it('overrides with symbol') do + new_symbol = :foo hash_ros[:bar] = new_symbol expect(hash_ros['bar']).to be new_symbol end @@ -114,8 +120,10 @@ end context 'when preserve_original_keys is not enabled' do - context 'transforms original keys to symbols' do - subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true) } + # rubocop:disable RSpec/MultipleMemoizedHelpers + describe 'transforms original keys to symbols' do + subject(:recursive) { described_class.new(recursive_hash, recurse_over_arrays: true) } + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } let(:symbolized_recursive_hash) { { foo: [{ bar: [{ foo: :bar }] }] } } let(:symbolized_modified_hash) { { foo: [{ bar: [{ foo: :foo }] }] } } @@ -134,13 +142,16 @@ expect(recursive.to_h).to eq symbolized_modified_hash end end + # rubocop:enable RSpec/MultipleMemoizedHelpers end context 'when preserve_original_keys is enabled' do - context 'preserves the original keys' do + # rubocop:disable RSpec/MultipleMemoizedHelpers + describe 'preserves the original keys' do subject(:recursive) do - RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true, preserve_original_keys: true) + described_class.new(recursive_hash, recurse_over_arrays: true, preserve_original_keys: true) end + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } let(:modified_hash) { { foo: [{ 'bar' => [{ 'foo' => :foo }] }] } } @@ -159,11 +170,13 @@ expect(recursive.to_h).to eq modified_hash end end + # rubocop:enable RSpec/MultipleMemoizedHelpers end context 'when undefined method' do context 'when raise_on_missing is enabled' do - subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, raise_on_missing: true) } + subject(:recursive) { described_class.new(recursive_hash, raise_on_missing: true) } + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } specify 'raises NoMethodError' do @@ -174,13 +187,12 @@ end context 'when raise_on_missing is disabled' do - context 'preserves the original keys' do - subject(:recursive) { RecursiveOpenStruct.new(recursive_hash) } - let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } + subject(:recursive) { described_class.new(recursive_hash) } + + let(:recursive_hash) { { foo: [{ 'bar' => [{ 'foo' => :bar }] }] } } - specify 'returns nil' do - expect(recursive.undefined_method).to be_nil - end + specify 'returns nil' do + expect(recursive.undefined_method).to be_nil end end end diff --git a/spec/recursive_open_struct/open_struct_behavior_spec.rb b/spec/recursive_open_struct/open_struct_behavior_spec.rb index fa5ff23..2fb0b71 100644 --- a/spec/recursive_open_struct/open_struct_behavior_spec.rb +++ b/spec/recursive_open_struct/open_struct_behavior_spec.rb @@ -4,12 +4,14 @@ require 'recursive_open_struct' describe RecursiveOpenStruct do + subject(:ros) { described_class.new(hash) } + let(:hash) { {} } - subject(:ros) { RecursiveOpenStruct.new(hash) } describe 'behavior it inherits from OpenStruct' do context 'when not initialized from anything' do - subject(:ros) { RecursiveOpenStruct.new } + subject(:ros) { described_class.new } + it 'can represent arbitrary data objects' do ros.blah = 'John Smith' expect(ros.blah).to eq 'John Smith' @@ -22,6 +24,7 @@ context 'when initialized with nil' do let(:hash) { nil } + it 'returns nil for missing attributes' do expect(ros.foo).to be_nil end @@ -36,7 +39,7 @@ context 'when initialized from a hash' do let(:hash) { { asdf: 'John Smith' } } - context 'that contains symbol keys' do + context 'when it contains symbol keys' do it 'turns those symbol keys into method names' do expect(ros.asdf).to eq 'John Smith' end @@ -47,14 +50,15 @@ expect(ros.asdf).to eq 'George Washington' end - context 'that contains string keys' do + context 'when it contains string keys' do let(:hash) { { 'asdf' => 'John Smith' } } + it 'turns those string keys into method names' do expect(ros.asdf).to eq 'John Smith' end end - context 'that contains keys that mirror existing private methods' do + context 'when it contains keys that mirror existing private methods' do let(:hash) { { test: :foo, rand: 'not a number' } } # https://github.com/aetherknight/recursive-open-struct/issues/42 @@ -67,8 +71,9 @@ end end - context 'that contains keys that mirror existing public methods inherited from Object' do + context 'when it contains keys that mirror existing public methods inherited from Object' do let(:hash) { { method: :something } } + it 'handles subscript notation without calling the existing methods' do expect(ros[:method]).to eq :something expect(ros['method']).to eq :something @@ -91,23 +96,24 @@ end describe 'handling of arbitrary attributes' do - subject { RecursiveOpenStruct.new } - before(:each) do - subject.blah = 'John Smith' + subject(:ros) { described_class.new } + + before do + ros.blah = 'John Smith' end describe '#respond?' do - it { expect(subject).to respond_to :blah } - it { expect(subject).to respond_to :blah= } - it { expect(subject).to_not respond_to :asdf } - it { expect(subject).to_not respond_to :asdf= } + it { expect(ros).to respond_to :blah } + it { expect(ros).to respond_to :blah= } + it { expect(ros).not_to respond_to :asdf } + it { expect(ros).not_to respond_to :asdf= } end # describe #respond? describe '#methods' do - it { expect(subject.methods.map(&:to_sym)).to include :blah } - it { expect(subject.methods.map(&:to_sym)).to include :blah= } - it { expect(subject.methods.map(&:to_sym)).to_not include :asdf } - it { expect(subject.methods.map(&:to_sym)).to_not include :asdf= } + it { expect(ros.methods.map(&:to_sym)).to include :blah } + it { expect(ros.methods.map(&:to_sym)).to include :blah= } + it { expect(ros.methods.map(&:to_sym)).not_to include :asdf } + it { expect(ros.methods.map(&:to_sym)).not_to include :asdf= } end # describe #methods end # describe handling of arbitrary attributes diff --git a/spec/recursive_open_struct/ostruct_2_0_0_spec.rb b/spec/recursive_open_struct/ostruct_2_0_0_spec.rb index b774452..3f9554e 100644 --- a/spec/recursive_open_struct/ostruct_2_0_0_spec.rb +++ b/spec/recursive_open_struct/ostruct_2_0_0_spec.rb @@ -4,90 +4,94 @@ require 'recursive_open_struct' describe RecursiveOpenStruct do + subject(:ros) { described_class.new(hash) } + let(:hash) { { :foo => 'foo', 'bar' => :bar } } - subject(:ros) { RecursiveOpenStruct.new(hash) } describe 'OpenStruct 2.0+ methods' do - context 'Hash style setter' do + describe '#[]=' do it 'method exists' do - expect(ros.respond_to?('[]=')).to be_truthy + expect(ros).to respond_to '[]=' end it 'changes the value' do ros[:foo] = :foo - ros.foo = :foo + expect(ros.foo).to be :foo end end - context 'delete_field' do - before(:each) { ros.delete_field :foo } + describe '#delete_field' do + before { ros.delete_field :foo } it 'removes the value' do expect(ros.foo).to be_nil - expect(ros.to_h).to_not include(:foo) + expect(ros.to_h).not_to include(:foo) end it 'removes the getter method' do - is_expected.to_not respond_to :foo + expect(ros).not_to respond_to :foo end it 'removes the setter method' do - expect(ros.respond_to?('foo=')).to be_falsey + expect(ros).not_to respond_to 'foo=' end it 'works with indifferent access' do expect(ros.delete_field(:bar)).to eq :bar - is_expected.to_not respond_to :bar - is_expected.to_not respond_to 'bar=' + expect(ros).not_to respond_to :bar + expect(ros).not_to respond_to 'bar=' expect(ros.to_h).to be_empty end end - context 'eql?' do + describe '#eql?' do subject(:new_ros) { ros.dup } context 'with identical ROS' do subject { ros } - it { is_expected.to be_eql ros } + + it { is_expected.to eql ros } end context 'with similar ROS' do - subject { RecursiveOpenStruct.new(hash) } - it { is_expected.to be_eql ros } + subject { described_class.new(hash) } + + it { is_expected.to eql ros } end context 'with same Hash' do - subject { RecursiveOpenStruct.new(hash, recurse_over_arrays: true) } - it { is_expected.to be_eql ros } + subject { described_class.new(hash, recurse_over_arrays: true) } + + it { is_expected.to eql ros } end context 'with duplicated ROS' do - subject { ros.dup } + subject(:duped_ros) { ros.dup } it 'fails on different value' do - subject.foo = 'bar' - is_expected.not_to be_eql ros + duped_ros.foo = 'bar' + expect(duped_ros).not_to eql ros end it 'fails on missing field' do - subject.delete_field :bar - is_expected.not_to be_eql ros + duped_ros.delete_field :bar + expect(duped_ros).not_to eql ros end it 'fails on added field' do - subject.baz = :baz - is_expected.not_to be_eql ros + duped_ros.baz = :baz + expect(duped_ros).not_to eql ros end end end - context 'hash' do + describe '#hash' do it 'calculates table hash' do expect(ros.hash).to eq(ros.instance_variable_get('@table').hash) end end - context 'each_pair' do + describe '#each_pair' do it 'iterates over hash keys, with keys as symbol' do ros_pairs = [] ros.each_pair { |k, v| ros_pairs << [k, v] } diff --git a/spec/recursive_open_struct/ostruct_2_3_0_spec.rb b/spec/recursive_open_struct/ostruct_2_3_0_spec.rb index cf6573f..6e7e5d3 100644 --- a/spec/recursive_open_struct/ostruct_2_3_0_spec.rb +++ b/spec/recursive_open_struct/ostruct_2_3_0_spec.rb @@ -9,43 +9,45 @@ describe '#dig' do # We only care when Ruby supports `#dig`. if OpenStruct.public_instance_methods.include? :dig - context 'recurse_over_arrays: false' do - subject { RecursiveOpenStruct.new(a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] }) } + context 'when recurse_over_arrays: false' do + subject(:ros) { described_class.new(a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] }) } describe 'OpenStruct-like behavior' do - it { expect(subject.dig(:a, :b)).to eq 2 } - it { expect(subject.dig(:a, :c, 0)).to eq 'doo' } - it { expect(subject.dig(:a, :c, 2, :inner)).to eq 'one' } + it { expect(ros.dig(:a, :b)).to eq 2 } + it { expect(ros.dig(:a, :c, 0)).to eq 'doo' } + it { expect(ros.dig(:a, :c, 2, :inner)).to eq 'one' } end describe 'recursive behavior' do it { - expect(subject.dig(:a)).to eq RecursiveOpenStruct.new( + expect(ros.dig(:a)).to eq described_class.new( { b: 2, c: ['doo', 'bee', { inner: 'one' }] } ) } - it { expect(subject.dig(:a, :c, 2)).to eq({ inner: 'one' }) } + + it { expect(ros.dig(:a, :c, 2)).to eq({ inner: 'one' }) } end end - context 'recurse_over_arrays: true' do - subject do - RecursiveOpenStruct.new({ a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] } }, recurse_over_arrays: true) + context 'when recurse_over_arrays: true' do + subject(:ros) do + described_class.new({ a: { b: 2, c: ['doo', 'bee', { inner: 'one' }] } }, recurse_over_arrays: true) end describe 'OpenStruct-like behavior' do - it { expect(subject.dig(:a, :b)).to eq 2 } - it { expect(subject.dig(:a, :c, 0)).to eq 'doo' } - it { expect(subject.dig(:a, :c, 2, :inner)).to eq 'one' } + it { expect(ros.dig(:a, :b)).to eq 2 } + it { expect(ros.dig(:a, :c, 0)).to eq 'doo' } + it { expect(ros.dig(:a, :c, 2, :inner)).to eq 'one' } end describe 'recursive behavior' do it { - expect(subject.dig(:a)).to eq RecursiveOpenStruct.new( + expect(ros.dig(:a)).to eq described_class.new( { b: 2, c: ['doo', 'bee', { inner: 'one' }] } ) } - it { expect(subject.dig(:a, :c, 2)).to eq RecursiveOpenStruct.new(inner: 'one') } + + it { expect(ros.dig(:a, :c, 2)).to eq described_class.new(inner: 'one') } end end end diff --git a/spec/recursive_open_struct/recursion_and_subclassing_spec.rb b/spec/recursive_open_struct/recursion_and_subclassing_spec.rb index 4662791..4d5a458 100644 --- a/spec/recursive_open_struct/recursion_and_subclassing_spec.rb +++ b/spec/recursive_open_struct/recursion_and_subclassing_spec.rb @@ -5,10 +5,10 @@ describe RecursiveOpenStruct do describe 'subclassing RecursiveOpenStruct' do - let(:subclass) { Class.new(RecursiveOpenStruct) } - subject(:rossc) { subclass.new({ one: [{ two: :three }] }, recurse_over_arrays: true) } + let(:subclass) { Class.new(described_class) } + specify 'nested objects use the subclass of the parent' do expect(rossc.one.first.class).to eq subclass end diff --git a/spec/recursive_open_struct/recursion_spec.rb b/spec/recursive_open_struct/recursion_spec.rb index 5e26979..393c8db 100644 --- a/spec/recursive_open_struct/recursion_spec.rb +++ b/spec/recursive_open_struct/recursion_spec.rb @@ -6,28 +6,29 @@ describe RecursiveOpenStruct do describe 'recursive behavior' do + subject(:ros) { described_class.new(h) } + let(:h) { { blah: { another: 'value' } } } - subject(:ros) { RecursiveOpenStruct.new(h) } it 'can convert the entire hash tree back into a hash' do blank_obj = Object.new h = { asdf: 'John Smith', foo: [{ bar: blank_obj }, { baz: nil }] } - ros = RecursiveOpenStruct.new(h) + ros = described_class.new(h) expect(ros.to_h).to eq h expect(ros.to_hash).to eq h end it 'returns accessed hashes as RecursiveOpenStructs instead of hashes' do - expect(subject.blah.another).to eq 'value' + expect(ros.blah.another).to eq 'value' end it 'handles subscript notation the same way as dotted notation' do - expect(subject.blah.another).to eq subject[:blah].another + expect(ros.blah.another).to eq ros[:blah].another end it 'uses #key_as_a_hash to return key as a Hash' do - expect(subject.blah_as_a_hash).to eq({ another: 'value' }) + expect(ros.blah_as_a_hash).to eq({ another: 'value' }) end it 'handles sub-element replacement with dotted notation before member setup' do @@ -40,69 +41,68 @@ end it 'handles being dump then loaded by Marshal' do - foo_struct = [RecursiveOpenStruct.new] - bar_struct = RecursiveOpenStruct.new(foo: foo_struct) + foo_struct = [described_class.new] + bar_struct = described_class.new(foo: foo_struct) serialized = Marshal.dump(bar_struct) expect(Marshal.load(serialized).foo).to eq(foo_struct) end describe 'handling loops in the original Hashes' do - let(:h1) { { a: 'a' } } - let(:h2) { { a: 'b', h1: h1 } } - before(:each) { h1[:h2] = h2 } - - subject { RecursiveOpenStruct.new(h2) } - - it { expect(subject.h1.a).to eq 'a' } - it { expect(subject.h1.h2.a).to eq 'b' } - it { expect(subject.h1.h2.h1.a).to eq 'a' } - it { expect(subject.h1.h2.h1.h2.a).to eq 'b' } - it { expect(subject.h1).to eq subject.h1.h2.h1 } - it { expect(subject.h1).to_not eq subject.h1.h2 } + subject(:ros) { described_class.new(h) } + + let(:nested_h) { { a: 'a' } } + let(:h) { { a: 'b', nested_h: nested_h } } + + before { nested_h[:h] = h } + + it { expect(ros.nested_h.a).to eq 'a' } + it { expect(ros.nested_h.h.a).to eq 'b' } + it { expect(ros.nested_h.h.nested_h.a).to eq 'a' } + it { expect(ros.nested_h.h.nested_h.h.a).to eq 'b' } + it { expect(ros.nested_h).to eq ros.nested_h.h.nested_h } + it { expect(ros.nested_h).not_to eq ros.nested_h.h } end # describe handling loops in the origin Hashes it 'can modify a key of a sub-element' do - h = { - blah: { - blargh: 'Brad' - } - } - ros = RecursiveOpenStruct.new(h) + h = { blah: { blargh: 'Brad' } } + ros = described_class.new(h) ros.blah.blargh = 'Janet' expect(ros.blah.blargh).to eq 'Janet' end describe 'subscript mutation notation' do + let(:diff) { { different: 'thing' } } + it 'handles the basic case' do - subject[:blah] = 12_345 - expect(subject.blah).to eql 12_345 + ros[:blah] = 12_345 + expect(ros.blah).to be 12_345 end it 'recurses properly' do - subject[:blah][:another] = 'abc' - expect(subject.blah.another).to eql 'abc' - expect(subject.blah_as_a_hash).to eql({ another: 'abc' }) + ros[:blah][:another] = 'abc' + expect(ros.blah.another).to eql 'abc' + expect(ros.blah_as_a_hash).to eql({ another: 'abc' }) end - let(:diff) { { different: 'thing' } } - it 'can replace the entire hash' do - expect(subject.to_h).to eql(h) - subject[:blah] = diff - expect(subject.to_h).to eql({ blah: diff }) + expect(ros.to_h).to eql(h) + ros[:blah] = diff + expect(ros.to_h).to eql({ blah: diff }) end it 'updates sub-element cache' do - expect(subject.blah.different).to be_nil - subject[:blah] = diff - expect(subject.blah.different).to eql 'thing' - expect(subject.blah_as_a_hash).to eql(diff) + expect(ros.blah.different).to be_nil + ros[:blah] = diff + expect(ros.blah.different).to eql 'thing' + expect(ros.blah_as_a_hash).to eql(diff) end end - context 'after a sub-element has been modified' do + context 'when a sub-element has been modified' do + subject(:ros) { described_class.new(hash) } + let(:hash) do { blah: { blargh: 'Brad' }, some_array: [1, 2, 3] } end @@ -110,19 +110,17 @@ { blah: { blargh: 'Janet' }, some_array: [1, 2, 3] } end - subject { RecursiveOpenStruct.new(hash) } - - before(:each) { subject.blah.blargh = 'Janet' } + before { ros.blah.blargh = 'Janet' } describe '.to_h' do it 'returns a hash tree that contains those modifications' do - expect(subject.to_h).to eq updated_hash + expect(ros.to_h).to eq updated_hash end specify 'modifying the returned hash tree does not modify the ROS' do - subject.to_h[:blah][:blargh] = 'Dr Scott' + ros.to_h[:blah][:blargh] = 'Dr Scott' - expect(subject.blah.blargh).to eq 'Janet' + expect(ros.blah.blargh).to eq 'Janet' end end @@ -131,50 +129,50 @@ end it 'limits the deep-copy to the initial hash tree' do - subject.some_array[0] = 4 + ros.some_array[0] = 4 expect(hash[:some_array][0]).to eq 4 end describe '#dup' do - let(:duped_subject) { subject.dup } + let(:duped_subject) { ros.dup } it 'preserves sub-element modifications' do - expect(duped_subject.blah.blargh).to eq subject.blah.blargh + expect(duped_subject.blah.blargh).to eq ros.blah.blargh end it "allows the copy's sub-elements to be modified independently from the original's" do - expect(subject.blah.blargh).to eq 'Janet' + expect(ros.blah.blargh).to eq 'Janet' duped_subject.blah.blargh = 'Dr. Scott' - expect(subject.blah.blargh).to eq 'Janet' + expect(ros.blah.blargh).to eq 'Janet' expect(duped_subject.blah.blargh).to eq 'Dr. Scott' end end end context 'when memoizing and then modifying entire recursive structures' do - subject do - RecursiveOpenStruct.new( + subject(:ros) do + described_class.new( { blah: original_blah }, recurse_over_arrays: true ) end - before(:each) { subject.blah } # enforce memoization + before { ros.blah } # enforce memoization context 'when modifying an entire Hash' do let(:original_blah) { { a: 'A', b: 'B' } } let(:new_blah) { { something_new: 'C' } } - before(:each) { subject.blah = new_blah } + before { ros.blah = new_blah } it 'returns the modified value instead of the memoized one' do - expect(subject.blah.something_new).to eq 'C' + expect(ros.blah.something_new).to eq 'C' end specify 'the old value no longer exists' do - expect(subject.blah.a).to be_nil + expect(ros.blah.a).to be_nil end end @@ -183,8 +181,8 @@ it 'returns the modified value instead of the memoized one' do new_blah = [4, 5, 6] - subject.blah = new_blah - expect(subject.blah).to eq new_blah + ros.blah = new_blah + expect(ros.blah).to eq new_blah end end end @@ -194,148 +192,152 @@ let(:h) { { blah: blah_list } } context 'when dump and loaded by Marshal' do - let(:test) { RecursiveOpenStruct.new(h, recurse_over_arrays: true) } - subject { Marshal.load(Marshal.dump(test)) } - - it { expect(subject.blah.length).to eq 3 } - it { expect(subject.blah[0].foo).to eq '1' } - it { expect(subject.blah[1].foo).to eq '2' } - it { expect(subject.blah_as_a_hash).to eq blah_list } - it { expect(subject.blah[2]).to eq 'baz' } + subject(:ros) { Marshal.load(Marshal.dump(test)) } + + let(:test) { described_class.new(h, recurse_over_arrays: true) } + + it { expect(ros.blah.length).to eq 3 } + it { expect(ros.blah[0].foo).to eq '1' } + it { expect(ros.blah[1].foo).to eq '2' } + it { expect(ros.blah_as_a_hash).to eq blah_list } + it { expect(ros.blah[2]).to eq 'baz' } end context 'when recursing over arrays is enabled' do - subject { RecursiveOpenStruct.new(h, recurse_over_arrays: true) } + subject(:ros) { described_class.new(h, recurse_over_arrays: true) } - it { expect(subject.blah.length).to eq 3 } - it { expect(subject.blah[0].foo).to eq '1' } - it { expect(subject.blah[1].foo).to eq '2' } - it { expect(subject.blah_as_a_hash).to eq blah_list } - it { expect(subject.blah[2]).to eq 'baz' } + it { expect(ros.blah.length).to eq 3 } + it { expect(ros.blah[0].foo).to eq '1' } + it { expect(ros.blah[1].foo).to eq '2' } + it { expect(ros.blah_as_a_hash).to eq blah_list } + it { expect(ros.blah[2]).to eq 'baz' } context 'when an inner value changes' do let(:updated_blah_list) { [{ foo: '1' }, { foo: 'Dr Scott' }, 'baz'] } let(:updated_h) { { blah: updated_blah_list } } - before(:each) { subject.blah[1].foo = 'Dr Scott' } + before { ros.blah[1].foo = 'Dr Scott' } it 'Retains changes across Array lookups' do - expect(subject.blah[1].foo).to eq 'Dr Scott' + expect(ros.blah[1].foo).to eq 'Dr Scott' end it 'propagates the changes through to .to_h across Array lookups' do - expect(subject.to_h).to eq({ - blah: [{ foo: '1' }, { foo: 'Dr Scott' }, 'baz'] - }) + expect(ros.to_h).to eq({ + blah: [{ foo: '1' }, { foo: 'Dr Scott' }, 'baz'] + }) end it 'deep-copies hashes within Arrays' do - subject.to_h[:blah][1][:foo] = 'Rocky' + ros.to_h[:blah][1][:foo] = 'Rocky' - expect(subject.blah[1].foo).to eq 'Dr Scott' + expect(ros.blah[1].foo).to eq 'Dr Scott' end - it 'does not mutate the input hash passed to the constructor' do - expect(h[:blah][1][:foo]).to eq '2' - end - - it 'the deep copy recurses over Arrays as well' do + it 'does not mutate the input hash passed to the constructor (works when recursing over arrays too)' do expect(h[:blah][1][:foo]).to eq '2' end describe '#dup' do - let(:duped_subject) { subject.dup } + let(:duped_subject) { ros.dup } it 'preserves sub-element modifications' do - expect(duped_subject.blah[1].foo).to eq subject.blah[1].foo + expect(duped_subject.blah[1].foo).to eq ros.blah[1].foo end it "allows the copy's sub-elements to be modified independently from the original's" do duped_subject.blah[1].foo = 'Rocky' expect(duped_subject.blah[1].foo).to eq 'Rocky' - expect(subject.blah[1].foo).to eq 'Dr Scott' + expect(ros.blah[1].foo).to eq 'Dr Scott' end end end context 'when array is nested deeper' do + subject(:ros) { described_class.new(deep_hash, recurse_over_arrays: true) } + let(:deep_hash) { { foo: { blah: blah_list } } } - subject { RecursiveOpenStruct.new(deep_hash, recurse_over_arrays: true) } - it { expect(subject.foo.blah.length).to eq 3 } + it { expect(ros.foo.blah.length).to eq 3 } + it 'Retains changes across Array lookups' do - subject.foo.blah[1].foo = 'Dr Scott' - expect(subject.foo.blah[1].foo).to eq 'Dr Scott' + ros.foo.blah[1].foo = 'Dr Scott' + expect(ros.foo.blah[1].foo).to eq 'Dr Scott' end end context 'when array is in an array' do + subject(:ros) { described_class.new(haah, recurse_over_arrays: true) } + let(:haah) { { blah: [blah_list] } } - subject { RecursiveOpenStruct.new(haah, recurse_over_arrays: true) } - it { expect(subject.blah.length).to eq 1 } - it { expect(subject.blah[0].length).to eq 3 } + it { expect(ros.blah.length).to eq 1 } + it { expect(ros.blah[0].length).to eq 3 } + it 'Retains changes across Array lookups' do - subject.blah[0][1].foo = 'Dr Scott' + ros.blah[0][1].foo = 'Dr Scott' - expect(subject.blah[0][1].foo).to eq 'Dr Scott' + expect(ros.blah[0][1].foo).to eq 'Dr Scott' end end end # when recursing over arrays is enabled context 'when recursing over arrays is disabled' do - subject { RecursiveOpenStruct.new(h) } + subject(:ros) { described_class.new(h) } - it { expect(subject.blah.length).to eq 3 } - it { expect(subject.blah[0]).to eq({ foo: '1' }) } - it { expect(subject.blah[0][:foo]).to eq '1' } + it { expect(ros.blah.length).to eq 3 } + it { expect(ros.blah[0]).to eq({ foo: '1' }) } + it { expect(ros.blah[0][:foo]).to eq '1' } end # when recursing over arrays is disabled describe 'modifying an array and recursing over it' do + subject(:ros) { described_class.new(h, recurse_over_arrays: true) } + let(:h) { {} } - subject { RecursiveOpenStruct.new(h, recurse_over_arrays: true) } context 'when adding an array with hashes into the tree' do - before(:each) do - subject.mystery = {} - subject.mystery.science = [{ theatre: 9000 }] + before do + ros.mystery = {} + ros.mystery.science = [{ theatre: 9000 }] end it "ROS's it" do - expect(subject.mystery.science[0].theatre).to eq 9000 + expect(ros.mystery.science[0].theatre).to eq 9000 end end context 'when appending a hash to an array' do - before(:each) do - subject.mystery = {} - subject.mystery.science = [] - subject.mystery.science << { theatre: 9000 } + before do + ros.mystery = {} + ros.mystery.science = [] + ros.mystery.science << { theatre: 9000 } end it "ROS's it" do - expect(subject.mystery.science[0].theatre).to eq 9000 + expect(ros.mystery.science[0].theatre).to eq 9000 end specify 'the changes show up in .to_h' do - expect(subject.to_h).to eq({ mystery: { science: [{ theatre: 9000 }] } }) + expect(ros.to_h).to eq({ mystery: { science: [{ theatre: 9000 }] } }) + end + + specify 'and the new ROS/hash can have new values set' do + ros.mystery.science[0].gizmoplex = 9000 + expect(ros.mystery.science[0].gizmoplex).to eq 9000 end end - context 'after appending a hash to an array' do - before(:each) do - subject.mystery = {} - subject.mystery.science = [] - subject.mystery.science[0] = {} + context 'when assigning a hash to an array' do + before do + ros.mystery = {} + ros.mystery.science = [] + ros.mystery.science[0] = {} end it 'can have new values be set' do - expect do - subject.mystery.science[0].theatre = 9000 - end.to_not raise_error - - expect(subject.mystery.science[0].theatre).to eq 9000 + ros.mystery.science[0].theatre = 9000 + expect(ros.mystery.science[0].theatre).to eq 9000 end end end # modifying an array and then recursing @@ -343,12 +345,13 @@ describe 'nested nil values' do let(:h) { { foo: { bar: nil } } } + it 'returns nil' do - expect(subject.foo.bar).to be_nil + expect(ros.foo.bar).to be_nil end it 'returns a hash with the key and a nil value' do - expect(subject.to_hash).to eq({ foo: { bar: nil } }) + expect(ros.to_hash).to eq({ foo: { bar: nil } }) end end # nested nil values end # recursive behavior diff --git a/spec/recursive_open_struct/wrapping_spec.rb b/spec/recursive_open_struct/wrapping_spec.rb index 4b9f2c2..6d23d8b 100644 --- a/spec/recursive_open_struct/wrapping_spec.rb +++ b/spec/recursive_open_struct/wrapping_spec.rb @@ -5,15 +5,16 @@ describe RecursiveOpenStruct do describe 'wrapping RecursiveOpenStruct' do + subject(:ros) { described_class.new(described_class.new(h)) } + let(:h) { { blah: { another: 'value' } } } - subject(:ros) { RecursiveOpenStruct.new(RecursiveOpenStruct.new(h)) } it 'can convert the entire hash tree back into a hash' do expect(ros.to_h).to eq h end it 'can access the flat keys' do - expect(ros.blah).to be_a(RecursiveOpenStruct) + expect(ros.blah).to be_a(described_class) end it 'can access the nested keys' do @@ -27,15 +28,16 @@ end describe 'wrapping OpenStruct' do + subject(:ros) { described_class.new(OpenStruct.new(h)) } + let(:h) { { blah: { another: 'value' } } } - subject(:ros) { RecursiveOpenStruct.new(OpenStruct.new(h)) } it 'can convert the entire hash tree back into a hash' do expect(ros.to_h).to eq h end it 'can access the flat keys' do - expect(ros.blah).to be_a(RecursiveOpenStruct) + expect(ros.blah).to be_a(described_class) end it 'can access the nested keys' do @@ -49,16 +51,17 @@ end describe 'wrapping a subclass' do - let(:h) { { blah: { another: 'value' } } } - let(:subclass) { Class.new(RecursiveOpenStruct) } subject(:ros) { subclass.new(subclass.new(h)) } + let(:h) { { blah: { another: 'value' } } } + let(:subclass) { Class.new(described_class) } + it 'can convert the entire hash tree back into a hash' do expect(ros.to_h).to eq h end it 'can access the flat keys' do - expect(ros.blah).to be_a(RecursiveOpenStruct) + expect(ros.blah).to be_a(described_class) end it 'can access the nested keys' do From bcc8490f80b509476bba6b207d234b8e4ce7551c Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 17:27:12 -0700 Subject: [PATCH 08/10] Add notes about testing and development to CONTRIBUTING.md --- CONTRIBUTING.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 011a348..863c191 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,3 +49,31 @@ When contributing code that changes behavior or fixes bugs, please include unit tests to cover the new behavior or to provide regression testing for bugs. Also, treat the unit tests as documentation --- make sure they are clean, clear, and concise, and well organized. + +## Testing and Development + +- This project uses RSpec to both document and test behavior. Please structure + your tests help someone understand what is being tested. +- It also uses rubocop to autoformat and lint the project. Run it locally + before committing. + +You can run both the test suite and the linter with the following commands: + +```sh +# install/update dependencies +bundle + +# run the test suite and linter +bundle exec rake + +# have the linter apply all safe fixes (eg, autoformatting) +bundle exec rake rubocop:autocorrect +``` + +## Release Process + +1. Update CHANGELOG.md and lib/recursive_open_struct/version.rb +2. Run `bundle exec rake update_authors` +3. Make a version release commit +4. Run the release task (it will tag, build, push code, push package): + `bundle exec rake release` From f560e2d5689b1300fe0794c9a2d92459433b105f Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 17:31:15 -0700 Subject: [PATCH 09/10] Sort the rspec support files for older rubies --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8fcab27..c913d7b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,7 @@ # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } RSpec.configure do |config| config.run_all_when_everything_filtered = true From 10d1d0994c2bfa62e9a0762c54d23de1156f08a6 Mon Sep 17 00:00:00 2001 From: "Wynn (B.J.) Snow Orvis" Date: Sat, 18 Apr 2026 17:35:57 -0700 Subject: [PATCH 10/10] Pin many dev dependencies to semver ranges --- recursive-open-struct.gemspec | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/recursive-open-struct.gemspec b/recursive-open-struct.gemspec index db86b09..48302d6 100644 --- a/recursive-open-struct.gemspec +++ b/recursive-open-struct.gemspec @@ -36,14 +36,14 @@ Gem::Specification.new do |s| 'README.md' ] - s.add_development_dependency('bundler', ['>= 0']) + s.add_development_dependency('bundler', ['>= 2']) s.add_development_dependency('pry', ['>= 0']) - s.add_development_dependency('rake', ['>= 0']) - s.add_development_dependency('rdoc', ['>= 0']) - s.add_development_dependency('rspec', '~> 3.2') - s.add_development_dependency('rubocop', ['>= 0']) - s.add_development_dependency('rubocop-rake', ['>= 0']) - s.add_development_dependency('rubocop-rspec', ['>= 0']) + s.add_development_dependency('rake', ['~>13.4']) + s.add_development_dependency('rdoc', ['~>7.2']) + s.add_development_dependency('rspec', '~> 3.13') + s.add_development_dependency('rubocop', ['~>1.86']) + s.add_development_dependency('rubocop-rake', ['~>0.7']) + s.add_development_dependency('rubocop-rspec', ['~>3.9']) s.add_development_dependency('simplecov', ['>= 0']) s.add_dependency('ostruct')