その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

DBのcolumn名や、何らかドメインの意味を持つ概念に `type` を名付けない方が良いのではないか

type そのもの、 あるいは _type のようなsuffixを持つ名前を変数や、構造体・クラスのメンバーや、データベースのcolumnなどに付けてしまうことがしばしばあると思うのですが、個人的にはあまりこれはやらない方が良いのではないかと考えています。

理由としては

  • type はいくつかのプログラミング言語において予約語になっており、そのセマンティクスにおいて特別な役割を果たすことが多い。
  • Ruby on RailsにおいてはDelegated Typesという機能において、 _type というcolumnは特別な意味を持つ (もちろんアプリケーションコード側でDelegated Typesであるという宣言をしなければ副作用は無いのですが)。

というものがあると思っており、そういった概念との衝突を避けるために特別かつ強い理由が無い限りは type という命名は避けようというような気持ちで日々を過ごしています。
代替としては kindmethod などが使えそうであると思っておりこれらを採用することが多いです。methodは別の概念との衝突がある場合もありそうですが、そこは文脈に沿って、という感じですかね。どうしても type でしか表現できないものはあると思うのですが (例えば言語処理系を作っていて、本当に「型」を取り扱う必要がある場合など)、そういった場合は頑張りましょう。


かつてRustで `typ` という変数名を乱発していたことに対する戒めを込めています。

Kaigi on Rails 2025でasync gemを使ってSSE機能をRails Appで作るという話をしました #kaigionrails

speakerdeck.com

発表資料はこちらです。以下に資料に入れそびれた・話しそびれたことを以下に記しておきます。

実際のところSSEを導入したのはなぜなのか

今回例示したような「長い時間がかかる処理の進捗報告・完了通知」のような機能はポーリングでも実現可能なものであり、我々のプロダクトでも当初はポーリングを前提として設計がされていたのですが、以下のような理由からSSEを採用するに至って今回の発表に繋がっています:

  • ポーリングよりも基本的にはユーザー体験が良くなるはず (ポーリングの場合は変化までに高々インターバル秒かかる可能性がある) *1
  • 技術的な挑戦 (おたのしみ)

後者が地味に大事だと考えていて、何か新しい機能やサービスを作ったりする時に「既存のやり方」のみで固定化するのではなく *2、技術的に新しい取り組みをすることによって技術面・組織面での「実力」を高めたいというモチベーションが背景にありました。あと技術的に新しいことをやると純粋に楽しいですからね (楽しいだけではもちろん駄目ですが……)。僕はこれを「おたのしみ」と呼んでいて、新しいものを作る時にはできる限り取り組みたいと考えています。

Active Recordのコネクションをfiber nativeにする方法

スライドに書き忘れていました。

config.active_support.isolation_level = :fiber としてあげると、スライドの49ページ目に書いたようなworkaroundが不要になります *3

我々も当初この設定を入れようとしていたのですが、既に動いているそこそこの規模のシステムのisolation_levelを変更することに一定のリスクを感じたので見送っていたのでした。とはいえ多くの場合では問題が起こらない気もしますし (もちろん要検証)、少なくとも新たにrails newする時には問題無いと思うので、この設定でトライするのが良いと考えています。

MyModel.with_connection が使える

@ioquatixさんから発表後に教えてもらったのですが、表題のように MyModel.with_connection としてconnectionをborrowできるようになっているとのことです。べんり。

詳細については@ioquatixさんが書いてくださっているgistを参照してください: Show how `with_connection` and `lease_connection` interact. · GitHub

余談ですが、上記gistにあるように config.active_record.permanent_connection_checkout = :disallowed を設定に書いておくのは良さそうに思いました。

Rack 3以降は ActionController::Live を使わなくてもストリーミングレスポンスを返却できる

こちらについても@ioquatixさんから発表後に教えてもらいました。表題の通り、以下のように記述するとSSEでのレスポンスが可能なようです:

