Skip to content

Commit efa11e6

Browse files
Merge pull request rails#49863 from yogeshjain999/service-as-proc
Allow accepting `service` as a proc in `has_one_attached` and `has_many_attached`
2 parents 6049a22 + 8bf1353 commit efa11e6

File tree

6 files changed

+107
-26
lines changed

6 files changed

+107
-26
lines changed

activestorage/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
* Allow accepting `service` as a proc as well in `has_one_attached` and `has_many_attached`.
2+
3+
*Yogesh Khater*
14

25
Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activestorage/CHANGELOG.md) for previous changes.

activestorage/app/models/active_storage/blob.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
1818
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
1919
class ActiveStorage::Blob < ActiveStorage::Record
20-
include Analyzable
21-
include Identifiable
22-
include Representable
23-
2420
self.table_name = "active_storage_blobs"
2521

2622
MINIMUM_TOKEN_LENGTH = 28
@@ -155,8 +151,28 @@ def compose(blobs, filename:, content_type: nil, metadata: nil)
155151
combined_blob.save!
156152
end
157153
end
154+
155+
def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
156+
if service_name
157+
services.fetch(service_name) do
158+
raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
159+
end
160+
else
161+
validate_global_service_configuration
162+
end
163+
end
164+
165+
def validate_global_service_configuration # :nodoc:
166+
if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
167+
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
168+
end
169+
end
158170
end
159171

172+
include Analyzable
173+
include Identifiable
174+
include Representable
175+
160176
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
161177
def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
162178
super

activestorage/lib/active_storage/attached/changes/create_one.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,12 @@ def find_or_build_blob
118118
end
119119

120120
def attachment_service_name
121-
record.attachment_reflections[name].options[:service_name]
121+
service_name = record.attachment_reflections[name].options[:service_name]
122+
if service_name.is_a?(Proc)
123+
service_name = service_name.call(record)
124+
ActiveStorage::Blob.validate_service_configuration(service_name, record.class, name)
125+
end
126+
service_name
122127
end
123128
end
124129
end

activestorage/lib/active_storage/attached/model.rb

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,18 @@ module Attached::Model
7878
# (i.e. destroyed) whenever the record is destroyed.
7979
#
8080
# If you need the attachment to use a service which differs from the globally configured one,
81-
# pass the +:service+ option. For instance:
81+
# pass the +:service+ option. For example:
8282
#
8383
# class User < ActiveRecord::Base
8484
# has_one_attached :avatar, service: :s3
8585
# end
8686
#
87+
# +:service+ can also be specified as a proc, and it will be called with the model instance:
88+
#
89+
# class User < ActiveRecord::Base
90+
# has_one_attached :avatar, service: ->(user) { user.in_europe_region? ? :s3_europe : :s3_usa }
91+
# end
92+
#
8793
# If you need to enable +strict_loading+ to prevent lazy loading of attachment,
8894
# pass the +:strict_loading+ option. You can do:
8995
#
@@ -92,7 +98,7 @@ module Attached::Model
9298
# end
9399
#
94100
def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
95-
validate_service_configuration(name, service)
101+
ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
96102

97103
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
98104
# frozen_string_literal: true
@@ -163,12 +169,18 @@ def #{name}=(attachable)
163169
# (i.e. destroyed) whenever the record is destroyed.
164170
#
165171
# If you need the attachment to use a service which differs from the globally configured one,
166-
# pass the +:service+ option. For instance:
172+
# pass the +:service+ option. For example:
167173
#
168174
# class Gallery < ActiveRecord::Base
169175
# has_many_attached :photos, service: :s3
170176
# end
171177
#
178+
# +:service+ can also be specified as a proc, and it will be called with the model instance:
179+
#
180+
# class Gallery < ActiveRecord::Base
181+
# has_many_attached :photos, service: ->(gallery) { gallery.personal? ? :personal_s3 : :s3 }
182+
# end
183+
#
172184
# If you need to enable +strict_loading+ to prevent lazy loading of attachments,
173185
# pass the +:strict_loading+ option. You can do:
174186
#
@@ -177,7 +189,7 @@ def #{name}=(attachable)
177189
# end
178190
#
179191
def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
180-
validate_service_configuration(name, service)
192+
ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
181193

