Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ruby/from_hash/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Metrics/CyclomaticComplexity:
Max: 12

Metrics/MethodLength:
Max: 21
Max: 24

Metrics/PerceivedComplexity:
Max: 10
Expand Down
34 changes: 19 additions & 15 deletions ruby/from_hash/lib/optify_from_hash/from_hashable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -159,5 +161,7 @@ def _convert_value_to_hash(value)
end
end
end

private_class_method :_convert_value_for_to_h
end
end
5 changes: 2 additions & 3 deletions ruby/from_hash/rbi/optify_from_hash.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
3 changes: 1 addition & 2 deletions ruby/from_hash/sig/optify_from_hash.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
11 changes: 11 additions & 0 deletions ruby/from_hash/test/to_json_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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