😸

TypeSpecを利用する場合のdirectory approach

2024/10/25に公開

概要

TypeSpecを利用して、OpenAPIv3などを経由したApproachの多様性、そもそもTypeSpec自体が生のOpenAPIを記載するよりも記載しやすいことに感化されたため記載します。

基礎となるTypeSpec Directory

.
├── .gitignore
├── .spectral.yaml
├── package-lock.json
├── package.json
├── scripts
│   └── sync
│       └── main.ts
├── src
│   ├── blog
│   ├── main.tsp
│   ├── news
│   ├── rest-util
│   ├── tspconfig.yaml
│   └── util
├── tsconfig.json
└── tsp-output

src/**
TypeSpecを入れるdirectoryファイルです。
あとで記載します。

.spectral.yaml
spectralを利用して生成したopenapiにlintをかけるconfigファイル。
TypeSpec -> OpenAPI -> Committee or Orval
でファイルを出力するため、噛み合わせの悪いコードを出力しないことを防止するための措置です。
swagger uiのみの場合はある程度のinvalidな状態なものも表示できるため、linterまたはtranspilerなどを準備する。

scripts/
internalのscriptコマンド。
CIやpackage.jsonで利用するscriptファイルを想定しています。

package.json

"scripts": {
    "fmt": "tsp format \"**/*.tsp\"",
    "fmt:check": "tsp format --check \"**/*.tsp\"",
    "build": "tsp compile ./src",
    "sync:ui": "ts-node ./scripts/sync/main.ts",
    "lint:openapi": "spectral lint ./tsp-output/@typespec/openapi3/openapi.yaml"
  },

モノシリックなコードベース向けアプローチ

モノシリックについてはこちら。

思惑

open apiのschemaファイル自体は1つを想定しており、
open apiのファイルが大きくなった場合や、micro serviceなどに展開が必要になった場合などを想定して比較的柔軟に対応できる少し冗長的な構成を作ります。
OpenAPI Schemaから各プロジェクトへのcopyについては、CIやlocal syncなどでも利用可能だと思います。

方針

src以下のmain.tsptspconfig.yamlファイルについては、モノシリック用と想定していただければ。

上記2ファイル以外のblog, news directoryについては、feature moduleとして、directoryの構成としては以下となります。

TypeSpec src directoryのapproach

├── src
│   ├── main.tsp
│   ├── tspconfig.yaml
│   ├── blog
│   │   ├── model.tsp
│   │   └── route.tsp
│   ├── news
│   │   ├── model.tsp
│   │   └── rotue.tsp
│   ├── rest-util
│   │   └── page.tsp
│   └── util
│       └── uuid.tsp

src/main.tsp
dist用のファイルとしてあつかいます。
ここではロジックなどを利用せずに、各(Feature)Moduleごとに記載したOpenAPIファイルをimportするのみに利用します。

src/main.tsp
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";
import "./blog/route.tsp";
import "./news/rotue.tsp";
`src/tspconfig.yaml`
emit:
  - "@typespec/openapi3"
src/{feature-module}
├── model.tsp
├── route.tsp

src/{feature-module}/model.tsp
Protocolに問わないモデルとモデルのOperationのみを記載します。
方針はPojoのように、Plainな形式で記載することを推奨します。

src/{feature-module}/model.tsp
namespace BlogModule;
model Blog {
  @visibility("read")
  @key
  id: uint64;

  @visibility("create")
  title: blogTitle;
}

@minLength(1)
@maxLength(255)
scalar blogTitle extends string;

@withVisibility("create")
model CreateBlogContent {
  ...Blog;
}

op createBlogCotnent(dto: CreateBlogContent): Blog;

src/{feature-module}/route.tsp
feature-moduleのroutingファイルになります。
OpenAPIは、OpenAPI用のRPCならRPC用の構成を作ることを想定しています。

src/{feature-module}/route.tsp
import "@typespec/http";
import "./model.tsp";
import "../rest-util/page.tsp";

using TypeSpec.Http;
using BlogModule;

@tag("blog")
@route("/blogs")
interface Blogs {
  create is createBlogCotnent;
  list(): Page<Blog>;
}

想定するフロー

TypeSpec経由で、OpenAPIを生成。
Spectralでlintを実施後、各Projectにcopyすることを想定しています。
雑に記載すると以下のような、scriptになります。

scripts/sync/main.ts
import { join } from "path"
import { copyFileSync, mkdirSync } from "fs"
import { cwd } from "process";

const main = () => {
  try {
    const openApiFile = join(cwd(), "tsp-output/@typespec/openapi3/openapi.yaml")
  
    const distUIDir = join(cwd(), "..", "web-app/src/generated")
    const distUIFile = join(distUIDir, "openapi.yaml")

    const distAPIDir = join(cwd(), "..", "api/schema")
    const distAPIFile = join(distAPIDir, "openapi.yaml")

    Array.from([distAPIDir, distUIDir]).forEach(dir => {
      mkdirSync(dir, { recursive: true })
    });

    Array.from([distUIFile, distAPIFile]).forEach(filePath => {
      copyFileSync(openApiFile, filePath)
    })

  } catch (error: unknown) {
    console.error(error)
  }
}

main();

typespecファイルからopen api3を生成する
Spectralを利用して、OpenAPI, SwaggerのコードをLintする

Discussion