rails の rspec be matcherについて
- be_xxx — predicate matcher
オブジェクトの xxx? メソッドを呼び出して、その戻り値が truthy かを検証
# user.general_role? を呼び出す
expect(user).to be_general_role
user.valid? を呼び出す
expect(user).to be_valid
user.persisted? を呼び出す
expect(user).to be_persisted
be_ の後の部分に ? を付けたメソッドが自動的に呼ばれる仕組み
- be 単体 — truthy/falsy 判定
expect(value).to be # truthy であること(nil/false 以外)
expect(value).not_to be # falsy であること(nil または false)
- be_a / be_an — 型チェック
expect(user).to be_a(User) # user.is_a?(User) を検証
- be + 比較演算子
expect(count).to be > 0 expect(count).to be >= 1
Railsで使うENV.fetchについて
はじめに
ENVの書き方の理解をミスっていたかもしれない。
ということで備忘録
※ 結論、大丈夫でした
何がミスってたか
RailsでよくENVがなかったら それ以外の値を使いたいという意図で
hoge = ENV['hoge'] || 'default
という書き方をしていた。
しかしこれでは思わぬ挙動を示す。
falsyな値
そもそも Railsにおける || 演算子はどのような役割かというと
左辺を評価し、結果が真であった場合にはその値を返します。左辺の評価結果が偽であった場合には右辺を評価しその評価結果を返します。 or は同じ働きをする優先順位の低い演算子です
rubyにおける 偽の定義、falsyになりうる値はというと falseかnilということです。
ということは、つまりどういうことかというと
この時予期せぬ事態に
ENVの結果が仮にfalseであれば falseを取りたいのに 'default'が帰ってくるということ。
まぁENVにtrueを入れたいことなんてそんなないか?
そんな時のために
ENV.fetchを使う
ENV.fetch (Ruby 3.4 リファレンスマニュアル)
これであればたとえfalseであっても大丈夫
ENV.fetch('hoge', 'default')
と書いてもOK
企画倒れ
とやっていて rails cで時折確認しながらやっていたのですが
(app):24:in '[]=': no implicit conversion of false into String (TypeError)
from (app):24:in '<main>'
ENVの= が 文字列に変換できないとダメそうですね。
つまり、falseが入れられないといことです。
よって、そもそも起こり得ないということでした。
ENV.=自体が valueを to_strによって暗黙に型変換?されるそうで。
OpenAPIを言葉しか知らない段階から使えるようになるまで
はじめに
みなさん、こんちは torihaziです。
OpenAPIは知っていますか?
OpenAIではないです。
自分は言葉だけしか知らないです。
ふんわり程度。
ただこのままでは良くないと思い、色々勉強してみました。
Railsから色々操れるようになるまでがゴールです。
追記: 雰囲気までは掴めた。rspecについてほぼ忘れているのでそっちをまず入れたほうが良さそう。
どのようなものか
まずOpenAPIとは何か。
資料によると APIの仕様を記述するためのフォーマットであるらしい。これを使用することでAPIの仕様をドキュメント化でき、APIエンドポイントの実コードを観に行かなくてもよくなるらしい。
確かにfrontend側の人間が APIを叩いてどんな情報がもらえるか、渡せば良いかだけ知れれば話は早そう。
また認識合わせの際にも楽そう。
OpenAPI自体はYAML or JSONで書くものらしく、公式docもあるみたい。
書き方も色々決まっているらしいが、versionもあるらしい。
これを書けばある種の自動化もいけるらしい、そしてそれをどのようにrailsの開発に転用できるのかはまだ未知数。
docに落とせるから version管理もできる、故に差分検知がしやすい。
もう少し資料を漁ってみる。
ググったら、色々出てきた
- RESTAPIのインターフェースの仕様を記述したもの」
- APIの設計、開発、ドキュメント化、利用を標準化する仕組み
- ドキュメント化しているのでコミュニケーションが取りやすい。
少し言語化して整理
OpenAPIは RESTAPIインターフェースの仕様を標準化、可視化することでシステムの開発効率を上げるための仕組みである
となるのかな?狭義では フォーマットとされていたりするから IT特有の文脈によって意味が変わる感じかな。
とにかく個人的に上の言語化がしっくりきた。
ということであるべき理由について概要を掴んだから今度は使い方か。どのように使えば恩恵を受けられるのか。
と思ったけど Swaggerというものも出てきたな。
これとの関係性はなんだ?
これについてもググってみるか。
元々はSwaggerという名称でしたが、2015年にOpenAPI Initiativeに寄贈されてからOpenAPIと呼ばれるようになり、現在は「Swagger」はOpenAPI仕様に基づいたツールのセットを指す
であるそうだ。
OpenAPI仕様のものを楽に作れるようになる便利セットなのかな?
Swagger: OpenAPI仕様を基に作られた、API開発、文書化、テストのためのツールのセット。 Swagger Editor: OpenAPI定義書の編集ツール。 Swagger UI: OpenAPI定義書からUI形式のドキュメントを生成するツール。 Swagger Codegen: OpenAPI定義書からコードを自動生成するツール。
ほー。
色々あるのね。
ツールが色々あるみたい。
今思ったが、やっぱり OpenAPIというのは特定のフォーマットを指すのか?
システム開発の効率化というのは特定のフォーマットにすることによって得られる副産物であるから。
claudeに聞いてみてもやっぱりフォーマットっていう言語化の方が良いみたい。
OpenAPIは、REST APIインターフェースの仕様を記述するための標準化されたフォーマットである
OpenAPI = フォーマット = 決まった書き方・記法 という理解で良さそう。
ということでツール紹介
Stoplight Studio OpenAPIに則ったdocをGUIで簡単に作成することができるツールみたい。
Redoc OpenAPIに則ったdocをHTMLベースで生成、成果物を閲覧することができるツールみたい。Swagger UIに近しいそう。
Prism OpenAPIに則ったdoc(YAML形式)を読み込ませることで モックサーバを作ることができるツールみたい。
OpenAPI Generator OpenAPIに則ったdocから APIサーバ / フロントエンドのコードを自動生成してくれるツールみたい。
色々ツールがあるんやな。
で OpenAPIの公式 docを見てみる。
OpenAPI = OAS = OpenAPI Specificationらしい。
歴史的には Swaggerというものだったけどそれが色々あって OpenAPI Specificationになったそう。
OK、多分背景情報は理解できたはず。
ここからは実際の書き方についてみてみる。
まずはCRUDのOpenAPI仕様書を書いてみるとするか。
githubみてみた。
githubの説明が一番的を得ていたきがした。綺麗で完結、過不足ない言語化だったと思う。
で公式の書き方docがこれか
なんかちらっとversionで書き方が違うとか言っていたから多分これもキャッチアップコストかかるんだろうな。
んーー、情報としては正しいと思うが、長すぎてまるでわからん。
成果物としてどのようなものができて、とかから始めた方が良い。
ということでzennやqiitaを漁る。
Swagger Editorを使用すれば相互変換が可能とある。
sample.ymlという感じ。ファイル名に命名規則はない感じかな?
openapi: 3.0.0 info: ... servers: ... paths: ... components: ... security: ... tags: ... externalDocs: ...
ていうかこのyamlにおいて openapiとかinfoってどういう呼び方だっけ。
key ? jsonだったら keyとvalueという呼び方だったけど、qiita曰く オブジェクトという感じか?フィールドか。
まぁそこはそんなに問題じゃないか。keyだとラベルの名前みたいなイメージだけどフィールドだとまとまりみたいなイメージがあるから
そんな感じでいいか。
でエンドポイントのメインになりそうなのが pathsフィールド。
ここに実際の /users に getでアクセスしたら このような httpステータスコードと response を返しますよという感じになるのね。
tagsフィールドで定義して paths のtagに記述すると 見せる上でグルーピングして表示できるようになるのね。
components フィールドで記載する内容が モデルを表すそうだけど ふーんんという感じか。
securityも大事だけど、ね。って感じや。
一旦わかったので書いてみるか。
Railsで使ってみよう。
ググる。
gemを使うと思うが、一覧で示して欲しいな。
というか Railsの開発に OpenAPIを取り入れる上で考え方が二つあるみたい。
Integrating OpenAPI with a Ruby on Rails application can be achieved through various methods, primarily focusing on either generating OpenAPI documentation from your existing code (code-first) or using an OpenAPI specification to guide your development and testing (design-first).
code-firstかdesign-first
既存のrailsコードからOpenAPI docを作って取り入れるのか、OpenAPI docを使って開発などを効率的に進めるのか。
今回は効率的とか既存とかもない。ただ、普通に使ってみたいだけなので、あんま気にしない。
知りたいのは 一から自分でrails 上の1ファイルとして OpenAPI準拠のYAMLファイルを書くのかそれともRailsウェイにある程度乗っかりながら記載できるのかということ。
rspec使って生成できるとかいうのはどこかで聞いたことある。
OpenAPI Rails とかで調べて片っ端から列挙していく
committie readmeにも書かれてない。何ができて、何の目的で入れるのか。どのように使うのかは書いているけど。
rswag rspecのrequest specを拡張して rswag独自の書き方で書くことによってOpenAPI docをYAML or JSON形式で出力できる またdocを作った後のビジュアライズもswagger-uiを通していけるそうな? なんかこれ使っておけば良さそう。 swaggerの仕組みをrails way に載せてくれる gemとみた。
あと rspecのいろはを忘れているわ。
まぁいいかその都度入れてこう。
readmeをclaudeに食わせて要約
1. テストを書くと仕様書が自動生成される RSpecという普通のテストを書くだけで、OpenAPIファイル(YAMLやJSON)が自動で作られる 2. テストと仕様書が一体化 テストコードの中にAPI仕様を書くから、テストと仕様書がズレない 3. Swagger UIが使える 自動生成した仕様書から、ブラウザで見られる綺麗なAPIドキュメントページが作れる
なるほど、そろそろ書いてみるか
適当にscaffold
# ブログ記事API rails generate scaffold Post title:string content:text published:boolean # ユーザーAPI rails generate scaffold User name:string email:string # コメントAPI rails generate scaffold Comment post:references content:text
なんとなくわかった。
ただこれは rspecにある程度習熟してからのほうが良さそう。
参考資料
OpenAPIとRuby on Railsを組み合わせた開発
OpenAPIとは|SwaggerとAPI仕様書作成の仕組み | AeyeScan
Ruby で access_tokenも含めて、Google Drive APIとSheet APIを触り尽くす - 2章
はじめに
前回の続きです
追記: 結論、どうしたらrefreshされるのかは分かりませんが、どうしてdrive.authorizationに指定する必要があるのかは分かりました。
前回までの結論をおさらいすると
- list_filesがやっているのはクエリパラメータを添えて、httpリクエストを投げているだけ
- drive.authorizationに設定できるのは文字列か apply!(Hash)というメソッドを持ったObject
本章でauthorizationのapply!の方を詳しく突き詰めることができると思う
authorizationについて
確か途中でSignetかapplyメソッドを持つものかとかあって、結局文字列指定すれば良いの?
と今思ったのでもう一回頭から整理し直す。
最初
drive = Google::Apis::DriveV3::DriveService.new drive.authorization = ... # See Googleauth or Signet libraries
ここで定義。
drive.authorizationの正体
drive.authorization=から
BaseServiceのこれ
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client]
# OAuth2 credentials
def authorization=(authorization)
request_options.authorization = authorization
end
BaseServiceのrequest_optionsはこれ。 self.request_options = Google::Apis::RequestOptions.default.dup
RequestOptionsはこれ
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
入れたrequest_optionsを list_filesとか具体のコマンド叩くときに ApiCommand < HttpCommand作るときに指定 command.options = request_options.merge(options)
これでcommandのoptionsにhashではいる。 <= ここの理解が違った。
オープンクラスとかいうやつ
いい質問です!確認してみましょう。
コードを見ると:
# 最初にStructで定義 RequestOptions = Struct.new( :authorization, :retries, # ... ) # その後、同じ名前でクラス定義 class RequestOptions # @!attribute [rw] authorization # ... def self.default @options ||= RequestOptions.new end def merge(options) # ... end end
これはクラスの再オープン(reopening) です!
Rubyでは、Struct.newで作ったクラスを後からclassキーワードで再オープンして、メソッドを追加できます。
つまり:
1. Struct.newで基本的な構造(属性とアクセサ)を持つRequestOptionsクラスが作られる
2. その後class RequestOptionsで同じクラスを再オープンして、mergeメソッドやself.defaultメソッドを追加している
結果として、RequestOptionsは:
- Structの機能(ハッシュライクなアクセス、dupなど)を持ちつつ
- カスタムメソッド(merge、default)も持つ
というStructベースのクラスになっています。
dupする対象は、Structで作られた上にカスタムメソッドが追加されたクラスということですね!
command.optionsはこれ
# Request options
# @return [Google::Apis::RequestOptions]
attr_accessor :options
hashではない。だからオブジェクトみたいに入れられる。
つまり Google::Apis::RequestOptions が入ると。 optionsにはauthoriozationがある
でこのcommandに設定した optionsに含まれる authorizationが
httpリクエストに送られる直前で設定される。
def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String)
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end
req_header.update(header)
end
再まとめ
drive.authorization =
=> BaseServiceの authorization= (設定できるのはSignet::OAuth2::Client)
=> これがrequest_options.authorizationに入れてる
=> request_options側では Signet::OAuth2::Client, #apply(Hash) が可能
=> 具体のlist_files用の make_simple_command(:get, 'files', options)
=> command.options = request_options.merge(options) (optionsはlist_filesで指定しない限りnil)
=> optionsがnilならrequest_optionsをそのまま入れる。
=> これによってcommand.options.authorizationとかでアクセスできる。
=> これが リクエスト直前にapply_request_headerでreq_headerを更新
=> (apply_request_headerの戻り値を受け取ってないけどrubyのハッシュは参照の値渡しだから良いみたい)
=> 知らなかった。
=> これを元に @http_res = client.run_request(method, url.to_s, body, request_header)
OK、authorizationの情報はhttpリクエスト時のheaderに関わってくる。
次の段階。
度々出てきている Signet::OAuth2::Clientやapplyとかはなぜ必要なんだろう。
とにかくapply!メソッドがrequest_headerの書き換えに使われている事は理解した。
まずは Signet::OAuth2::Client
Signet::OAuth2::Client
OAuth認証に使うみたい。
Googleが作っているのかな?
なぜこれを渡す必要があるんだろう。
確かこれだったな。
# See Googleauth or Signet libraries
Signet::OAuth2::Clientを見てみたけど apply!メソッドがないっぽい。
claudeに聞いてみた。
おっしゃる通りです!Signet::OAuth2::Clientにはapply!メソッドが定義されていません。 実は、apply!メソッドは別のgemであるgoogleauthの中のGoogle::Authモジュールで定義されています。 Signet::OAuth2::Clientは低レベルのライブラリで、googleauthがそれをラップしてapply!メソッドなどの便利な機能を追加しているんです!
ほー。ということで信じてみよう。
次なるターゲット Googleauth gem
確かに検索してみるとSignetの文字がチラチラあるな。
でapply!メソッドはあるかというと。
あ、あるな。
でどれもhashが引数として渡されている。
試しに1つ覗いてみる
module BaseClient
AUTH_METADATA_KEY = :authorization
# Updates a_hash updated with the authentication token
def apply! a_hash, opts = {}
# fetch the access token there is currently not one, or if the client
# has expired
fetch_access_token! opts if needs_access_token?
token = send token_type
a_hash[AUTH_METADATA_KEY] = "Bearer #{token}"
logger&.debug do
hash = Digest::SHA256.hexdigest token
Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})"
end
a_hash[AUTH_METADATA_KEY]
end
あー、hashの更新で、これもauthorizationヘッダについて Beaderトークンを設定しているな。
hashを渡して関数内で更新すれば、親元も更新される(rubyのハッシュは参照の値渡しなので)
どれ使えばいいの?
とにかくこのgoogleauthを使って作ったやつをnewして drive.authorizationに設定すれば良さそう
そのnewしたやつは apply!メソッドがあるやつで。
ただ?
どれを使えばいいの?
そしてそのどれがどのくらいあるの?
ただ結局access_tokenがあればそれで良い事はわかっている。
なので一番シンプルなやつからやってみよう。
それがREADMEに載っているやつだと思う。知らんけど。
あ、これか??めっちゃぽいな
The closest thing to a base credentials class is the BaseClient module. It includes functionality common to most credentials, such as applying authentication tokens to request headers, managing token expiration and refresh, handling logging, and providing updater procs for API clients. Many credentials classes inherit from Signet::OAuth2::Client (lib/googleauth/signet.rb) class which provides OAuth-based authentication. The Signet::OAuth2::Client includes the BaseClient functionality. Most credential types either inherit from Signet::OAuth2::Client or include the BaseClient module directly. Notably, Google::Auth::Credentials (lib/googleauth/credentials.rb) is not a base type or a credentials type per se. It is a wrapper for other credential classes that exposes common initialization functionality, such as creating credentials from environment variables, default paths, or application defaults. It is used and subclassed by Google's API client libraries.
どれもBaseclient moduleをincludeしていますよ、そして Signet::OAuth2::Client includes the BaseClientだそう。
元々のSignetのものをgoogleauth側で拡張しているってことなのかな?
module Signet
# OAuth2 supports OAuth2 authentication.
module OAuth2
# Signet::OAuth2::Client creates an OAuth2 client
#
# This reopens Client to add #apply and #apply! methods which update a
# hash with the fetched authentication token.
class Client
include Google::Auth::BaseClient
確かにしているわ。
理解。
でさっきのCredentials.mdにあった基本のものからやっていくか。
一番シンプルなのはこれっぽい。
Google::Auth::BearerTokenCredentials - lib/googleauth/bearer_token.rb Includes Google::Auth::BaseClient module Implements Bearer Token authentication Bearer tokens are strings representing an authorization grant Can be OAuth2 tokens, JWTs, ID tokens, or any token sent as a Bearer in an Authorization header Used when the end-user is managing the token separately (e.g., with another service) Token lifetime tracking and refresh are outside this class's scope No JSON representation for this type of credentials
Google::Auth::BearerTokenCredentials
既に取得済みのトークン文字列(Bearer token)を使って認証するクラス。
特徴: - トークン文字列をそのまま使う(OAuth2トークン、JWT、IDトークンなど何でもOK) - トークンの管理(有効期限、更新など)はこのクラスの責任外 - ユーザーが外部で別途トークンを管理している場合に使用
使い方:
credentials = Google::Auth::BearerTokenCredentials.new("ya29.a0AfH6...")
service.authorization = credentials
シンプルに「すでにあるトークンをAPIリクエストに添付するだけ」のクラス。
とclaudeに教えてもらったので、中身を見てみる。
Google::Auth::BearerTokenCredentials、君に決めた
def initialize options = {}
raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
@token = options[:token]
@expires_at = case options[:expires_at]
when Time
options[:expires_at]
when Numeric
Time.at options[:expires_at]
end
@universe_domain = options[:universe_domain] || "googleapis.com"
end
ホイホイ。という事はまずaccess_tokenをomniauthで適当に取るか。
サンプルがあったので。
あーそっか、access_tokenどこに載せようか問題で、そのまま止まってたんだった。
sessionでいいかな。
(裏でガチャガチャやり中〜〜〜)
rails cでとりあえずやった。
drive = Google::Apis::Drive::DriveService.new
drive.authorization = Google::Auth::BearerTokenCredentials.new({token: access_token})
としてdrive.list_files(q: "hoge")としたら
403 Caught error PERMISSION_DENIED: Request had insufficient authentication scopes.
と出た。
scopeが足りてないですよと。
これって今のtokenに付与されているスコープって見れないのかな?
まぁomniauth側で定義しているのが emailとprofileだからそうだと思うんだけど。
require 'net/http'
require 'json'
access_token = "ya29.a0AfH6..."
uri = URI("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=#{access_token}")
response = Net::HTTP.get(uri)
token_info = JSON.parse(response)
puts token_info["scope"]
# => "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/calendar"
これでいけるっぽい。
やってみるか。
rails c
https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid
やっぱりそうだ。
で、今回google drive触るにはそれ専用のscopeつける必要があったはず。
ここからproject移動してスコープに何を選択してるか見てみる
何も選択してなさそう。
ということでdriveを追加する