github.com

上記の例はFalconのものとなっていますがPumaでも問題無く動くとのことでした。これは知らなかった、良いですね。




ご覧の通り、async gemや数々の便利なライブラリ・ツールを作るなどのご活躍をされており、Kaigi on Rails 2025のキーノートスピーカーの@ioquatixさんから色々と教えていただけました。ありがとうございます。

テックカンファレンスはこういう交流ができるというのが良いところですね。

*1:ポーリングのほうが仕組みとしてシンプルであるというのは正。そしてSSEであってもコネクション数などの観点でのパフォーマンス優位性はそこまでないので……

*2:もちろん「型」を持っておくのは重要なのですが

*3:https://railsguides.jp/configuring.html#config-active-support-isolation-level

[令和最新]Resemblaをビルドして動かす方法

github.com

類似文字列検索ライブラリであるところのResemblaですが、利用に際してはビルド済みのパッケージが配布されていないため自分でビルドする必要があります。
が、Wikiに書かれているインストールドキュメントがCentOS 7のものになっており *1 、現代の環境で動かすにあたってはちょっと工夫が必要……ということで、2025年現代の環境で動作するDockerfileをここに共有します。

gist.github.com

ポイントとしては

  • mecab-ipadicではなくmecab-ipadic-utf8 を使う (thanks id:tomo_ari and id:ssig33)
  • icuのバージョンは59.1で固定
  • grpcのバージョンはv1.2.5で固定
    • 新しめのgccだとgettidがバッティングしてgrpcがビルドできないので、コードから除去するワークアラウンドを入れる
    • 新しめのgccだと警告出まくってビルドできないのでC++標準バージョンを下げつつ警告を無視してmake: make CFLAGS="-w -std=c11" CXXFLAGS="-w -std=c++14"
  • Resemblaのビルドに際しても新しめのgccだとエラーが出るのでcstddefをincludeするようにワークアラウンドを入れる *2

という感じでしょうか。このようにしておくと動き、grpcを使ったインターフェイスも動作するところまで持ってゆくことができます。

mecab-unidicをインストールしたい場合、こちらにもちょっとコツが必要なので頑張りましょう:

github.com

以上です。ご活用ください。

be-let-it-be書いた

github.com

be-let-it-beというコマンドラインツールを書きました。これはRSpecのスペックファイル中に存在する letlet! を自動的に可能な限り let_it_be に書き換えるというものです。

$ be-let-it-be convert path/to/your_spec.rb

というふうに簡単に使うことができます。

let_it_beの効用

test-profに含まれているメソッド (宣言?) です。

letlet! はテストごとに実行されるのに対して let_it_be はテスト間で使い回されます *1。データベースがからむプロジェクトでRSpecを使っているとしばしば let 等の中でfactory_botを呼び出してレコードを作ったりすることになるわけですが、 let_it_be を使うとそれがテスト間で1回で済むことになるためテストの実行時間を短くできる可能性が出てきます。

let_it_be の効用の詳細については既に先行している有用な情報がありますのでそちらに譲ります:

というわけで、be-let-it-beによって可能な限り let_it_be に書き換えると自動的にテストの実行時間が短くなることが見込めるわけです。

どのように動くのか

富豪的な挙動ではあって、

  1. 与えられたスペックファイル内の letlet! を抽出してくる
  2. let/let! の出現した順から let_it_be に書き換えてテストを実行する。通ったら let_it_be の書き換えを維持し、通らなかったら元に戻す
  3. 2を全て終わるまで繰り返す

というものになっています。つまり let/let! の個数ぶんの回数テストが実行されることになる……のでこのあたりはもうちょっとなんとかしたいなとは思っているという感じです *2


また、出現した順にやっていくというのも最適解では無いとは思っており、本来であれば一番テスト実行時間が短くなる組み合わせを探索するのが良いのだろうとは思うのですが、しかし組み合わせ爆発が起こることが容易に想像できることや、ローカルでのテスト実行時間が案外ブレるのでそれを素朴に指標として使うことができないなどといった理由からここには手を付けていないという状況になっています。


