From 8b9c4ec3f4f95ea2ff6ae67a0f5783d4b9dd59f5 Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Fri, 29 Nov 2024 16:15:20 +0100 Subject: [PATCH] Initial working version with tests --- .gitignore | 1 + Gemfile | 2 + lib/flickwerk.rb | 10 +- lib/flickwerk/patch_loader.rb | 38 +++ lib/flickwerk/railtie.rb | 11 + .../app/models/concerns/user_methods.rb | 5 + test/dummy_app/app/models/user.rb | 5 + test/dummy_app/app/patches/page_patch.rb | 7 + test/dummy_app/app/patches/user_patch.rb | 11 + test/dummy_app/config/application.rb | 29 +++ test/dummy_app/log/development.log | 218 ++++++++++++++++++ test/dummy_blog/app/models/dummy_blog/post.rb | 5 + test/dummy_blog/lib/dummy_blog.rb | 1 + test/dummy_blog/lib/dummy_blog/engine.rb | 5 + test/dummy_cms/app/models/dummy_cms/page.rb | 2 + .../app/patches/dummy_blog_post_patch.rb | 7 + test/dummy_cms/lib/dummy_cms.rb | 4 + test/dummy_cms/lib/dummy_cms/engine.rb | 3 + test/test_flickwerk.rb | 56 ++++- test/test_helper.rb | 3 + 20 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 lib/flickwerk/patch_loader.rb create mode 100644 lib/flickwerk/railtie.rb create mode 100644 test/dummy_app/app/models/concerns/user_methods.rb create mode 100644 test/dummy_app/app/models/user.rb create mode 100644 test/dummy_app/app/patches/page_patch.rb create mode 100644 test/dummy_app/app/patches/user_patch.rb create mode 100644 test/dummy_app/config/application.rb create mode 100644 test/dummy_app/log/development.log create mode 100644 test/dummy_blog/app/models/dummy_blog/post.rb create mode 100644 test/dummy_blog/lib/dummy_blog.rb create mode 100644 test/dummy_blog/lib/dummy_blog/engine.rb create mode 100644 test/dummy_cms/app/models/dummy_cms/page.rb create mode 100644 test/dummy_cms/app/patches/dummy_blog_post_patch.rb create mode 100644 test/dummy_cms/lib/dummy_cms.rb create mode 100644 test/dummy_cms/lib/dummy_cms/engine.rb diff --git a/.gitignore b/.gitignore index 4ea5798..1e1fdfd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /spec/reports/ /tmp/ Gemfile.lock +test/log/ diff --git a/Gemfile b/Gemfile index 8617bfa..3792761 100644 --- a/Gemfile +++ b/Gemfile @@ -10,3 +10,5 @@ gem "rake", "~> 13.0" gem "minitest", "~> 5.16" gem "standard", "~> 1.3" + +gem "rails" diff --git a/lib/flickwerk.rb b/lib/flickwerk.rb index 5a017df..f1dc9e8 100644 --- a/lib/flickwerk.rb +++ b/lib/flickwerk.rb @@ -1,8 +1,16 @@ # frozen_string_literal: true require_relative "flickwerk/version" +require "active_support/core_ext/module/attribute_accessors" +require "flickwerk/railtie" if defined?(Rails) module Flickwerk class Error < StandardError; end - # Your code goes here... + + mattr_accessor :patch_paths + self.patch_paths = [] + + def self.included(engine) + patch_paths << engine.root.join("app/patches") + end end diff --git a/lib/flickwerk/patch_loader.rb b/lib/flickwerk/patch_loader.rb new file mode 100644 index 0000000..7ab25ca --- /dev/null +++ b/lib/flickwerk/patch_loader.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Flickwerk + class PatchLoader + DECORATED_CLASS_PATTERN = /(?[A-Z][a-zA-Z:]+)(\.prepend[\s(])/ + + attr_reader :path, :autoloader + + def initialize(path, autoloader: Rails.autoloaders.main) + @path = path + @autoloader = autoloader + end + + def call + path.glob("**/*_patch.rb") do |patch_path| + # Match all the classes that are prepended in the file + matches = File.read(patch_path).scan(DECORATED_CLASS_PATTERN).flatten + + # Don't do a thing if there's no prepending. + next unless matches.present? + + # For each unique match, make sure we load the decorator when the base class is loaded + matches.uniq.each do |decorated_class| + # Zeitwerk tells us which constant it expects a file to provide. + decorator_constant = autoloader.cpath_expected_at(patch_path) + # Sprinkle some debugging. + Rails.logger.debug("Preparing to autoload #{decorated_class} with #{decorator_constant}") + # If the class has not been loaded, we can add a hook to load the decorator when it is. + # Multiple hooks are no problem, as long as all decorators are namespaced appropriately. + autoloader.on_load(decorated_class) do |base| + Rails.logger.debug("Loading #{decorator_constant} in order to modify #{base}") + decorator_constant.constantize + end + end + end + end + end +end diff --git a/lib/flickwerk/railtie.rb b/lib/flickwerk/railtie.rb new file mode 100644 index 0000000..ed9ad30 --- /dev/null +++ b/lib/flickwerk/railtie.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "flickwerk/patch_loader" + +class Flickwerk::Railtie < Rails::Railtie + initializer "flickwerk.add_patches", after: :setup_main_autoloader do + Flickwerk.patch_paths.each do |path| + Flickwerk::PatchLoader.new(path).call + end + end +end diff --git a/test/dummy_app/app/models/concerns/user_methods.rb b/test/dummy_app/app/models/concerns/user_methods.rb new file mode 100644 index 0000000..a621846 --- /dev/null +++ b/test/dummy_app/app/models/concerns/user_methods.rb @@ -0,0 +1,5 @@ +module UserMethods + def last_name + "Vader" + end +end diff --git a/test/dummy_app/app/models/user.rb b/test/dummy_app/app/models/user.rb new file mode 100644 index 0000000..6bdf33d --- /dev/null +++ b/test/dummy_app/app/models/user.rb @@ -0,0 +1,5 @@ +class User + def name + "Darth" + end +end diff --git a/test/dummy_app/app/patches/page_patch.rb b/test/dummy_app/app/patches/page_patch.rb new file mode 100644 index 0000000..b75b8b7 --- /dev/null +++ b/test/dummy_app/app/patches/page_patch.rb @@ -0,0 +1,7 @@ +module PagePatch + def title + "Changed from Host app" + end + + DummyCms::Page.prepend(self) +end diff --git a/test/dummy_app/app/patches/user_patch.rb b/test/dummy_app/app/patches/user_patch.rb new file mode 100644 index 0000000..71380ca --- /dev/null +++ b/test/dummy_app/app/patches/user_patch.rb @@ -0,0 +1,11 @@ +module UserPatch + def self.prepended(base) + base.include(UserMethods) + end + + def age + 26 + end + + User.prepend(self) +end diff --git a/test/dummy_app/config/application.rb b/test/dummy_app/config/application.rb new file mode 100644 index 0000000..202f722 --- /dev/null +++ b/test/dummy_app/config/application.rb @@ -0,0 +1,29 @@ +require "rails" +require "flickwerk/railtie" + +module DummyApp + class Application < ::Rails::Application + config.root = File.expand_path("../", __dir__) + include Flickwerk + config.autoload_paths << File.expand_path("../app/models", __dir__) + + config.load_defaults("#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}") + + # It needs to be explicitly set from Rails 7 + # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-6-1-to-rails-7-0-spring + config.cache_classes = true + + config.active_support.deprecation = :stderr + config.log_level = :debug + + # Improve test suite performance: + config.eager_load = false + config.cache_store = :memory_store + + # We don't use a web server, so we let Rails serve assets. + config.public_file_server.enabled = true + + # No need to use credentials file in a test environment. + config.secret_key_base = 'SECRET_TOKEN' + end +end diff --git a/test/dummy_app/log/development.log b/test/dummy_app/log/development.log new file mode 100644 index 0000000..35afb57 --- /dev/null +++ b/test/dummy_app/log/development.log @@ -0,0 +1,218 @@ +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Preparing to autoload Blog::Post with BlogPostPatch +Preparing to autoload Blog::Post with BlogPostPatch +Preparing to autoload Blog::Post with BlogPostPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading BlogPostPatch in order to modify Blog::Post +Loading UserPatch in order to modify User +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload Cms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Loading BlogPostPatch in order to modify Blog::Post +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Loading PostPatch in order to modify DummyBlog::Post +Loading UserPatch in order to modify User +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with PostPatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading PostPatch in order to modify DummyBlog::Post +Loading UserPatch in order to modify User +Loading PostPatch in order to modify DummyBlog::Post +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload Blog::Post with BlogPostPatch +Loading UserPatch in order to modify User +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading UserPatch in order to modify User +Loading DummyBlogPostPatch in order to modify DummyBlog::Post +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading UserPatch in order to modify User +Loading PagePatch in order to modify DummyCms::Page +Loading DummyBlogPostPatch in order to modify DummyBlog::Post +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading UserPatch in order to modify User +Loading PagePatch in order to modify DummyCms::Page +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Loading PagePatch in order to modify DummyCms::Page +Loading UserPatch in order to modify User +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading UserPatch in order to modify User +Loading DummyBlogPostPatch in order to modify DummyBlog::Post +Loading PagePatch in order to modify DummyCms::Page +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading PagePatch in order to modify DummyCms::Page +Loading UserPatch in order to modify User +Loading DummyBlogPostPatch in order to modify DummyBlog::Post +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyCms::Page with PagePatch +Preparing to autoload User with UserPatch +Preparing to autoload DummyBlog::Post with DummyBlogPostPatch +Loading UserPatch in order to modify User +Loading PagePatch in order to modify DummyCms::Page +Loading DummyBlogPostPatch in order to modify DummyBlog::Post diff --git a/test/dummy_blog/app/models/dummy_blog/post.rb b/test/dummy_blog/app/models/dummy_blog/post.rb new file mode 100644 index 0000000..499a627 --- /dev/null +++ b/test/dummy_blog/app/models/dummy_blog/post.rb @@ -0,0 +1,5 @@ +class DummyBlog::Post + def title + "Original title" + end +end diff --git a/test/dummy_blog/lib/dummy_blog.rb b/test/dummy_blog/lib/dummy_blog.rb new file mode 100644 index 0000000..1e8b7de --- /dev/null +++ b/test/dummy_blog/lib/dummy_blog.rb @@ -0,0 +1 @@ +require "dummy_blog/engine" diff --git a/test/dummy_blog/lib/dummy_blog/engine.rb b/test/dummy_blog/lib/dummy_blog/engine.rb new file mode 100644 index 0000000..be48ffb --- /dev/null +++ b/test/dummy_blog/lib/dummy_blog/engine.rb @@ -0,0 +1,5 @@ +require "flickwerk" + +class BlogEngine < Rails::Engine + include Flickwerk +end diff --git a/test/dummy_cms/app/models/dummy_cms/page.rb b/test/dummy_cms/app/models/dummy_cms/page.rb new file mode 100644 index 0000000..381d608 --- /dev/null +++ b/test/dummy_cms/app/models/dummy_cms/page.rb @@ -0,0 +1,2 @@ +class DummyCms::Page +end diff --git a/test/dummy_cms/app/patches/dummy_blog_post_patch.rb b/test/dummy_cms/app/patches/dummy_blog_post_patch.rb new file mode 100644 index 0000000..e8d4112 --- /dev/null +++ b/test/dummy_cms/app/patches/dummy_blog_post_patch.rb @@ -0,0 +1,7 @@ +module DummyBlogPostPatch + def title + "Edited from CMS" + end + + DummyBlog::Post.prepend(self) +end diff --git a/test/dummy_cms/lib/dummy_cms.rb b/test/dummy_cms/lib/dummy_cms.rb new file mode 100644 index 0000000..6cedbf5 --- /dev/null +++ b/test/dummy_cms/lib/dummy_cms.rb @@ -0,0 +1,4 @@ +module DummyCms +end + +require "dummy_cms/engine" diff --git a/test/dummy_cms/lib/dummy_cms/engine.rb b/test/dummy_cms/lib/dummy_cms/engine.rb new file mode 100644 index 0000000..5b60b9a --- /dev/null +++ b/test/dummy_cms/lib/dummy_cms/engine.rb @@ -0,0 +1,3 @@ +class DummyCms::Engine < Rails::Engine + include Flickwerk +end diff --git a/test/test_flickwerk.rb b/test/test_flickwerk.rb index 396c1eb..41ad52a 100644 --- a/test/test_flickwerk.rb +++ b/test/test_flickwerk.rb @@ -1,13 +1,59 @@ # frozen_string_literal: true require "test_helper" +require "active_support/test_case" +require "active_support/testing/isolation" -class TestFlickwerk < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::Flickwerk::VERSION +class ZeitwerkIntegrationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + require_relative "dummy_app/config/application" + end + + def boot + DummyApp::Application.initialize! + end + + test "autoloading patches" do + boot + + assert User + assert User.new.respond_to?(:name) + assert User.new.respond_to?(:age) + assert_equal 26, User.new.age + assert_equal "Vader", User.new.last_name + assert_equal "Darth", User.new.name + end + + test "autoloading patches for an engine" do + require "dummy_cms" + + boot + + assert DummyCms::Page + assert DummyCms::Page.new.respond_to?(:title) + assert_equal "Changed from Host app", DummyCms::Page.new.title + end + + test "autoloading the unpatched blog engine" do + require "dummy_blog" + + boot + + assert DummyBlog::Post + assert DummyBlog::Post.new.respond_to?(:title) + assert_equal "Original title", DummyBlog::Post.new.title end - def test_it_does_something_useful - assert false + test "autoloading patches from an engine" do + require "dummy_blog" + require "dummy_cms" + + boot + + assert DummyBlog::Post + assert DummyBlog::Post.new.respond_to?(:title) + assert_equal "Edited from CMS", DummyBlog::Post.new.title end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7dbacda..d90420e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true $LOAD_PATH.unshift File.expand_path("../lib", __dir__) +$LOAD_PATH.unshift File.expand_path("dummy_cms/lib", __dir__) +$LOAD_PATH.unshift File.expand_path("dummy_blog/lib", __dir__) + require "flickwerk" require "minitest/autorun"