🎉

爆速で多言語SDKをリリースする「Stainless」の紹介

2024/12/20に公開

こちらはGaudiy アドベントカレンダー 202420 日目の記事です。社内のことを書こうと思ったのですが間に合わなかったので、今回は自分が調査したサービスについて紹介しようと思います。

概要

OpenAPI から数種類のプログラミング言語の SDK が生成できるStainlessの紹介と、実際にデモで使ってみた手順や感想を述べます。

導入

弊社のようなプラットフォームを提供する会社では、サービスの API や SDK を提供するケースは多いかと思います。しかし、多言語の SDK を開発・運用することは簡単ではありません。

例えば Node.js の SDK を作る必要が出てきたとき、必要なセットアップはいくつか存在します。

  • コード
  • 環境構築
    • TypeScript 整備
    • 使用するライブラリの選定
    • ESlint や Prittier 等のツール
  • npm package の設定
  • CICD の設定

会社側の理想としては、できるだけ多くのプログラミング言語をサポートしたいと思いますが、言語が増えれば増えるほど専門的な知識が必要になるため、コストが膨らんでしまいます。

Stainless

そんな折にStainlessという多言語に対応した SDK 自動生成のサービスを見つけました。
https://www.stainlessapi.com/

簡単に説明すると、OpenAPI を読み込むことで多言語の SDK を開発・リリースまでできるエンタープライズ向けのサービスです。

特徴

  • OpenAPI の定義 / SDK の実装 / リリースまで一括管理
  • 言語特有のセットアップを Stainless 側が用意することで、開発者はコード開発に注力できる
  • 生成コードの編集が可能

利用企業

現在、OpenAICloudflare などが採用しています。(2024/12/19 現在)
https://github.com/openai/openai-python
https://github.com/cloudflare/cloudflare-go/tree/v3

対応言語

現状対応してるのは以下の 5 種類ですが、今後RubyTerraformなども対応予定です。(2024/12/19 現在)

※Link 先は全て実際に使われている SDK です。

料金

https://www.stainlessapi.com/pricing

Demo

公式が出しているデモ動画があるのでこちらを確認するのが、一番わかりやすいかと思います。
(下記で実際に使ってみたサンプルも載せます)
https://www.youtube.com/watch?v=Vhs-vgw6yEI

実際に使ってみた

ここでは、例として Node.js SDK のリリースを行います。

アカウント / プロジェクト の作成

ログインページから Github を連携し、Account と Project を作成します。今回は、画面下にある 「Continue with an example spec」からデモ用の OpenAPI を使います。

openapi.yaml
openapi: 3.0.3
info:
  title: OpenAPI 3.0 Pet Store
  description: |-
    This is a sample Pet Store Server based on the OpenAPI 3.0 specification.
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.11
servers:
  - url: https://petstore3.swagger.io/api/v3
tags:
  - name: pet
    description: Everything about your Pets
  - name: store
    description: Access to Petstore orders
  - name: user
    description: Operations about user