というような感じではありますがちゃんと動くし、その結果についても一定満足というような感じではあります。


余談ではありますが、最初のバージョンでは let/let! の抽出にperser、コードの書き換えにunperserを使っていたのですが、 id:tomo_ariさんから「Prism使ったら良いんじゃないすか」というアドバイスをもらったのでそのようにしました。Prismを使うとparser挙動はもちろん、unparser的なことも一挙にできて便利で良かったです。

まとめ

be-let-it-beのご紹介でした。実戦投入されています。

実際に実行時間が短縮されている様子

なかなか良い感じです。ぜひご活用ください。

*1:つまり高々1回の呼び出しで済む

*2:二分探索的にやれるようにするとか?

openapi-fetch-genを作った

English article is here: openapi-fetch-gen – Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript - DEV Community

npm registryにも公開されています。

従って以下のようにダウンロード可能です。

npm install @moznion/openapi-fetch-gen

これは何 / 背景

OpenAPI 3の仕様からopenapi-ts/openapi-typescriptにより生成されたスキーマ (.d.ts) ファイルからTypeScriptのAPIクライアントを自動生成するツールです。

openapi-ts/openapi-typescriptはかなりパワフルかつ完成度が高いツールで、比較的複雑なOpenAPIの定義からでも綺麗なスキーマファイルを生成することができます。なおかつopenapi-fetchというライブラリも併せて提供されており、これを用いることで生成されたスキーマファイルと組み合わせてAPI Clientをtype-safeな形で作成することができます。

ただこれには1点課題があり、openapi-fetchはあくまで「スキーマを読み込んでAPI Clientが作れるライブラリ」であり「APIクライアントを自動生成するものではない」ため、各エンドポイントについてAPIクライアントをマニュアルで作成する必要がありました。

という課題を解決するためにこの度作成されたのがこのopenapi-fetch-genです。

用法

以下のようなOpenAPI 3の定義YAMLを例に取ります。


schema.yaml (ちょっと長いですが、まあよくあるREST APIの定義だと思ってください)

openapi: 3.0.3
info:
  title: Fictional Library User Management Service API
  version: 1.0.0
  description: |
    A RESTful API for managing library user records and their loan information.
servers:
  - url: https://api.fictionallibrary.example.com/v1
    description: Production server

paths:
  /users/{userId}:
    parameters:
      - $ref: '#/components/parameters/userId'
      - name: Authorization
        in: header
        required: true
        schema:
          type: string
        description: Authorization Header
      - name: Application-Version
        in: header
        required: true
        schema:
          type: string
        description: Application version
      - name: Something-Id
        in: header
        required: true
        schema:
          type: string
        description: Identifier of something
    get:
      summary: Get user details
      description: Retrieve detailed information for a specific user.
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      summary: Replace user
      description: Replace a user's entire record.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdate'
      responses:
        '200':
          description: Updated user record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      summary: Update user fields
      description: Partially update a user's information.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserPatch'
      responses:
        '200':
          description: Updated user record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      summary: Delete user
      description: Soft-delete a user record.
      responses:
        '204':
          description: User deleted (no content)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  parameters:
    userId:
      name: userId
      in: path
      required: true
      description: Unique user identifier (UUID)
      schema:
        type: string
        format: uuid

  responses:
    BadRequest:
      description: Bad request due to invalid input
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Authentication required or failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Insufficient permissions to access resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: Conflict with current state of the resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      properties:
        code:
          type: integer
          description: HTTP status code
        message:
          type: string
          description: Error message detailing the cause
      required:
        - code
        - message

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        membershipType:
          type: string
          enum: [REGULAR, PREMIUM, STUDENT]
        registeredAt:
          type: string
          format: date-time
        address:
          $ref: '#/components/schemas/Address'
      required:
        - id
        - name
        - email
        - membershipType
        - registeredAt

    Address:
      type: object
      properties:
        postalCode:
          type: string
        street:
          type: string
        city:
          type: string
        country:
          type: string
      required:
        - street
        - city
        - country

    UserCreate:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        membershipType:
          type: string
          enum: [REGULAR, PREMIUM, STUDENT]
        address:
          $ref: '#/components/schemas/Address'
      required:
        - name
        - email
        - membershipType

    UserUpdate:
      allOf:
        - $ref: '#/components/schemas/UserCreate'
        - type: object
          properties:
            id:
              type: string
              format: uuid
          required:
            - id

    UserPatch:
      type: object
      description: Schema for partial updates – include only fields to change
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        membershipType:
          type: string
          enum: [REGULAR, PREMIUM, STUDENT]
        address:
          $ref: '#/components/schemas/Address'

