# frozen_string_literal: true# typed: strictmoduleT::PropsmodulePrivate# Generates a specialized `deserialize` implementation for a subclass of# T::Props::Serializable.## The basic idea is that we analyze the props and for each prop, generate# the simplest possible logic as a block of Ruby source, so that we don't# pay the cost of supporting types like T:::Hash[CustomType, SubstructType]# when deserializing a simple Integer. Then we join those together,# with a little shared logic to be able to detect when we get input keys# that don't match any prop.moduleDeserializerGeneratorextendT::Sig# Generate a method that takes a T::Hash[String, T.untyped] representing# serialized props, sets instance variables for each prop found in the# input, and returns the count of we props set (which we can use to check# for unexpected input keys with minimal effect on the fast path).sigdoparams(props: T::Hash[Symbol,T::Hash[Symbol,T.untyped]],defaults: T::Hash[Symbol,T::Props::Private::ApplyDefault],).returns(String).checked(:never)enddefself.generate(props,defaults)stored_props=props.reject{|_,rules|rules[:dont_store]}parts=stored_props.mapdo|prop,rules|# All of these strings should already be validated (directly or# indirectly) in `validate_prop_name`, so we don't bother with a nice# error message, but we double check here to prevent a refactoring# from introducing a security vulnerability.raiseunlessT::Props::Decorator::SAFE_NAME.match?(prop.to_s)hash_key=rules.fetch(:serialized_form)raiseunlessT::Props::Decorator::SAFE_NAME.match?(hash_key)ivar_name=rules.fetch(:accessor_key).to_sraiseunlessivar_name.start_with?('@')&&T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])transformed_val=SerdeTransform.generate(T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),SerdeTransform::Mode::DESERIALIZE,'val')||'val'nil_handler=generate_nil_handler(prop: prop,serialized_form: hash_key,default: defaults[prop],nilable_type: T::Props::Utils.optional_prop?(rules),raise_on_nil_write: !!rules[:raise_on_nil_write],)<<~RUBY
val = hash[#{hash_key.inspect}]
#{ivar_name} = if val.nil?
found -= 1 unless hash.key?(#{hash_key.inspect})
#{nil_handler}
else
#{transformed_val}
end
RUBYend<<~RUBY
def __t_props_generated_deserialize(hash)
found = #{stored_props.size}#{parts.join("\n\n")}
found
end
RUBYend# This is very similar to what we do in ApplyDefault, but has a few# key differences that mean we don't just re-use the code:## 1. Where the logic in construction is that we generate a default# if & only if the prop key isn't present in the input, here we'll# generate a default even to override an explicit nil, but only# if the prop is actually required.# 2. Since we're generating raw Ruby source, we can remove a layer# of indirection for marginally better performance; this seems worth# it for the common cases of literals and empty arrays/hashes.# 3. We need to care about the distinction between `raise_on_nil_write`# and actually non-nilable, where new-instance construction doesn't.## So we fall back to ApplyDefault only when one of the cases just# mentioned doesn't apply.sigdoparams(prop: Symbol,serialized_form: String,default: T.nilable(ApplyDefault),nilable_type: T::Boolean,raise_on_nil_write: T::Boolean,).returns(String).checked(:never)endprivate_class_methoddefself.generate_nil_handler(prop:,serialized_form:,default:,nilable_type:,raise_on_nil_write:
)if!nilable_typecasedefaultwhenNilClass"self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"whenApplyPrimitiveDefaultliteral=default.defaultcaseliteralwhenString,Integer,Symbol,Float,TrueClass,FalseClass,NilClassliteral.inspectelse"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"endwhenApplyEmptyArrayDefault'[]'whenApplyEmptyHashDefault'{}'else"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"endelsifraise_on_nil_write"required_prop_missing_from_deserialize(#{prop.inspect})"else'nil'endendendendend