paths:
  /pet:
    put:
      tags:
        - pet
      summary: Update an existing pet
      description: Update an existing pet by Id
      operationId: updatePet
      requestBody:
        description: Update an existent pet in the store
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
          application/xml:
            schema:
              $ref: "#/components/schemas/Pet"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/Pet"
        required: true
      responses:
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid ID supplied
        "404":
          description: Pet not found
        "422":
          description: Validation exception
    post:
      tags:
        - pet
      summary: Add a new pet to the store
      description: Add a new pet to the store
      operationId: addPet
      requestBody:
        description: Create a new pet in the store
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
          application/xml:
            schema:
              $ref: "#/components/schemas/Pet"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/Pet"
        required: true
      responses:
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid input
        "422":
          description: Validation exception
  /pet/findByStatus:
    get:
      tags:
        - pet
      summary: Finds Pets by status
      description: Multiple status values can be provided with comma separated strings
      operationId: findPetsByStatus
      parameters:
        - name: status
          in: query
          description: Status values that need to be considered for filter
          required: false
          explode: true
          schema:
            type: string
            default: available
            enum:
              - available
              - pending
              - sold
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid status value
  /pet/findByTags:
    get:
      tags:
        - pet
      summary: Finds Pets by tags
      description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
      operationId: findPetsByTags
      parameters:
        - name: tags
          in: query
          description: Tags to filter by
          required: false
          explode: true
          schema:
            type: array
            items:
              type: string
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid tag value
  /pet/{petId}:
    get:
      tags:
        - pet
      summary: Find pet by ID
      description: Returns a single pet
      operationId: getPetById
      parameters:
        - name: petId
          in: path
          description: ID of pet to return
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid ID supplied
        "404":
          description: Pet not found
      security:
        - api_key: []
    post:
      tags:
        - pet
      summary: Updates a pet in the store with form data
      description: ""
      operationId: updatePetWithForm
      parameters:
        - name: petId
          in: path
          description: ID of pet that needs to be updated
          required: true
          schema:
            type: integer
            format: int64
        - name: name
          in: query
          description: Name of pet that needs to be updated
          schema:
            type: string
        - name: status
          in: query
          description: Status of pet that needs to be updated
          schema:
            type: string
      responses:
        "400":
          description: Invalid input
    delete:
      tags:
        - pet
      summary: Deletes a pet
      description: delete a pet
      operationId: deletePet
      parameters:
        - name: api_key
          in: header
          description: ""
          required: false
          schema:
            type: string
        - name: petId
          in: path
          description: Pet id to delete
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "400":
          description: Invalid pet value
  /pet/{petId}/uploadImage:
    post:
      tags:
        - pet
      summary: uploads an image
      description: ""
      operationId: uploadFile
      parameters:
        - name: petId
          in: path
          description: ID of pet to update
          required: true
          schema:
            type: integer
            format: int64
        - name: additionalMetadata
          in: query
          description: Additional Metadata
          required: false
          schema:
            type: string
      requestBody:
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiResponse"
  /store/inventory:
    get:
      tags:
        - store
      summary: Returns pet inventories by status
      description: Returns a map of status codes to quantities
      operationId: getInventory
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
      security:
        - api_key: []
  /store/order:
    post:
      tags:
        - store
      summary: Place an order for a pet
      description: Place a new order in the store
      operationId: placeOrder
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Order"
          application/xml:
            schema:
              $ref: "#/components/schemas/Order"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/Order"
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          description: Invalid input
        "422":
          description: Validation exception
  /store/order/{orderId}:
    get:
      tags:
        - store
      summary: Find purchase order by ID
      description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
      operationId: getOrderById
      parameters:
        - name: orderId
          in: path
          description: ID of order that needs to be fetched
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
            application/xml:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          description: Invalid ID supplied
        "404":
          description: Order not found
    delete:
      tags:
        - store
      summary: Delete purchase order by ID
      description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
      operationId: deleteOrder
      parameters:
        - name: orderId
          in: path
          description: ID of the order that needs to be deleted
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "400":
          description: Invalid ID supplied
        "404":
          description: Order not found
  /user:
    post:
      tags:
        - user
      summary: Create user
      description: This can only be done by the logged in user.
      operationId: createUser
      requestBody:
        description: Created user object
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/User"
          application/xml:
            schema:
              $ref: "#/components/schemas/User"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/User"
      responses:
        default:
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
            application/xml:
              schema:
                $ref: "#/components/schemas/User"
  /user/createWithList:
    post:
      tags:
        - user
      summary: Creates list of users with given input array
      description: Creates list of users with given input array
      operationId: createUsersWithListInput
      requestBody:
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/User"
      responses:
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
            application/xml:
              schema:
                $ref: "#/components/schemas/User"
        default:
          description: successful operation
  /user/login:
    get:
      tags:
        - user
      summary: Logs user into the system
      description: ""
      operationId: loginUser
      parameters:
        - name: username
          in: query
          description: The user name for login
          required: false
          schema:
            type: string
        - name: password
          in: query
          description: The password for login in clear text
          required: false
          schema:
            type: string
      responses:
        "200":
          description: successful operation
          headers:
            X-Rate-Limit:
              description: calls per hour allowed by the user
              schema:
                type: integer
                format: int32
            X-Expires-After:
              description: date in UTC when token expires
              schema:
                type: string
                format: date-time
          content:
            application/xml:
              schema:
                type: string
            application/json:
              schema:
                type: string
        "400":
          description: Invalid username/password supplied
  /user/logout:
    get:
      tags:
        - user
      summary: Logs out current logged in user session
      description: ""
      operationId: logoutUser
      parameters: []
      responses:
        default:
          description: successful operation
  /user/{username}:
    get:
      tags:
        - user
      summary: Get user by user name
      description: ""
      operationId: getUserByName
      parameters:
        - name: username
          in: path
          description: "The name that needs to be fetched. Use user1 for testing. "
          required: true
          schema:
            type: string
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
            application/xml:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid username supplied
        "404":
          description: User not found
    put:
      tags:
        - user
      summary: Update user
      description: This can only be done by the logged in user.
      operationId: updateUser
      parameters:
        - name: username
          in: path
          description: The username that needs to be replaced
          required: true
          schema:
            x-stainless-param: existingUsername
            type: string
      requestBody:
        description: Update an existent user in the store
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/User"
          application/xml:
            schema:
              $ref: "#/components/schemas/User"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/User"
      responses:
        default:
          description: successful operation
    delete:
      tags:
        - user
      summary: Delete user
      description: This can only be done by the logged in user.
      operationId: deleteUser
      parameters:
        - name: username
          in: path
          description: The name that needs to be deleted
          required: true
          schema:
            type: string
      responses:
        "400":
          description: Invalid username supplied
        "404":
          description: User not found
components:
  schemas:
    Order:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        petId:
          type: integer
          format: int64
          example: 198772
        quantity:
          type: integer
          format: int32
          example: 7
        shipDate:
          type: string
          format: date-time
        status:
          type: string
          description: Order Status
          example: approved
          enum:
            - placed
            - approved
            - delivered
        complete:
          type: boolean
      xml:
        name: order
    Customer:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 100000
        username:
          type: string
          example: fehguy
        address:
          type: array
          xml:
            name: addresses
            wrapped: true
          items:
            $ref: "#/components/schemas/Address"
      xml:
        name: customer
    Address:
      type: object
      properties:
        street:
          type: string
          example: 437 Lytton
        city:
          type: string
          example: Palo Alto
        state:
          type: string
          example: CA
        zip:
          type: string
          example: "94301"
      xml:
        name: address
    Category:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1
        name:
          type: string
          example: Dogs
      xml:
        name: category
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        username:
          type: string
          example: theUser
        firstName:
          type: string
          example: John
        lastName:
          type: string
          example: James
        email:
          type: string
          example: john@email.com
        password:
          type: string
          example: "12345"
        phone:
          type: string
          example: "12345"
        userStatus:
          type: integer
          description: User Status
          format: int32
          example: 1
      xml:
        name: user
    Tag:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      xml:
        name: tag
    Pet:
      required:
        - name
        - photoUrls
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        name:
          type: string
          example: doggie
        category:
          $ref: "#/components/schemas/Category"
        photoUrls:
          type: array
          xml:
            wrapped: true
          items:
            type: string
            xml:
              name: photoUrl
        tags:
          type: array
          xml:
            wrapped: true
          items:
            $ref: "#/components/schemas/Tag"
        status:
          type: string
          description: pet status in the store
          enum:
            - available
            - pending
            - sold
      xml:
        name: pet
    ApiResponse:
      type: object
      properties:
        code:
          type: integer
          format: int32
        type:
          type: string
        message:
          type: string
      xml:
        name: "##default"
  requestBodies:
    Pet:
      description: Pet object that needs to be added to the store
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Pet"
        application/xml:
          schema:
            $ref: "#/components/schemas/Pet"
    UserArray:
      description: List of user object
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: "#/components/schemas/User"
  securitySchemes:
    api_key:
      type: apiKey
      name: api_key
      in: header
