爆速で多言語SDKをリリースする「Stainless」の紹介
こちらはGaudiy アドベントカレンダー 202420 日目の記事です。社内のことを書こうと思ったのですが間に合わなかったので、今回は自分が調査したサービスについて紹介しようと思います。
概要
OpenAPI から数種類のプログラミング言語の SDK が生成できるStainlessの紹介と、実際にデモで使ってみた手順や感想を述べます。
導入
弊社のようなプラットフォームを提供する会社では、サービスの API や SDK を提供するケースは多いかと思います。しかし、多言語の SDK を開発・運用することは簡単ではありません。
例えば Node.js の SDK を作る必要が出てきたとき、必要なセットアップはいくつか存在します。
- コード
-
環境構築
- TypeScript 整備
- 使用するライブラリの選定
- ESlint や Prittier 等のツール
- npm package の設定
- CICD の設定
会社側の理想としては、できるだけ多くのプログラミング言語をサポートしたいと思いますが、言語が増えれば増えるほど専門的な知識が必要になるため、コストが膨らんでしまいます。
Stainless
そんな折にStainlessという多言語に対応した SDK 自動生成のサービスを見つけました。
簡単に説明すると、OpenAPI を読み込むことで多言語の SDK を開発・リリースまでできるエンタープライズ向けのサービスです。
特徴
- OpenAPI の定義 / SDK の実装 / リリースまで一括管理
- 言語特有のセットアップを Stainless 側が用意することで、開発者はコード開発に注力できる
- 生成コードの編集が可能
利用企業
現在、OpenAI や Cloudflare などが採用しています。(2024/12/19 現在)
対応言語
現状対応してるのは以下の 5 種類ですが、今後RubyやTerraformなども対応予定です。(2024/12/19 現在)
※Link 先は全て実際に使われている SDK です。
料金
Demo
公式が出しているデモ動画があるのでこちらを確認するのが、一番わかりやすいかと思います。
(下記で実際に使ってみたサンプルも載せます)
実際に使ってみた
ここでは、例として 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 用の Repository が https://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 への接続方法は以下を参照してください。
先ほど設定した 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 を使い、
- ディレクトリ構成の変更
- OpenAPI の編集
を行います。
1. ディレクトリ構成の変更
今回は、Root に作られた Go ファイルを区分分けしてディレクトリを作りたいと思います。
diff
生成されたコードをカスタマイズするには、Staging 用の Repository を Clone して、新たなブランチを作成し PR を出してマージします。
Staging マージ後、Production に「コンフリクトしたので直してくれ」って PR が来ました。直してリリースします。
2. OpenAPI の編集
ディレクトリ構成の変更後、OpenAPI を少し変えてみました。Age(年齢)を User に追加します。
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 を作れるようになったら最高だなと思います。
まとめ
多言語に渡る SDK の提供は、専門的知識や一貫性が必要であり、多くのメンテナンスコストがかかります。Stainless を使用することで、そのコストを大幅に削減し、高品質なプロダクト開発への注力ができるようになります。
今後サポートするプログラミング言語が増えた際には、新たな言語を SDK として提供する際にも PR 1 つ出すだけ で提供できるようになるかもしれません。
Gaudiy Advent Calendar 2024、次回の担当は Game Team の プロダクトマネージャー 「haseyan」 さんです!
Game Team の 1 年間を振り返ります。お楽しみに〜〜
Web3スタートアップ「Gaudiy(ガウディ)」所属エンジニアの個人発信をまとめたPublicationです。公式Tech Blog:techblog.gaudiy.com/
Discussion