From deccb7fc6271226e5f2a2b7b779da1e3a2088ecf Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 18 May 2024 19:05:55 -0500 Subject: [PATCH] Add associated object generator (#23) * Add associated object generator Run with: `bin/rails g associated Organization::Seats` Co-authored-by: Garrett Dimon * Simplify our destination_root a bit * Chomp the plain method accessor test, not too sure if we need it * Reuse some of the naming methods and go through object reader * Let's leave this up to apps and their linter * Trim some of our methods we don't need now * Think this is a little clearer * Better, hm? * Tweak usage more * Surface in documentation --------- Co-authored-by: Garrett Dimon --- README.md | 6 ++ lib/generators/associated/USAGE | 15 ++++ .../associated/associated_generator.rb | 27 +++++++ .../associated/templates/associated.rb.tt | 5 ++ .../templates/associated_test.rb.tt | 8 +++ test/boot/active_record.rb | 2 +- .../generators/associated_generator_test.rb | 72 +++++++++++++++++++ 7 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 lib/generators/associated/USAGE create mode 100644 lib/generators/associated/associated_generator.rb create mode 100644 lib/generators/associated/templates/associated.rb.tt create mode 100644 lib/generators/associated/templates/associated_test.rb.tt create mode 100644 test/lib/generators/associated_generator_test.rb diff --git a/README.md b/README.md index 40b9c96..2ccffd3 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ class Post::Publisher < ActiveRecord::AssociatedObject end ``` +### Use the generator to help write Associated Objects + +To set up the `Post::Publisher` from above, you can call `bin/rails generate associated Post::Publisher`. + +See `bin/rails generate associated --help` for more info. + ### Forwarding callbacks onto the associated object To further help illustrate how your collaborator Associated Objects interact with your domain model, you can forward callbacks. diff --git a/lib/generators/associated/USAGE b/lib/generators/associated/USAGE new file mode 100644 index 0000000..8d3f169 --- /dev/null +++ b/lib/generators/associated/USAGE @@ -0,0 +1,15 @@ +Description: + Create a PORO collaborator associated object inheriting from `ActiveRecord::AssociatedObject` that's associated with an Active Record record class. + + It'll be associated on the record with `has_object`. + + Note: associated object names support pluralized class names. So "Seats" remain "seats" in all cases, and "Seat" remains "seat" in all cases. +Example: + bin/rails generate associated Organization::Seats + + This will create: + app/models/organization/seats.rb + test/models/organization/seats_test.rb + + And in Organization, this will insert: + has_object :seats diff --git a/lib/generators/associated/associated_generator.rb b/lib/generators/associated/associated_generator.rb new file mode 100644 index 0000000..b63084a --- /dev/null +++ b/lib/generators/associated/associated_generator.rb @@ -0,0 +1,27 @@ +class AssociatedGenerator < Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + + def generate_associated_object_files + template "associated.rb", "app/models/#{name.underscore}.rb" + template "associated_test.rb", "test/models/#{name.underscore}_test.rb" + end + + def connect_associated_object + record_file = "#{destination_root}/app/models/#{record_path}.rb" + raise "Record class '#{record_klass}' does not exist" unless File.exist?(record_file) + + inject_into_class record_file, record_klass do + optimize_indentation "has_object :#{associated_object_path}", 2 + end + end + + private + + # The `:name` argument can handle model names, but associated object class names aren't singularized. + # So these record and associated_object methods prevent that. + def record_path = record_klass.downcase.underscore + def record_klass = name.deconstantize + + def associated_object_path = associated_object_class.downcase.underscore + def associated_object_class = name.demodulize +end diff --git a/lib/generators/associated/templates/associated.rb.tt b/lib/generators/associated/templates/associated.rb.tt new file mode 100644 index 0000000..1086361 --- /dev/null +++ b/lib/generators/associated/templates/associated.rb.tt @@ -0,0 +1,5 @@ +class <%= name %> < ActiveRecord::AssociatedObject + extension do + # Extend <%= record_klass %> here + end +end diff --git a/lib/generators/associated/templates/associated_test.rb.tt b/lib/generators/associated/templates/associated_test.rb.tt new file mode 100644 index 0000000..6af4b7f --- /dev/null +++ b/lib/generators/associated/templates/associated_test.rb.tt @@ -0,0 +1,8 @@ +require "test_helper" + +class <%= name %>Test < ActiveSupport::TestCase + setup do + # @<%= record_path %> = <%= record_path.pluralize %>(:TODO_fixture_name) + # @<%= associated_object_path %> = @<%= record_path %>.<%= associated_object_path %> + end +end diff --git a/test/boot/active_record.rb b/test/boot/active_record.rb index 66d839f..f60503b 100644 --- a/test/boot/active_record.rb +++ b/test/boot/active_record.rb @@ -1,5 +1,5 @@ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") -ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Base.logger = Logger.new(STDOUT) if ENV["VERBOSE"] || ENV["CI"] ActiveRecord::Schema.define do create_table :authors, force: true do |t| diff --git a/test/lib/generators/associated_generator_test.rb b/test/lib/generators/associated_generator_test.rb new file mode 100644 index 0000000..28b15e8 --- /dev/null +++ b/test/lib/generators/associated_generator_test.rb @@ -0,0 +1,72 @@ +require "test_helper" +require "pathname" +require "rails/generators" +require "generators/associated/associated_generator" + +class AssociatedGeneratorTest < Rails::Generators::TestCase + tests AssociatedGenerator + destination Pathname(__dir__).join("../../../tmp/generators") + + setup :prepare_destination, :create_record_file, :create_record_test_file + arguments %w[Organization::Seats] + + test "generator runs without errors" do + assert_nothing_raised { run_generator } + end + + test "generates an object.rb file" do + run_generator + + assert_file "app/models/organization/seats.rb", <<~RUBY + class Organization::Seats < ActiveRecord::AssociatedObject + extension do + # Extend Organization here + end + end + RUBY + end + + test "generates an object_test.rb file" do + run_generator + + assert_file "test/models/organization/seats_test.rb", /Organization::SeatsTest/ + end + + test "connects record" do + run_generator + + assert_file "app/models/organization.rb", <<~RUBY + class Organization + has_object :seats + end + RUBY + end + + test "raises error if associated record doesn't exist" do + assert_raise RuntimeError do + run_generator ["Business::Monkey"] + end + end + + private + + def create_record_file + create_file "app/models/organization.rb", <<~RUBY + class Organization + end + RUBY + end + + def create_record_test_file + create_file "test/models/organization_test.rb", <<~RUBY + require "test_helper" + + class OrganizationTest < ActiveSupport::TestCase + end + RUBY + end + + def create_file(path, content) + destination_root.join(path).tap { _1.dirname.mkpath }.write content + end +end