security:
  - api_key: []

Project を作成すると、Dashboard が表示される

Project が作られると、Staging 用の Repositoryhttps://github.com/stainless-sdks/stainless-sdks/${PROJECT_NAME} に作られます。

Staging Repository

生成コード

コードを見てみると、開発者が直接書いたかのようなシンプルなコードが生成されています。

pet.ts
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { APIResource } from "../resource";
import { isRequestOptions } from "../core";
import * as Core from "../core";
import { type BlobLike } from "../uploads";

export class Pets extends APIResource {
  /**
   * Add a new pet to the store
   */
  create(
    body: PetCreateParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<Pet> {
    return this._client.post("/pet", { body, ...options });
  }

  /**
   * Returns a single pet
   */
  retrieve(petId: number, options?: Core.RequestOptions): Core.APIPromise<Pet> {
    return this._client.get(`/pet/${petId}`, options);
  }

  /**
   * Update an existing pet by Id
   */
  update(
    body: PetUpdateParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<Pet> {
    return this._client.put("/pet", { body, ...options });
  }

  /**
   * delete a pet
   */
  delete(petId: number, options?: Core.RequestOptions): Core.APIPromise<void> {
    return this._client.delete(`/pet/${petId}`, {
      ...options,
      headers: { Accept: "*/*", ...options?.headers },
    });
  }

  /**
   * Multiple status values can be provided with comma separated strings
   */
  findByStatus(
    query?: PetFindByStatusParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByStatusResponse>;
  findByStatus(
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByStatusResponse>;
  findByStatus(
    query: PetFindByStatusParams | Core.RequestOptions = {},
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByStatusResponse> {
    if (isRequestOptions(query)) {
      return this.findByStatus({}, query);
    }
    return this._client.get("/pet/findByStatus", { query, ...options });
  }

  /**
   * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3
   * for testing.
   */
  findByTags(
    query?: PetFindByTagsParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByTagsResponse>;
  findByTags(
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByTagsResponse>;
  findByTags(
    query: PetFindByTagsParams | Core.RequestOptions = {},
    options?: Core.RequestOptions
  ): Core.APIPromise<PetFindByTagsResponse> {
    if (isRequestOptions(query)) {
      return this.findByTags({}, query);
    }
    return this._client.get("/pet/findByTags", { query, ...options });
  }

  /**
   * Updates a pet in the store with form data
   */
  updateById(
    petId: number,
    params?: PetUpdateByIDParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<void>;
  updateById(
    petId: number,
    options?: Core.RequestOptions
  ): Core.APIPromise<void>;
  updateById(
    petId: number,
    params: PetUpdateByIDParams | Core.RequestOptions = {},
    options?: Core.RequestOptions
  ): Core.APIPromise<void> {
    if (isRequestOptions(params)) {
      return this.updateById(petId, {}, params);
    }
    const { name, status } = params;
    return this._client.post(`/pet/${petId}`, {
      query: { name, status },
      ...options,
      headers: { Accept: "*/*", ...options?.headers },
    });
  }

  /**
   * uploads an image
   */
  uploadImage(
    petId: number,
    params: PetUploadImageParams,
    options?: Core.RequestOptions
  ): Core.APIPromise<APIResponse> {
    const { image, additionalMetadata } = params;
    return this._client.post(`/pet/${petId}/uploadImage`, {
      query: { additionalMetadata },
      body: image,
      ...options,
      headers: {
        "Content-Type": "application/octet-stream",
        ...options?.headers,
      },
      __binaryRequest: true,
    });
  }
}

export interface APIResponse {
  code?: number;

  message?: string;

  type?: string;
}

export interface Category {
  id?: number;

  name?: string;
}

export interface Pet {
  name: string;

  photoUrls: Array<string>;

  id?: number;

  category?: Category;

  /**
   * pet status in the store
   */
  status?: "available" | "pending" | "sold";

  tags?: Array<Pet.Tag>;
}

export namespace Pet {
  export interface Tag {
    id?: number;

    name?: string;
  }
}

export type PetFindByStatusResponse = Array<Pet>;

export type PetFindByTagsResponse = Array<Pet>;

export interface PetCreateParams {
  name: string;

  photoUrls: Array<string>;

  id?: number;

  category?: Category;

  /**
   * pet status in the store
   */
  status?: "available" | "pending" | "sold";

  tags?: Array<PetCreateParams.Tag>;
}

export namespace PetCreateParams {
  export interface Tag {
    id?: number;

    name?: string;
  }
}

export interface PetUpdateParams {
  name: string;

  photoUrls: Array<string>;

  id?: number;

  category?: Category;

  /**
   * pet status in the store
   */
  status?: "available" | "pending" | "sold";

  tags?: Array<PetUpdateParams.Tag>;
}

export namespace PetUpdateParams {
  export interface Tag {
    id?: number;

    name?: string;
  }
}

export interface PetFindByStatusParams {
  /**
   * Status values that need to be considered for filter
   */
  status?: "available" | "pending" | "sold";
}

export interface PetFindByTagsParams {
  /**
   * Tags to filter by
   */
  tags?: Array<string>;
}

export interface PetUpdateByIDParams {
  /**
   * Name of pet that needs to be updated
   */
  name?: string;

  /**
   * Status of pet that needs to be updated
   */
  status?: string;
}

export interface PetUploadImageParams {
  /**
   * Body param:
   */
  image: string | ArrayBufferView | ArrayBuffer | BlobLike;

  /**
   * Query param: Additional Metadata
   */
  additionalMetadata?: string;
}

export declare namespace Pets {
  export {
    type APIResponse as APIResponse,
    type Category as Category,
    type Pet as Pet,
    type PetFindByStatusResponse as PetFindByStatusResponse,
    type PetFindByTagsResponse as PetFindByTagsResponse,
    type PetCreateParams as PetCreateParams,
    type PetUpdateParams as PetUpdateParams,
    type PetFindByStatusParams as PetFindByStatusParams,
    type PetFindByTagsParams as PetFindByTagsParams,
    type PetUpdateByIDParams as PetUpdateByIDParams,
    type PetUploadImageParams as PetUploadImageParams,
  };
}
pet.py
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

from __future__ import annotations

from typing import List, Iterable
from typing_extensions import Literal

import httpx

from ..types import (
    pet_create_params,
    pet_update_params,
    pet_find_by_tags_params,
    pet_update_by_id_params,
    pet_upload_image_params,
    pet_find_by_status_params,
)
from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes
from .._utils import (
    maybe_transform,
    async_maybe_transform,
)
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
    to_raw_response_wrapper,
    to_streamed_response_wrapper,
    async_to_raw_response_wrapper,
    async_to_streamed_response_wrapper,
)
from ..types.pet import Pet
from .._base_client import make_request_options
from ..types.api_response import APIResponse
from ..types.category_param import CategoryParam
from ..types.pet_find_by_tags_response import PetFindByTagsResponse
from ..types.pet_find_by_status_response import PetFindByStatusResponse

__all__ = ["PetsResource", "AsyncPetsResource"]


class PetsResource(SyncAPIResource):
    @cached_property
    def with_raw_response(self) -> PetsResourceWithRawResponse:
        """
        This property can be used as a prefix for any HTTP method call to return the
        the raw response object instead of the parsed content.

        For more information, see https://www.github.com/AndooBomber/example-stainless-py#accessing-raw-response-data-eg-headers
        """
        return PetsResourceWithRawResponse(self)

    @cached_property
    def with_streaming_response(self) -> PetsResourceWithStreamingResponse:
        """
        An alternative to `.with_raw_response` that doesn't eagerly read the response body.

        For more information, see https://www.github.com/AndooBomber/example-stainless-py#with_streaming_response
        """
        return PetsResourceWithStreamingResponse(self)

    def create(
        self,
        *,
        name: str,
        photo_urls: List[str],
        id: int | NotGiven = NOT_GIVEN,
        category: CategoryParam | NotGiven = NOT_GIVEN,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        tags: Iterable[pet_create_params.Tag] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Add a new pet to the store

        Args:
          status: pet status in the store

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._post(
            "/pet",
            body=maybe_transform(
                {
                    "name": name,
                    "photo_urls": photo_urls,
                    "id": id,
                    "category": category,
                    "status": status,
                    "tags": tags,
                },
                pet_create_params.PetCreateParams,
            ),
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    def retrieve(
        self,
        pet_id: int,
        *,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Returns a single pet

        Args:
          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._get(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    def update(
        self,
        *,
        name: str,
        photo_urls: List[str],
        id: int | NotGiven = NOT_GIVEN,
        category: CategoryParam | NotGiven = NOT_GIVEN,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        tags: Iterable[pet_update_params.Tag] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Update an existing pet by Id

        Args:
          status: pet status in the store

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._put(
            "/pet",
            body=maybe_transform(
                {
                    "name": name,
                    "photo_urls": photo_urls,
                    "id": id,
                    "category": category,
                    "status": status,
                    "tags": tags,
                },
                pet_update_params.PetUpdateParams,
            ),
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    def delete(
        self,
        pet_id: int,
        *,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> None:
        """
        delete a pet

        Args:
          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        extra_headers = {"Accept": "*/*", **(extra_headers or {})}
        return self._delete(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=NoneType,
        )

    def find_by_status(
        self,
        *,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> PetFindByStatusResponse:
        """
        Multiple status values can be provided with comma separated strings

        Args:
          status: Status values that need to be considered for filter

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._get(
            "/pet/findByStatus",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=maybe_transform({"status": status}, pet_find_by_status_params.PetFindByStatusParams),
            ),
            cast_to=PetFindByStatusResponse,
        )

    def find_by_tags(
        self,
        *,
        tags: List[str] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> PetFindByTagsResponse:
        """Multiple tags can be provided with comma separated strings.

        Use tag1, tag2, tag3
        for testing.

        Args:
          tags: Tags to filter by

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._get(
            "/pet/findByTags",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=maybe_transform({"tags": tags}, pet_find_by_tags_params.PetFindByTagsParams),
            ),
            cast_to=PetFindByTagsResponse,
        )

    def update_by_id(
        self,
        pet_id: int,
        *,
        name: str | NotGiven = NOT_GIVEN,
        status: str | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> None:
        """
        Updates a pet in the store with form data

        Args:
          name: Name of pet that needs to be updated

          status: Status of pet that needs to be updated

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        extra_headers = {"Accept": "*/*", **(extra_headers or {})}
        return self._post(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=maybe_transform(
                    {
                        "name": name,
                        "status": status,
                    },
                    pet_update_by_id_params.PetUpdateByIDParams,
                ),
            ),
            cast_to=NoneType,
        )

    def upload_image(
        self,
        pet_id: int,
        *,
        image: FileTypes,
        additional_metadata: str | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> APIResponse:
        """
        uploads an image

        Args:
          additional_metadata: Additional Metadata

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return self._post(
            f"/pet/{pet_id}/uploadImage",
            body=maybe_transform(image, pet_upload_image_params.PetUploadImageParams),
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=maybe_transform(
                    {"additional_metadata": additional_metadata}, pet_upload_image_params.PetUploadImageParams
                ),
            ),
            cast_to=APIResponse,
        )


class AsyncPetsResource(AsyncAPIResource):
    @cached_property
    def with_raw_response(self) -> AsyncPetsResourceWithRawResponse:
        """
        This property can be used as a prefix for any HTTP method call to return the
        the raw response object instead of the parsed content.

        For more information, see https://www.github.com/AndooBomber/example-stainless-py#accessing-raw-response-data-eg-headers
        """
        return AsyncPetsResourceWithRawResponse(self)

    @cached_property
    def with_streaming_response(self) -> AsyncPetsResourceWithStreamingResponse:
        """
        An alternative to `.with_raw_response` that doesn't eagerly read the response body.

        For more information, see https://www.github.com/AndooBomber/example-stainless-py#with_streaming_response
        """
        return AsyncPetsResourceWithStreamingResponse(self)

    async def create(
        self,
        *,
        name: str,
        photo_urls: List[str],
        id: int | NotGiven = NOT_GIVEN,
        category: CategoryParam | NotGiven = NOT_GIVEN,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        tags: Iterable[pet_create_params.Tag] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Add a new pet to the store

        Args:
          status: pet status in the store

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._post(
            "/pet",
            body=await async_maybe_transform(
                {
                    "name": name,
                    "photo_urls": photo_urls,
                    "id": id,
                    "category": category,
                    "status": status,
                    "tags": tags,
                },
                pet_create_params.PetCreateParams,
            ),
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    async def retrieve(
        self,
        pet_id: int,
        *,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Returns a single pet

        Args:
          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._get(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    async def update(
        self,
        *,
        name: str,
        photo_urls: List[str],
        id: int | NotGiven = NOT_GIVEN,
        category: CategoryParam | NotGiven = NOT_GIVEN,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        tags: Iterable[pet_update_params.Tag] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> Pet:
        """
        Update an existing pet by Id

        Args:
          status: pet status in the store

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._put(
            "/pet",
            body=await async_maybe_transform(
                {
                    "name": name,
                    "photo_urls": photo_urls,
                    "id": id,
                    "category": category,
                    "status": status,
                    "tags": tags,
                },
                pet_update_params.PetUpdateParams,
            ),
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=Pet,
        )

    async def delete(
        self,
        pet_id: int,
        *,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> None:
        """
        delete a pet

        Args:
          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        extra_headers = {"Accept": "*/*", **(extra_headers or {})}
        return await self._delete(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
            ),
            cast_to=NoneType,
        )

    async def find_by_status(
        self,
        *,
        status: Literal["available", "pending", "sold"] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> PetFindByStatusResponse:
        """
        Multiple status values can be provided with comma separated strings

        Args:
          status: Status values that need to be considered for filter

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._get(
            "/pet/findByStatus",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=await async_maybe_transform({"status": status}, pet_find_by_status_params.PetFindByStatusParams),
            ),
            cast_to=PetFindByStatusResponse,
        )

    async def find_by_tags(
        self,
        *,
        tags: List[str] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> PetFindByTagsResponse:
        """Multiple tags can be provided with comma separated strings.

        Use tag1, tag2, tag3
        for testing.

        Args:
          tags: Tags to filter by

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._get(
            "/pet/findByTags",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=await async_maybe_transform({"tags": tags}, pet_find_by_tags_params.PetFindByTagsParams),
            ),
            cast_to=PetFindByTagsResponse,
        )

    async def update_by_id(
        self,
        pet_id: int,
        *,
        name: str | NotGiven = NOT_GIVEN,
        status: str | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> None:
        """
        Updates a pet in the store with form data

        Args:
          name: Name of pet that needs to be updated

          status: Status of pet that needs to be updated

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        extra_headers = {"Accept": "*/*", **(extra_headers or {})}
        return await self._post(
            f"/pet/{pet_id}",
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=await async_maybe_transform(
                    {
                        "name": name,
                        "status": status,
                    },
                    pet_update_by_id_params.PetUpdateByIDParams,
                ),
            ),
            cast_to=NoneType,
        )

    async def upload_image(
        self,
        pet_id: int,
        *,
        image: FileTypes,
        additional_metadata: str | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
    ) -> APIResponse:
        """
        uploads an image

        Args:
          additional_metadata: Additional Metadata

          extra_headers: Send extra headers

          extra_query: Add additional query parameters to the request

          extra_body: Add additional JSON properties to the request

          timeout: Override the client-level default timeout for this request, in seconds
        """
        return await self._post(
            f"/pet/{pet_id}/uploadImage",
            body=await async_maybe_transform(image, pet_upload_image_params.PetUploadImageParams),
            options=make_request_options(
                extra_headers=extra_headers,
                extra_query=extra_query,
                extra_body=extra_body,
                timeout=timeout,
                query=await async_maybe_transform(
                    {"additional_metadata": additional_metadata}, pet_upload_image_params.PetUploadImageParams
                ),
            ),
            cast_to=APIResponse,
        )


class PetsResourceWithRawResponse:
    def __init__(self, pets: PetsResource) -> None:
        self._pets = pets

        self.create = to_raw_response_wrapper(
            pets.create,
        )
        self.retrieve = to_raw_response_wrapper(
            pets.retrieve,
        )
        self.update = to_raw_response_wrapper(
            pets.update,
        )
        self.delete = to_raw_response_wrapper(
            pets.delete,
        )
        self.find_by_status = to_raw_response_wrapper(
            pets.find_by_status,
        )
        self.find_by_tags = to_raw_response_wrapper(
            pets.find_by_tags,
        )
        self.update_by_id = to_raw_response_wrapper(
            pets.update_by_id,
        )
        self.upload_image = to_raw_response_wrapper(
            pets.upload_image,
        )


class AsyncPetsResourceWithRawResponse:
    def __init__(self, pets: AsyncPetsResource) -> None:
        self._pets = pets

        self.create = async_to_raw_response_wrapper(
            pets.create,
        )
        self.retrieve = async_to_raw_response_wrapper(
            pets.retrieve,
        )
        self.update = async_to_raw_response_wrapper(
            pets.update,
        )
        self.delete = async_to_raw_response_wrapper(
            pets.delete,
        )
        self.find_by_status = async_to_raw_response_wrapper(
            pets.find_by_status,
        )
        self.find_by_tags = async_to_raw_response_wrapper(
            pets.find_by_tags,
        )
        self.update_by_id = async_to_raw_response_wrapper(
            pets.update_by_id,
        )
        self.upload_image = async_to_raw_response_wrapper(
            pets.upload_image,
        )


class PetsResourceWithStreamingResponse:
    def __init__(self, pets: PetsResource) -> None:
        self._pets = pets

        self.create = to_streamed_response_wrapper(
            pets.create,
        )
        self.retrieve = to_streamed_response_wrapper(
            pets.retrieve,
        )
        self.update = to_streamed_response_wrapper(
            pets.update,
        )
        self.delete = to_streamed_response_wrapper(
            pets.delete,
        )
        self.find_by_status = to_streamed_response_wrapper(
            pets.find_by_status,
        )
        self.find_by_tags = to_streamed_response_wrapper(
            pets.find_by_tags,
        )
        self.update_by_id = to_streamed_response_wrapper(
            pets.update_by_id,
        )
        self.upload_image = to_streamed_response_wrapper(
            pets.upload_image,
        )


class AsyncPetsResourceWithStreamingResponse:
    def __init__(self, pets: AsyncPetsResource) -> None:
        self._pets = pets

        self.create = async_to_streamed_response_wrapper(
            pets.create,
        )
        self.retrieve = async_to_streamed_response_wrapper(
            pets.retrieve,
        )
        self.update = async_to_streamed_response_wrapper(
            pets.update,
        )
        self.delete = async_to_streamed_response_wrapper(
            pets.delete,
        )
        self.find_by_status = async_to_streamed_response_wrapper(
            pets.find_by_status,
        )
        self.find_by_tags = async_to_streamed_response_wrapper(
            pets.find_by_tags,
        )
        self.update_by_id = async_to_streamed_response_wrapper(
            pets.update_by_id,
        )
        self.upload_image = async_to_streamed_response_wrapper(
            pets.upload_image,
        )
pet.go
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

package pet

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"

	"github.com/AndooBomber/example-stainless-go/internal/apiform"
	"github.com/AndooBomber/example-stainless-go/internal/apijson"
	"github.com/AndooBomber/example-stainless-go/internal/apiquery"
	"github.com/AndooBomber/example-stainless-go/internal/param"
	"github.com/AndooBomber/example-stainless-go/internal/requestconfig"
	"github.com/AndooBomber/example-stainless-go/option"
)

// PetService contains methods and other services that help with interacting with
// the petstore API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewPetService] method instead.
type PetService struct {
	Options []option.RequestOption
}

// NewPetService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewPetService(opts ...option.RequestOption) (r *PetService) {
	r = &PetService{}
	r.Options = opts
	return
}

// Add a new pet to the store
func (r *PetService) New(ctx context.Context, body PetNewParams, opts ...option.RequestOption) (res *Pet, err error) {
	opts = append(r.Options[:], opts...)
	path := "pet"
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
	return
}

// Returns a single pet
func (r *PetService) Get(ctx context.Context, petID int64, opts ...option.RequestOption) (res *Pet, err error) {
	opts = append(r.Options[:], opts...)
	path := fmt.Sprintf("pet/%v", petID)
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
	return
}

// Update an existing pet by Id
func (r *PetService) Update(ctx context.Context, body PetUpdateParams, opts ...option.RequestOption) (res *Pet, err error) {
	opts = append(r.Options[:], opts...)
	path := "pet"
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...)
	return
}

// delete a pet
func (r *PetService) Delete(ctx context.Context, petID int64, opts ...option.RequestOption) (err error) {
	opts = append(r.Options[:], opts...)
	opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...)
	path := fmt.Sprintf("pet/%v", petID)
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
	return
}

// Multiple status values can be provided with comma separated strings
func (r *PetService) FindByStatus(ctx context.Context, query PetFindByStatusParams, opts ...option.RequestOption) (res *[]Pet, err error) {
	opts = append(r.Options[:], opts...)
	path := "pet/findByStatus"
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
	return
}

// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3
// for testing.
func (r *PetService) FindByTags(ctx context.Context, query PetFindByTagsParams, opts ...option.RequestOption) (res *[]Pet, err error) {
	opts = append(r.Options[:], opts...)
	path := "pet/findByTags"
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
	return
}

// Updates a pet in the store with form data
func (r *PetService) UpdateByID(ctx context.Context, petID int64, body PetUpdateByIDParams, opts ...option.RequestOption) (err error) {
	opts = append(r.Options[:], opts...)
	opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...)
	path := fmt.Sprintf("pet/%v", petID)
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, nil, opts...)
	return
}

// uploads an image
func (r *PetService) UploadImage(ctx context.Context, petID int64, params PetUploadImageParams, opts ...option.RequestOption) (res *APIResponse, err error) {
	opts = append(r.Options[:], opts...)
	path := fmt.Sprintf("pet/%v/uploadImage", petID)
	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...)
	return
}

type APIResponse struct {
	Code    int64           `json:"code"`
	Message string          `json:"message"`
	Type    string          `json:"type"`
	JSON    apiResponseJSON `json:"-"`
}

// apiResponseJSON contains the JSON metadata for the struct [APIResponse]
type apiResponseJSON struct {
	Code        apijson.Field
	Message     apijson.Field
	Type        apijson.Field
	raw         string
	ExtraFields map[string]apijson.Field
}

func (r *APIResponse) UnmarshalJSON(data []byte) (err error) {
	return apijson.UnmarshalRoot(data, r)
}

func (r apiResponseJSON) RawJSON() string {
	return r.raw
}

type Category struct {
	ID   int64        `json:"id"`
	Name string       `json:"name"`
	JSON categoryJSON `json:"-"`
}

// categoryJSON contains the JSON metadata for the struct [Category]
type categoryJSON struct {
	ID          apijson.Field
	Name        apijson.Field
	raw         string
	ExtraFields map[string]apijson.Field
}

func (r *Category) UnmarshalJSON(data []byte) (err error) {
	return apijson.UnmarshalRoot(data, r)
}

func (r categoryJSON) RawJSON() string {
	return r.raw
}

type CategoryParam struct {
	ID   param.Field[int64]  `json:"id"`
	Name param.Field[string] `json:"name"`
}

func (r CategoryParam) MarshalJSON() (data []byte, err error) {
	return apijson.MarshalRoot(r)
}

type Pet struct {
	Name      string   `json:"name,required"`
	PhotoURLs []string `json:"photoUrls,required"`
	ID        int64    `json:"id"`
	Category  Category `json:"category"`
	First     string   `json:"first"`
	// pet status in the store
	Status PetStatus `json:"status"`
	Tags   []PetTag  `json:"tags"`
	JSON   petJSON   `json:"-"`
}

// petJSON contains the JSON metadata for the struct [Pet]
type petJSON struct {
	Name        apijson.Field
	PhotoURLs   apijson.Field
	ID          apijson.Field
	Category    apijson.Field
	First       apijson.Field
	Status      apijson.Field
	Tags        apijson.Field
	raw         string
	ExtraFields map[string]apijson.Field
}

func (r *Pet) UnmarshalJSON(data []byte) (err error) {
	return apijson.UnmarshalRoot(data, r)
}

func (r petJSON) RawJSON() string {
	return r.raw
}

// pet status in the store
type PetStatus string

const (
	PetStatusAvailable PetStatus = "available"
	PetStatusPending   PetStatus = "pending"
	PetStatusSold      PetStatus = "sold"
)

func (r PetStatus) IsKnown() bool {
	switch r {
	case PetStatusAvailable, PetStatusPending, PetStatusSold:
		return true
	}
	return false
}

type PetTag struct {
	ID   int64      `json:"id"`
	Name string     `json:"name"`
	JSON petTagJSON `json:"-"`
}

// petTagJSON contains the JSON metadata for the struct [PetTag]
type petTagJSON struct {
	ID          apijson.Field
	Name        apijson.Field
	raw         string
	ExtraFields map[string]apijson.Field
}

func (r *PetTag) UnmarshalJSON(data []byte) (err error) {
	return apijson.UnmarshalRoot(data, r)
}

func (r petTagJSON) RawJSON() string {
	return r.raw
}

type PetParam struct {
	Name      param.Field[string]        `json:"name,required"`
	PhotoURLs param.Field[[]string]      `json:"photoUrls,required"`
	ID        param.Field[int64]         `json:"id"`
	Category  param.Field[CategoryParam] `json:"category"`
	First     param.Field[string]        `json:"first"`
	// pet status in the store
	Status param.Field[PetStatus]     `json:"status"`
	Tags   param.Field[[]PetTagParam] `json:"tags"`
}

func (r PetParam) MarshalJSON() (data []byte, err error) {
	return apijson.MarshalRoot(r)
}

type PetTagParam struct {
	ID   param.Field[int64]  `json:"id"`
	Name param.Field[string] `json:"name"`
}

func (r PetTagParam) MarshalJSON() (data []byte, err error) {
	return apijson.MarshalRoot(r)
}

type PetNewParams struct {
	Pet PetParam `json:"pet,required"`
}

func (r PetNewParams) MarshalJSON() (data []byte, err error) {
	return apijson.MarshalRoot(r.Pet)
}

type PetUpdateParams struct {
	Pet PetParam `json:"pet,required"`
}

func (r PetUpdateParams) MarshalJSON() (data []byte, err error) {
	return apijson.MarshalRoot(r.Pet)
}

type PetFindByStatusParams struct {
	// Status values that need to be considered for filter
	Status param.Field[PetFindByStatusParamsStatus] `query:"status"`
}

// URLQuery serializes [PetFindByStatusParams]'s query parameters as `url.Values`.
func (r PetFindByStatusParams) URLQuery() (v url.Values) {
	return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
		ArrayFormat:  apiquery.ArrayQueryFormatComma,
		NestedFormat: apiquery.NestedQueryFormatBrackets,
	})
}

// Status values that need to be considered for filter
type PetFindByStatusParamsStatus string

const (
	PetFindByStatusParamsStatusAvailable PetFindByStatusParamsStatus = "available"
	PetFindByStatusParamsStatusPending   PetFindByStatusParamsStatus = "pending"
	PetFindByStatusParamsStatusSold      PetFindByStatusParamsStatus = "sold"
)

func (r PetFindByStatusParamsStatus) IsKnown() bool {
	switch r {
	case PetFindByStatusParamsStatusAvailable, PetFindByStatusParamsStatusPending, PetFindByStatusParamsStatusSold:
		return true
	}
	return false
}

type PetFindByTagsParams struct {
	// Tags to filter by
	Tags param.Field[[]string] `query:"tags"`
}

// URLQuery serializes [PetFindByTagsParams]'s query parameters as `url.Values`.
func (r PetFindByTagsParams) URLQuery() (v url.Values) {
	return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
		ArrayFormat:  apiquery.ArrayQueryFormatComma,
		NestedFormat: apiquery.NestedQueryFormatBrackets,
	})
}

type PetUpdateByIDParams struct {
	// Name of pet that needs to be updated
	Name param.Field[string] `query:"name"`
	// Status of pet that needs to be updated
	Status param.Field[string] `query:"status"`
}

// URLQuery serializes [PetUpdateByIDParams]'s query parameters as `url.Values`.
func (r PetUpdateByIDParams) URLQuery() (v url.Values) {
	return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
		ArrayFormat:  apiquery.ArrayQueryFormatComma,
		NestedFormat: apiquery.NestedQueryFormatBrackets,
	})
}

type PetUploadImageParams struct {
	Image io.Reader `json:"image,required" format:"binary"`
	// Additional Metadata
	AdditionalMetadata param.Field[string] `query:"additionalMetadata"`
}

func (r PetUploadImageParams) MarshalMultipart() (data []byte, contentType string, err error) {
	buf := bytes.NewBuffer(nil)
	writer := multipart.NewWriter(buf)
	err = apiform.MarshalRoot(r, writer)
	if err != nil {
		writer.Close()
		return nil, "", err
	}
	err = writer.Close()
	if err != nil {
		return nil, "", err
	}
	return buf.Bytes(), writer.FormDataContentType(), nil
}

// URLQuery serializes [PetUploadImageParams]'s query parameters as `url.Values`.
func (r PetUploadImageParams) URLQuery() (v url.Values) {
	return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
		ArrayFormat:  apiquery.ArrayQueryFormatComma,
		NestedFormat: apiquery.NestedQueryFormatBrackets,
	})
}

