176
136

More than 3 years have passed since last update.

作って理解する OpenAPI 3.0 / connexion

Posted at

この記事の目的

DeepLearning 全盛期のこの時代、サーバーも python で書くということも多いのではないでしょうか。
この記事では python を使っていい感じの Rest API サーバーを作れるようになることを目的とします。

Rest API サーバー

Rest API サーバーの指針

個人的には以下の記事が rest の設計において大変わかりやすく、そして実用的だったのでおすすめです。
翻訳: WebAPI 設計のベストプラクティス

さて、上記の記事の中にも「かっこいい仕様書を作ろう」という項目があります。

API の価値はそのドキュメントの良さが表すと言っても過言ではありません。ドキュメントは誰もが簡単に閲覧できるようにしておく必要があります。ほとんどの開発者は、API の利用を検討する際にまずはドキュメントをチェックしますが、それが PDF だったり、閲覧に何かしらの登録が必要だったりすると、彼らにリーチするのはとても難しくなってしまいます。

API はインターフェースであり、インターフェースとは仕様です。
API と内部の実装がずれてしまうととても残念なことになります。一方で我々エンジニアは面倒くさがりなので実装を変えたときに一緒にドキュメントを更新するのを忘れがちです。

そこでおすすめなのが Open API です。

OpenAPI Specification (OAS) 3.0

Open API Specification (OAS) は Rest API の仕様を記述するフォーマットです。 yaml や json で書かれます。

簡単なOASサンプル
openapi: 3.0.0
info:
  title: OpenAPI Tutorial
  description: OpenAPI Tutorial by halhorn
  version: 0.0.0
servers:
  - url: https://example.com/api/v0
    description: プロダクション API
  - url: http://{host}:{port}/api/v0
    description: 開発用
    variables:
      host:
        default: localhost
      port:
        default: '10080'
paths:
  /health:
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        '200':
          description: サーバーは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_health_response'
components:
  schemas:
    get_health_response:
      description: サーバーの状態のレスポンス
      type: object
      properties:
        status:
          type: string
          enum:
            - ok
      required:
        - status

もともとは Smarter Bear 社の Swagger というサービスだったものが Microsoft や Google によって設立された OpenAPI Initiative に寄贈され OpenAPI Specification になったそうです。
Swagger2.0 と OpenAPI2.0 は同一のものです。

さて、 OAS による統一された記法で Rest API を記述することで以下のような恩恵を受けられます。

1. API のドキュメント生成

OAS を記述し、 swagger-ui というサーバーを建てることで以下のようなドキュメントを web に公開できます。
image.png

API のドキュメントをグラフィカルに見れるだけではなく、なんとこのドキュメント上からリクエストを投げて動作確認することまでできてしまいます。(裏側のサーバーは作る必要がありますが)
image.png

このリクエストを投げられる機能は非常に便利で、サーバー開発時の動作確認が圧倒的に効率的になります。また、 API を使うクライアントの開発者も具体的にどのようなリクエストを投げればよいのかが(curl コマンドの例も示されるので)一目瞭然で大変素晴らしいです。

2. サーバー作成

OpenAPI Generator を使うことで、 OAS から各種言語でサーバーのスタブのコードを生成することができます。
また、コードを自動生成するところまで行かなくとも今回紹介する connexion のように、 OAS で記述した仕様に基づいてサーバーを立ててくれるフレームワークもあります。

connexion

python で OAS に基づいたサーバーを作る場合 connexion が便利です。(OpenAPI Generator を使った場合でも connexion を使ったサーバーのコードが生成されます。)
connexion を使うことで以下が OAS から自動で行われます。

  • エンドポイントの作成
  • 入力のバリデーション
  • 出力のバリデーション(オプショナル)

詳しくは後の章で実際に OpenAPI と connexion を使ったサーバーを作ることで説明していきます。

3. クライアントの作成

同様に OpenAPI Generator を使うことで、 OAS から各種言語でクライアントの API クライアントコードを生成することができます。

