NodeのAzure FunctionsでOpenAPI Specificationを簡単に作成したい

2024年4月22日

少し宣伝させてください!Azureの試験対策本を執筆しました。

タイトルの通り、上記を達成するために調査していました。

以下のリポジトリでやりたいことが達成できると思い確認しましたが、issueにある通りv4には対応しないとのことでした。

https://github.com/xiaotiantakumi/az-samples/tree/main/az-func-ts-openapi

なので、改造して使うことにします。

リポジトリの内容をChatgptにまとめてもらいました。

GitHubリポジトリ「azure-functions-nodejs-openapi」は、Aaron Powellが開発したもので、Azure Functionsから注釈付きでOpenAPI仕様ファイルを生成するための拡張機能を提供します。この拡張機能はAPIの発見性と管理を向上させるのに役立ちます。TypeScriptで実装されており、OpenAPIのバージョン2、3、および3.1をサポートしています。

主な特徴は以下の通りです:

  • TypeScriptを使用してAzure FunctionsにOpenAPIメタデータを注釈付けする機能。
  • Azure Functionsから直接OpenAPI仕様ファイルを生成するサポート。
  • 生成された仕様をAzure API Managementに統合するサポート。これにより、APIのライフサイクル管理やセキュリティが強化されます。

この拡張機能の使用方法には、OpenAPIのメタデータでAzure Functionを注釈付けし、その後専用のAzure Functionを作成してOpenAPI仕様ファイルを生成および提供する手順が含まれます。リポジトリには、OpenAPIの異なるバージョンを使用する方法を示す例が含まれています。

冒頭でも述べた通り、v4には対応しないとのことだったので改造できないか確認してみました。 おそらく以下の部分を改造すればいいとあたりをつけました。

azure-functions-nodejs-openapi/src/openAPIv3_1.ts

import { AzureFunction, Context } from "@azure/functions";
import { OpenAPIV3_1 } from "openapi-types";

const paths: {
  [key: string]: OpenAPIV3_1.PathItemObject;
} = {};

function mapOpenApi(
  func: AzureFunction,
  routes: string | string[],
  specs: OpenAPIV3_1.PathItemObject | OpenAPIV3_1.PathItemObject[]
) {
  if (Array.isArray(routes) && Array.isArray(specs)) {
    routes.map((route: string, index: number) => {
      paths[route] = { ...paths[route], ...specs[index] };
    });
  } else if (!Array.isArray(routes) && !Array.isArray(specs)) {
    paths[routes] = { ...paths[routes], ...specs };
  }

  return func;
}

const generateOpenApiSpec =
  (doc: Omit<OpenAPIV3_1.Document, "paths" | "openapi">): AzureFunction =>
  (context: Context) => {
    const body: OpenAPIV3_1.Document = { ...doc, openapi: "3.1.0", paths };
    context.res = { body };
    context.done();
  };

export { mapOpenApi, generateOpenApiSpec };

上記を既存のソースに貼り付けたところ、いくつかエラーが出ました。

@azure/functionsからインポートする AzureFunction, Contextで怒られます。

どうも、破壊的な変更が入ったようで、これらが参照できなくなっていました。

これを今風に書き直してみました。

src/openapi.ts

import { HttpResponseInit, InvocationContext,HttpRequest } from "@azure/functions";
import { OpenAPIV3 } from "openapi-types";

const paths: {
  [key: string]: OpenAPIV3.PathItemObject;
} = {};

function mapOpenApi(
  routes: string | string[],
  specs: OpenAPIV3.PathItemObject | OpenAPIV3.PathItemObject[]
) {
  if (Array.isArray(routes) && Array.isArray(specs)) {
    routes.map((route: string, index: number) => {
      paths[route] = { ...paths[route], ...specs[index] };
    });
  } else if (!Array.isArray(routes) && !Array.isArray(specs)) {
    paths[routes] = { ...paths[routes], ...specs };
  }
}

const generateOpenApiSpec = (doc: Omit<OpenAPIV3.Document, "paths" | "openapi">) => 
    async (req: HttpRequest,context: InvocationContext, ): Promise<HttpResponseInit> => {
      const body: OpenAPIV3.Document = { ...doc, openapi: "3.1.0", paths };
      return {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body)
      };
};

export { mapOpenApi, generateOpenApiSpec };

大して書き換えてないですがこんな感じになりました。

使い方としては、元々のリポジトリにある説明の通りですが、以下のように使えます。 まずは定義の部分。 src/functions/httpTrigger.ts

import {
  app,
  HttpRequest,
  HttpResponseInit,
  InvocationContext,
} from "@azure/functions";
import { mapOpenApi,generateOpenApiSpec } from "../openapi";

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext,
): Promise<HttpResponseInit> {
  context.log(`Http function processed request for url "${request.url}"`);

  const name = request.query.get("name") || (await request.text()) || "world";
  return { body: `Hello, ${name}!` };
}

app.http("httpTrigger", {
  methods: ["GET", "POST"],
  authLevel: "anonymous",
  handler: httpTrigger,
});


export default mapOpenApi("/httpTrigger", {
  get: {
    parameters: [
      {
        name: "name",
        in: "query",
        required: true,
        schema: {
          type: "string",
        },
      },
    ],
    responses: {
      "200": {
        description: "Gets a message from the Function",
        content: {
          "application/json": {
            example:
              "Hello World. This is a HTTP triggered function executed successfully.",
          },
        },
      },
    },
  },
});

OpenAPI Specificationを出力するところ。

src/functions/swaggerTrigger.ts

import {
  app,
} from "@azure/functions";
import { generateOpenApiSpec } from "../openapi";


app.http("swaggerTrigger", {
  methods: ["GET", "POST"],
  authLevel: "anonymous",
  handler: generateOpenApiSpec({
    info: {
      title: "Azure Function Swagger v3.1 demo",
      version: "1.0.0",
    },
  }),
});

これでとりあえず動くようになりました。