リリース

ダッシュボードの Release タブから Production 用の Repository を作成します。
(既に Repository が存在する場合は「Edit Repo Permissions」 から Github App をインストールし、Repository に権限を付与します。)

Repository 作成

連携がうまくいくと、Initial commit が作られます。
Production Repository

Release タブから NPM package の設定をします。
NPM Package 設定

Save Publishing Setup を押下し、npm credentials の設定を行います。各言語の Package Registry への接続方法は以下を参照してください。
https://app.stainlessapi.com/docs/guides/publish#connect-package-registries

先ほど設定した NPM package の設定を元に、リリースの PR が作られます。
リリース PR

PR をマージすると、Github Action が起動し npm への publish を行います。アカウント作成からここまで 10 分程度でした。めっちゃ簡単ですね。
npm

使用してみる

ローカルでnpm installしたところ、無事呼び出すことができました。VSCode の Quick Info も上手く動いています。

$ npm install fuando-example-stainless

VSCode

生成コードの編集

自動生成コードの懸念として、柔軟な変更が効かないことがあると思いますが、Stainless は生成されたコードをある程度カスタムすることができます。追加したカスタムコードは後の OpenAPI 更新時に上書きされることなく残ります。

今回は実験として Go SDK を使い、

  1. ディレクトリ構成の変更
  2. OpenAPI の編集