https://www.googleapis.com/auth/drive
セキュリティ的に広すぎるかもだがやり方がわからない。教えてください。
a = drive.list_files Sending HTTP get https://www.googleapis.com/drive/v3/files? 200 a.class Google::Apis::DriveV3::FileList
できたー。
一区切り。
これってアクセストークンが切れた時の挙動って時間待つしかないのかな
偽のトークンセットすれば良いみたい。
あー401になった。ただretryしてるな。なぜだ。
まぁいっか。
で試したいこと2つ目。
authorizationにtokenの文字列直指定でもいけるのか問題。
見た感じはいけると思うが。
これまで読んできた感じ。
あ、いけたわ。そりゃそうだよね。
わーいできたー。
でも実用性はそんなない。
やっぱりAPI叩く直前に勝手にtokenリフレッシュやってくれた方が良い。
でもこれってどうするのが適切なんだろう。
大抵、アプリ側のログインで取ってきたtokenを使うことがほとんどだし、
という事はbefore_actionとかで毎回確認してからセットするみたいなことした方が良いのかな。
こっちで更新してその場しのぎしてもそれを親元のアプリの方に連携するのって
どうやるのかわからんし、多分めんどそう。というかできるのかな。
次はrefreshの機構を試す。
さっきのCredentials.mdの中に以下があった。
User Authentication 1. Google::Auth::UserRefreshCredentials < Signet::OAuth2::Client - lib/googleauth/user_refresh.rb * For user refresh token authentication (from 3-legged OAuth flow) * Authenticates on behalf of a user who has authorized the application * Handles token refresh when original access token expires * Typically obtained through web or installed application flow. The JSON form of this credential type has a "type" field with the value "authorized_user". Google::Auth::UserAuthorizer (lib/googleauth/user_authorizer.rb) and Google::Auth::WebUserAuthorizer (lib/googleauth/web_user_authorizer.rb) are used to facilitate user authentication. The UserAuthorizer handles interactive 3-Legged-OAuth2 (3LO) user consent authorization for command-line applications. The WebUserAuthorizer is a variation of UserAuthorizer adapted for Rack-based web applications that manages OAuth state and provides callback handling.
こいつ。Handles token refresh when original access token expiresとある。
さて元をのぞいてみる。
def initialize options = {}
options ||= {}
options[:token_credential_uri] ||= TOKEN_CRED_URI
options[:authorization_uri] ||= AUTHORIZATION_URI
@project_id = options[:project_id]
@project_id ||= CredentialsLoader.load_gcloud_project_id
@quota_project_id = options[:quota_project_id]
super options
end
このsuperってどこだ。誰の?
あー、元のSignetの方か。
def initialize options = {}
@authorization_uri = nil
@token_credential_uri = nil
@client_id = nil
@client_secret = nil
@code = nil
@expires_at = nil
@issued_at = nil
@issuer = nil
@password = nil
@principal = nil
@redirect_uri = nil
@scope = nil
@target_audience = nil
@state = nil
@username = nil
@access_type = nil
@granted_scopes = nil
update! options
end
てかこっちには access_tokenとかrefresh_tokenとか指定しなくて良いんだ。
うそ、あるわ、
update!の次のupdate_token!とかいうのでやってる
という事でUserRefreshCredentialsを作る。
app(dev)* drive.authorization = Google::Auth::UserRefreshCredentials.new ({
app(dev)* access_token: token,
app(dev)* refresh_token: ref,
app(dev)* client_id: ENV['GOOGLE_CLIENT_ID'],
app(dev)* client_secret: ENV['GOOGLE_CLIENT_SECRET']
app(dev)> })
こんな。
おし、list_filesいけた。
これで試しにtokenを適当にしてもう一回チャレンジして成功すればrefreshしているということ。
やってみよう。
app(dev)* drive.authorization = Google::Auth::UserRefreshCredentials.new ({
app(dev)* access_token: "hogehoge",
app(dev)* refresh_token: ref,
app(dev)* client_id: ENV['GOOGLE_CLIENT_ID'],
app(dev)* client_secret: ENV['GOOGLE_CLIENT_SECRET']
app(dev)> })
ダメだな
Caught error Unauthorized (Google::Apis::AuthorizationError)
何でだ。指定しているものが足りないのか?
そもそもどうやってrefreshしているのか。
あと少しだ。
多分、retryの発火はここでやっているはず。HttpCommandのdo_retry
def do_retry func, client
begin
Retriable.retriable tries: options.retries + 1,
max_elapsed_time: options.max_elapsed_time,
base_interval: options.base_interval,
max_interval: options.max_interval,
multiplier: options.multiplier,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
send(func, client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
end
いかにもみたいな感じ。
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
多分このonの中の例外になったら on_retryするとかじゃないの?
refresh_authorizationはというと
# Refresh the authorization authorization after a 401 error
#
# @private
# @return [void]
def refresh_authorization
# Handled implicitly by auth lib, here in case need to override
logger.debug('Retrying after authentication failure')
end
overrideしているのかと思い、googleauth覗きに行ったけどない。
振り出しか?
あれかな、project_id指定しないといけないのか?やってみるか。
結果変わらず。
なんでだよ。
これは3章か
結論
drive.authorizationに指定できるのはこれまでに3つわかった
- oauth2.0で取得した access_tokenをそのまま直指定
- Google::Auth::BearerTokenCredentials.new({token: access_token})
- Google::Auth::UserRefreshCredentials.new (これはrefreshのやり方は未定)
このauthorizationに設定できるのは apply!(hash)メソッドを持っており、
そのapply!メソッドが内部でもらったハッシュの[:authorization]に "Bearer #{token}"を入れる
てことをしてる。
次回は どうしたらrefreshできるか。それがわかれば あとは 枝葉。
list_filesとかの使い方は sourceコード見ればわかる。
Ruby で access_tokenも含めて、Google Drive APIとSheet APIを触り尽くす - 1章
はじめに
みなさん、こんちは torihaziです。
最近、Ruby で Google API(google drive APIとgoogle sheet api)を触っているのですが
あんまりドキュメントなくて困ったのでこれを機にまとめてみようかと
追記 まだまとまっていないです。1章ではなぜ drive.authorizationには何を設定できるのか
そこを詳しく突き詰めるとこまでと list_filesが実際には何をしているかというところまでです。
- はじめに
- 使用するgemというか大元
- やっていき
- 探求の旅
- まず Google::Apis::RequestOptions.default.dup
- list_filesが何をしているのか
- まずはmake_simple_command
- verify_universe_domain! 何してるか。
- fullpath
- Templateって何。
- command
- HttpCommand
- ただいま、list_files
- execute_or_queue_command
- batch_command
- command.execute
- prepare!
- opencensus_begin_span
- do_retry
- execute_once
- apply_request_options(request_header)
- 結論
使用するgemというか大元
The client gems are named according to the pattern google-apis-<servicename>_<serviceversion>. For example, the client for the Google Drive V3 API is google-apis-drive_v3.
gemに命名規則があるそうなので調べてみましょう。

右上の Search に "drive"と入力してenter


左下に "google-apis-drive_v3"とかありますね。多分これです。
それか gemの一覧とかで調べられないのかな?
ruby でほしいgemがあるか調べたい! - Torihaji's Growth Diary
ということで "google-apis-drive_v3"を入れる。
入れました。
Installing faraday-follow_redirects 0.4.0 Installing google-cloud-env 2.3.1 Installing representable 3.2.0 Installing addressable 2.8.7 Fetching signet 0.21.0 Installing signet 0.21.0 Fetching googleauth 1.16.0 Installing googleauth 1.16.0 Fetching google-apis-core 1.0.2 Installing google-apis-core 1.0.2 Fetching google-apis-drive_v3 0.73.0 Installing google-apis-drive_v3 0.73.0
色々依存で入った。
- faraday-follow_redirects 0.4.0 HTTPリクエストライブラリ「Faraday」の拡張 リダイレクト(301/302など)を自動で追従
- google-cloud-env 2.3.1 Google Cloud環境の検出と設定 実行環境(GCP、ローカルなど)の判定と環境変数の取得
- representable 3.2.0 オブジェクトのシリアライズ/デシリアライズ JSON/XMLなどの変換を定義可能にする
- addressable 2.8.7 URI/URLの処理 パース、正規化、エンコード/デコード
- signet 0.21.0 OAuth署名の実装 Google APIの認証で使用
- googleauth 1.16.0 Google API認証 OAuth 2.0、サービスアカウント、アプリケーション認証情報の処理
- google-apis-core 1.0.2 Google APIクライアントの共通基盤 HTTP通信、エラーハンドリング、リトライなどの共通機能
- google-apis-drive_v3 0.73.0 Google Drive API v3のクライアント ファイルの取得・作成・更新・削除など
やっていき
では使っていきましょう。
それぞれがなんで必要かは使い慣れてからで。
さぁ rails cで色々触っていきましょう。
irb との違いは 以下。irbはrubyの純粋な対話型実行環境、rails cはRailsの機能を使える対話型実行環境
で以下とあるので使ってみましょう。
Then, to use it, require the file and instantiate the service. For example to use the Drive API:
app(dev)> client = Google::Apis::DriveV3::DriveService.new => #<Google::Apis::DriveV3::DriveService:0x0000ffffa1f83670
はいできました。
authorizationが何か知りませんが一旦やってみましょう。
files = client.list_files(q: "title contains 'finances'") app(dev)* files.items.each do |file| app(dev)* puts file.title app(dev)> end Sending HTTP get https://www.googleapis.com/drive/v3/files?q=title%20contains%20%27finances%27 403
実際にはHTTPリクエストを飛ばす上で、クエリを構築して送ってるらしい。
HTTPステータスコード403 Forbiddenは、リクエストされたページにアクセスする権限がないために、サーバーがリクエストを拒否した状態
{
"error": {
"code": 403,
"message": "Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.",
"errors": [
{
"message": "Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.",
"domain": "global",
"reason": "forbidden"
}
],
"status": "PERMISSION_DENIED"
}
}
claudeに聞いてみると
Google Drive APIへのリクエストに認証情報(APIキーまたはOAuthトークン)が含まれていない
まぁそうかと。
ではその認証情報をどうセットするのか。
drive.authorization = ... # See Googleauth or Signet libraries
これっぽい。
何をしているのか。元をのぞいてみる。
探求の旅
def initialize
super(DEFAULT_ENDPOINT_TEMPLATE, 'drive/v3/',
client_name: 'google-apis-drive_v3',
client_version: Google::Apis::DriveV3::GEM_VERSION)
@batch_path = 'batch/drive/v3'
end
近くにこんな記述が。
# @return [String]
# API key. Your API key identifies your project and provides you with API access,
# quota, and reports. Required unless you provide an OAuth 2.0 token.
attr_accessor :key
なるほど。API keyを与えるか OAuth 2.0 tokenを与える必要があるそうな。
今回はAPI keyを使わないので OAuth 2.0 tokenの路線で行く。
もし keyを使うなら
client = Google::Apis::DriveV3::DriveService.new client.key = your_api_key
みたいな感じにしたら使えるとみた。
はい、でこのtokenはどうやってやるのかな。
docに書いてない。わかりやすいとこにはないな。
drive.authorization = ... # See Googleauth or Signet libraries
とあるから多分これなんだろうけど。
書いておいてほしいな。
usage みたいな感じでデカデカと。
初学者にはきつい。
ということでこのauthorizationがなんなのか、掘ってみる。
先ほどと同じく画面右上の searchでauthorizationとすると

それだと
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client]
# OAuth2 credentials
def authorization=(authorization)
request_options.authorization = authorization
end
def authorization
request_options.authorization
end
これっぽい??
DriveはGoogle::Apis::Core::BaseServiceを継承していたし。
いやだとしても ???だな。
とりあえずrequest_options.authorizationに入れている。
ちなみにそれは親元のBaseServiceの
self.request_options = Google::Apis::RequestOptions.default.dup
を入れているらしい。
これも合わせて流し読み
1個ずつ倒していこう。
まず Google::Apis::RequestOptions.default.dup
# Request options
class RequestOptions
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
# @!attribute [rw] retries
# @return [Integer] Number of times to retry requests on server error.
# @!attribute [rw] max_elapsed_time
# @return [Integer] Total time in seconds that requests are allowed to keep being retried.
# @!attribute [rw] base_interval
# @return [Float] The initial interval in seconds between tries.
# @!attribute [rw] max_interval
# @return [Integer] The maximum interval in seconds that any individual retry can reach.
# @!attribute [rw] multiplier
# @return [Numeric] Each successive interval grows by this factor. A multipler of 1.5 means the next interval
# will be 1.5x the current interval.
# @!attribute [rw] header
# @return [Hash<String,String>] Additional HTTP headers to include in requests.
# @!attribute [rw] normalize_unicode
# @return [Boolean] True if unicode strings should be normalized in path parameters.
# @!attribute [rw] skip_serialization
# @return [Boolean] True if body object should be treated as raw text instead of an object.
# @!attribute [rw] skip_deserialization
# @return [Boolean] True if response should be returned in raw form instead of deserialized.
# @!attribute [rw] api_format_version
# @return [Integer] Version of the error format to request/expect.
# @!attribute [rw] use_opencensus
# @return [Boolean] Whether OpenCensus spans should be generated for requests. Default is true.
# @!attribute [rw] quota_project
# @return [String] Project ID to charge quota, or `nil` to default to the credentials-specified project.
# @!attribute [rw] query
# @return [Hash<String,String>] Additional HTTP URL query parameters to include in requests.
# @!attribute [rw] add_invocation_id_header
# @return [Boolean] True if the header gccl-invocation-id need to be set
# @!attribute [rw] upload_chunk_size
# @return [Integer] The chunk size of storage upload. The default value is 100 MB.
# Get the default options
# @return [Google::Apis::RequestOptions]
def self.default
@options ||= RequestOptions.new
end
def merge(options)
return self if options.nil?
new_options = dup
members.each do |opt|
opt = opt.to_sym
new_options[opt] = options[opt] unless options[opt].nil?
end
new_options
end
end
dupは元のオブジェクトのコピーだから、良い。
とにかく、RequestOptionsとかいうものを作っているっぽい。
その中にいた
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
そもそもこの書き方はなんだ
YARDというらしい。
これはこれでまた記事が書けそう
書き方が二つあって、こっちがdirectiveという書き方。属性とか実際のメソッドに与える引数とか書く時に使うものとして理解。
claudeった。
authorizationという読み書き可能な属性があり、それはOAuth2認証情報を保持するもので、Signet::OAuth2::Clientオブジェクトか、apply(Hash)メソッドを持つオブジェクトを返せるし、 authorizationに設定もできる
らしい。
あ、でもBaseServiceの方はSignet::OAuth2::Clientオブジェクト だけだな。
まぁいっか。
でこいつを入れて、どのようにhttpリクエスト時に使うんだろう。
とりあえずauthorizationというものを keyを設定しない限りは使う必要がありそうという事は覚えておこう。
今の結論
authorization: Signet::OAuth2::Client or apply(Hash)というメソッドがあるオブジェクト
で次だ。
さっき試しに使った list_filesが実際は何をしているのか・
これを突き詰めていけば、さっきのauthorizationの正体とかなぜ必要なのかもわかるはず。
list_filesが何をしているのか
さてまた検索する