私はまだクライアントの生成周りは真面目に触っていないのでわからない部分も多いですが、 API との通信部分や、各種リクエスト・レスポンスのエンティティクラスなどを自動生成してくれるのは非常にありがたいでしょう。
ただし、 2019/09 現在 Swift は 4.x 系までしか対応しておらず、 5.x 系は未対応です。残念。

4. 素晴らしいエディタ

https://editor.swagger.io/ にアクセスしてみてください。
Swagger / OpenAPI 専用のエディタが開きます。
image.png
(デフォで Swagger2.0==OpenAPI2.0 のサンプルが表示されてしまうので、 OpenAPI 3.0 のサンプルを楽しみたい方はこの記事のチュートリアルのサンプルをコピペしてみてください。)

  • OAS に特化した補完が効く
  • リアルタイムにエラー箇所を教えてくれる
  • リアルタイムにドキュメントをレンダリング
  • インストール・サーバー建て不要ですぐ使える

この Swagger-Editor を使うことで効率的に OAS を記述することができます。

さて、以下では OpenAPI Specification を書きながら理解し、更にその後 connexion を使って python の API サーバーを建てて行きます。

OpenAPI Specification 3.0 を書く

以下のチュートリアルは https://github.com/halhorn/openapi_tutorial にあります。
手早く試したい人は README に従ってインストール、 run してみてください。
pyenv がインストールされていることが必要です。

また網羅的なドキュメントは https://swagger.io/specification/ にあります。
各項目どのような要素を持つのかなど詳しく見たい人はこのチュートリアルのあと一通り目を通すと良いでしょう。

ただの health エンドポイントがあるだけの簡単な OAS を書く

まずは https://editor.swagger.io/ にアクセスしましょう。
これから API 作成を行うなら、デフォルトで表示される v2.0 の書式ではなく v3.0 をベースにすると良いでしょう。
以下のコードをコピペしてください。

簡単なOASサンプル
openapi: 3.0.0
info:
  title: OpenAPI Tutorial
  description: OpenAPI Tutorial by halhorn
  version: 0.0.0
servers:
  - url: https://example.com/api/v0
    description: プロダクション API
  - url: http://{host}:{port}/api/v0
    description: 開発用
    variables:
      host:
        default: localhost
      port:
        default: '10080'
paths:
  /health:
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        '200':
          description: サーバーは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_health_response'
components:
  schemas:
    get_health_response:
      description: サーバーの状態のレスポンス
      type: object
      properties:
        status:
          type: string
          enum:
            - ok
      required:
        - status

右側にレンダリングされたドキュメントが表示されましたね。
image.png
それでは各部分解説していきます。

openapi / info


openapi: 3.0.0
info:
  title: OpenAPI Tutorial
  description: OpenAPI Tutorial by halhorn
  version: 0.0.0

この API に関する一般的な情報を書きます。

  • openapi: openapi の書式のバージョンです。3系を使うので3.0.0とします。
  • info:
    • title / description: この API の名前や説明
    • version: この API のバージョン。
    • その他の要素はこちら

バージョンはセマンティックバージョニングに従いましょう。

x.y.z というバージョンが有った時、大雑把には

  • x: メジャーバージョン。後方互換性の無い変更を行った場合に上げます
    • このときは OAS のスペック自体も別のファイルにしてしまうのが良いかもしれません。
  • y: マイナーバージョン。後方互換性を損なわない新たな機能を追加した場合に上げます
  • z: パッチバージョン。後方互換性を損なわないバグの修正。

詳細は https://semver.org/lang/ja/ を参照してください。

servers

servers:
  - url: https://example.com/api/v0
    description: プロダクション API
  - url: http://{host}:{port}/api/v0
    description: 開発用
    variables:
      host:
        default: localhost
      port:
        default: '10080'

サーバーの URL や、 API のベースパスを指定します。
複数のサーバーを指定できます。