この時に以下のようなコマンドを実行するとopenapi-typescriptを用いてスキーマファイル (.d.ts) が生成されます:

openapi-typescript --output schema.d.ts ./schema.yaml

この時に生成されるスキーマファイルは以下のようになっています


schema.d.ts

/**
 * This file was auto-generated by openapi-typescript.
 * Do not make direct changes to the file.
 */

export interface paths {
    "/users/{userId}": {
        parameters: {
            query?: never;
            header: {
                /** @description Authorization Header */
                Authorization: string;
                /** @description Application version */
                "Application-Version": string;
                /** @description Identifier of something */
                "Something-Id": string;
            };
            path: {
                /** @description Unique user identifier (UUID) */
                userId: components["parameters"]["userId"];
            };
            cookie?: never;
        };
        /**
         * Get user details
         * @description Retrieve detailed information for a specific user.
         */
        get: {
            parameters: {
                query?: never;
                header: {
                    /** @description Authorization Header */
                    Authorization: string;
                    /** @description Application version */
                    "Application-Version": string;
                    /** @description Identifier of something */
                    "Something-Id": string;
                };
                path: {
                    /** @description Unique user identifier (UUID) */
                    userId: components["parameters"]["userId"];
                };
                cookie?: never;
            };
            requestBody?: never;
            responses: {
                /** @description User details */
                200: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": components["schemas"]["User"];
                    };
                };
                400: components["responses"]["BadRequest"];
                401: components["responses"]["Unauthorized"];
                403: components["responses"]["Forbidden"];
                404: components["responses"]["NotFound"];
            };
        };
        /**
         * Replace user
         * @description Replace a user's entire record.
         */
        put: {
            parameters: {
                query?: never;
                header: {
                    /** @description Authorization Header */
                    Authorization: string;
                    /** @description Application version */
                    "Application-Version": string;
                    /** @description Identifier of something */
                    "Something-Id": string;
                };
                path: {
                    /** @description Unique user identifier (UUID) */
                    userId: components["parameters"]["userId"];
                };
                cookie?: never;
            };
            requestBody: {
                content: {
                    "application/json": components["schemas"]["UserUpdate"];
                };
            };
            responses: {
                /** @description Updated user record */
                200: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": components["schemas"]["User"];
                    };
                };
                400: components["responses"]["BadRequest"];
                401: components["responses"]["Unauthorized"];
                403: components["responses"]["Forbidden"];
                404: components["responses"]["NotFound"];
            };
        };
        post?: never;
        /**
         * Delete user
         * @description Soft-delete a user record.
         */
        delete: {
            parameters: {
                query?: never;
                header: {
                    /** @description Authorization Header */
                    Authorization: string;
                    /** @description Application version */
                    "Application-Version": string;
                    /** @description Identifier of something */
                    "Something-Id": string;
                };
                path: {
                    /** @description Unique user identifier (UUID) */
                    userId: components["parameters"]["userId"];
                };
                cookie?: never;
            };
            requestBody?: never;
            responses: {
                /** @description User deleted (no content) */
                204: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content?: never;
                };
                400: components["responses"]["BadRequest"];
                401: components["responses"]["Unauthorized"];
                403: components["responses"]["Forbidden"];
                404: components["responses"]["NotFound"];
            };
        };
        options?: never;
        head?: never;
        /**
         * Update user fields
         * @description Partially update a user's information.
         */
        patch: {
            parameters: {
                query?: never;
                header: {
                    /** @description Authorization Header */
                    Authorization: string;
                    /** @description Application version */
                    "Application-Version": string;
                    /** @description Identifier of something */
                    "Something-Id": string;
                };
                path: {
                    /** @description Unique user identifier (UUID) */
                    userId: components["parameters"]["userId"];
                };
                cookie?: never;
            };
            requestBody: {
                content: {
                    "application/json": components["schemas"]["UserPatch"];
                };
            };
            responses: {
                /** @description Updated user record */
                200: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": components["schemas"]["User"];
                    };
                };
                400: components["responses"]["BadRequest"];
                401: components["responses"]["Unauthorized"];
                403: components["responses"]["Forbidden"];
                404: components["responses"]["NotFound"];
            };
        };
        trace?: never;
    };
}
export type webhooks = Record<string, never>;
export interface components {
    schemas: {
        Error: {
            /** @description HTTP status code */
            code: number;
            /** @description Error message detailing the cause */
            message: string;
        };
        User: {
            /** Format: uuid */
            id: string;
            name: string;
            /** Format: email */
            email: string;
            /** @enum {string} */
            membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
            /** Format: date-time */
            registeredAt: string;
            address?: components["schemas"]["Address"];
        };
        Address: {
            postalCode?: string;
            street: string;
            city: string;
            country: string;
        };
        UserCreate: {
            name: string;
            /** Format: email */
            email: string;
            /** @enum {string} */
            membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
            address?: components["schemas"]["Address"];
        };
        UserUpdate: components["schemas"]["UserCreate"] & {
            /** Format: uuid */
            id: string;
        };
        /** @description Schema for partial updates – include only fields to change */
        UserPatch: {
            name?: string;
            /** Format: email */
            email?: string;
            /** @enum {string} */
            membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
            address?: components["schemas"]["Address"];
        };
    };
    responses: {
        /** @description Bad request due to invalid input */
        BadRequest: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/json": components["schemas"]["Error"];
            };
        };
        /** @description Authentication required or failed */
        Unauthorized: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/json": components["schemas"]["Error"];
            };
        };
        /** @description Insufficient permissions to access resource */
        Forbidden: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/json": components["schemas"]["Error"];
            };
        };
        /** @description Resource not found */
        NotFound: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/json": components["schemas"]["Error"];
            };
        };
        /** @description Conflict with current state of the resource */
        Conflict: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/json": components["schemas"]["Error"];
            };
        };
    };
    parameters: {
        /** @description Unique user identifier (UUID) */
        userId: string;
    };
    requestBodies: never;
    headers: never;
    pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;

