swagger-marshmallow-codegen でpaths以下も見るようにした

swagger-marshmallow-codegen でpaths以下も見るようにした。あまりきれいとはいえない感じかもしれないけれど。

paths以下を見るということ

今まではdefinitions以下しか見なかったのだけれど。通常swaggerでapiの定義をするときにはpaths以下にも色々書く。というよりrequestとresponseがどのようになっているかはpaths以下の定義を見る事が多い。例えば以下のようなAPI定義(apiaryのtuotialからもらってきた)はどうなっているかというと。

paths:
  /message/{name}:
    x-summary: Message operations
    x-description: Operation description in Markdown
    get:
      summary: Get a message of the day
      description: |
       Description of the operation in Markdown
      operationId: getMessage
      parameters:
        - name: name
          in: path
          description: name to include in the message
          type: string
          x-example: 'Hello, Adam!'
      responses:
        default:
          description: Bad request
        200:
          description: Successful response
          schema:
            $ref: '#/definitions/Message'
          examples:
            'application/json':
              message: 'Hello, Adam!'
definitions:
  Message:
    required:
      - message
    properties:
      message:
        type: string
        default: 'Hello, Adam!'

これは GET /message/{name} というようなAPIが存在していて、そのrequestの形式で許可するものはpathのみ(apiaryのsampleはqueryになっていたけれどそれは間違い。こちらでは修正している)。また、outputとしてstatus=200のresponseは Message のschemaになっている。

Input, Output

先程のAPI定義からswagger-marshmallo-codegenを使ってschemaのコードを生成してみる。今度からInput,Outputというclassも生成されるようになった。具体的には以下の様なもの。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)


class Message(Schema):
    message = fields.String(required=True, missing=lambda: 'Hello, Adam!')


class MessageNameInput(object):
    class Get(object):
        """Get a message of the day"""

        class Path(Schema):
            name = fields.String(description='name to include in the message')


class MessageNameOutput(object):
    class Get200(Message):
        """Successful response"""
        pass

APIのrequestとresponse毎にInput,Outputが存在していてそのバリエーション毎にクラスが別れている。外側のクラスはnamespaceみたいなもの。

今回のAPIに関して言えば、以下のようなrequestとresponseになる。

GET /message/adam
{"message": "Hello, Adam!"}

それぞれ MessageNameInputMessageNameOutput が対応している。

x-marshmallow-name

Input,Outputの名前はpathのpatternからすごく雑に変換して決めている。

/message/{name} -> /message/name -> message, name -> MessageName

気に入らない場合もあるだろうから、 x-marshmallow-name で名前を決められるようにした。

--- 00schema.yaml    2017-01-17 06:29:00.000000000 +0900
+++ 01schema.yaml 2017-01-17 06:33:33.000000000 +0900
@@ -2,6 +2,7 @@
   /message/{name}:
     x-summary: Message operations
     x-description: Operation description in Markdown
+    x-marshmallow-name: Message
     get:
       summary: Get a message of the day
       description: |

以下のような修正を加えると出力結果は以下の様に変わる。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)


class Message(Schema):
    message = fields.String(required=True, missing=lambda: 'Hello, Adam!')


class MessageInput(object):
    class Get(object):
        """Get a message of the day"""

        class Path(Schema):
            name = fields.String(description='name to include in the message')


class MessageOutput(object):
    class Get200(Message):
        """Successful response"""
        pass

もう少し複雑なもの

Path以外にもparametersは色々ある。query,formData,body,header (詳しくはここ)。それらも見る。

例えばGithubAPIの一部のAPI定義をすこしだけ弄った以下のようなyamを渡すと以下のようなコードを生成する。

definitions:
  emailsPost:
    items:
      type: string
      pattern: ".+@.+"
    type: array
  label:
    properties:
      color:
        maxLength: 6
        minLength: 6
        type: string
      name:
        type: string
      url:
        type: string
    type: object
  labels:
    items:
      $ref: '#/definitions/label'
    type: array
  labelsBody:
    items:
      type: string
    type: array