を行います。

1. ディレクトリ構成の変更

今回は、Root に作られた Go ファイルを区分分けしてディレクトリを作りたいと思います。


diff

生成されたコードをカスタマイズするには、Staging 用の Repository を Clone して、新たなブランチを作成し PR を出してマージします。
https://app.stainlessapi.com/docs/guides/patch-custom-code

Staging マージ後、Production に「コンフリクトしたので直してくれ」って PR が来ました。直してリリースします。

2. OpenAPI の編集

ディレクトリ構成の変更後、OpenAPI を少し変えてみました。Age(年齢)を User に追加します。

openapi.yaml
User:
  type: object
  properties:
+   age:
+     type: integer
+     format: int64
+     example: 20

成果物を PR で見ると、先ほどのディレクトリ構成の変更を踏まえた上でちゃんと user/user.go を編集してくれました。すごい。

感想

OSS の Code Generator とは違い、各言語のセットアップ高品質な SDK の設計を Stainless 側の専門エンジニアが用意してくれるので、開発者は SDK のメンテナンスに時間を取られることなくコア機能の開発に注力できるのが良いなと思いました。一貫性のある SDK を専門家がいなくても開発できるのが魅力的です。

また Code Generator としては、かなり柔軟な開発ができるなと思いました。標準でも高品質なコードですが Stainless config を使えば、SDK としての主要機能は大体実装できるし、独自にカスタムコードを追加することも可能です。
(※今回 PoC として調査しただけなので、実際どこまでフレキシブルな開発ができるのかは分かっておりません。)

今後期待する機能としては、OpenAPI をベースに Cloudflare のような Document を作れるようになったら最高だなと思います。
https://developers.cloudflare.com/api/

まとめ

多言語に渡る SDK の提供は、専門的知識や一貫性が必要であり、多くのメンテナンスコストがかかります。Stainless を使用することで、そのコストを大幅に削減し、高品質なプロダクト開発への注力ができるようになります。
今後サポートするプログラミング言語が増えた際には、新たな言語を SDK として提供する際にも PR 1 つ出すだけ で提供できるようになるかもしれません。

Gaudiy Advent Calendar 2024、次回の担当は Game Team の プロダクトマネージャー 「haseyan」 さんです!
Game Team の 1 年間を振り返ります。お楽しみに〜〜

https://adventar.org/calendars/10079

Gaudiy Engineers' Blog

Discussion