annotaterb icon indicating copy to clipboard operation
annotaterb copied to clipboard

Annotate models using Zeitwerk namespaces

Open guigs opened this issue 1 year ago • 21 comments

I tried to migrate from annotate gem, to see if this gem fixes an issue we had with the original in which it does not work with Zeitwerk namespaces.

Our project structure like this:

/app/models/
/app/namespace/models/
/spec/models/
/spec/fabricators/
/spec/namespace/models/
/spec/namespace/fabricators/

In an initializor we have:

Object.const_set('Namespace', Module.new) unless Object.const_defined?('Namespace')
Rails.autoloaders.main.push_dir(Rails.root.join("app/namespace/models"), namespace: Namespace)

In .annotate.yml I tried with this setup:

:model_dir:
- app/models
- app/namespace/models

When I run annotaterb models the models in app/models/ are annotated correctly, but I see these errors for models in app/namespace/models dir:

Unable to process app/namespace/models/foo.rb: file doesn't contain a valid model class
Unable to process app/namespace/models/bar.rb: file doesn't contain a valid model class

The file app/namespace/models/foo.rb is like

module Namespace
  class Foo < ApplicationRecord
  end
end

I also tried using root_dir config, but got same error:

:root_dir:
- 'app'
- 'app/namespace'

For the original annotate gem version 3.2.0 I have developed the following monkey patch to make this setup work in our project:

# frozen_string_literal: true

annotate_gem_specs = Gem.loaded_specs['annotate']

return unless annotate_gem_specs

module AnnotateModelsWithZeitwerkNamespaces
  module FilePatterns
    def test_files(root_directory)
      super + NAMESPACE_DIRS_WITH_MODELS.map do |namespace_dir|
        File.join(root_directory, 'spec', namespace_dir.to_s, 'models', '%MODEL_NAME_WITHOUT_NS%_spec.rb')
      end
    end

    def factory_files(root_directory)
      super + NAMESPACE_DIRS_WITH_MODELS.map do |namespace_dir|
        File.join(root_directory, 'spec', namespace_dir.to_s, 'fabricators', '%MODEL_NAME_WITHOUT_NS%_fabricator.rb')
      end
    end
  end

  def get_model_class(file)
    loader = Rails.autoloaders.main
    root_dirs = loader.dirs(namespaces: true)
    expanded_file = File.expand_path(file)
    root_dir, namespace = root_dirs.find do |dir, _|
      expanded_file.start_with?(dir)
    end
    _, filepath_relative_to_root_dir = expanded_file.split(root_dir)
    filepath_relative_to_root_dir = filepath_relative_to_root_dir[1..].sub(/\.rb$/, '')
    camelize = loader.inflector.camelize(filepath_relative_to_root_dir, nil)
    namespace.const_get(camelize)
  end

  def resolve_filename(filename_template, model_name, table_name)
    super.gsub('%MODEL_NAME_WITHOUT_NS%', model_name.split('/', 2).last)
  end
end

AnnotateModels.singleton_class.prepend(AnnotateModelsWithZeitwerkNamespaces)
AnnotateModels::FilePatterns.singleton_class.prepend(AnnotateModelsWithZeitwerkNamespaces::FilePatterns)

guigs avatar Feb 09 '24 16:02 guigs