これまでは、この生成されたスキーマファイルとopenapi-fetchを併用して手でAPIクライアントを書く必要がありましたが、openapi-fetch-genを使うとスキーマファイルを入力として以下のように実行することでAPIクライアントを自動生成することができます:

openapi-fetch-gen --input ./schema.d.ts --output ./generated_client.ts

generated_client.tsschema.d.tsを元に自動生成されたTypeScriptレベルでtype-safeなAPIクライアントです。一部を抽出すると以下のようなコードとなります:

import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript

export class Client {
  private readonly client;

  constructor(clientOptions: ClientOptions) {
    this.client = createClient<paths>(clientOptions);
  }

  ...

  /**
   * Replace user
   */
  async putUsersUserid(
    params: {
      header: {
        Authorization: string;
        "Application-Version": string;
        "Something-Id": string;
      };
      path: { userId: string };
    },
    body: {
      name: string;
      email: string;
      membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
      address?: {
        postalCode?: string;
        street: string;
        city: string;
        country: string;
      };
    } & { id: string },
  ) {
    return await this.client.PUT("/users/{userId}", {
      params,
      body,
    });
  }
  ...
}


FYI: 生成されるAPIクライアント (フル)

// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen.
// DO NOT EDIT THIS FILE MANUALLY.
// See Also: https://github.com/moznion/openapi-fetch-gen
import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript

