lib/types/props/private/serde_transform.rb



# frozen_string_literal: true
# typed: strict

module T::Props
  module Private
    module SerdeTransform
      extend T::Sig

      class Serialize; end
      private_constant :Serialize
      class Deserialize; end
      private_constant :Deserialize
      ModeType = T.type_alias {T.any(Serialize, Deserialize)}
      private_constant :ModeType

      module Mode
        SERIALIZE = T.let(Serialize.new.freeze, Serialize)
        DESERIALIZE = T.let(Deserialize.new.freeze, Deserialize)
      end

      NO_TRANSFORM_TYPES = T.let(
        [TrueClass, FalseClass, NilClass, Symbol, String].freeze,
        T::Array[Module],
      )
      private_constant :NO_TRANSFORM_TYPES

      sig do
        params(
          type: T::Types::Base,
          mode: ModeType,
          varname: String,
        )
        .returns(T.nilable(String))
        .checked(:never)
      end
      def self.generate(type, mode, varname)
        case type
        when T::Types::TypedArray
          inner = generate(type.type, mode, 'v')
          if inner.nil?
            "#{varname}.dup"
          else
            "#{varname}.map {|v| #{inner}}"
          end
        when T::Types::TypedSet
          inner = generate(type.type, mode, 'v')
          if inner.nil?
            "#{varname}.dup"
          else
            "Set.new(#{varname}) {|v| #{inner}}"
          end
        when T::Types::TypedHash
          keys = generate(type.keys, mode, 'k')
          values = generate(type.values, mode, 'v')
          if keys && values
            "#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
          elsif keys
            "#{varname}.transform_keys {|k| #{keys}}"
          elsif values
            "#{varname}.transform_values {|v| #{values}}"
          else
            "#{varname}.dup"
          end
        when T::Types::Simple
          raw = type.raw_type
          if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
            nil
          elsif raw <= Float
            case mode
            when Deserialize then "#{varname}.to_f"
            when Serialize then nil
            else T.absurd(mode)
            end
          elsif raw <= Numeric
            nil
          elsif raw < T::Props::Serializable
            handle_serializable_subtype(varname, raw, mode)
          elsif raw.singleton_class < T::Props::CustomType
            handle_custom_type(varname, T.unsafe(raw), mode)
          elsif T::Configuration.scalar_types.include?(raw.name)
            # It's a bit of a hack that this is separate from NO_TRANSFORM_TYPES
            # and doesn't check inheritance (like `T::Props::CustomType.scalar_type?`
            # does), but it covers the main use case (pay-server's custom `Boolean`
            # module) without either requiring `T::Configuration.scalar_types` to
            # accept modules instead of strings (which produces load-order issues
            # and subtle behavior changes) or eating the performance cost of doing
            # an inheritance check by manually crawling a class hierarchy and doing
            # string comparisons.
            nil
          else
            "T::Props::Utils.deep_clone_object(#{varname})"
          end
        when T::Types::Union
          non_nil_type = T::Utils.unwrap_nilable(type)
          if non_nil_type
            inner = generate(non_nil_type, mode, varname)
            if inner.nil?
              nil
            else
              "#{varname}.nil? ? nil : #{inner}"
            end
          elsif type.types.all? {|t| generate(t, mode, varname).nil?}
            # Handle, e.g., T::Boolean
            nil
          else
            # We currently deep_clone_object if the type was T.any(Integer, Float).
            # When we get better support for union types (maybe this specific
            # union type, because it would be a replacement for
            # Chalk::ODM::DeprecatedNumemric), we could opt to special case
            # this union to have no specific serde transform (the only reason
            # why Float has a special case is because round tripping through
            # JSON might normalize Floats to Integers)
            "T::Props::Utils.deep_clone_object(#{varname})"
          end
        when T::Types::Intersection
          dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"

          # Transformations for any members of the intersection type where we
          # know what we need to do and did not have to fall back to the
          # dynamic deep clone method.
          #
          # NB: This deliberately does include `nil`, which means we know we
          # don't need to do any transforming.
          inner_known = type.types
            .map {|t| generate(t, mode, varname)}
            .reject {|t| t == dynamic_fallback}
            .uniq

          if inner_known.size != 1
            # If there were no cases where we could tell what we need to do,
            # e.g. if this is `T.all(SomethingWeird, WhoKnows)`, just use the
            # dynamic fallback.
            #
            # If there were multiple cases and they weren't consistent, e.g.
            # if this is `T.all(String, T::Array[Integer])`, the type is probably
            # bogus/uninhabited, but use the dynamic fallback because we still
            # don't have a better option, and this isn't the place to raise that
            # error.
            dynamic_fallback
          else
            # This is probably something like `T.all(String, SomeMarker)` or
            # `T.all(SomeEnum, T.deprecated_enum(SomeEnum::FOO))` and we should
            # treat it like String or SomeEnum even if we don't know what to do
            # with the rest of the type.
            inner_known.first
          end
        when T::Types::Enum
          generate(T::Utils.lift_enum(type), mode, varname)
        else
          "T::Props::Utils.deep_clone_object(#{varname})"
        end
      end

      sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
      private_class_method def self.handle_serializable_subtype(varname, type, mode)
        case mode
        when Serialize
          "#{varname}.serialize(strict)"
        when Deserialize
          type_name = T.must(module_name(type))
          "#{type_name}.from_hash(#{varname})"
        else
          T.absurd(mode)
        end
      end

      sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
      private_class_method def self.handle_custom_type(varname, type, mode)
        case mode
        when Serialize
          "T::Props::CustomType.checked_serialize(#{varname})"
        when Deserialize
          type_name = T.must(module_name(type))
          "#{type_name}.deserialize(#{varname})"
        else
          T.absurd(mode)
        end
      end

      sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
      private_class_method def self.module_name(type)
        T::Configuration.module_name_mangler.call(type)
      end
    end
  end
end