Skip to content

Commit

Permalink
[Validator] Improve performance leveraging ActiveStorage::Blob#metada…
Browse files Browse the repository at this point in the history
…ta metadata (#340)
  • Loading branch information
Mth0158 committed Jan 1, 2025
1 parent f252a59 commit 8d639a7
Show file tree
Hide file tree
Showing 18 changed files with 209 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ Added features:
- `dimension` validator now supports videos
- `aspect_ratio` validator now supports videos
- `processable_image` validator is now `processable_file` validator and supports image/video/audio
- Major performance improvment have been added: we now only perform the expensive io analysis operation on the newly attached files. For previously attached files, we validate them using Rails `ActiveStorage::Blob#metadata` internal mecanism ([more here](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob/analyzable.rb)).
- All error messages have been given an upgrade and new variables that you can use

But this major version bump also comes with some breaking changes. Below are the main breaking changes you need to be aware of:
Expand Down
5 changes: 3 additions & 2 deletions lib/active_storage_validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
require 'active_storage_validations/analyzer/video_analyzer'
require 'active_storage_validations/analyzer/audio_analyzer'

require 'active_storage_validations/extensors/asv_blob_metadatable'
require 'active_storage_validations/extensors/asv_marcelable'

require 'active_storage_validations/railtie'
require 'active_storage_validations/engine'
require 'active_storage_validations/attached_validator'
Expand All @@ -23,8 +26,6 @@
require 'active_storage_validations/size_validator'
require 'active_storage_validations/total_size_validator'

require 'active_storage_validations/marcel_extensor'

ActiveSupport.on_load(:active_record) do
send :include, ActiveStorageValidations
end
2 changes: 1 addition & 1 deletion lib/active_storage_validations/duration_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def validate_each(record, attribute, _value)
flat_options = set_flat_options(record)

attachables_and_blobs(record, attribute).each do |attachable, blob|
duration = metadata_for(attachable)[:duration]
duration = metadata_for(blob, attachable)&.fetch(:duration, nil)

if duration.to_i <= 0
errors_options = initialize_error_options(options, attachable)
Expand Down
18 changes: 18 additions & 0 deletions lib/active_storage_validations/extensors/asv_blob_metadatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module ActiveStorageValidations
module ASVBlobMetadatable
extend ActiveSupport::Concern

included do
def active_storage_validations_metadata
metadata.dig('custom', 'active_storage_validations')
end

def active_storage_validations_metadata=(value)
metadata['custom'] ||= {}
metadata['custom']['active_storage_validations'] = value
end
end
end
end
5 changes: 5 additions & 0 deletions lib/active_storage_validations/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@

module ActiveStorageValidations
class Railtie < ::Rails::Railtie
initializer 'active_storage_validations.extend_active_storage_blob' do
Rails.application.config.to_prepare do
ActiveStorage::Blob.include(ActiveStorageValidations::ASVBlobMetadatable)
end
end
end
end
12 changes: 11 additions & 1 deletion lib/active_storage_validations/shared/asv_analyzable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ module ASVAnalyzable

private

def metadata_for(attachable)
# Retrieve the ASV metadata from the blob.
# If the blob has not been analyzed by our gem yet, the gem will analyze the
# attachable with the corresponding analyzer and set the metadata in the
# blob.
def metadata_for(blob, attachable)
return blob.active_storage_validations_metadata if blob.active_storage_validations_metadata.present?

blob.active_storage_validations_metadata = generate_metadata_for(attachable)
end

def generate_metadata_for(attachable)
analyzer_for(attachable).metadata
end

Expand Down
2 changes: 1 addition & 1 deletion lib/active_storage_validations/shared/asv_attachable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module ASVAttachable
# to perform file analyses.
def validate_changed_files_from_metadata(record, attribute)
attachables_and_blobs(record, attribute).each do |attachable, blob|
is_valid?(record, attribute, attachable, metadata_for(attachable))
is_valid?(record, attribute, attachable, metadata_for(blob, attachable))
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: aspect_ratio_validator_is_performance_optimizeds
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#

class AspectRatio::Validator::IsPerformanceOptimized < ApplicationRecord
has_one_attached :is_performance_optimized
has_many_attached :is_performance_optimizeds
validates :is_performance_optimized, aspect_ratio: :square
validates :is_performance_optimizeds, aspect_ratio: :square
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: dimension_validator_is_performance_optimizeds
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#

class Dimension::Validator::IsPerformanceOptimized < ApplicationRecord
has_one_attached :is_performance_optimized
has_many_attached :is_performance_optimizeds
validates :is_performance_optimized, dimension: { width: 150, height: 150 }
validates :is_performance_optimizeds, dimension: { width: 150, height: 150 }
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: duration_validator_is_performance_optimizeds
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#

class Duration::Validator::IsPerformanceOptimized < ApplicationRecord
has_one_attached :is_performance_optimized
has_many_attached :is_performance_optimizeds
validates :is_performance_optimized, duration: { less_than: 2.seconds }
validates :is_performance_optimizeds, duration: { less_than: 2.seconds }
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: processable_file_validator_is_performance_optimizeds
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#

class ProcessableFile::Validator::IsPerformanceOptimized < ApplicationRecord
has_one_attached :is_performance_optimized
has_many_attached :is_performance_optimizeds
validates :is_performance_optimized, processable_file: true
validates :is_performance_optimizeds, processable_file: true
end
12 changes: 12 additions & 0 deletions test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@
end
end

%i(
aspect_ratio
dimension
duration
processable_file
).each do |validator|
create_table :"#{validator}_validator_is_performance_optimizeds", force: :cascade do |t|
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
end
end

%w(proc_option invalid_named_argument invalid_is_xy_argument).each do |invalid_case|
create_table :"aspect_ratio_validator_check_validity_#{invalid_case.pluralize}", force: :cascade do |t|
t.datetime :created_at, null: false
Expand Down
16 changes: 16 additions & 0 deletions test/extensors/asv_blob_metadatable_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require 'test_helper'

describe ActiveStorageValidations::ASVBlobMetadatable do
let(:blob) { ActiveStorage::Blob.new }

it "adds our gem's getter method to ActiveStorage::Blob custom metadata" do
assert blob.respond_to?(:active_storage_validations_metadata)
end

it "adds our gem's setter method to ActiveStorage::Blob custom metadata" do
blob.active_storage_validations_metadata = { 'duration' => '1.0' }
assert blob.active_storage_validations_metadata == { 'duration' => '1.0' }
end
end
13 changes: 13 additions & 0 deletions test/validators/aspect_ratio_validator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'test_helper'
require 'validators/shared_examples/checks_validator_validity'
require 'validators/shared_examples/is_performance_optimized'
require 'validators/shared_examples/works_fine_with_attachables'
require 'validators/shared_examples/works_with_all_rails_common_validation_options'

Expand Down Expand Up @@ -242,6 +243,18 @@
end
end

describe 'Blob Metadata' do
let(:attachable) do
{
io: File.open(Rails.root.join('public', 'image_150x150.png')),
filename: 'image_150x150.png',
content_type: 'image/png'
}
end

include IsPerformanceOptimized
end

describe 'Rails options' do
include WorksWithAllRailsCommonValidationOptions
end
Expand Down
13 changes: 13 additions & 0 deletions test/validators/duration_validator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'test_helper'
require 'validators/shared_examples/checks_validator_validity'
require 'validators/shared_examples/is_performance_optimized'
require 'validators/shared_examples/works_with_all_rails_common_validation_options'

describe ActiveStorageValidations::DurationValidator do
Expand Down Expand Up @@ -248,6 +249,18 @@
end
end

describe 'Blob Metadata' do
let(:attachable) do
{
io: File.open(Rails.root.join('public', 'audio.mp3')),
filename: 'audio.mp3',
content_type: 'audio/mp3'
}
end

include IsPerformanceOptimized
end

describe 'Rails options' do
include WorksWithAllRailsCommonValidationOptions
end
Expand Down
13 changes: 13 additions & 0 deletions test/validators/processable_file_validator_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'test_helper'
require 'validators/shared_examples/is_performance_optimized'
require 'validators/shared_examples/works_fine_with_attachables'
require 'validators/shared_examples/works_with_all_rails_common_validation_options'

Expand Down Expand Up @@ -55,6 +56,18 @@
end
end

describe 'Blob Metadata' do
let(:attachable) do
{
io: File.open(Rails.root.join('public', 'audio.mp3')),
filename: 'audio.mp3',
content_type: 'audio/mp3'
}
end

include IsPerformanceOptimized
end

describe 'Rails options' do
include WorksWithAllRailsCommonValidationOptions
end
Expand Down
34 changes: 34 additions & 0 deletions test/validators/shared_examples/is_performance_optimized.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module IsPerformanceOptimized
extend ActiveSupport::Concern

included do
subject { validator_test_class::IsPerformanceOptimized.new(params) }

let(:validator_class) { "ActiveStorageValidations::#{validator_test_class.name.delete('::')}".constantize }

describe "when the attachable blob has not been analyzed by our gem yet" do
before { subject.is_performance_optimized.attach(attachable) }

it "calls the corresponding media analyzer (expensive operation) once" do
assert_called_on_instance_of(validator_class, :generate_metadata_for, times: 1, returns: {}) do
subject.valid?
end
end
end

describe "when an attachable blob has already been analyzed by our gem" do
before do
subject.is_performance_optimizeds.attach(attachable)
subject.save!
end

it "only calls the corresponding media analyzer (expensive operation) on the new attachable" do
assert_called_on_instance_of(validator_class, :generate_metadata_for, times: 1, returns: {}) do
subject.is_performance_optimizeds.attach(attachable)
end
end
end
end
end

0 comments on commit 8d639a7

Please sign in to comment.