Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add associated object generator #23

Merged
merged 10 commits into from
May 19, 2024
Merged
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions lib/generators/associated/USAGE
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions lib/generators/associated/associated_generator.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/generators/associated/templates/associated.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class <%= name %> < ActiveRecord::AssociatedObject
extension do
# Extend <%= record_klass %> here
end
end
8 changes: 8 additions & 0 deletions lib/generators/associated/templates/associated_test.rb.tt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/boot/active_record.rb
Original file line number Diff line number Diff line change
@@ -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|
Expand Down
72 changes: 72 additions & 0 deletions test/lib/generators/associated_generator_test.rb
Original file line number Diff line number Diff line change
@@ -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