From 0b379f374e00409c52f9fb7e48e61f0d49c8f018 Mon Sep 17 00:00:00 2001 From: Justin Harris Date: Thu, 9 Apr 2026 17:37:21 -0400 Subject: [PATCH] [Ruby][from_hash] Fix nilable Symbol conversion and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract _unwrap_nilable helper to deduplicate nilable type unwrapping across Symbol and Array handling in _convert_value. 🤖 Generated with the help of Claude Code. This commit message was generated by AI, but the user has most likely reviewed all of the code changes. Co-Authored-By: Claude --- ruby/from_hash/.rubocop.yml | 5 +++- ruby/from_hash/Gemfile.lock | 2 +- .../lib/optify_from_hash/from_hashable.rb | 24 +++++++++++------ ruby/from_hash/optify-from_hash.gemspec | 2 +- ruby/from_hash/test/.rubocop.yml | 2 +- ruby/from_hash/test/from_hash_test.rb | 27 +++++++++++++++++++ 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/ruby/from_hash/.rubocop.yml b/ruby/from_hash/.rubocop.yml index 3199c060..de158c2c 100644 --- a/ruby/from_hash/.rubocop.yml +++ b/ruby/from_hash/.rubocop.yml @@ -14,6 +14,9 @@ Layout/LineLength: Metrics/AbcSize: Max: 22 +Metrics/ClassLength: + Max: 106 + Metrics/CyclomaticComplexity: Max: 12 @@ -26,4 +29,4 @@ Metrics/PerceivedComplexity: Naming/FileName: Exclude: # Filename deliberately matches gem name - - 'lib/optify-from_hash.rb' \ No newline at end of file + - 'lib/optify-from_hash.rb' diff --git a/ruby/from_hash/Gemfile.lock b/ruby/from_hash/Gemfile.lock index 3c3759c9..df972ef5 100644 --- a/ruby/from_hash/Gemfile.lock +++ b/ruby/from_hash/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - optify-from_hash (0.2.1) + optify-from_hash (0.2.2) sorbet-runtime (>= 0.5, < 1) GEM diff --git a/ruby/from_hash/lib/optify_from_hash/from_hashable.rb b/ruby/from_hash/lib/optify_from_hash/from_hashable.rb index 27b83951..e4b5db71 100644 --- a/ruby/from_hash/lib/optify_from_hash/from_hashable.rb +++ b/ruby/from_hash/lib/optify_from_hash/from_hashable.rb @@ -46,16 +46,13 @@ def self._convert_value(value, type) return value end - return value.to_sym if type.is_a?(T::Types::Simple) && type.raw_type == Symbol + unwrapped_type = _unwrap_nilable(type) + return value&.to_sym if unwrapped_type.is_a?(T::Types::Simple) && unwrapped_type.raw_type == Symbol case value when Array - # Handle `T.nilable(T::Array[...])` - if type.respond_to?(:unwrap_nilable) - type = type #: as untyped - .unwrap_nilable - end - inner_type = type.type + inner_type = unwrapped_type #: as untyped + .type return value.map { |v| _convert_value(v, inner_type) }.freeze when Hash # Handle `T.nilable(T::Hash[...])` and `T.any(...)`. @@ -104,7 +101,18 @@ def self._convert_hash(hash, type) raise TypeError, "Could not convert hash #{hash} to `#{type}`." end - private_class_method :_convert_hash, :_convert_value + # Unwrap `T.nilable(...)` to get the inner type, or return the type as-is. + #: (T::Types::Base) -> T::Types::Base + def self._unwrap_nilable(type) + if type.respond_to?(:unwrap_nilable) + type #: as untyped + .unwrap_nilable + else + type + end + end + + private_class_method :_convert_hash, :_convert_value, :_unwrap_nilable # Compare this object with another object for equality. # @param other The object to compare. diff --git a/ruby/from_hash/optify-from_hash.gemspec b/ruby/from_hash/optify-from_hash.gemspec index 7ff88a71..d561368f 100644 --- a/ruby/from_hash/optify-from_hash.gemspec +++ b/ruby/from_hash/optify-from_hash.gemspec @@ -1,6 +1,6 @@ # frozen_string_literal: true -LIB_VERSION = '0.2.1' +LIB_VERSION = '0.2.2' Gem::Specification.new do |spec| spec.name = 'optify-from_hash' diff --git a/ruby/from_hash/test/.rubocop.yml b/ruby/from_hash/test/.rubocop.yml index 7e4d44a4..8e48f2ea 100644 --- a/ruby/from_hash/test/.rubocop.yml +++ b/ruby/from_hash/test/.rubocop.yml @@ -7,7 +7,7 @@ Metrics/AbcSize: Max: 45 Metrics/ClassLength: - Max: 260 + Max: 280 Metrics/MethodLength: Max: 40 \ No newline at end of file diff --git a/ruby/from_hash/test/from_hash_test.rb b/ruby/from_hash/test/from_hash_test.rb index 39ab855b..d3a72a64 100644 --- a/ruby/from_hash/test/from_hash_test.rb +++ b/ruby/from_hash/test/from_hash_test.rb @@ -50,6 +50,12 @@ class TestConfig < Optify::FromHashable sig { returns(T::Array[T::Hash[Symbol, TestObject]]) } attr_reader :hashes + sig { returns(Symbol) } + attr_reader :symbol + + sig { returns(T.nilable(Symbol)) } + attr_reader :nilable_symbol + sig { returns(T::Hash[Symbol, Symbol]) } attr_reader :symbol_to_symbol @@ -365,6 +371,27 @@ def skip_test_nilable_hash_with_string_or_object_or_object2_invalid_value exception.message) end + def test_symbol + hash = { 'symbol' => 'hello' } + c = TestConfig.from_hash(hash) + assert_instance_of(Symbol, c.symbol) + assert_equal(:hello, c.symbol) + assert_equal({ symbol: :hello }, c.to_h) + end + + def test_nilable_symbol + hash = { 'nilable_symbol' => 'world' } + c = TestConfig.from_hash(hash) + assert_instance_of(Symbol, c.nilable_symbol) + assert_equal(:world, c.nilable_symbol) + assert_equal({ nilable_symbol: :world }, c.to_h) + + hash = { 'nilable_symbol' => nil } + c = TestConfig.from_hash(hash) + assert_nil(c.nilable_symbol) + assert_equal({ nilable_symbol: nil }, c.to_h) + end + def test_symbol_to_symbol hash = { 'symbol_to_symbol' => { 'key' => 'value' } } c = TestConfig.from_hash(hash)