feat: extend mime with Prism and Mime

This commit is contained in:
dualfroz committed 2026-09-06 21:18:38 +02:00
1 parent 6825941c0b
commit 464bcda96f
3 files changed
+197 -1

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
module Prism
module Mime
class UnknownMimeType < StandardError; end
TYPES = {
html: 'text/html',
json: 'application/json',
xml: 'application/xml',
text: 'text/plain',
csv: 'text/csv',
js: 'application/javascript'
}.freeze
EXTENSIONS = TYPES.invert.freeze
class << self
def [](symbol)
TYPES.fetch(symbol.to_sym) { raise UnknownMimeType, "unknown mime type #{symbol.inspect}" }
end
def known?(symbol)
TYPES.key?(symbol.to_sym)
end
def symbol_for_content_type(content_type)
return nil unless content_type
base = content_type.split(';').first.to_s.strip
EXTENSIONS[base]
end
# Returns the best-matching format symbol for an HTTP Accept header,
# honoring q-values and falling back to :html for a bare */*.
def best_from_accept(accept_header)
return nil if accept_header.nil? || accept_header.strip.empty?
parse_accept(accept_header).each do |media_type|
return :html if media_type == '*/*'
symbol = EXTENSIONS[media_type]
return symbol if symbol
end
nil
end
private
def parse_accept(header)
entries = header.split(',').filter_map do |part|
type, *params = part.strip.split(';').map(&:strip)
next if type.nil? || type.empty?
quality = 1.0
params.each { |p| quality = p.sub('q=', '').to_f if p.start_with?('q=') }
[type, quality]
end
entries.sort_by.with_index { |(_, quality), index| [-quality, index] }.map(&:first)
end
end
end
end
+117
View File
@@ -0,0 +1,117 @@
require 'tempfile'
module Prism
# A single uploaded file extracted from a multipart/form-data request.
# Backed by a Tempfile so large uploads do not sit fully in memory as
# part of the request/params hash (Rails' UploadedFile does the same).
class UploadedFile
attr_reader :original_filename, :content_type, :tempfile
def initialize(original_filename:, content_type:, tempfile:)
@original_filename = original_filename
@content_type = content_type
@tempfile = tempfile
end
def path
tempfile.path
end
def size
tempfile.size
end
def read(*args)
tempfile.read(*args)
end
def rewind
tempfile.rewind
end
def to_s
original_filename.to_s
end
end
# Parses `multipart/form-data` request bodies (RFC 2046/7578) into a flat
# hash of field name => value, where file parts become UploadedFile
# instances and everything else stays a String, mirroring how the rest of
# Request#parse_body treats JSON/urlencoded bodies.
module Multipart
MalformedError = Class.new(StandardError)
CONTENT_TYPE_RE = %r{\Amultipart/form-data}i.freeze
module_function
def multipart?(content_type)
!content_type.nil? && CONTENT_TYPE_RE.match?(content_type)
end
def boundary_from(content_type)
return nil unless content_type
match = content_type.match(/boundary=(?:"([^"]*)"|([^;\s]+))/i)
return nil unless match
match[1] || match[2]
end
def parse(body, boundary)
return Prism::Params.new if boundary.nil? || boundary.empty? || body.nil?
body = body.dup.force_encoding(Encoding::BINARY)
delimiter = "--#{boundary}"
pieces = body.split(delimiter)
# pieces[0] is the preamble before the first delimiter, pieces[-1] is
# the "--\r\n" closing marker (or trailing epilogue) after the last one.
pairs = pieces[1..-2].to_a.filter_map do |piece|
part = piece.sub(/\A\r\n/, '').sub(/\r\n\z/, '')
part.empty? ? nil : part_pair(part)
end.compact
Prism::Params.nested_from(pairs)
end
def part_pair(part)
header_block, separator, content = part.partition("\r\n\r\n")
return nil if separator.empty?
headers = parse_headers(header_block)
disposition = headers['content-disposition']
return nil unless disposition
name = extract_token(disposition, 'name')
return nil unless name
filename = extract_token(disposition, 'filename')
value =
if filename
build_uploaded_file(filename, headers['content-type'], content)
else
content.dup.force_encoding(Encoding::UTF_8)
end
[name, value]
end
def build_uploaded_file(filename, content_type, content)
tempfile = Tempfile.new('prism-upload')
tempfile.binmode
tempfile.write(content)
tempfile.rewind
UploadedFile.new(original_filename: filename, content_type: content_type, tempfile: tempfile)
end
def parse_headers(block)
block.split("\r\n").each_with_object({}) do |line, acc|
key, value = line.split(':', 2)
next unless key && value
acc[key.strip.downcase] = value.strip
end
end
def extract_token(header_value, key)
match = header_value.match(/#{key}="([^"]*)"/) || header_value.match(/#{key}=([^;\s]+)/)
match && match[1]
end
end
end
+22 -1
View File
@@ -1,8 +1,29 @@
require_relative 'orm/store'
require_relative 'orm/transactions'
require_relative 'orm/errors'
require_relative 'orm/type_casting'
require_relative 'orm/attributes'
require_relative 'orm/query/predicates'
require_relative 'orm/query/condition_node'
require_relative 'orm/pagination'
require_relative 'orm/serialization'
require_relative 'orm/enum'
require_relative 'orm/relation'
require_relative 'orm/validatable'
require_relative 'orm/callbacks'
require_relative 'orm/dirty'
require_relative 'orm/locking'
require_relative 'orm/pessimistic'
require_relative 'orm/paranoia'
require_relative 'orm/list_position'
require_relative 'orm/secure_password'
require_relative 'orm/secure_token'
require_relative 'orm/token_for'
require_relative 'orm/encryption'
require_relative 'orm/habtm_collection'
require_relative 'orm/n_plus_one_detector'
require_relative 'orm/associable'
require_relative 'orm/nested_attributes'
require_relative 'orm/model'
require_relative 'orm/migration'
require_relative 'orm/migrator'
@@ -23,7 +44,7 @@ module Prism
end
def transaction(&block)
store.transaction(&block)
store.transaction_with_callbacks(&block)
end
end
end