export class Client {
  private readonly client;

  constructor(clientOptions: ClientOptions) {
    this.client = createClient<paths>(clientOptions);
  }

  /**
   * Get user details
   */
  async getUsersUserid(params: {
    header: {
      Authorization: string;
      "Application-Version": string;
      "Something-Id": string;
    };
    path: { userId: string };
  }) {
    return await this.client.GET("/users/{userId}", {
      params,
    });
  }

  /**
   * Replace user
   */
  async putUsersUserid(
    params: {
      header: {
        Authorization: string;
        "Application-Version": string;
        "Something-Id": string;
      };
      path: { userId: string };
    },
    body: {
      name: string;
      email: string;
      membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
      address?: {
        postalCode?: string;
        street: string;
        city: string;
        country: string;
      };
    } & { id: string },
  ) {
    return await this.client.PUT("/users/{userId}", {
      params,
      body,
    });
  }

  /**
   * Delete user
   */
  async deleteUsersUserid(params: {
    header: {
      Authorization: string;
      "Application-Version": string;
      "Something-Id": string;
    };
    path: { userId: string };
  }) {
    return await this.client.DELETE("/users/{userId}", {
      params,
    });
  }

  /**
   * Update user fields
   */
  async patchUsersUserid(
    params: {
      header: {
        Authorization: string;
        "Application-Version": string;
        "Something-Id": string;
      };
      path: { userId: string };
    },
    body: {
      name?: string;
      email?: string;
      membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
      address?: {
        postalCode?: string;
        street: string;
        city: string;
        country: string;
      };
    },
  ) {
    return await this.client.PATCH("/users/{userId}", {
      params,
      body,
    });
  }
}

なかなか便利なのではないでしょうか。

中身としてはどうなっているのか

TypeScript Compiler APIdsherret/ts-morphライブラリを介して利用することで d.ts ファイルを解釈し、その内容・構造に基いてAPIクライアントコードを生成しています。また、上記の例のように生成されるAPIクライアントのコードの中ではopenapi-fetchが用いられています。

まとめ

というわけで、openapi-typescriptが出力するTypeScriptのスキーマからTypeScriptのAPIクライアントを自動生成するツールであるopenapi-fetch-genのご紹介でした。どうぞご利用ください。

ところで、これに加えて "Default HTTP Header" 機能というものも用意しており、これは本来の生成クライアントコードが要求してくる「APIエンドポイントを呼び出すメソッドそれぞれに明示的にHTTP Headerを渡さなければならない *1」というものを省力化するものです。良くある例としては、 Authorization ヘッダなんかはほとんどのエンドポイントで共通して渡す必要があったりしますが、これをすべてのAPIメソッド呼び出しメソッドで渡さなければならないとなると面倒ですよね。というわけでopenapi-fetch-genではジェネリクスを活用することでデフォルトのヘッダを渡せるようにしており、加えてちょっとしたTypeScriptの型のテクニックを内部的に使うことで、デフォルトヘッダとして指定されたヘッダを各API呼び出しメソッドで省略できるようにしています。
興味のある方はDefault HTTP Headersのセクションをご参照ください。

*1:上記の生成コード例におけるメソッドの引数中のparams.headerがそれに当たります

GNU Goldのリンカがdeprecatedになる予定だそうなので暫定的にcgoでGNU ldの利用を明示する

www.phoronix.com