おすすめなのは、2つめの設定のように開発用のサーバーを入れておくことです。
ここで variables を使って任意のホスト、ポートを設定できるようにすることで、この API を各個人の開発環境などからテストすることができます。
ここで指定した host, port などの任意の variable は、ドキュメント上で指定することができます。

image.png

variables の詳細な仕様などはこちら

paths

paths:
  /health:
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        '200':
          description: サーバーは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_health_response'

paths に各種 API のエンドポイントを指定します。 OAS のメインの部分です。
ここでは必要最小限の説明を行い、細かい諸々の実装方法は次の章で説明します。

path
paths:
  /health:

paths 直下にエンドポイントのパスを書きます /health, /users etc.
path の設計については 翻訳: WebAPI 設計のベストプラクティス が参考になります。

  • エンドポイントは操作対象の名詞
    • users, devices, dialogues, etc...
  • /コレクション(複数形)/←のID/コレクション(複数形)/←のID といった書式に
    • GET /users/123, POST /users/123/dialogues
    • このような形式を守ることでリソースの階層関係が理解しやすくなります
    • 一方、すべての API でこの書式を守るのは現実的ではないと個人的には思っています
      • 上のサンプルの /health も上記に従っていません。これは慣例的にヘルスチェックエンドポイントは /health とされるためです
operation
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        ...

path の直下には get, post, delete などのオペレーションを書きます。
更にその中にそのオペレーションへのリクエスト・レスポンスの書式等を記述します。

  • operationId: そのオペレーションの識別子。
    • connexion を使う場合ここに、このオペレーションと紐付けられた python のモジュール(と処理する関数)を指定します
  • summary / description: このオペレーションの説明です
  • parameters: URL のパスやクエリ上のリクエスト (このサンプルにはありません。後述。)
  • requestBody: リクエストの body。 (このサンプルにはありません。後述。)
  • response: このオペレーションのレスポンス (後述。)

$ref となっているところは、後述する components の内容を参照することができます。
リクエストやレスポンスの型を paths の中に直接(inline に)書くこともできますがおすすめしません。
(OpenAPI-Generator を使ってコード生成をするときに InlineObject1 といったクラスが自動生成されてしまうため。)

components

components:
  schemas:
    get_health_response:
      description: サーバーの状態のレスポンス
      type: object
      properties:
        status:
          type: string
          enum:
            - ok
      required:
        - status

components は paths 等から使えるコンポーネントを書きます。
ここでは paths から $ref を使って呼び出している get_health_response を定義しています。

書式は

components:
  schemas:
    スキーマ名:
      スキーマ定義...

のような形になります。
詳細に関しては次の章で説明します。

components には schemas の他にも認証の仕組みを書く securitySchemes なども入ってきます。

実用的な OAS を書く

OpenAPI Specification のおおまかな書き方がわかったところで、次はユーザー情報の取得と追加の API のための実用的な OAS を作っていきます。
この OAS では、以下が追加されます

  • GET/ POST それぞれでの、リクエストとレスポンスの詳細な書き方
  • api_key を使った認証
openapi: 3.0.0
info:
  title: OpenAPI Tutorial
  description: OpenAPI Tutorial by halhorn
  version: 1.0.0
servers:
  - url: https://example.com/api/v1
    description: プロダクション API
  - url: http://{host}:{port}/api/v1
    description: 開発用
    variables:
      host:
        default: localhost
      port:
        default: '10080'
security:
  - api_key: []  # デフォルトのセキュリティ