正体はこれ。
def list_files(corpora: nil, corpus: nil, drive_id: nil, include_items_from_all_drives: nil, include_labels: nil, include_permissions_for_view: nil, include_team_drive_items: nil, order_by: nil, page_size: nil, page_token: nil, q: nil, spaces: nil, supports_all_drives: nil, supports_team_drives: nil, team_drive_id: nil, fields: nil, quota_user: nil, options: nil, &block)
command = make_simple_command(:get, 'files', options)
command.response_representation = Google::Apis::DriveV3::FileList::Representation
command.response_class = Google::Apis::DriveV3::FileList
command.query['corpora'] = corpora unless corpora.nil?
command.query['corpus'] = corpus unless corpus.nil?
command.query['driveId'] = drive_id unless drive_id.nil?
command.query['includeItemsFromAllDrives'] = include_items_from_all_drives unless include_items_from_all_drives.nil?
command.query['includeLabels'] = include_labels unless include_labels.nil?
command.query['includePermissionsForView'] = include_permissions_for_view unless include_permissions_for_view.nil?
command.query['includeTeamDriveItems'] = include_team_drive_items unless include_team_drive_items.nil?
command.query['orderBy'] = order_by unless order_by.nil?
command.query['pageSize'] = page_size unless page_size.nil?
command.query['pageToken'] = page_token unless page_token.nil?
command.query['q'] = q unless q.nil?
command.query['spaces'] = spaces unless spaces.nil?
command.query['supportsAllDrives'] = supports_all_drives unless supports_all_drives.nil?
command.query['supportsTeamDrives'] = supports_team_drives unless supports_team_drives.nil?
command.query['teamDriveId'] = team_drive_id unless team_drive_id.nil?
command.query['fields'] = fields unless fields.nil?
command.query['quotaUser'] = quota_user unless quota_user.nil?
execute_or_queue_command(command, &block)
end
なんか色々指定できるそうな。値設定したらそれをqueryに添えてやるらしい。
queryはその名の通り、?hoge=1&huga=1みたいに設定していく感じなのかな?
command = make_simple_command(:get, 'files', options) ~~~ execute_or_queue_command(command, &block)
これが何してるかだ。
まずはmake_simple_command

