Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/lib/flex/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Attributes
extend ActiveSupport::Concern
include Flex::Attributes::AddressAttribute
include Flex::Attributes::ArrayAttribute
include Flex::Attributes::DocumentAttribute
include Flex::Attributes::MemorableDateAttribute
include Flex::Attributes::MoneyAttribute
include Flex::Attributes::NameAttribute
Expand Down
36 changes: 36 additions & 0 deletions app/lib/flex/attributes/document_attribute.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Flex
module Attributes
# DocumentAttribute provides a DSL for defining attributes representing
# a collection of documents using ActiveStorage.
#
# @example Defining document attributes
# class Application < ApplicationRecord
# include Flex::Attributes
#
# flex_attribute :identity_documents, :document
# flex_attribute :proof_of_income, :document
# end
#
# application = Application.new
# application.identity_documents.attach(params[:identity_documents])
# application.proof_of_income.attach(params[:income_docs])
#
module DocumentAttribute
extend ActiveSupport::Concern

class_methods do
# Defines a document attribute that uses ActiveStorage for file handling.
#
# @param [Symbol] name The base name for the attribute
# @param [Hash] options Options for the attribute
# @return [void]
def document_attribute(name, options = {})
# Set up ActiveStorage has_many_attached
has_many_attached name

# Define custom methods or validations here if needed in the future
end
end
end
end
end
1 change: 1 addition & 0 deletions spec/fixtures/files/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions spec/fixtures/files/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a test file for document attribute specs.
180 changes: 180 additions & 0 deletions spec/lib/flex/attributes/document_attribute_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
require "rails_helper"

RSpec.describe Flex::Attributes::DocumentAttribute do
include ActiveJob::TestHelper
let(:test_file) { Rack::Test::UploadedFile.new(File.expand_path("../../../fixtures/files/test.txt", __dir__), "text/plain") }
let(:test_image) { Rack::Test::UploadedFile.new(File.expand_path("../../../fixtures/files/test.jpg", __dir__), "image/jpeg") }

should_setup_active_storage = !ActiveRecord::Base.connection.table_exists?(:active_storage_blobs) ||
!ActiveRecord::Base.connection.table_exists?(:active_storage_attachments) ||
!ActiveRecord::Base.connection.table_exists?(:active_storage_variant_records)

before(:all) do # rubocop:disable RSpec/BeforeAfterAll
# Create ActiveStorage tables

if should_setup_active_storage
ActiveRecord::Base.connection.create_table :active_storage_blobs, force: true do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum, null: false

t.timestamps

t.index [ :key ], unique: true
end

ActiveRecord::Base.connection.create_table :active_storage_attachments, force: true do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false
t.references :blob, null: false

t.timestamps

t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
end

ActiveRecord::Base.connection.create_table :active_storage_variant_records, force: true do |t|
t.belongs_to :blob, null: false, index: false
t.string :variation_digest, null: false

t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
end
end

ActiveRecord::Base.connection.create_table :test_models, force: true do |t|
t.timestamps
end

class TestModel < ApplicationRecord # rubocop:disable RSpec/LeakyConstantDeclaration
include Flex::Attributes
flex_attribute :documents, :document
flex_attribute :profile_pictures, :document
end
end

after(:all) do # rubocop:disable RSpec/BeforeAfterAll
ActiveRecord::Base.connection.drop_table :test_models
if should_setup_active_storage
ActiveRecord::Base.connection.drop_table :active_storage_variant_records
ActiveRecord::Base.connection.drop_table :active_storage_attachments
ActiveRecord::Base.connection.drop_table :active_storage_blobs
end
Object.send(:remove_const, :TestModel) # rubocop:disable RSpec/RemoveConst
end

after do
model.documents.purge
model.profile_pictures.purge
end

let(:model) { TestModel.new } # rubocop:disable RSpec/ScatteredLet

describe "attachment handling" do
it "allows attaching single files" do
model.documents.attach(test_file)
model.profile_pictures.attach(test_image)

expect(model.documents).to be_attached
expect(model.documents.count).to eq(1)
expect(model.documents.first.filename.to_s).to eq("test.txt")
expect(model.documents.first.content_type).to eq("text/plain")

expect(model.profile_pictures).to be_attached
expect(model.profile_pictures.count).to eq(1)
expect(model.profile_pictures.first.filename.to_s).to eq("test.jpg")
expect(model.profile_pictures.first.content_type).to eq("image/jpeg")
end

it "allows attaching multiple files" do
model.documents.attach([ test_file, test_image ])
model.profile_pictures.attach([ test_image, test_file ])

expect(model.documents).to be_attached
expect(model.documents.count).to eq(2)
expect(model.documents.first.content_type).to eq("text/plain")
expect(model.documents.last.content_type).to eq("image/jpeg")

expect(model.profile_pictures).to be_attached
expect(model.profile_pictures.count).to eq(2)
expect(model.profile_pictures.first.content_type).to eq("image/jpeg")
expect(model.profile_pictures.last.content_type).to eq("text/plain")
end

it "supports multiple document attributes on the same model" do
model.documents.attach(test_file)
model.profile_pictures.attach(test_image)

expect(model.documents).to be_attached
expect(model.profile_pictures).to be_attached
expect(model.documents.first.content_type).to eq("text/plain")
expect(model.profile_pictures.first.content_type).to eq("image/jpeg")
end
end

describe "file operations" do
before do
model.documents.attach(test_file)
model.profile_pictures.attach(test_image)
end

it "allows purging attached files" do
model.documents.purge
model.profile_pictures.purge

expect(model.documents).not_to be_attached
expect(model.profile_pictures).not_to be_attached
end

it "provides access to blob attributes" do
blob = model.documents.first.blob

expect(blob).to respond_to(:byte_size)
expect(blob).to respond_to(:checksum)
expect(blob).to respond_to(:content_type)
expect(blob).to respond_to(:filename)
end
end

describe "persistence" do
it "persists attached files" do
model.documents.attach(test_file)
model.profile_pictures.attach(test_image)
model.save!

reloaded_model = TestModel.find(model.id)
expect(reloaded_model.documents).to be_attached
expect(reloaded_model.documents.first.filename.to_s).to eq("test.txt")

expect(reloaded_model.profile_pictures).to be_attached
expect(reloaded_model.profile_pictures.first.filename.to_s).to eq("test.jpg")
end

it "allows replacing attached files" do
model.documents.attach(test_file)
model.save!

model.documents.purge # Need to purge first to replace
model.documents.attach(test_image)
model.save!

reloaded_model = TestModel.find(model.id)
expect(reloaded_model.documents.count).to eq(1)
expect(reloaded_model.documents.first.content_type).to eq("image/jpeg")
end
end

describe "error handling" do
it "handles attempting to attach to an unsaved record" do
expect { model.documents.attach(test_file) }.not_to raise_error
expect { model.profile_pictures.attach(test_image) }.not_to raise_error
end

it "handles invalid attachments" do
expect { model.documents.attach(Object.new) }.to raise_error(ArgumentError, /Could not find or build blob/)
end
end
end