paths:
  /health:
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        '200':
          description: サーバーは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_health_response'
      security: []
  /users/{user_id}:
    get:
      operationId: openapitutorial.controller.get_user.call
      tags:
        - users
      summary: ユーザー情報を取得します
      description: ユーザー情報を取得します。
      parameters:
        - name: user_id
          in: path
          description: 取得対象のユーザー ID
          required: true
          schema:
            $ref: '#/components/schemas/user_id'
      responses:
        '200':
          description: 取得に成功しました。
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/user'
    post:
      operationId: openapitutorial.controller.post_user.call
      tags:
        - users
      summary: ユーザーを追加します
      description: ユーザーを追加します。 user_id は自動で割り振られます。
      parameters:
        - name: user_id
          in: path
          description: 取得対象のユーザー ID
          required: true
          schema:
            $ref: '#/components/schemas/user_id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/user'
        required: true
      responses:
        '201':
          description: ユーザーの作成に成功しました。
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/user'
components:
  schemas:
    # スカラー共通系
    user_id:
      description: ユーザー識別子
      type: string
      pattern: '[a-zA-Z0-9_-]+'
      maxLength: 127
      example: halhorn
      readOnly: true
    gendar:
      description: 性別
      type: string
      enum:
        - male
        - female
        - other
    interest:
      type: string
      enum:
        - photo
        - bouldering
        - trip
        - cycling
        - birdwatching
    # オブジェクト共通系
    user:
      description: ユーザー
      type: object
      properties:
        user_id:
          $ref: '#/components/schemas/user_id'
        name:
          description: 氏名
          example: 信田春満
          type: string
          maxLength: 127
        mail_address:
          description: メールアドレス
          type: string
          format: email
        age:
          description: 年齢
          example: 32
          type: integer
          minimum: 0
        gendar:
          $ref: '#/components/schemas/gendar'
        interests:
          type: array
          items:
            $ref: '#/components/schemas/interest'
      required:
        - user_id
        - name
        - mail_address
        - age
        - gendar
    # 個別エンドポイント用
    get_health_response:
      description: サーバーの状態のレスポンス
      type: object
      properties:
        status:
          type: string
          enum:
            - ok
      required:
        - status
  securitySchemes:
    api_key:
      type: apiKey
      name: x-api-key
      in: header
      x-apikeyInfoFunc: openapitutorial.controller.auth.call
tags:
  - name: users
    description: ユーザーに関する API 群です。

追加されたのは GET /users/{user_id}POST /users 及びそれに付随する components です。

parameters

parameters は、 パス、クエリ、ヘッダ、クッキーなどに入れるパラメータを表します。
(body に入れる情報は requestBody として独立しているので parameters には入りません。次の節で説明します。)

      parameters:
        - name: user_id
          in: path
          description: 取得対象のユーザー ID
          required: true
          schema:
            $ref: '#/components/schemas/user_id'

ユーザーの取得には /users/{user_id} というように URL の PATH の中でユーザー ID を指定します。
その場合、 parameters という項目を追加して上で書いた {user_id} の定義を与える必要があります。

  • name: パラメータの名前です。パスに含める場合上の {user_id} の部分と合わせる必要があります
  • in: そのパラメータがどこに入るか。
    • path: 上の例だと path の一部として入っているので path
    • query: 他に、 xxxx?key=value のように query に入れる場合 query とします
    • 他に header, cookie があります
  • description: 説明
  • required: この項目を必須にするなら true
  • schema: この user_id の string, integer などの型を決めています。
    • このサンプルでは $ref を使って後述する components で定義される schema を参照しています。
    • schema の書式は後述します。

この parameters は GET でなくとも POST でも当然使えます

requestBody

body に入れる情報は requestBody に入れます。
GET では組み合わせるサーバーによっては使えない・・かもしれません。

      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/user'
        required: true

基本的に上記の書式になることが多いと思います。

schema の部分に直接 body の型構造を書くこともできますがおすすめしません。
これは、 OpenAPI Generator などでコードを自動生成する際に、上記のように ref を使っておけば User という「名前付きの」型が自動生成されるのが、直接 schema を書いてしまうと InlineObject1 といった無名の型が生成されてしまい可読性が悪いためです。

schema の書き方は parameters と同じで後述します。

response

サーバーレスポンスの種類や書式を定義します。

      responses:
        '201':
          description: ユーザーの作成に成功しました。
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/user'

レスポンスコード:内容 の書式で書きます。 description は必須です 忘れると怒られるのでお気をつけを。
内容部分は基本的に requestBody と同じですね。

