From 675da78565db3f759e3c1fdb899296d8b81c72fd Mon Sep 17 00:00:00 2001 From: Justin Harris Date: Tue, 9 Dec 2025 11:47:20 -0500 Subject: [PATCH] [Ruby][from_hash] Improve type hints --- ruby/from_hash/.rubocop.yml | 2 +- .../lib/optify_from_hash/from_hashable.rb | 34 +++++++++++-------- ruby/from_hash/rbi/optify_from_hash.rbi | 5 ++- ruby/from_hash/sig/optify_from_hash.rbs | 3 +- ruby/from_hash/test/to_json_test.rb | 11 ++++++ 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/ruby/from_hash/.rubocop.yml b/ruby/from_hash/.rubocop.yml index 1f9e63b9..3199c060 100644 --- a/ruby/from_hash/.rubocop.yml +++ b/ruby/from_hash/.rubocop.yml @@ -18,7 +18,7 @@ Metrics/CyclomaticComplexity: Max: 12 Metrics/MethodLength: - Max: 21 + Max: 24 Metrics/PerceivedComplexity: Max: 10 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 1611a714..27b83951 100644 --- a/ruby/from_hash/lib/optify_from_hash/from_hashable.rb +++ b/ruby/from_hash/lib/optify_from_hash/from_hashable.rb @@ -14,7 +14,6 @@ class FromHashable # Create a new immutable instance of the class from a hash. # - # This is a class method so that it can set members with private setters. # @param hash The hash to create the instance from. # @return The new instance. #: (Hash[untyped, untyped]) -> instance @@ -40,7 +39,7 @@ def self.from_hash(hash) instance.freeze end - #: (untyped, untyped) -> untyped + #: (untyped, T::Types::Base) -> untyped def self._convert_value(value, type) if type.is_a?(T::Types::Untyped) # No preferred type is given, so return the value as is. @@ -52,7 +51,10 @@ def self._convert_value(value, type) case value when Array # Handle `T.nilable(T::Array[...])` - type = type.unwrap_nilable if type.respond_to?(:unwrap_nilable) + if type.respond_to?(:unwrap_nilable) + type = type #: as untyped + .unwrap_nilable + end inner_type = type.type return value.map { |v| _convert_value(v, inner_type) }.freeze when Hash @@ -61,7 +63,8 @@ def self._convert_value(value, type) # `T.any(...)` because using `.types` works for both cases. if type.respond_to?(:types) # Find a type that works for the hash. - type.types.each do |t| + type #: as untyped + .types.each do |t| return _convert_hash(value, t).freeze rescue StandardError # Ignore and try the next type. @@ -76,12 +79,13 @@ def self._convert_value(value, type) value end - #: (Hash[untyped, untyped], untyped) -> untyped + #: (Hash[untyped, untyped], T::Types::Base) -> untyped def self._convert_hash(hash, type) if type.respond_to?(:raw_type) # There is an object for the hash. # It could be a custom class, a String, or maybe something else. - type_for_hash = type.raw_type + type_for_hash = type #: as untyped + .raw_type return type_for_hash.from_hash(hash) if type_for_hash.respond_to?(:from_hash) elsif type.is_a?(T::Types::TypedHash) # The hash should be a hash, but the values might be objects to convert. @@ -116,9 +120,9 @@ def ==(other) end # Convert this object to a JSON string. - #: (*untyped) -> String - def to_json(*args) - to_h.to_json(args) + #: (?JSON::State?) -> String + def to_json(state = nil) + to_h.to_json(state) end # Convert this object to a Hash recursively. @@ -134,21 +138,19 @@ def to_h # Remove the @ prefix to get the method name method_name = var_name.to_s[1..] #: as !nil value = instance_variable_get(var_name) - result[method_name.to_sym] = _convert_value_to_hash(value) + result[method_name.to_sym] = self.class.send(:_convert_value_for_to_h, value) end result end - private - #: (untyped) -> untyped - def _convert_value_to_hash(value) + def self._convert_value_for_to_h(value) case value when Array - value.map { |v| _convert_value_to_hash(v) } + value.map { |v| _convert_value_for_to_h(v) } when Hash - value.transform_values { |v| _convert_value_to_hash(v) } + value.transform_values { |v| _convert_value_for_to_h(v) } when nil nil else @@ -159,5 +161,7 @@ def _convert_value_to_hash(value) end end end + + private_class_method :_convert_value_for_to_h end end diff --git a/ruby/from_hash/rbi/optify_from_hash.rbi b/ruby/from_hash/rbi/optify_from_hash.rbi index 5a945fca..34b80d04 100644 --- a/ruby/from_hash/rbi/optify_from_hash.rbi +++ b/ruby/from_hash/rbi/optify_from_hash.rbi @@ -9,15 +9,14 @@ module Optify # Create a new instance of the class from a hash. # - # This is a class method that so that it can set members with private setters. # @param hash The hash to create the instance from. # @return The new instance. sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T.attached_class) } def self.from_hash(hash); end # Convert this object to a JSON string. - sig { params(args: T.untyped).returns(String) } - def to_json(*args); end + sig { params(state: T.nilable(JSON::State)).returns(String) } + def to_json(state = nil); end # Convert this object to a Hash recursively. # This is mostly the reverse operation of `from_hash`, diff --git a/ruby/from_hash/sig/optify_from_hash.rbs b/ruby/from_hash/sig/optify_from_hash.rbs index 876dc6b1..2e608942 100644 --- a/ruby/from_hash/sig/optify_from_hash.rbs +++ b/ruby/from_hash/sig/optify_from_hash.rbs @@ -6,13 +6,12 @@ end class Optify::FromHashable # Create a new instance of the class from a hash. # - # This is a class method that so that it can set members with private setters. # @param hash The hash to create the instance from. # @return The new instance. def self.from_hash: (::Hash[untyped, untyped] hash) -> instance # Convert this object to a JSON string. - def to_json: (*untyped args) -> String + def to_json: (?JSON::State? state) -> String # Convert this object to a Hash recursively. # This is mostly the reverse operation of `from_hash`, diff --git a/ruby/from_hash/test/to_json_test.rb b/ruby/from_hash/test/to_json_test.rb index 60b60881..5a617080 100644 --- a/ruby/from_hash/test/to_json_test.rb +++ b/ruby/from_hash/test/to_json_test.rb @@ -16,5 +16,16 @@ def test_to_json assert_equal('{"rootString":"hello","myObject":{"two":2}}', actual) assert_equal(h.to_json, actual) end + + #: -> void + def test_to_json_with_args + h = { rootString: 'hello', myObject: { two: 2 } } + m = MyConfig.from_hash(h) + json_state = JSON::State.new(indent: ' ', space: ' ', object_nl: "\n") + actual = m.to_json(json_state) + assert_equal("{\n \"rootString\": \"hello\",\n \"myObject\": {\n \"two\": 2\n }\n}", actual) + expected = h.to_json(json_state) + assert_equal(expected, actual) + end end end