Skip to content

Commit 58d9431

Browse files
committed
Add File as valid param type
With a type declaration of `File`, the input parameter hash of the uploaded file is coerce into a new UploadedFile object which conforms to the `Rack::Multipart::UploadedFile` interface. The input parameter hash must be of the shape: - `filename`: the original file name of the uploaded file - `head`: the header lines of the multipart request - `name`: the parameter name - `tempfile`: the `Tempfile` created from the content of the multipart request - `type`: the content media/MIME type Fixes #103
1 parent a9626c3 commit 58d9431

5 files changed

Lines changed: 96 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ By declaring parameter types, incoming parameters will automatically be transfor
6868
* `Array` _("1,2,3,4,5")_
6969
* `Hash` _(key1:value1,key2:value2)_
7070
* `Date`, `Time`, & `DateTime`
71+
* `File`
7172

7273
### Validations
7374

lib/sinatra/param.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'sinatra/base'
2+
require 'sinatra/param/uploaded_file'
23
require 'sinatra/param/version'
34
require 'date'
45
require 'time'
@@ -121,6 +122,7 @@ def coerce(param, type, options = {})
121122
return DateTime.parse(param) if type == DateTime
122123
return Array(param.split(options[:delimiter] || ",")) if type == Array
123124
return Hash[param.split(options[:delimiter] || ",").map{|c| c.split(options[:separator] || ":")}] if type == Hash
125+
return UploadedFile.from_param(param) if type == File
124126
if [TrueClass, FalseClass, Boolean].include? type
125127
coerced = /^(false|f|no|n|0)$/i === param.to_s ? false : /^(true|t|yes|y|1)$/i === param.to_s ? true : nil
126128
raise ArgumentError if coerced.nil?

lib/sinatra/param/uploaded_file.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
module Sinatra
4+
module Param
5+
# A wrapper/delegator to an uploaded file.
6+
#
7+
# The attributes are the same as the original parameter hash keys with
8+
# extra aliases conforming to the `Rack::Multipart::UploadedFile` interface.
9+
class UploadedFile < SimpleDelegator
10+
attr_reader :filename, :head, :name, :tempfile, :type
11+
12+
def initialize(filename:, head:, name:, tempfile:, type:)
13+
super(tempfile)
14+
@filename = filename
15+
@head = head
16+
@name = name
17+
@tempfile = tempfile
18+
@type = type
19+
end
20+
21+
alias content_type type
22+
alias original_filename filename
23+
24+
def self.from_param(param)
25+
new(
26+
filename: param.fetch(:filename),
27+
head: param.fetch(:head),
28+
name: param.fetch(:name),
29+
tempfile: param.fetch(:tempfile),
30+
type: param.fetch(:type),
31+
)
32+
rescue KeyError, NoMethodError => e
33+
raise ArgumentError, e.message
34+
end
35+
end
36+
end
37+
end

spec/dummy/app.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ class App < Sinatra::Base
7070
params.to_json
7171
end
7272

73+
put '/coerce/file' do
74+
param :arg, File
75+
{
76+
arg: {
77+
body: params[:arg].read,
78+
content_type: params[:arg].content_type,
79+
filename: params[:arg].filename,
80+
head: params[:arg].head,
81+
name: params[:arg].name,
82+
original_filename: params[:arg].original_filename,
83+
type: params[:arg].type,
84+
},
85+
}.to_json
86+
end
87+
7388
get '/coerce/boolean' do
7489
param :arg, Boolean
7590
params.to_json

spec/parameter_type_coercion_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,47 @@
130130
end
131131
end
132132

133+
describe 'File' do
134+
let(:arg) { Rack::Test::UploadedFile.new(content, 'text/csv', original_filename: 'file.csv') }
135+
let(:content) { StringIO.new('content') }
136+
let(:head) do
137+
<<~HEAD
138+
Content-Disposition: form-data; name="arg"; filename="file.csv"\r
139+
Content-Type: text/csv\r
140+
Content-Length: 7\r
141+
HEAD
142+
end
143+
144+
it 'coerces files' do
145+
put('/coerce/file', arg: arg) do |response|
146+
expect(response.status).to eql 200
147+
parsed_body = JSON.parse(response.body)
148+
expect(parsed_body['arg']).to be_a(Hash)
149+
expect(parsed_body['arg']).to eq({
150+
'body' => 'content',
151+
'content_type' => 'text/csv',
152+
'filename' => 'file.csv',
153+
'head' => head,
154+
'name' => 'arg',
155+
'original_filename' => 'file.csv',
156+
'type' => 'text/csv',
157+
})
158+
end
159+
end
160+
161+
it 'returns 400 when not a hash' do
162+
put('/coerce/file', arg: 'arg') do |response|
163+
expect(response.status).to eql 400
164+
end
165+
end
166+
167+
it 'returns 400 when not a file upload hash' do
168+
put('/coerce/file', arg: { 'a' => 'a' }) do |response|
169+
expect(response.status).to eql 400
170+
end
171+
end
172+
end
173+
133174
describe 'Boolean' do
134175
it 'coerces truthy booleans to true' do
135176
%w(1 true t yes y).each do |bool|

0 commit comments

Comments
 (0)