これが実態。
# Create a new command.
#
# @param [symbol] method
# HTTP method (:get, :post, :delete, etc...)
# @param [String] path
# Additional path, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::DownloadCommand]
def make_simple_command(method, path, options)
verify_universe_domain!
full_path =
if path.start_with? "/"
path[1..-1]
else
base_path + path
end
template = Addressable::Template.new(root_url + full_path)
command = ApiCommand.new(method, template, client_version: client_version)
command.options = request_options.merge(options)
apply_command_defaults(command)
command
end
verify_universe_domain! 何してるか。
検索の画像載せるのがめんどくなってきた。
# Verify that the universe domain setting matches the universe domain
# in the credentials, if present.
#
# @raise [Google::Apis::UniverseDomainError] if there is a mismatch
def verify_universe_domain!
auth = authorization
auth_universe_domain = auth.universe_domain if auth.respond_to? :universe_domain
if auth_universe_domain && auth_universe_domain != universe_domain
raise UniverseDomainError,
"Universe domain is #{universe_domain} but credentials are in #{auth_universe_domain}"
end
true
end
authorizationはさっきのrequest_optionsで入れたやつ。
そこから そのauthに universe_domainメソッドがあったらなんかチェックして例外を投げて、
それ以外はtrueを返す。
まぁ多分そんな気にしなくて良いと思う。とりあえずtrue返すっていう事で次。
fullpath
full_path =
if path.start_with? "/"
path[1..-1]
else
base_path + path
end
pathっていうのは command = make_simple_command(:get, 'files', options)
なので 'files'。
という事でここでいうfull_pathは base_path + 'files'
base_pathがなんかはわからないけど、このgemはGoogle 公式が提供しているREST APIへの
アクセスをより簡素にしてくれてると思うから
Method: files.list | Google Drive | Google for Developers
結論、ここに飛ばしてくれてるはず。
なのでbase_pathは "https://www.googleapis.com/drive/v3" だと思う。
これに今回の 'files'をつけたら、それっぽくなるし。
Templateって何。
template = Addressable::Template.new(root_url + full_path)
これはなんだ。
Addressableというものをリポジトリ内でも検索してもなし。
外部のgemか?
なんかURLを構築してるのかな?
gemがあった。URL管理を便利にしてくれるやつっぽい。
次。
command
command = ApiCommand.new(method, template, client_version: client_version)
methodは httpメソッドがシンボルで渡される。templateは url、versoinは多分固定の文字列とか数値。
なので ApiComandが何してるかだけ気にすれば良い。
本体。
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
# @param [String, #read] body
# Request body
def initialize(method, url, body: nil, client_version: nil)
super(method, url, body: body)
self.client_version = client_version || Core::VERSION
end
これをさらにsuperして親のものを使うと。
これ作った人よくこんな抽象化できるな。
すごいなー。
でこのsuperは HttpCommandなのでそれをみにいく。
module Google
module Apis
module Core
# Command for executing most basic API request with JSON requests/responses
class ApiCommand < HttpCommand
HttpCommand
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
# @param [String, #read] body
# Request body
def initialize(method, url, body: nil)
self.options = Google::Apis::RequestOptions.default.dup
self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method
self.header = Hash.new
self.body = body
self.query = {}
self.params = {}
@opencensus_span = nil
if OPENCENSUS_AVAILABLE
logger.warn 'OpenCensus support is now deprecated. ' +
'Please refer https://github.com/googleapis/google-api-ruby-client#tracing for migrating to use OpenTelemetry.'
end
end
なんかよくわからないけど色々入れてるみたい。
とりあえずこれがmake_simple_commandで帰ってくると。
でlist_filesに戻ろう。なんかqueryにhashで色々入れられるらしいね。
ただいま、list_files
def list_files(corpora: nil, corpus: nil, drive_id: nil, include_items_from_all_drives: nil, include_labels: nil, include_permissions_for_view: nil, include_team_drive_items: nil, order_by: nil, page_size: nil, page_token: nil, q: nil, spaces: nil, supports_all_drives: nil, supports_team_drives: nil, team_drive_id: nil, fields: nil, quota_user: nil, options: nil, &block)
command = make_simple_command(:get, 'files', options)
command.response_representation = Google::Apis::DriveV3::FileList::Representation
command.response_class = Google::Apis::DriveV3::FileList
command.query['corpora'] = corpora unless corpora.nil?
command.query['corpus'] = corpus unless corpus.nil?
command.query['driveId'] = drive_id unless drive_id.nil?
command.query['includeItemsFromAllDrives'] = include_items_from_all_drives unless include_items_from_all_drives.nil?
command.query['includeLabels'] = include_labels unless include_labels.nil?
command.query['includePermissionsForView'] = include_permissions_for_view unless include_permissions_for_view.nil?
command.query['includeTeamDriveItems'] = include_team_drive_items unless include_team_drive_items.nil?
command.query['orderBy'] = order_by unless order_by.nil?
command.query['pageSize'] = page_size unless page_size.nil?
command.query['pageToken'] = page_token unless page_token.nil?
command.query['q'] = q unless q.nil?
command.query['spaces'] = spaces unless spaces.nil?
command.query['supportsAllDrives'] = supports_all_drives unless supports_all_drives.nil?
command.query['supportsTeamDrives'] = supports_team_drives unless supports_team_drives.nil?
command.query['teamDriveId'] = team_drive_id unless team_drive_id.nil?
command.query['fields'] = fields unless fields.nil?
command.query['quotaUser'] = quota_user unless quota_user.nil?
execute_or_queue_command(command, &block)
end
で途中のcommandに設定するqueryは無視できるので、ここから次の相手。
execute_or_queue_command
execute_or_queue_command(command, &block)は何してるのか。
# Execute the request. If a batch is in progress, the request is added to the batch instead.
#
# @param [Google::Apis::Core::HttpCommand] command
# Command to execute
# @return [Object] response object if command executed and no callback supplied
# @yield [result, err] Result & error if block supplied
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_or_queue_command(command, &callback)
batch_command = current_batch
if batch_command
fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
batch_command.add(command, &callback)
nil
else
command.execute(client, &callback)
end
end
ようやくゴールが見えてきた気がする。
試しにさっきのlist_filesの例外を捕捉してなんのクラスだったかみてみよう。
app(dev)* begin app(dev)* files = client.list_files(q: "hoge") app(dev)* rescue => e app(dev)* pp e.class app(dev)* pp e.message app(dev)> end Google::Apis::ClientError "forbidden: Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API."
およ、AuthorizationErrorじゃなかった。
まぁそもそもこのclientでどこにアクセスしたいのっていうのもわかりっこないからあれだけど
まぁいっか。先に進もう、いずれわかるでしょ。
batch_command
多分batchだったらみたいな感じかな。この場合は今回関係ないと思うから skip
command.execute
これが本体。
command.execute(client, &callback)
このコマンドというのはさっきのHttpCommandだから。
# Execute the command, retrying as necessary
#
# @param [Faraday::Connection] client
# Faraday connection
# @yield [result, err] Result or error if block supplied
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute(client, &block)
prepare!
opencensus_begin_span
do_retry :execute_once, client, &block
ensure
opencensus_end_span
@http_res = nil
release!
end
clientというはFaradayとかいうrubyのhttpクライアント。日本語あっているか知らないけど。
prepare!
# Prepare the request (e.g. calculate headers, add query params, serialize data, etc) before sending
#
# @private
# @return [void]
def prepare!
normalize_unicode = true
if options
header.update(options.header) if options.header
query.update(options.query) if options.query
normalize_unicode = options.normalize_unicode
end
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
url.query_values = normalize_query_values(query).merge(url.query_values || {})
if allow_form_encoding?
@form_encoded = true
self.body = Addressable::URI.form_encode(url.query_values(Array))
self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
else
@form_encoded = false
end
self.body = '' if self.body.nil? && [:post, :put, :patch].include?(method)
if defined?(::Google::Apis::Core::CompositeIO) && body.is_a?(::Google::Apis::Core::CompositeIO)
header["Content-Length"] ||= body.size.to_s
end
end
リクエストをこれまでの情報を元にセットするっぽい。
opencensus_begin_span
何してるかわからん。
do_retry
まだ認証情報セットしてるようには見えないな。
見逃したか??
def do_retry func, client
begin
Retriable.retriable tries: options.retries + 1,
max_elapsed_time: options.max_elapsed_time,
base_interval: options.base_interval,
max_interval: options.max_interval,
multiplier: options.multiplier,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
send(func, client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
end
何してるの??
retryはしてそう。
けど やっているのは execute_onceだからそれをみることにする。
execute_once
# Execute the command once.
#
# @private
# @param [Faraday::Connection] client
# Faraday connection
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client)
body.rewind if body.respond_to?(:rewind)
begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
request_header = header.dup
apply_request_options(request_header)
@http_res = client.run_request(method, url.to_s, body, request_header)
logger.debug { @http_res.status }
logger.debug { safe_single_line_representation @http_res }
response = process_response(@http_res.status.to_i, @http_res.headers, @http_res.body)
success(response)
rescue => e
logger.debug { sprintf('Caught error %s', e) }
error(e, rethrow: true)
end
end
あ、なんか見覚えあるlogger
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
これで最初のログが出るのか。
app(dev)* begin app(dev)* files = client.list_files(q: "hoge") app(dev)* rescue => e app(dev)* pp e.class app(dev)* pp e.message app(dev)> end Sending HTTP get https://www.googleapis.com/drive/v3/files?q=hoge <= これ。 403
次。
apply_request_options(request_header)
これが何してるか。
# Update the request with any specified options.
# @param [Hash] req_header
# HTTP headers
# @return [void]
def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String)
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end
req_header.update(header)
end
あ、authorizationあったーーーーーーーー。
なるほど、ここで applyメソッドがあるかみてるな。
authorizationを見て、applyがあれば そのapply!メソッドを使って、多分 headerの値を書き換えて、
文字列であれば、AuthorizatoinヘッダーにBearerトークンを付与している。
あーだからこのままaccess_tokenとか付与してもいけるのか。
いけるのか??知らんけど。
とりあえずauthorizationに設定できるものは 2種類そう。
string(多分、token直渡し or apply!メソッドあるやつ)
あれでもSignetだっけ、あれはどこ行った??あれも apply!メソッdがあるのかな?
ていうかgoogle-apis-drive_v4 apiはhttpリクエスト自体にFaradayを使っているんだ。
Faraday使ったことないから詳しくは知らないけど。
なんかはてぶろが重くなってきたから次章へ。
結論
今回理解したのは
client = Google::Apis::DriveV3::DriveService.new files = client.list_files(q: "title contains 'finances'")
とかやっているけど裏でやっているのは
httpリクエストに クエリパラメータつけたりしてるだけ。
そのリクエストはFaradayを使っている。
drive.authorizationで指定できるのは 2つ
文字列なら AuthorizationヘッダでBearerトークン
apply!メソッドがあるオブジェクトを渡したらそれ専用の処理をする。
では次へ。
ruby でほしいgemがあるか調べたい!
はじめに
みなさん、こんちは torihaziです
今日は webでrubyのgem調べても なんとなく出てこないって時に
コマンドでならいけるんじゃねと思い、調べてみました。
対象
google drive のapiです。
webで調べるとこんな感じ

