From e56978155d398e40d515f23e1529d3ae20b8b680 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 6 Nov 2026 23:09:58 +0100 Subject: [PATCH 1/2] Reduce resource service allocations with argument forwarding Co-authored-by: Codex --- lib/resources_controller.rb | 18 +-- spec/lib/resource_service_forwarding_spec.rb | 122 +++++++++++++++++++ 2 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 spec/lib/resource_service_forwarding_spec.rb diff --git a/lib/resources_controller.rb b/lib/resources_controller.rb index c5516e6..1d678a6 100644 --- a/lib/resources_controller.rb +++ b/lib/resources_controller.rb @@ -755,20 +755,20 @@ def initialize(controller) @controller = controller end - def method_missing(method, *args, **kwargs, &block) - service.__send__(method, *args, **kwargs, &block) + def method_missing(method, ...) + service.__send__(method, ...) end - def find(*args, **kwargs, &block) + def find(...) resource_specification.find ? resource_specification.find_custom(controller) : super end # build association on the enclosing resource if there is one, otherwise call new - def new(*args, **kwargs, &block) + def new(...) if enclosing_resource - service.build(*args, **kwargs, &block) + service.build(...) else - service.new(*args, **kwargs, &block) + service.new(...) end end @@ -806,11 +806,11 @@ def find(*args, **kwargs) end # build association on the enclosing resource if there is one, otherwise call new - def new(*args, **kwargs, &block) + def new(...) if enclosing_resource - enclosing_resource.send("build_#{resource_specification.source}", *args, **kwargs, &block) + enclosing_resource.send("build_#{resource_specification.source}", ...) else - service.new(*args, **kwargs, &block) + service.new(...) end end diff --git a/spec/lib/resource_service_forwarding_spec.rb b/spec/lib/resource_service_forwarding_spec.rb new file mode 100644 index 0000000..383e8f9 --- /dev/null +++ b/spec/lib/resource_service_forwarding_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +module ResourceServiceForwardingSpec + class Service + def probe(*args, **keywords, &block) + [args, keywords, block] + end + + def search(filters, limit:) + [:search, filters, limit, yield(:search)] + end + + def find(id, filters, limit:) + [:find, id, filters, limit, yield(:find)] + end + + def new(attributes, validate:) + [:new, attributes, validate, yield(:new)] + end + + def build(attributes, validate:) + [:build, attributes, validate, yield(:build)] + end + end + + Controller = Struct.new(:resource_class, :enclosing_resource, :resource_specification) + Specification = Struct.new(:source, :find) + + class EnclosingResource < Struct.new(:items) + def build_item(attributes, validate:) + [:build_item, attributes, validate, yield(:build_item)] + end + end + + describe "Resource service argument forwarding" do + let(:service) { Service.new } + let(:enclosing_resource) { nil } + let(:specification) { Specification.new(:items) } + let(:controller) { Controller.new(service, enclosing_resource, specification) } + let(:proxy_class) { ResourcesController::ResourceService } + let(:proxy) { proxy_class.new(controller) } + let(:attributes) { { :name => 'Example', :validate => true } } + let(:filters) { { :limit => 1 } } + + it "preserves a trailing positional hash without adding keywords or a block" do + expect(proxy.probe({ :limit => 1 })).to eq([[{ :limit => 1 }], {}, nil]) + end + + it "forwards positional hashes, keywords and blocks to dynamic service methods" do + result = proxy.search(filters, limit: 2) { |operation| "block #{operation}" } + + expect(result).to eq([:search, filters, 2, 'block search']) + end + + it "forwards positional hashes, keywords and blocks through find" do + result = proxy.find(42, filters, limit: 2) { |operation| "block #{operation}" } + + expect(result).to eq([:find, 42, filters, 2, 'block find']) + end + + context "with a custom finder block" do + let(:specification) do + ResourcesController::Specification.new(:item, :class => Service) { [:custom, resource_class] } + end + + it "runs the configured finder in the controller and ignores caller arguments and block" do + result = proxy.find(42, filters, limit: 2) { raise 'caller block should not run' } + + expect(result).to eq([:custom, service]) + end + end + + context "with a custom finder method" do + let(:specification) do + ResourcesController::Specification.new(:item, :class => Service, :find => :resource_class) + end + + it "returns the controller method's result and ignores caller arguments and block" do + result = proxy.find(42, filters, limit: 2) { raise 'caller block should not run' } + + expect(result).to equal(service) + end + end + + it "forwards positional hashes, keywords and blocks to top-level new" do + result = proxy.new(attributes, validate: false) { |operation| "block #{operation}" } + + expect(result).to eq([:new, attributes, false, 'block new']) + end + + context "with an enclosing resource" do + let(:enclosing_resource) { EnclosingResource.new(service) } + + it "forwards positional hashes, keywords and blocks to the association's build" do + result = proxy.new(attributes, validate: false) { |operation| "block #{operation}" } + + expect(result).to eq([:build, attributes, false, 'block build']) + end + end + + context "with a singleton resource service" do + let(:proxy_class) { ResourcesController::SingletonResourceService } + let(:specification) { Specification.new(:item) } + + it "forwards positional hashes, keywords and blocks to top-level new" do + result = proxy.new(attributes, validate: false) { |operation| "block #{operation}" } + + expect(result).to eq([:new, attributes, false, 'block new']) + end + + context "with an enclosing resource" do + let(:enclosing_resource) { EnclosingResource.new(service) } + + it "forwards positional hashes, keywords and blocks to the singleton builder" do + result = proxy.new(attributes, validate: false) { |operation| "block #{operation}" } + + expect(result).to eq([:build_item, attributes, false, 'block build_item']) + end + end + end + end +end From 0a62d2d5fff5e3a26f163882d57a1ab4f5e67b4e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 6 Nov 2026 23:14:52 +0100 Subject: [PATCH 2/2] Bump version to 5.0.3 Co-authored-by: Codex --- CHANGELOG | 4 ++++ lib/resources_controller/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 864222d..1a379a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ += 5.0.3 + * use native argument forwarding in resource service proxies to reduce temporary allocations, + with regression coverage for positional hashes, keywords, blocks, and custom finders + = 5.0.2 * read :resource_path aliases only from request.path_parameters, preventing query and body parameters from removing enclosing resources and bypassing the current user's scoped association. Route-defined diff --git a/lib/resources_controller/version.rb b/lib/resources_controller/version.rb index 2155a37..5522cc2 100644 --- a/lib/resources_controller/version.rb +++ b/lib/resources_controller/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ResourcesController - VERSION = "5.0.2" + VERSION = "5.0.3" end