components と schema

ようやくここまできました! schema を定義することこそが OpenAPI を使った仕事の一番のお仕事です。

components には他のセクションから参照される諸々を書きます。
components.schemas には、他のセクションから参照されるスキーマ==型定義を書きます。
entity を記述する感じです。

components:
  schemas:
    # スカラー共通系
    user_id:
      ...

スキーマの定義は以下の書式をベースにします。

スキーマ名:
  description: 説明だよ
  example: サンプルの値だよ。できるだけ設定しようね。
  type: string  # ここが核心
  (type ごとの追加の設定)

string, int の schema

    user_id:
      description: ユーザー識別子
      type: string
      pattern: '[a-zA-Z0-9_-]+'
      maxLength: 127
      example: halhorn
      readOnly: true

文字列系のスキーマは type: string とします。
数値系の場合、 type: integer(整数) type: number(小数) とします。詳細は後述する format を参照してください。
例のように pattern で正規表現で制限を入れたり、 maxLength で長さの制限を設けたりできます。

connexion を使うと、 schema 定義に従って自動でバリデーションを行ってくれるため、これらの制限は真面目に書いておくことをおすすめします。そうすればプログラム上で自分で if len(user_id) > 127: ... とかかかなくて済みます。

使える制限は主に以下のようなものがあります。

format: メールアドレスなど、一般的な型に制約を加える
schema:
  type: object
  properties:
    mail:
      type: string
      format: email  # <- ここ!
  required:
    - mail

format の値としては以下のものがあります。 出典
ちなみに以下の表にはありませんが email も使えるようです。

Common Name type format Comments
integer integer int32 signed 32 bits
long integer int64 signed 64 bits
float number float
double number double
string string
byte string byte base64 encoded characters
binary string binary any sequence of octets
boolean boolean
date string date As defined by full-date - RFC3339
dateTime string date-time As defined by date-time - RFC3339
password string password A hint to UIs to obscure input.
pattern: regex で制約を入れる
schema:
  type: object
  properties:
    device_id:
      type: string
      pattern: '^[a-z0-9_]+$'  # <- ここ!
  required:
    - device_id

パターンは部分一致になるので、 ^$ で全体が一致するようするのを忘れずに。

ちなみにここで指定したパターンに合致しない場合、 connextion はエラーの json にルールである正規表現を表示します。セキュリティ上嫌であればなにか対策したほうがいいかもしれません。

文字列の長さに制約を入れる
schema:
  type: object
  properties:
    device_id:
      type: string
      minLength: 5
      maxLength: 255
  required:
    - device_id
  • maxLength
  • minLength

の片方のみの指定でもかまいません

数値に制約を入れる
schema:
  type: object
  properties:
    age:
      type: integer
      minimum: 0
  required:
    - device_id

数値に関する制約は以下の通りです

  • maximum
  • exclusiveMaximum
  • minimum
  • exclusiveMinimum

enum の schema

string, int などの型は enum にすることができます。

    gendar:
      description: 性別
      type: string
      enum:
        - male
        - female
        - other

これで gendar は male, female, other のどれかの値のみを取ることを表現できます。

object の schema

object とは python で言うところの dict です。 key-value な値を表現します。

    user:
      description: ユーザー
      type: object
      properties:
        user_id:
          $ref: '#/components/schemas/user_id'
        name:
          description: 氏名
          example: 信田春満
          type: string
          maxLength: 127
        mail_address:
          description: メールアドレス
          type: string
          format: email
        age:
          description: 年齢
          example: 32
          type: integer
          minimum: 0
        gendar:
          $ref: '#/components/schemas/gendar'
        interests:
          type: array
          items:
            $ref: '#/components/schemas/interest'
      required:
        - user_id
        - name
        - mail_address
        - age
        - gendar

