annotaterb
annotaterb copied to clipboard
Annotate models using Zeitwerk namespaces
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)