module T::Props::Private::DeserializerGenerator

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.

Public Class Methods

generate(props, defaults) click to toggle source
# File lib/types/props/private/deserializer_generator.rb, line 31
      def self.generate(props, defaults)
        stored_props = props.reject {|_, rules| rules[:dont_store]}
        parts = stored_props.map do |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.
          raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)

          hash_key = rules.fetch(:serialized_form)
          raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)

          ivar_name = rules.fetch(:accessor_key).to_s
          raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])

          transformation = SerdeTransform.generate(
            T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
            SerdeTransform::Mode::DESERIALIZE,
            'val'
          )
          transformed_val = if transformation
            # Rescuing exactly NoMethodError is intended as a temporary hack
            # to preserve the semantics from before codegen. More generally
            # we are inconsistent about typechecking on deser and need to decide
            # our strategy here.
            <<~RUBY
              begin
                #{transformation}
              rescue NoMethodError => e
                raise_deserialization_error(
                  #{prop.inspect},
                  val,
                  e,
                )
                val
              end
            RUBY
          else
            'val'
          end

          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
          RUBY
        end

        <<~RUBY
          def __t_props_generated_deserialize(hash)
            found = #{stored_props.size}
            #{parts.join("\n\n")}
            found
          end
        RUBY
      end

Private Class Methods

generate_nil_handler( prop:, serialized_form:, default:, nilable_type:, raise_on_nil_write: ) click to toggle source
# File lib/types/props/private/deserializer_generator.rb, line 126
                     def self.generate_nil_handler(
  prop:,
  serialized_form:,
  default:,
  nilable_type:,
  raise_on_nil_write:
)
  if !nilable_type
    case default
    when NilClass
      "self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"
    when ApplyPrimitiveDefault
      literal = default.default
      case literal
      when String, Integer, Symbol, Float, TrueClass, FalseClass, NilClass
        literal.inspect
      else
        "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
      end
    when ApplyEmptyArrayDefault
      '[]'
    when ApplyEmptyHashDefault
      '{}'
    else
      "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
    end
  elsif raise_on_nil_write
    "required_prop_missing_from_deserialize(#{prop.inspect})"
  else
    'nil'
  end
end