非常に長いですが、増えたのは propertiesrequired だけです。
properties には、 object の各 key, value の定義を書きます。
value 部分の書式は schema です。なので、ネストした object なども簡単にかけます。
required は required であるプロパティをすべて列挙しないといけません。個人的にこれは required に入れ忘れそうで面倒くさい仕様です。

array の schema

        interests:
          type: array
          items:
            $ref: '#/components/schemas/interest'

items に各要素の schema を記述します。

readOnly / writeOnly

    user_id:
      description: ユーザー識別子
      type: string
      pattern: '[a-zA-Z0-9_-]+'
      maxLength: 127
      example: halhorn
      readOnly: true

「id はリクエストボディには含まれないが、レスポンスには含まれる」ということがよくあります。

例えば

  • POST /resources といった id はサーバー側が生成する場合
  • POST /resources/{resource_id} といった、 id はボディではなくパスに入っている場合

このような時に使うのが readOnly です。
ある属性に readOnly をつけておくと、クライアントから見た read つまりレスポンスにのみそのプロパティが含まれるようになります。
公式ドキュメント によると

Relevant only for Schema "properties" definitions. Declares the property as "read only". This means that it MAY be sent as part of a response but SHOULD NOT be sent as part of the request. If the property is marked as readOnly being true and is in the required list, the required will take effect on the response only.

とあるとおり、 readOnly は object の properties に入っているときにしか効力を発揮しません。
よって、今回の例ではパスに含まれる user_id はちゃんと required=true としてみなされます。

schema の参照

これまでの例でもでてきたとおり、 schema は $ref を使うことで他の schema を参照することができます。
深いネストがある schema はできるだけ $ref を使って構造化することをおすすめします。

securitySchemes と security

securitySchemes はセキュリティ(認証認可)のスキームを記述します。

  securitySchemes:
    api_key:
      type: apiKey
      name: x-api-key
      in: header
      x-apikeyInfoFunc: openapitutorial.controller.auth.call

名前: 設定 といった形で記述します。(api_key はこの設定につけられた名前です。)
ここでは apiKey での認可のみを説明します。
その他については https://swagger.io/specification/#securitySchemeObject を参照してください。

  • type: 認証認可の種類。ここでは apiKey を
  • in: apiKey をどこに入れるか。ここではヘッダに入れるようにしています。
  • name: ヘッダにいれる apiKey のキー名。 x-api-key: xxxxxxxxx といった形でヘッダに入ることになります。
  • x-apikeyInfoFunc: こちらは connexion を使ってサーバーを作る際に使用します。 apiKey の認可検証を行う python の関数を指定します。(operationId と同じ書式です。)

次に、定義した securitySchemes を使う場所を指定します。

全体に適用
security:
  - api_key: []  # デフォルトのセキュリティ
paths:
  /health:
    ...

上記のように書くと、 API 全体に対して api_key スキームを適用できます。
[] の部分は apiKey ではなく OAuth 認証を行う場合に使われます。 apiKey では気にしなくて問題ありません。

オペレーション毎に適用
  /health:
    get:
      operationId: openapitutorial.controller.health.call
      summary: サーバーの状態を返します
      description: サーバーの状態を返します。
      responses:
        '200':
          description: サーバーは正常に動作しています
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_health_response'
      security: []

上記のように get, post などのオペレーションの要素に security を入れることで、個別のオペレーションに対してセキュリティ設定を上書きすることができます。
この例では空配列 [] を指定しているので、 GET /health オペレーションに対しては apiKey を求めない設定をしています。

security の設定忘れによって本来アクセスできてはいけない情報へのアクセス事故を防ぐため、全体で統一したセキュリティを適用しつつ、セキュリティが不要なオペレーションについてはそれを無効化する運用が良いと思っています。

tag

最後に tag です。 これは Swagger-UI で API をグルーピングして表示するために使われます。

paths:
  ...
  /users:
    post:
      operationId: openapitutorial.controller.post_users.call
      tags:
        - users
...
tags:
  - name: users
    description: ユーザーに関する API 群です。