ということで、GNU Goldのリンカがdeprecatedになる予定だそうです。

arm64環境のgo *1 でcgoを利用しようとするとデフォルトでgoldのリンカを利用しようとするので、これをGNU ldを使うようにしたいという話です。かつてのGNU ldにはshow stopperとなるバグが存在していましたが、少なくともバージョン2.35以降のGNU ldであれば利用しても良い状況となっているという理解です。
この裏にあるテーマとしてはAmazon Linux 2023だとgoldのbinutilsがシュッと入らず、GNU ldはあるけどgold ldが無いとcgoを使うツールのビルドでfailするという問題があり……このためにいちいちfedoraのrepoを追加するのも面倒なので。

[FYI] goldのldが無いとこのようにコケる:

$ go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5
# github.com/golangci/golangci-lint/cmd/golangci-lint
/usr/local/go/pkg/tool/linux_arm64/link: running gcc failed: exit status 1
/usr/bin/gcc -Wl,-z,now -Wl,-z,nocopyreloc -fuse-ld=gold -Wl,--build-id=0x7c06cf55d9e31c7aa2dab2e997ce40f506533556 -o $WORK/b001/exe/a.out -rdynamic /tmp/go-link-748591831/go.o /tmp/go-link-748591831/000000.o /tmp/go-link-748591831/000001.o /tmp/go-link-748591831/000002.o /tmp/go-link-748591831/000003.o /tmp/go-link-748591831/000004.o /tmp/go-link-748591831/000005.o /tmp/go-link-748591831/000006.o /tmp/go-link-748591831/000007.o /tmp/go-link-748591831/000008.o /tmp/go-link-748591831/000009.o /tmp/go-link-748591831/000010.o /tmp/go-link-748591831/000011.o /tmp/go-link-748591831/000012.o /tmp/go-link-748591831/000013.o /tmp/go-link-748591831/000014.o /tmp/go-link-748591831/000015.o /tmp/go-link-748591831/000016.o /tmp/go-link-748591831/000017.o /tmp/go-link-748591831/000018.o /tmp/go-link-748591831/000019.o /tmp/go-link-748591831/000020.o /tmp/go-link-748591831/000021.o /tmp/go-link-748591831/000022.o /tmp/go-link-748591831/000023.o /tmp/go-link-748591831/000024.o -O2 -g -lresolv -O2 -g -ldl -O2 -g -lpthread -O2 -g
collect2: fatal error: cannot find 'ld'

というわけでGNU ldを使うようにするには、

  • -ldflags="-extldflags=-fuse-ld=bfd" をオプションとして指定する (e.g. go install -ldflags="-extldflags=-fuse-ld=bfd" github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5)
  • CGO_LDFLAGS="-fuse-ld=bfd"環境変数として指定する

という2つの方法があり、用途に応じて使い分けると良さそうに思いました。我々のユースケースではdocker containerとして環境を作っているのでcontainerの構築時に環境変数を指定する感じで運用することとしています。

なおこのあたりは処理系のほうでも議論が成されているようで、

github.com

https://go-review.googlesource.com/c/go/+/391115

このパッチが入るとこのようなワークアラウンドは不要になるのではないかと思います。

*1:もしかしたらarm64以外もそうかも?

所属変更のお知らせ

2024年6月1日より下記の通り所属が変更されます。

旧: SB Intuitions株式会社(ソフトバンク株式会社からの100%出向)
新: 株式会社スマートバンク

前回の所属変更からわずか2ヶ月しか経っておらず非常に気まずい状況ですが解雇ではありません。色々ありました。前職在職期間中、コードらしいコードは1行も書いていません。お察しください。

新しい環境であるところのスマートバンクでは今度こそプロダクト(B/43など)に根ざしたソフトウェアエンジニアとして活動する予定です。
奇しくも何の因果か、新しい会社も略称がSBです。面白いですね。

よろしくお願いします!