Skip to content

Commit

Permalink
chore: generate vercel api client
Browse files Browse the repository at this point in the history
  • Loading branch information
kahirokunn committed Nov 14, 2023
1 parent 2f54c46 commit 8c3f226
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 0 deletions.
224 changes: 224 additions & 0 deletions examples/generate-vercel-client/api-client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import fetch, { FetchError } from 'node-fetch'
import * as https from 'node:https'
import * as k8s from '@kubernetes/client-node'

function removeNullableProperties<T extends Record<string, unknown>>(
obj: T
): T {
Object.keys(obj).forEach(
(key) => (obj[key] === undefined || obj[key] === null) && delete obj[key]
)
return obj
}

/**
* Exponential backoff based on the attempt number.
*
* @remarks
* 1. 600ms * random(0.4, 1.4)
* 2. 1200ms * random(0.4, 1.4)
* 3. 2400ms * random(0.4, 1.4)
* 4. 4800ms * random(0.4, 1.4)
* 5. 9600ms * random(0.4, 1.4)
*
* @param attempt - Current attempt
* @param maxRetries - Maximum number of retries
*/
async function defaultBackoff(attempt: number, maxRetries: number) {
const attempts = Math.min(attempt, maxRetries)

const timeout = ~~((Math.random() + 0.4) * (300 << attempts))
await new Promise((resolve) =>
setTimeout((res: any) => resolve(res), timeout)
)
}

const isPlainObject = (value: any) => value?.constructor === Object

type QueryArgsSpec = {
path: string
method?: 'GET' | 'DELETE' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS' | 'HEAD'
body?: any
contentType?: string
params?: any
headers?: Record<string, string | number | undefined> | undefined
}

type MaybePromise<T> = T | Promise<T>

type InterceptorArgs = {
args: QueryArgsSpec
opts: https.RequestOptions
}
type Interceptor = (
args: InterceptorArgs,
options: Options
) => MaybePromise<https.RequestOptions>

const interceptors: Interceptor[] = []

type RetryConditionFunction = (extraArgs: {
res?: Response
error: unknown
args: QueryArgsSpec
attempt: number
options: RetryOptions
}) => boolean | Promise<boolean>

type RetryOptions = {
retryCondition?: RetryConditionFunction | undefined
maxRetries?: number | undefined
}

type HttpHeaderOptions = {
headers?: Record<string, string | number | undefined> | undefined
}

export type Options = RetryOptions & HttpHeaderOptions

export async function apiClient<Response>(
args: QueryArgsSpec,
extraOptions?: Options
): Promise<Response> {
const maxRetries = extraOptions?.maxRetries ?? 3

const defaultRetryCondition: RetryConditionFunction = ({ ...obj }) => {
const { res, attempt, error } = obj
if (attempt > maxRetries) {
return false
}

if (error instanceof FetchError) {
return true
}
if (res && res.status >= 500) {
return true
}
return false
}

const options = {
maxRetries,
backoff: defaultBackoff,
retryCondition: defaultRetryCondition,
...extraOptions,
}

let { path, method, params, body, contentType } = { ...args }

let httpsOptions: https.RequestOptions = {
path,
headers: removeNullableProperties({
...args.headers,
...options.headers,
}),
}
if (method) {
httpsOptions.method = method
}

for (const interceptor of interceptors) {
httpsOptions = await interceptor(
{
args,
opts: httpsOptions,
},
options
)
}

if (
!httpsOptions.agent &&
(httpsOptions.ca || httpsOptions.cert || httpsOptions.key)
) {
const agent = new https.Agent(
removeNullableProperties({
ca: httpsOptions.ca,
cert: httpsOptions.cert,
key: httpsOptions.key,
port: httpsOptions.port ? Number(httpsOptions.port) : undefined,
})
)
httpsOptions.agent = agent
}

if (!httpsOptions.protocol) {
httpsOptions.protocol = 'http:'
}
const host = httpsOptions.host || httpsOptions.hostname
let baseUrl = `${httpsOptions.protocol}//${host}`
const searchParams = toSearchParams(params)
if (searchParams.size) {
baseUrl += (baseUrl.includes('?') ? '&' : '?') + toSearchParams(params)
}
const url = new URL(baseUrl)
if (httpsOptions.port) {
url.port = httpsOptions.port.toString()
}
if (httpsOptions.path) {
url.pathname = httpsOptions.path
}
let isJson = false
if (isPlainObject(body) || Array.isArray(body)) {
isJson = true
body = JSON.stringify(body)
}
const headers: Record<string, string> = {
...(httpsOptions.headers as any),
}
if (contentType) {
headers['Content-Type'] = contentType
} else if (!httpsOptions.headers?.['Content-Type'] && isJson) {
headers['Content-Type'] = 'application/json'
}

let retry = 0
while (true) {
try {
const res = await fetch(url, {
headers,
protocol: httpsOptions.protocol || undefined,
method,
agent: httpsOptions.agent,
body,
})

const isSuccess = res.status >= 200 && res.status < 300
const contentType = res.headers.get('content-type')
const isJsonResponse = contentType?.includes('application/json') ?? false

if (isSuccess && isJsonResponse) {
return (await res.json()) as Response
}

// helpful message for debugging
const text = await res.text()
if (res.status === 404 && text.includes('404 page not found')) {
console.info(
`Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}`
)
}
throw new Error(text)
} catch (e: any) {
retry++

if (
!(await options.retryCondition({
res: e?.value?.res,
error: e,
args,
attempt: retry,
options: options,
}))
) {
throw e
}

await options.backoff(retry, options.maxRetries)
}
}
}

const toSearchParams = (params: Record<string, string>) => {
return new URLSearchParams(removeNullableProperties(params))
}
9 changes: 9 additions & 0 deletions examples/generate-vercel-client/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
update-swagger:
#!/bin/sh
# Swaggerが壊れています. 一時的に手動でパッチしています.
# https://github.com/orgs/vercel/discussions/4813#discussioncomment-7539757
wget -O swagger.json https://openapi.vercel.sh/

generate-code:
#!/bin/sh
npx kubernetes-typescript-client-codegen-openapi ./vercel.config.ts
10 changes: 10 additions & 0 deletions examples/generate-vercel-client/vercel.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ConfigFile } from 'kubernetes-typescript-client-codegen-openapi'

const config: ConfigFile = {
schemaFile: './swagger.json',
apiFile: './api-client/client.ts',
outputFile: './generated/vercel.ts',
strict: false,
}

export default config

0 comments on commit 8c3f226

Please sign in to comment.