Skip to content

Commit ad0bf8d

Browse files
committed
feat(attachable): expand the image shortcut in the type key
- `type: 'image'` / `type: :image` expands lazily at query time against ActiveStorage.variable_content_types | web_image_content_types — never frozen at boot, and the lambda only runs once the model proved it has real attachments, so the gem still loads without ActiveStorage - a real content type always carries a slash, so the shortcut name is unambiguous; an unknown literal stays a plain equality that matches nothing (never converted into a silent no-op), an unknown symbol narrows nothing; shortcuts compose inside arrays next to literals
1 parent ed078d3 commit ad0bf8d

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎lib/filterable/attachable.rb‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ module Attachable
1515
# The explicit values that flip +present+ to its unattached side.
1616
FALSE_VALUES = [false, 0, '0', 'false'].freeze
1717

18+
# The symbolic +type+ shortcuts, each expanded lazily at query time against
19+
# ActiveStorage's configured content-type lists — never frozen at boot, and
20+
# only ever called once the model proved it has real attachments, so the
21+
# gem still loads without ActiveStorage.
22+
TYPE_SHORTCUTS = {
23+
'image' => -> { ActiveStorage.variable_content_types | ActiveStorage.web_image_content_types }
24+
}.freeze
25+
1826
module_function
1927

2028
# Narrows the scope from each accepted attachment key.
@@ -72,12 +80,37 @@ def accepted(params, scope)
7280
def entries(name, attachment, bounds)
7381
[
7482
[:present, presence_value(bounds)],
75-
[:type, Filterable::ValueNormalization.normalize(bounds[:type])],
83+
[:type, content_types(bounds[:type])],
7684
[:min_size, Filterable::Rangeable.parse(bounds[:min_size])],
7785
[:max_size, Filterable::Rangeable.parse(bounds[:max_size])]
7886
].filter_map { |key, value| [name, attachment, key, value, bounds[key]] unless value.nil? }
7987
end
8088

89+
# The normalized +type+ value, with symbolic shortcuts expanded. A real
90+
# content type always carries a slash, so a known shortcut name is
91+
# unambiguous; an unknown literal stays a plain equality that matches
92+
# nothing, never a silent no-op.
93+
#
94+
# @api private
95+
# @param raw [Object] the raw +type+ value.
96+
# @return [String, Array, nil] the usable content type(s), or nil.
97+
def content_types(raw)
98+
Filterable::ValueNormalization.normalize(expand_types(raw))
99+
end
100+
101+
# Expands every known shortcut in a raw +type+ value, leaving everything
102+
# else untouched.
103+
#
104+
# @api private
105+
# @param raw [Object] the raw +type+ value, scalar or array.
106+
# @return [Object] the value with shortcuts replaced by content-type lists.
107+
def expand_types(raw)
108+
return raw.flat_map { |value| expand_types(value) } if raw.is_a?(Array)
109+
110+
shortcut = TYPE_SHORTCUTS[raw.to_s]
111+
shortcut ? shortcut.call : raw
112+
end
113+
81114
# How a +present+ key reads: +:attached+ on a strict true or bare key,
82115
# +:missing+ on an explicit false, nil otherwise.
83116
#

‎spec/active_storage_spec.rb‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,32 @@ def attach(record, name, content_type, byte_size = 1)
154154
expect(result).to contain_exactly(pdf_contract, png_contract)
155155
end
156156

157+
it 'expands the image shortcut against the configured content types' do
158+
[:image, 'image'].each do |shortcut|
159+
result = Contract.filterable(filters: { document: { type: shortcut } })
160+
161+
expect(result).to contain_exactly(png_contract)
162+
end
163+
end
164+
165+
it 'expands the image shortcut inside an array, next to literal types' do
166+
result = Contract.filterable(filters: { document: { type: ['image', 'application/pdf'] } })
167+
168+
expect(result).to contain_exactly(pdf_contract, png_contract)
169+
end
170+
171+
it 'keeps an unknown literal type as an equality that matches nothing' do
172+
result = Contract.filterable(filters: { document: { type: 'garbage' } })
173+
174+
expect(result).to be_empty
175+
end
176+
177+
it 'ignores an unknown symbolic type' do
178+
result = Contract.filterable(filters: { document: { type: :video } })
179+
180+
expect(result).to contain_exactly(pdf_contract, png_contract, bare_contract)
181+
end
182+
157183
it 'applies inclusive byte size bounds' do
158184
attach(pdf_contract, 'annexes', 'text/csv', 100)
159185
attach(png_contract, 'annexes', 'text/csv', 5000)

0 commit comments

Comments
 (0)