parameters:
  owner:
    description: Name of repository owner.
    in: path
    name: owner
    required: true
    type: string
  repo:
    description: Name of repository.
    in: path
    name: repo
    required: true
    type: string
  number:
    description: Number of issue.
    in: path
    name: number
    required: true
    type: integer
  X-Github-Media-Type:
    description: |
      You can check the current version of media type in responses.
    in: header
    name: X-GitHub-Media-Type
    type: string
  Accept:
    description: Is used to set specified media type.
    in: header
    name: Accept
    type: string
  X-RateLimit-Limit:
    in: header
    name: X-RateLimit-Limit
    type: integer
  X-RateLimit-Remaining:
    in: header
    name: X-RateLimit-Remaining
    type: integer
  X-RateLimit-Reset:
    in: header
    name: X-RateLimit-Reset
    type: integer
  X-GitHub-Request-Id:
    in: header
    name: X-GitHub-Request-Id
    type: integer


responses:
  labels:
    description: OK
    schema:
      $ref: '#/definitions/labels'
  label-created:
    description: Created
    schema:
      $ref: '#/definitions/label'


paths:
  '/repos/{owner}/{repo}/issues/{number}/labels':
    delete:
      description: Remove all labels from an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
      responses:
        '204':
          description: |
            No content.
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    get:
      description: List labels on an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
      responses:
        '200':
          $ref: "#/responses/labels"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    x-marshmallow-name: IssuedLabels
    post:
      description: Add labels to an issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
        - in: body
          name: body
          required: true
          schema:
            $ref: '#/definitions/emailsPost'
      responses:
        '201':
          $ref: "#/responses/label-created"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.
    put:
      description: |
        Replace all labels for an issue.
        Sending an empty array ([]) will remove all Labels from the Issue.
      parameters:
        - $ref: "#/parameters/owner"
        - $ref: "#/parameters/repo"
        - $ref: "#/parameters/number"
        - $ref: "#/parameters/X-Github-Media-Type"
        - $ref: "#/parameters/Accept"
        - $ref: "#/parameters/X-RateLimit-Limit"
        - $ref: "#/parameters/X-RateLimit-Remaining"
        - $ref: "#/parameters/X-RateLimit-Reset"
        - $ref: "#/parameters/X-GitHub-Request-Id"
        - in: body
          name: body
          required: true
          schema:
            $ref: '#/definitions/emailsPost'
      responses:
        '201':
          $ref: "#/responses/label-created"
        '403':
          description: |
            API rate limit exceeded. See http://developer.github.com/v3/#rate-limiting
            for details.

こういう感じ。

# -*- coding:utf-8 -*-
from marshmallow import (
    Schema,
    fields
)
from marshmallow.validate import (
    Length,
    Regexp
)
from swagger_marshmallow_codegen.schema import (
    PrimitiveValueSchema
)
import re


class Label(Schema):
    color = fields.String(validate=[Length(min=6, max=6, equal=None)])
    name = fields.String()
    url = fields.String()


class IssuedLabelsInput(object):
    class Delete(object):
        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Get(object):
        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Post(object):
        class Body(PrimitiveValueSchema):
            v = fields.String(validate=[Regexp(regex=re.compile('.+@.+'))])

        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')


    class Put(object):
        class Body(PrimitiveValueSchema):
            v = fields.String(validate=[Regexp(regex=re.compile('.+@.+'))])

        class Header(Schema):
            X_GitHub_Media_Type = fields.String(description='You can check the current version of media type in responses.\n', dump_to='X-GitHub-Media-Type', load_from='X-GitHub-Media-Type')
            Accept = fields.String(description='Is used to set specified media type.')
            X_RateLimit_Limit = fields.Integer(dump_to='X-RateLimit-Limit', load_from='X-RateLimit-Limit')
            X_RateLimit_Remaining = fields.Integer(dump_to='X-RateLimit-Remaining', load_from='X-RateLimit-Remaining')
            X_RateLimit_Reset = fields.Integer(dump_to='X-RateLimit-Reset', load_from='X-RateLimit-Reset')
            X_GitHub_Request_Id = fields.Integer(dump_to='X-GitHub-Request-Id', load_from='X-GitHub-Request-Id')

        class Path(Schema):
            owner = fields.String(description='Name of repository owner.')
            repo = fields.String(description='Name of repository.')
            number = fields.Integer(description='Number of issue.')




class IssuedLabelsOutput(object):
    class Get200(Label):
        """OK"""
        def __init__(self, *args, **kwargs):
            kwargs['many'] = True
            super().__init__(*args, **kwargs)


    class Post201(Label):
        """Created"""
        pass

    class Put201(Label):
        """Created"""
        pass