Module: VisionAPI::Multipart
- Defined in:
- lib/vision_api/multipart.rb
Overview
multipart/form-data encoding, written out rather than pulled in.
The gem depends on nothing but the standard library, and this is the only piece net/http does not already give us.
Constant Summary collapse
- CONTENT_TYPES =
{ ".pdf" => "application/pdf", ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".webp" => "image/webp", ".tif" => "image/tiff", ".tiff" => "image/tiff" }.freeze
Class Method Summary collapse
- .content_type_for(name) ⇒ Object private
-
.encode(fields, file) ⇒ Object
Encodes a multipart body.
-
.resolve(file) ⇒ Object
Turns whatever the caller passed into [filename, bytes, content_type].
- .wrap(value) ⇒ Object private
Class Method Details
.content_type_for(name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
102 103 104 |
# File 'lib/vision_api/multipart.rb', line 102 def content_type_for(name) CONTENT_TYPES.fetch(File.extname(name.to_s).downcase, "application/octet-stream") end |
.encode(fields, file) ⇒ Object
Encodes a multipart body. Returns [body, content_type].
An array value becomes repeated parts, which is how questions is meant to arrive
over multipart; a hash is JSON-encoded, which is how schema arrives.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/vision_api/multipart.rb', line 60 def encode(fields, file) boundary = "----visionapi#{SecureRandom.hex(16)}" body = +"" fields.each do |name, value| next if value.nil? Array(wrap(value)).each do |item| body << "--#{boundary}\r\n" body << "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n" body << item body << "\r\n" end end if file filename, data, content_type = file body = body.b body << "--#{boundary}\r\n" body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n" body << "Content-Type: #{content_type}\r\n\r\n" body << data.b body << "\r\n" end body << "--#{boundary}--\r\n" [body, "multipart/form-data; boundary=#{boundary}"] end |
.resolve(file) ⇒ Object
Turns whatever the caller passed into [filename, bytes, content_type].
Accepts a path, a Pathname, an open binary IO, raw bytes, or an explicit [filename, bytes] pair. The filename is cosmetic — the server detects the type from magic bytes and ignores what we declare — but a sensible one makes multipart logs readable.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/vision_api/multipart.rb', line 30 def resolve(file) case file when Array name, data = file [name.to_s, data.to_s, content_type_for(name.to_s)] when ->(f) { f.respond_to?(:read) } name = file.respond_to?(:path) ? File.basename(file.path.to_s) : "upload" [name, file.read, content_type_for(name)] when String, ->(f) { f.respond_to?(:to_path) } path = file.respond_to?(:to_path) ? file.to_path : file if path.match?(%r{\Ahttps?://}i) raise UsageError, "Pass a URL as file_url:, not as file:. " \ "file: is a path on disk, an open IO, or the bytes themselves." end return [File.basename(path), File.binread(path), content_type_for(path)] if File.file?(path) # Not a path on disk, so treat it as the bytes themselves — which is what a caller # who read the file already has in hand. ["upload", file, "application/octet-stream"] else raise UsageError, "file: must be a path, an open IO, bytes, or [filename, bytes]." end rescue Errno::ENOENT, Errno::EACCES => e raise UsageError, "Could not read file: #{e.}" end |
.wrap(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
90 91 92 93 94 95 96 97 98 99 |
# File 'lib/vision_api/multipart.rb', line 90 def wrap(value) case value when Array then value.map { |item| item.is_a?(String) ? item : JSON.generate(item) } when Hash then JSON.generate(value) # Booleans go out as "true"/"false", not as JSON. Same body as the else branch, kept # separate because it is the one case a reader is likely to expect JSON encoding for. when true, false then value.to_s else value.to_s # rubocop:disable Lint/DuplicateBranch end end |