あ、出た。笑
みなかったことに。
ということでコマンドで調べてみます。
コマンド
gemコマンドです。
Usage: gem search [REGEXP] [options]
google drive を含むやつって感じでしょうか。
これを正規表現でやる感じですね。
gem search google
でやってみると、
多すぎる。
もっと絞りましょう。
google とdriveを含むようなもの。
これ正規表現でどうやるんでしょう。
肯定先読みっていうやつらしいです。
これもこれで記事書けそうですが、今回は一旦スルー。
今回は google ~~ drive という感じで命名規則が決まっているので調べてみます
gem search ^(?=.*google.*drive).*$
bashでやったら
# gem search ^(?=.*google.*drive).*$
bash: syntax error near unexpected token `('
およよ。
""で囲んだ
root@c83a0465ed0b:/app# gem search "^(?=.*google.*drive).*$"
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
stringio (>= 0)
Available/installed versions of this gem:
- 3.1.8
- 3.0.4
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
*** REMOTE GEMS ***
activestorage_google_drive (0.1.1)
backup-googledrive (0.1.0)
carrierwave-google_drive (0.0.2)
easy-google-drive (0.0.5)
fastlane-plugin-google_drive (0.10.0)
fastlane-plugin-googledrive_spquyt (0.1.9)
google-apis-drive_v2 (0.54.0)
google-apis-drive_v3 (0.73.0)
google-apis-driveactivity_v2 (0.22.0)
google-apis-drivelabels_v2 (0.20.0)
google-apis-drivelabels_v2beta (0.18.0)
google-drive-ruby (9001.0)
google_drive (3.0.7)
google_drive-persistent_session (0.3.0)
google_drive2 (3.0.9)
google_drive_companion (0.1.0)
google_drive_dotenv (0.5.1)
google_drive_handler (0.1.0)
google_drive_maintained (3.0.11)
google_drive_oauth (1.0.1)
google_driver (0.0.3)
googledrive-easy (0.1.9)
googledriver (0.0.2)
middleman-google_drive (0.3.13)
paperclip-google-drive (0.3.3)
paperclip-googledrive (0.1.3)
paperclip-googledrive-new (1.0)
parallel588_google_drive (0.3.4)
shrine-google_drive_storage (0.4.4)
simple_google_drive (0.5.1)
tumugi-plugin-google_drive (0.4.0)
わー。
多分 google-apis-drive_v3 (0.73.0) でしょう。
公式見ても google-apis-
新しいv3で良いはず。
ちなみに調べている時にこんなめんどいことしなくても gem list | grep を重ねる感じもありました。
あ、まずい、横道それ過ぎた。一度に全部は非効率。
機になるけど今回はsearchだけ知れたからよし。
おまけ
gemを調べるにはこっちもあるんですね。
作業ブランチからmainとの間でどのくらい diffがあるかを知りたい
はじめに
こんばんちは torihaziです
今日は PR出す時に あらかじめ だすPRが大きいか小さいか
それをあらかじめ知りたいときに使えるgit コマンドのアウトプットです
なんで
「PRが大きいです」
と言われてしまいました。
確かに、気にはしていたものの、多いよなと思っていたし、
そうだよなという感じです。
でもPR出すまで実際の差分ってどのくらいあるんだろうという時に
拡張機能のgit graph入れて、計算して、、、
ていうのはあまりにも非効率だと思ったので色々聞いてみた結果見つけました。
結論
git diff [<options>] <commit>..<commit> [--] [<path>…] これは2つの任意の<commit>間の変更を表示するために、以前のフォーム(「..」なし)と同義となります。片側の<commit>を省略すると、代わりにヘッドを使用した場合と同じになります。
commitをAからBまでの差分を知るには
git diff 2a01877..main
という感じですね。
そうするとあるコミットから mainまでの差分を出すことができます。
ただまぁみにくい。
知りたいのは差分の行数。
optionは3つあってそれぞれ好きなやつを選んでください。
--stat, --numstat, --shortstatです
🐶: ~/Documents/personal_dev/omniauth-ruby (main)git diff 2a01877..main --stat Gemfile | 2 ++ Gemfile.lock | 4 ++++ app/controllers/root_controller.rb | 3 +++ app/models/omniauth/auth_payload.rb | 8 +++----- config/application.rb | 10 ++-------- config/environments/development.rb | 4 ++-- config/initializers/omniauth.rb | 3 --- db/migrate/20251117133621_change_column_default_null_true.rb | 5 +++++ db/schema.rb | 4 ++-- 9 files changed, 23 insertions(+), 20 deletions(-) 🐶: ~/Documents/personal_dev/omniauth-ruby (main)git diff 2a01877..main --numstat 2 0 Gemfile 4 0 Gemfile.lock 3 0 app/controllers/root_controller.rb 3 5 app/models/omniauth/auth_payload.rb 2 8 config/application.rb 2 2 config/environments/development.rb 0 3 config/initializers/omniauth.rb 5 0 db/migrate/20251117133621_change_column_default_null_true.rb 2 2 db/schema.rb 🐶: ~/Documents/personal_dev/omniauth-ruby (main)git diff 2a01877..main --shortstat 9 files changed, 23 insertions(+), 20 deletions(-)
おまけ
かっこよく +23-20 みたいな感じでやってみましょう。
使うとしたらshortstatですかね。
rooに聞いたらこうなりました。
git diff 2a01877..main --shortstat | awk -F'[ ,()]+' '{print "+"$4"-"$7}'
こうすれば
+23-20
てな感じででます。
PR出す前に行数把握目的でやってみてください