# typed: strict # frozen_string_literal: true class ActiveRecordColumnTypeHelper extend T::Sig sig { params(constant: T.class_of(ActiveRecord::Base)).void } def initialize(constant) @constant = constant end sig { params(column_name: String).returns([String, String]) } def type_for(column_name) return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant) column_type = @constant.attribute_types[column_name] getter_type = case column_type when defined?(MoneyColumn) && MoneyColumn::ActiveRecordType "::Money" when ActiveRecord::Type::Integer "::Integer" when ActiveRecord::Type::String "::String" when ActiveRecord::Type::Date "::Date" when ActiveRecord::Type::Decimal "::BigDecimal" when ActiveRecord::Type::Float "::Float" when ActiveRecord::Type::Boolean "T::Boolean" when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time "::DateTime" when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter "::ActiveSupport::TimeWithZone" else handle_unknown_type(column_type) end column = @constant.columns_hash[column_name] setter_type = getter_type if column&.null return [as_nilable_type(getter_type), as_nilable_type(setter_type)] end if column_name == @constant.primary_key || column_name == "created_at" || column_name == "updated_at" getter_type = as_nilable_type(getter_type) end [getter_type, setter_type] end private sig { params(constant: Module).returns(T::Boolean) } def do_not_generate_strong_types?(constant) Object.const_defined?(:StrongTypeGeneration) && !(constant.singleton_class < Object.const_get(:StrongTypeGeneration)) end sig { params(type: String).returns(String) } def as_nilable_type(type) if type.start_with?("T.nilable(") || type == "T.untyped" type else "T.nilable(#{type})" end end sig { params(column_type: Object).returns(String) } def handle_unknown_type(column_type) return "T.untyped" unless ActiveModel::Type::Value === column_type return "T.untyped" if Tapioca::GenericTypeRegistry.generic_type_instance?(column_type) lookup_return_type_of_method(column_type, :deserialize) || lookup_return_type_of_method(column_type, :cast) || lookup_arg_type_of_method(column_type, :serialize) || "T.untyped" end sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) } def lookup_return_type_of_method(column_type, method) signature = T::Private::Methods.signature_for_method(column_type.method(method)) return unless signature return_type = signature.return_type return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped return_type.to_s end sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) } def lookup_arg_type_of_method(column_type, method) signature = T::Private::Methods.signature_for_method(column_type.method(method)) return unless signature # Arg types is an array [name, type] entries, so we desctructure the type of # first argument to get the first argument type _, first_argument_type = signature.arg_types.first first_argument_type.to_s end end