From 464bcda96fcf316837f327ebbf606c3dfd9fb601 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 21:18:38 +0200 Subject: [PATCH] feat: extend mime with Prism and Mime --- lib/prism/mime.rb | 58 ++++++++++++++++++++ lib/prism/multipart.rb | 117 +++++++++++++++++++++++++++++++++++++++++ lib/prism/orm.rb | 23 +++++++- 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 lib/prism/mime.rb create mode 100644 lib/prism/multipart.rb diff --git a/lib/prism/mime.rb b/lib/prism/mime.rb new file mode 100644 index 0000000..7da0e43 --- /dev/null +++ b/lib/prism/mime.rb @@ -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 diff --git a/lib/prism/multipart.rb b/lib/prism/multipart.rb new file mode 100644 index 0000000..ee18c19 --- /dev/null +++ b/lib/prism/multipart.rb @@ -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 diff --git a/lib/prism/orm.rb b/lib/prism/orm.rb index cd86691..d608596 100644 --- a/lib/prism/orm.rb +++ b/lib/prism/orm.rb @@ -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