虚無庵

徒然なるままに

microsoft_graph のコードを読む

Fukuoka.rb とかでちまちまと開発している msgraph であるが、ここ最近大きく実装の方針を変えようと思っている。

が、変えるためには既存の microsoft_graph の理解が不可欠だと判断した。そして、一人でメモっても気が滅入るので晒してツッコミを待つことにする。

主な目的は未来の自分へのメモ。

きっかけ

Go Conference 2019 Autumn で以下のセッションがありました。

gocon.jp

github.com

www.slideshare.net

俺が Ruby でやりたいことを Go で一人(?)で実現された方がいて、「負けてらんねぇ!」とモチベーションが上がったのが大きいです。また、このセッションで Microsoft Graph SDK Code Generator という存在を知った。

平たく言うと「OData Protocol を定義しているメタデータXML ファイル)からコードを生成するもの」と解釈しているが、残念ながら Ruby は対応されていない。しかし、Go も同じ状況でこの方が OData Protocol のメタデータファイルからコード生成をしていそうなので、自分も踏襲することにした。

元々は「既にエンドポイントが存在するのでそれを愚直にラッパーモジュールを作成する」「OpenAPI 3.0 の定義をしてコードを自動生成する」の二通りを考えて、前者を採択していたが正直しんどい。そんな状況での先述のセッションを知ったのである。

また既存の microsoft_graph もメタデータファイルをパースして API インタフェース部分を実現していそうなので、車輪の再発明になるがその方が早くて確実な気がしてきたので方針を変更することにした。

microsoft_graph

「そもそも microsoft_graph って何?」と問われれば、Microsoft Graph Client Library for Ruby である、が回答である。平たくいえば API Client です。

Microsoft Graph API そのものの仕様はリファレンスを参照してもらうとして、このブログでも度々言及しているが microsoft_graph 自体はメンテナンスされていない。

GitHub に載っている usage example は動かないので、以下のサンプルコードで話をすすめる。

require 'httpclient'
require 'microsoft_graph'
require 'json'
require 'nokogiri'
require 'net/http'

MS_BASE_URL        = "https://login.microsoftonline.com".freeze
TOKEN_REQUEST_PATH = "oauth2/v2.0/token".freeze

# Your configuration
client_id     = 'xxxxx-xxxx-xxx-xxxxxx-xxxxxxx'
client_secret = 'xxxXXXxxXXXxxxXXXxxXXXXXXXXxxxxxx='
tenant_id     = 'xxxxx-xxxx-xxx-xxxxxx-xxxxxxx'

client = HTTPClient.new
response = client.post(
  "#{MS_BASE_URL}/#{tenant_id}/#{TOKEN_REQUEST_PATH}",
  {
    body: { client_id: client_id,
            client_secret: client_secret,
            scope: 'https://graph.microsoft.com/.default',
            grant_type: 'client_credentials',
          },
    'Content-Type' => 'application/json',
    multipart: true,
 }
)
raise "#{response.message}" unless response.code == 200
token = JSON.parse(response.body)['access_token']

# add the access token to the request header
callback = Proc.new { |r| r.headers["Authorization"] = "Bearer #{token}" }

graph = MicrosoftGraph.new(base_url: MicrosoftGraph::BASE_URL,
                           cached_metadata_file: File.join(MicrosoftGraph::CACHED_METADATA_DIRECTORY, "metadata_v1.0.xml"),
                           &callback
)

users = graph.users
users.each do |user|
  puts "Hello, I am #{user.display_name}"
end

MicrosoftGraph.new

トークンの発行は実は Microsoft Graph の責務ではないので割愛する。

「MicrosoftGraph.new は何をしているか」を読み解く。

github.com

require "odata"

Dir[
  File.join(
    File.dirname(__FILE__),
    'microsoft_graph',
    '*'
  )
].sort.each { |f| require f }

class MicrosoftGraph
  attr_reader :service
  BASE_URL = "https://graph.microsoft.com/v1.0/"

  def initialize(options = {}, &auth_callback)
    @service = OData::Service.new(
      api_version: options[:api_version],
      auth_callback: auth_callback,
      base_url: BASE_URL,
      metadata_file: options[:cached_metadata_file]
    )
    @association_collections = {}
    unless MicrosoftGraph::ClassBuilder.loaded?
      MicrosoftGraph::ClassBuilder.load!(service)
    end

  end

  def containing_navigation_property(type_name)
    navigation_properties.values.find do |navigation_property|
      navigation_property.collection? && navigation_property.type.name == "Collection(#{type_name})"
    end
  end

  def path; end
end

l.1 で自前の OData モジュールを require してます。microsoft_graph の肝の機能だと思うけど、今回は深堀りしない。

ll. 3 - 9 でやっているのは micorosoft_graph ディレクトリ配下のファイルを全て require している。途中で sort を使っているのでどうやら require する順序は気にしなくていいものらしい。

ll. 11 - 36 で MicrosoftGraph クラスを定義しています。「gem って Module じゃなくてもいいんだ」と思ったのが最初の印象です。実際どうなのかは詳しくは知らないので誰か教えて下さい。

アトリビュートとして service を、定数 BASE_URL を定義しています。 BASE_URL には Microsoft Graph API エンドポイントの共通 URI が定義されています。

そして initialize メソッドが options というハッシュ値の引数と、auth_callback というブロック変数が定義されていて、内部では OData::Service というクラスの初期化をしています。この OData::Service クラスの初期化に options[:api_ver] auth_callback BASE_URL options[:cache_metadata_file]が必要そうだと分かります。

その後、インスタンス変数 associateion_collections を空のハッシュで初期化し、MicrosoftGraph::ClassBuilder がビルドされていなければビルドをする、という流れのようです。

うん、さっぱり分からないですね。肝心なのは OData を扱っているモジュールのようなきがする。

(続く)