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

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.message}"
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