Faradayを使ってJSONと画像ファイルのマルチパート送信を行う方法

## はじめに
TumblrではAPIを使って、記事情報と記事に使用する画像を同時にアップロードできます。

しかし、Rubyでそれを行う方法を見つけ出すのに苦労したため、ここに方法を書き残しておきます。

## 動作確認環境

- Ruby 3.1.2
- faraday 2.7.2
- faraday-multipart 1.0.4

## 前提条件
OAuth2認証でアクセストークンを取得していること。

[https://www.tumblr.com/docs/en/api/v2#oauth2-authorization]()

## 使用するAPI
Tumblr APIの`/posts`

[https://www.tumblr.com/docs/en/api/v2#posts---createreblog-a-post-neue-post-format](https://www.tumblr.com/docs/en/api/v2#posts---createreblog-a-post-neue-post-format
)

APIドキュメントによると、こんな感じで記事本文のJSONデータと、添付画像をマルチパート送信する必要があるそうです。

```javascript 
{
    "content": [
        {
            "type": "image",
            "media": [
                {
                    "type": "image/jpeg",
                    "identifier": "some-identifier",
                    "width": 250,
                    "height": 200
                }
            ]
        }
    ]
}
```

```
--TumblrBoundary
Content-Disposition: form-data; name="json"
Content-Type: application/json

{JSON encoded parameters}
--TumblrBoundary
Content-Disposition: form-data; name="some-identifier"; filename="filename.jpg"
Content-Type: image/jpeg

{image data}
--TumblrBoundary--
```

## コード例
こんな感じで送信できました。

```ruby 
require 'json'
require 'faraday'
require 'faraday/multipart'

access_token = 'your_access_token'

connection = Faraday.new("https://api.tumblr.com") do |conn|
  conn.request :authorization, 'Bearer', access_token
  conn.request :multipart
end

# JSONのパートを作成
content = {
  content: [
    {
      "type": "image",
      "media": [
        {
          "type": "image/jpeg",
          "identifier": "my_image" # この名前は任意
        }
      ]
    },
    {
      type: 'text',
      text: 'Hello, world!'
    },
  ]
}.to_json
json_part = Faraday::Multipart::ParamPart.new(content, 'application/json')

# 画像のパートを作成
file_path = '/path/to/my_image.jpg'
file_part = Faraday::Multipart::FilePart.new(
  file_path, "image/jpeg", File.basename(file_path))

# "my_image" は content の identifier と一致させる
payload = { json: json_part, my_image: file_part }

# Tumblrに投稿
response = connection.post(
  "/v2/blog/your-awesome-blog.tumblr.com/posts", payload)

if response.status.to_i == 201
  puts '投稿に成功しました。'
else
  puts '投稿に失敗しました。'
  puts "エラーコード: #{response.status}"
  puts "エラーメッセージ: #{JSON.parse(response.body)['meta']['msg']}"
end
```

### 備考： Faraday::UploadIO はFaraday 2.0では使えない
ネットを検索すると、「Faradayでマルチパート送信を行うときは `Faraday::UploadIO` クラスを使いましょう」という記事がいくつか見つかったのですが、Faraday 2.0では使えなくなっていました。

Faraday 2.0以降では別途 faraday-multipart gem をインストールして、 上のコードのように `Faraday::Multipart::FilePart` クラスを使う必要があるようです。


## 試行錯誤したけどうまくいかなったやり方あれこれ
いろいろ試行錯誤した結果、最終的に上で紹介したコードに落ち着きました。
ここから下は試行錯誤中にうまくいかなかった方法を紹介します。

### oauth2 gem単体ではマルチパート送信 ~~できなかった~~ →できた
OAuth2認証をしてAPI連携するときはoauth2 gemが使えます。

https://gitlab.com/oauth-xx/oauth2/

画像アップロードを伴わない、テキストだけの記事作成であればoauth2 gemだけで送信できます。

```ruby 
consumer_key = 'your_consumer_key'
consumer_secret = 'your_consumer_secret'
access_token = 'your_access_token'

client = OAuth2::Client.new(
  consumer_key, consumer_secret, site: 'https://api.tumblr.com')
access_token_object = OAuth2::AccessToken.new(client, access_token)

params =  {
  headers: {
    'Content-Type' => 'application/json'
  },
  body: {
    content: [
      {
        type: 'text',
        text: 'Hello, world!'
      }
    ]
  }.to_json
}

response = access_token_object.post(
  "/v2/blog/your-awesome-blog.tumblr.com/posts", params)

if response.status.to_i == 201
  puts '投稿に成功しました。'
else
  puts '投稿に失敗しました。'
  puts "エラーコード: #{response.status}"
  puts "エラーメッセージ: #{JSON.parse(response.body)['meta']['msg']}"
end
```

~~oauth2 gemは内部的にFaradayを使っているので、うまくやればoauth2 gemからマルチパート送信できるのではないか？と思いましたが、結局うまくやる方法が見つからなかったので、直接Faradayを使う方法に切り替えました。~~

以下のようにすればoauth2 gemからもマルチパート送信できました。
とはいえ、これなら「Faradayを直接使ったコードと大差ないやん」って感じですが……。

```ruby 
consumer_key = 'your_consumer_key'
consumer_secret = 'your_consumer_secret'
access_token = 'your_access_token'

client = OAuth2::Client.new(consumer_key, consumer_secret, site: 'https://api.tumblr.com') do |conn|
  conn.request :multipart
end
access_token_object = OAuth2::AccessToken.new(client, access_token)

# JSONのパートを作成
content = {
  content: [
    {
      "type": "image",
      "media": [
        {
          "type": "image/jpeg",
          "identifier": "my_image" # この名前は任意
        }
      ]
    },
    {
      type: 'text',
      text: 'Hello, world!'
    },
  ]
}.to_json
json_part = Faraday::Multipart::ParamPart.new(content, 'application/json')

# 画像のパートを作成
file_path = '/path/to/my_image.jpg'
file_part = Faraday::Multipart::FilePart.new(
  file_path, "image/jpeg", File.basename(file_path))

params =  {
  body: {
    json: json_part,
    my_image: file_part
  }
}

response = access_token_object.post(
  "/v2/blog/your-awesome-blog.tumblr.com/posts", params)

if response.status.to_i == 201
  puts '投稿に成功しました。'
else
  puts '投稿に失敗しました。'
  puts "エラーコード: #{response.status}"
  puts "エラーメッセージ: #{JSON.parse(response.body)['meta']['msg']}"
end
```

### OAuth1認証もマルチパート送信ができなかった

Tumbler APIのドキュメントではOAuth1認証とOAuth2認証の二通り認証方法が紹介されています。

[https://www.tumblr.com/docs/en/api/v2#authentication](https://www.tumblr.com/docs/en/api/v2#authentication)

OAuth1認証の場合、 oauth gemを使って次のように画像アップロードを伴わない、テキストだけの記事作成は可能です。

```ruby 
require 'oauth'
require 'json'

access_token = 'your_access_token'
access_token_secret = 'your_access_token_secret'

consumer = OAuth::Consumer.new(
  consumer_key, consumer_secret, site: 'https://api.tumblr.com')
access_token = OAuth::AccessToken.new(
  consumer, access_token, access_token_secret)

params = [
  {
    content: [
      {
        type: 'text',
        text: 'Hello, world!'
      }
    ]
  }.to_json,
  {
    'Content-Type' => 'application/json'
  }
]
response = access_token.post(
  "/v2/blog/your-awesome-blog.tumblr.com/posts", *params)

if response.code.to_i == 201
  puts '投稿が成功しました。'
else
  puts '投稿に失敗しました。'
  puts "エラーコード: #{response.code}"
  puts "エラーメッセージ: #{JSON.parse(response.body)['meta']['msg']}"
end
```

しかし、oauth2 gemと同様、oauth gemでマルチパート送信する方法はわかりませんでした。

Faradayを使うにしても、Faraday 2.0ではoauth middlewareが使えなくなっており、Faraday 2.0以降ではどのように対応させればよいのかわかりませんでした。

```ruby 
# このコードは昔は動いたっぽいが、Faraday 2.0だと動かない
connection = Faraday.new("https://api.tumblr.com") do |conn|
  credentials = {
    consumer_key: consumer_key,
    consumer_secret: consumer_secret,
    token: access_token,
    token_secret: access_token_secret
  }
  conn.request :oauth, credentials
  #=> :oauth is not registered on Faraday::Request (Faraday::Error)
```

## まとめ（？）

以上のような経緯から、JSONと画像ファイルのマルチパート送信を行う場合は、FaradayとOAuth2認証を組み合わせるのがベスト（というか、それ以外の方法がわからない）という結論になりました。

Faradayはバージョン2.0（2022年1月リリース）でかなり後方互換性を捨てた変更が入っており、それ以前に書かれたネット記事のコードはそのままでは使えないケースがあるので注意が必要です。

https://github.com/lostisland/faraday/blob/main/UPGRADING.md

しかも、「その機能はFaraday 2.0では削除されたよ」って言われても、代替方法がよくわからないケースもあるので、「じゃあどうすればいいの！！」と迷子になってしまいがちです。

ちょっとニッチかもしれませんが、今回の僕と似たようなユースケースに遭遇した人はこの記事の内容を参考にしてみてください。