上記のようにオペレーション内にそのオペレーションが属するタグの一覧を書き、グローバルの tags 内に各 tag の定義を書きます。
すると下記のように Swagger-UI 上でオペレーションがグルーピングして表示されます。

image.png

以上で簡単な、しかし実用的な OAS がかけるようになりました。
次の章では connexion を使って上記の OAS に従って動く API サーバーを作っていきましょう。

connexion を使って API サーバーを作る

python のフレームワークである connexion を使って、これまで書いてきた OAS に従った API サーバーを簡単に作ってみます。
公式ドキュメントは https://connexion.readthedocs.io/en/latest/ です。

環境を作る

とりあえず私が作った環境を clone してみてください。
pyenv もしくは pipenv が入っていることが必要です。
https://github.com/halhorn/openapi_tutorial

git clone https://github.com/halhorn/openapi_tutorial.git
cd openapi_tutorial
pyenv install
pip install -U pip pipenv
pipenv install

一度サーバーを起動してみましょう。

pipenv run server

ブラウザで、 http://localhost:10080/api/v1/ui にアクセスすると Swagger-UI で API Specification が見れます。

image.png

サーバー本体の実装

openapitutorial/__main__.py では connexion を使ってサーバーを起動しています。
内部的には flask のサーバーが起動します。
しかし、 flask と違ってどのようなエンドポイントがあるか、どのようなリクエストが来るかは connexion が OAS の定義ファイルを読み込んで自動で生成してくれるので、下記のようにシンプルになります。

openapitutorial/__main__.py
import connexion


def setup_app():
    app = connexion.FlaskApp(
        'openapitutorial',
        host='0.0.0.0',
        port=10080,
        specification_dir='../apispec/',
        debug=True,
        options={
            'swagger_ui': True,
        },
    )
    app.add_api(
        'v0/openapi.yaml',
        validate_responses=True,
    )
    app.add_api(
        'v1/openapi.yaml',
        validate_responses=True,
    )
    return app


if __name__ == '__main__':
    app = setup_app()
    app.run()
  • specification_dir: OAS の定義ファイルをおくディレクトリ
  • options.swagger_ui: サーバー起動時に Swagger-UI も起動するか。(デフォルトで True なので、表示したくない場合 False にします。)

上記で app を作成したあと、 OAS のファイルを追加していきます。
このサンプルでは health のみ実装した version 0 と users を実装した version 1 を入れています。
通常は1つファイルがあれば十分です。

最も簡単なコントローラ

openapitutorial/controller/health.py はアクセスされるとただ {"status": "ok"} を返す最も簡単なコントローラです。

openapitutorial/controller/health.py
def call(**kwargs):
    return {'status': 'ok'}

OAS 側で GET /health の operationId を openapitutorial.controller.health.call にしました。
そのため GET /health のリクエストがあると connexion は openapitutorial.controller.health モジュールの、 call 関数を呼び出します。
返り値は JSON Serializable なものを返します。 dict, list, etc.

api_key による認可

次に作る users エンドポイントは、 api_key がないとアクセスできません。そこで認可の仕組みを作っていきます。
OAS の securitySchemes で、 x-apikeyInfoFuncopenapitutorial.controller.auth.call を指定しました。
なので、 api_key での認証が必要なオペレーションでは openapitutorial.controller.auth モジュールの call 関数が呼ばれます。

openapitutorial/controller/auth.py
from typing import List, Dict, Optional
from werkzeug.exceptions import Unauthorized


def call(api_key: str, required_scopes: None) -> Dict:
    auth = AuthData.get(api_key)
    if auth is None:
        raise Unauthorized()
    return {
        'sub': auth.user_id,
        'scope': auth.scopes,
    }


