# typed: strict# frozen_string_literal: truereturnunlessdefined?(Rails)&&defined?(ActiveSupport::TestCase)&&defined?(ActiveRecord::TestFixtures)moduleTapiocamoduleDslmoduleCompilers# `Tapioca::Dsl::Compilers::ActiveRecordFixtures` decorates RBIs for test fixture methods# that are created dynamically by Rails.## For example, given an application with a posts table, we can have a fixture file## ~~~yaml# first_post:# author: John# title: My post# ~~~## Rails will allow us to invoke `posts(:first_post)` in tests to get the fixture record.# The generated RBI by this compiler will produce the following## ~~~rbi# # test_case.rbi# # typed: true# class ActiveSupport::TestCase# sig { params(fixture_name: NilClass, other_fixtures: NilClass).returns(T::Array[Post]) }# sig { params(fixture_name: T.any(String, Symbol), other_fixtures: NilClass).returns(Post) }# sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol))# .returns(T::Array[Post]) }# def posts(fixture_name = nil, *other_fixtures); end# end# ~~~classActiveRecordFixtures<CompilerextendT::SigConstantType=type_member{{fixed: T.class_of(ActiveSupport::TestCase)}}MISSING=Object.newsig{override.void}defdecoratemethod_names=iffixture_loader.respond_to?(:fixture_sets)method_names_from_lazy_fixture_loaderelsemethod_names_from_eager_fixture_loaderendmethod_names.select!{|name|fixture_class_mapping_from_fixture_files[name]!=MISSING}returnifmethod_names.empty?root.create_path(constant)do|mod|method_names.eachdo|name|create_fixture_method(mod,name.to_s)endendendclass<<selfextendT::Sigsig{override.returns(T::Enumerable[Module])}defgather_constantsreturn[]unlessdefined?(Rails.application)&&Rails.application[ActiveSupport::TestCase]endendprivatesig{returns(T::Class[ActiveRecord::TestFixtures])}deffixture_loader@fixture_loader||=T.let(Class.newdoT.unsafe(self).include(ActiveRecord::TestFixtures)ifrespond_to?(:fixture_paths=)T.unsafe(self).fixture_paths=[Rails.root.join("test","fixtures")]elseT.unsafe(self).fixture_path=Rails.root.join("test","fixtures")end# https://github.com/rails/rails/blob/7c70791470fc517deb7c640bead9f1b47efb5539/activerecord/lib/active_record/test_fixtures.rb#L46singleton_class.define_method(:file_fixture_path)doRails.root.join("test","fixtures","files")endT.unsafe(self).fixtures(:all)end,T.nilable(T::Class[ActiveRecord::TestFixtures]),)endsig{returns(T::Array[String])}defmethod_names_from_lazy_fixture_loaderT.unsafe(fixture_loader).fixture_sets.keysendsig{returns(T::Array[String])}defmethod_names_from_eager_fixture_loaderfixture_loader.ancestors# get all ancestors from class that includes AR fixtures.drop(1)# drop the anonymous class itself from the array.reject(&:name)# only collect anonymous ancestors because fixture methods are always on an anonymous module.flat_mapdo|mod|mod.private_instance_methods(false).map(&:to_s)+mod.instance_methods(false).map(&:to_s)endendsig{params(mod: RBI::Scope,name: String).void}defcreate_fixture_method(mod,name)return_type=return_type_for_fixture(name)mod.create_method(name)do|node|node.add_opt_param("fixture_name","nil")node.add_rest_param("other_fixtures")node.add_sigdo|sig|sig.add_param("fixture_name","NilClass")sig.add_param("other_fixtures","NilClass")sig.return_type="T::Array[#{return_type}]"endnode.add_sigdo|sig|sig.add_param("fixture_name","T.any(String, Symbol)")sig.add_param("other_fixtures","NilClass")sig.return_type=return_typeendnode.add_sigdo|sig|sig.add_param("fixture_name","T.any(String, Symbol)")sig.add_param("other_fixtures","T.any(String, Symbol)")sig.return_type="T::Array[#{return_type}]"endendendsig{params(fixture_name: String).returns(String)}defreturn_type_for_fixture(fixture_name)fixture_class_mapping_from_fixture_files[fixture_name]||fixture_class_from_fixture_set(fixture_name)||fixture_class_from_active_record_base_class_mapping[fixture_name]||"T.untyped"endsig{params(fixture_name: String).returns(T.nilable(String))}deffixture_class_from_fixture_set(fixture_name)# only rails 7.1+ support fixture sets so this is conditionalreturnunlessfixture_loader.respond_to?(:fixture_sets)model_name_from_fixture_set=T.unsafe(fixture_loader).fixture_sets[fixture_name]returnunlessmodel_name_from_fixture_setmodel_name=ActiveRecord::FixtureSet.default_fixture_model_name(model_name_from_fixture_set)returnunlessObject.const_defined?(model_name)model_nameendsig{returns(T::Hash[String,String])}deffixture_class_from_active_record_base_class_mapping@fixture_class_mapping||=T.let(beginActiveRecord::Base.descendants.each_with_object({})do|model_class,mapping|class_name=model_class.namefixture_name=class_name.underscore.gsub("/","_")fixture_name=fixture_name.pluralizeifActiveRecord::Base.pluralize_table_namesmapping[fixture_name]=class_namemappingendend,T.nilable(T::Hash[String,String]),)endsig{returns(T::Hash[String,String])}deffixture_class_mapping_from_fixture_files@fixture_file_class_mapping||=T.let(beginfixture_paths=ifT.unsafe(fixture_loader).respond_to?(:fixture_paths)T.unsafe(fixture_loader).fixture_pathselseT.unsafe(fixture_loader).fixture_pathendArray(fixture_paths).each_with_object({})do|path,mapping|Dir["#{path}{.yml,/{**,*}/*.yml}"].selectdo|file|nextunless::File.file?(file)ActiveRecord::FixtureSet::File.open(file)do|fh|fixture_name=file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml")nextunlessfh.model_classmapping[fixture_name]=fh.model_classrescueActiveRecord::Fixture::FormatError# For fixtures that are not associated to any models and just contain raw data or fixtures that# contain invalid formatting, we want to skip them and avoid crashingmapping[fixture_name]=MISSINGendendendend,T.nilable(T::Hash[String,String]),)endendendendend