182194
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
183195
# frozen_string_literal: true
@@ -223,23 +235,6 @@ def #{name}=(attachables)
223235
yield reflection if block_given?
224236
ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
225237
end
226-
227-
private
228-
def validate_service_configuration(association_name, service)
229-
if service.present?
230-
ActiveStorage::Blob.services.fetch(service) do
231-
raise ArgumentError, "Cannot configure service :#{service} for #{name}##{association_name}"
232-
end
233-
else
234-
validate_global_service_configuration
235-
end
236-
end
237-
238-
def validate_global_service_configuration
239-
if connected? && ActiveStorage::Blob.table_exists? && Rails.configuration.active_storage.service.nil?
240-
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
241-
end
242-
end
243238
end
244239

245240
def attachment_changes # :nodoc:

activestorage/test/models/attached/many_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,23 @@ def highlights
772772
end
773773
end
774774

775+
test "attaching a new blob from an uploaded file with a service defined at runtime" do
776+
extra_attached = Class.new(User) do
777+
def self.name; superclass.name; end
778+
779+
has_many_attached :signatures, service: ->(user) { "disk_#{user.mirror_region}" }
780+
781+
def mirror_region
782+
:mirror_2
783+
end
784+
end
785+
786+
@user = @user.becomes(extra_attached)
787+
788+
@user.signatures.attach fixture_file_upload("cropped.pdf")
789+
assert_equal :disk_mirror_2, @user.signatures.first.service.name
790+
end
791+
775792
test "attaching blobs to a persisted, unchanged, and valid record, returns the attachments" do
776793
@user.highlights.attach create_blob(filename: "racecar.jpg")
777794
return_value = @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
@@ -823,6 +840,20 @@ def highlights
823840
assert_match(/Cannot configure service :unknown for User#featured_photos/, error.message)
824841
end
825842

843+
test "raises error when misconfigured service is defined at runtime" do
844+
extra_attached = Class.new(User) do
845+
def self.name; superclass.name; end
846+
847+
has_many_attached :featured_vlogs, service: ->(*) { :unknown }
848+
end
849+
850+
@user = @user.becomes(extra_attached)
851+
852+
assert_raises match: /Cannot configure service :unknown for .+#featured_vlog/ do
853+
@user.featured_vlogs.attach fixture_file_upload("video.mp4")
854+
end
855+
end
856+
826857
test "creating variation by variation name" do
827858
assert_no_enqueued_jobs only: ActiveStorage::TransformJob do
828859
@user.highlights_with_variants.attach fixture_file_upload("racecar.jpg")

activestorage/test/models/attached/one_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,23 @@ def avatar
743743
end
744744
end
745745

746+
test "attaching a new blob from an uploaded file with a service defined at runtime" do
747+
extra_attached = Class.new(User) do
748+
def self.name; superclass.name; end
749+
750+
has_one_attached :signature, service: ->(user) { "disk_#{user.mirror_region}" }
751+
752+
def mirror_region
753+
:mirror_2
754+
end
755+
end
756+
757+
@user = @user.becomes(extra_attached)
758+
759+
@user.signature.attach fixture_file_upload("cropped.pdf")
760+
assert_equal :disk_mirror_2, @user.signature.service.name
761+
end
762+
746763
test "raises error when global service configuration is missing" do
747764
Rails.configuration.active_storage.stub(:service, nil) do
748765
error = assert_raises RuntimeError do
@@ -765,6 +782,20 @@ def avatar
765782
assert_match(/Cannot configure service :unknown for User#featured_photo/, error.message)
766783
end
767784

785+
test "raises error when misconfigured service is defined at runtime" do
786+
extra_attached = Class.new(User) do
787+
def self.name; superclass.name; end
788+
789+
has_one_attached :featured_vlog, service: ->(*) { :unknown }
790+
end
791+
792+
@user = @user.becomes(extra_attached)
793+
794+
assert_raises match: /Cannot configure service :unknown for .+#featured_vlog/ do
795+
@user.featured_vlog.attach fixture_file_upload("video.mp4")
796+
end
797+
end
798+
768799
test "creating variation by variation name" do
769800
@user.avatar_with_variants.attach fixture_file_upload("racecar.jpg")
770801
variant = @user.avatar_with_variants.variant(:thumb).processed

0 commit comments

Comments
 (0)