class AuthData:
    api_key: str
    user_id: str
    scopes: List[str]

    def __init__(self, api_key: str, user_id: str, scopes: List[str]) -> None:
        self.api_key = api_key
        self.user_id = user_id
        self.scopes = scopes

    @classmethod
    def get(cls, api_key: str) -> Optional['AuthData']:
        record = cls._get_data_from_db(api_key)
        return cls(**record) if record else None

    @classmethod
    def _get_data_from_db(cls, api_key: str) -> Optional[Dict]:
        # TODO: implement
        data = {
            'abcd1234-1': {
                'user_id': 'halhorn',
                'scopes': ['user:read', 'user:write'],
                'api_key': 'abcd1234-1',
            },
            'abcd1234-2': {
                'user_id': 'nisehorn',
                'scopes': ['user:read'],
                'api_key': 'abcd1234-2',
            },
        }
        return data.get(api_key)

要は api_key を受け取って、その api_key に紐付いたユーザー(と必要に応じて権限)を返してあげれば良いです。
call の返り値の dict は以下の要素を含みます。

  • sub: 認可されるユーザー
  • scope: 認可されるスコープ

ただ、 scope はなくても動作するようで sub のみ必要です。
これらの dict はそのままの形で各種コントローラに渡されるため、任意のデータを渡すことができます。

api_key が存在しないものだったなど、 api_key に対する検証が失敗した場合は werkzeug.exception の適切な例外を投げることで、 connexion は対応する HTTP ステータスコードをレスポンスとして送出してくれます。

複雑なコントローラ

ここでは GET /users/{user_id} を実装してみましょう。

from typing import Dict, Optional
from werkzeug.exceptions import Forbidden, NotFound
from openapitutorial.entity.serializable import SerializableType
from openapitutorial.entity.gendar import Gendar
from openapitutorial.entity.interest import Interest
from openapitutorial.entity.user import User


def call(
        user_id: str,  # URL パラメータの user_id の値
        user: str,  # auth.call で api_key から引っ張ってきたユーザー ID (sub の中身)
        token_info: Dict,  # auth.call の返り値の dict
) -> SerializableType:
    if user_id != user or 'user:read' not in token_info['scope']:
        raise Forbidden()
    got_user = _get_user_from_db(user_id)
    if got_user is None:
        raise NotFound()
    return got_user.to_serializable()


def _get_user_from_db(user_id: str) -> Optional[User]:
    data = {
        'halhorn': User(
            user_id='halhorn',
            name='信田春満',
            mail_address='halhorn@exmaple.com',
            age=32,
            gendar=Gendar.MALE,
            interests=[Interest.BOULDERING, Interest.PHOTO],
        ),
        'nisehorn': User(
            user_id='nisehorn',
            name='偽田偽満',
            mail_address='nisehorn@exmaple.com',
            age=3,
            gendar=Gendar.OTHER,
        ),
    }
    return data.get(user_id)

call の引数は下記のようになります。

  • user_id: ここの名前は、パスに含まれる引数名 user_id とその値です。
    • もし /resources/{resource_id} ならここは user_id ではなく resource_id となります
  • user: 前節の認可コントローラの返り値である sub の値が user に入ります
  • token_info: 前節の認可コントローラの返り値の dict がまるっとここに入ります

前節で与えられた api_key がどのユーザーに紐付いた認可なのかは user 引数で渡ってきています。
したがってこのコントローラ では、認可されたユーザー(user)と捜査対象のユーザー(user_id) かなどをチェックしています。

その後レスポンスを計算し、返します。
レスポンスは OAS によって json であることがわかっているので、 Json Serializable なもの(この例では dict)を返せばあとはよしなに変換してくれます。

おわりに

以上で、 OpenAPI Specification を記述するところから python でサーバーを作成し起動することができました。
OpenAPI Specification を使うことで以下のメリットを享受できます。

  • API のドキュメント自動生成
  • サーバーの部分的自動作成
  • API クライアントの作成
  • 専用エディタによる快適なコーディング

不特定多数のデベロッパーが使う API を開発する場合にはもちろん、自社サービスの開発などにおいてもクライアントとサーバーを同時に別々の人が開発するような場面では大変役立つでしょう。

176
136
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
176
136