Skip to content

Commit

Permalink
Add File as valid param type
Browse files Browse the repository at this point in the history
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 mattt#103
  • Loading branch information
gi committed Oct 2, 2020
1 parent a9626c3 commit 58d9431
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ By declaring parameter types, incoming parameters will automatically be transfor
* `Array` _("1,2,3,4,5")_
* `Hash` _(key1:value1,key2:value2)_
* `Date`, `Time`, & `DateTime`
* `File`

### Validations

Expand Down
2 changes: 2 additions & 0 deletions lib/sinatra/param.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'sinatra/base'
require 'sinatra/param/uploaded_file'
require 'sinatra/param/version'
require 'date'
require 'time'
Expand Down Expand Up @@ -121,6 +122,7 @@ def coerce(param, type, options = {})
return DateTime.parse(param) if type == DateTime
return Array(param.split(options[:delimiter] || ",")) if type == Array
return Hash[param.split(options[:delimiter] || ",").map{|c| c.split(options[:separator] || ":")}] if type == Hash
return UploadedFile.from_param(param) if type == File
if [TrueClass, FalseClass, Boolean].include? type
coerced = /^(false|f|no|n|0)$/i === param.to_s ? false : /^(true|t|yes|y|1)$/i === param.to_s ? true : nil
raise ArgumentError if coerced.nil?
Expand Down
37 changes: 37 additions & 0 deletions lib/sinatra/param/uploaded_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Sinatra
module Param
# A wrapper/delegator to an uploaded file.
#
# The attributes are the same as the original parameter hash keys with
# extra aliases conforming to the `Rack::Multipart::UploadedFile` interface.
class UploadedFile < SimpleDelegator
attr_reader :filename, :head, :name, :tempfile, :type

def initialize(filename:, head:, name:, tempfile:, type:)
super(tempfile)
@filename = filename
@head = head
@name = name
@tempfile = tempfile
@type = type
end

alias content_type type
alias original_filename filename

def self.from_param(param)
new(
filename: param.fetch(:filename),
head: param.fetch(:head),
name: param.fetch(:name),
tempfile: param.fetch(:tempfile),
type: param.fetch(:type),
)
rescue KeyError, NoMethodError => e
raise ArgumentError, e.message
end
end
end
end
15 changes: 15 additions & 0 deletions spec/dummy/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ class App < Sinatra::Base
params.to_json
end

put '/coerce/file' do
param :arg, File
{
arg: {
body: params[:arg].read,
content_type: params[:arg].content_type,
filename: params[:arg].filename,
head: params[:arg].head,
name: params[:arg].name,
original_filename: params[:arg].original_filename,
type: params[:arg].type,
},
}.to_json
end

get '/coerce/boolean' do
param :arg, Boolean
params.to_json
Expand Down
41 changes: 41 additions & 0 deletions spec/parameter_type_coercion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,47 @@
end
end

describe 'File' do
let(:arg) { Rack::Test::UploadedFile.new(content, 'text/csv', original_filename: 'file.csv') }
let(:content) { StringIO.new('content') }
let(:head) do
<<~HEAD
Content-Disposition: form-data; name="arg"; filename="file.csv"\r
Content-Type: text/csv\r
Content-Length: 7\r
HEAD
end

it 'coerces files' do
put('/coerce/file', arg: arg) do |response|
expect(response.status).to eql 200
parsed_body = JSON.parse(response.body)
expect(parsed_body['arg']).to be_a(Hash)
expect(parsed_body['arg']).to eq({
'body' => 'content',
'content_type' => 'text/csv',
'filename' => 'file.csv',
'head' => head,
'name' => 'arg',
'original_filename' => 'file.csv',
'type' => 'text/csv',
})
end
end

it 'returns 400 when not a hash' do
put('/coerce/file', arg: 'arg') do |response|
expect(response.status).to eql 400
end
end

it 'returns 400 when not a file upload hash' do
put('/coerce/file', arg: { 'a' => 'a' }) do |response|
expect(response.status).to eql 400
end
end
end

describe 'Boolean' do
it 'coerces truthy booleans to true' do
%w(1 true t yes y).each do |bool|
Expand Down

0 comments on commit 58d9431

Please sign in to comment.