Skip to content

Commit 4c72db6

Browse files
authored
Merge pull request #2958 from SebastiaanWouters/feat/ky-client-plugin
feat: add ky client plugin
2 parents e5e00bf + 212fcaf commit 4c72db6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+6133
-210
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
**clients**: add support for Ky client

examples/openapi-ts-ky/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @example/openapi-ts-ky
2+
3+
## 0.0.1
4+
5+
### Patch Changes
6+
7+
- Initial release of ky client example

examples/openapi-ts-ky/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Hey API + Fetch API Demo</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineConfig } from '@hey-api/openapi-ts';
2+
3+
export default defineConfig({
4+
input:
5+
'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml',
6+
output: {
7+
format: 'prettier',
8+
lint: 'eslint',
9+
path: './src/client',
10+
},
11+
plugins: [
12+
'@hey-api/client-ky',
13+
'@hey-api/schemas',
14+
'@hey-api/sdk',
15+
{
16+
enums: 'javascript',
17+
name: '@hey-api/typescript',
18+
},
19+
],
20+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@example/openapi-ts-ky",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc && vite build",
8+
"dev": "vite",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10+
"openapi-ts": "openapi-ts",
11+
"preview": "vite preview",
12+
"typecheck": "tsc --noEmit"
13+
},
14+
"dependencies": {
15+
"@radix-ui/react-form": "0.1.1",
16+
"@radix-ui/react-icons": "1.3.2",
17+
"@radix-ui/themes": "3.1.6",
18+
"ky": "1.14.0",
19+
"react": "19.0.0",
20+
"react-dom": "19.0.0"
21+
},
22+
"devDependencies": {
23+
"@config/vite-base": "workspace:*",
24+
"@hey-api/openapi-ts": "workspace:*",
25+
"@types/react": "19.0.1",
26+
"@types/react-dom": "19.0.1",
27+
"@typescript-eslint/eslint-plugin": "8.29.1",
28+
"@typescript-eslint/parser": "8.29.1",
29+
"@vitejs/plugin-react": "4.4.0-beta.1",
30+
"autoprefixer": "10.4.19",
31+
"eslint": "9.17.0",
32+
"eslint-plugin-react-hooks": "5.2.0",
33+
"eslint-plugin-react-refresh": "0.4.7",
34+
"postcss": "8.4.41",
35+
"prettier": "3.4.2",
36+
"tailwindcss": "3.4.9",
37+
"typescript": "5.8.3",
38+
"vite": "7.1.2"
39+
}
40+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
autoprefixer: {},
4+
tailwindcss: {},
5+
},
6+
};

examples/openapi-ts-ky/src/App.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;

examples/openapi-ts-ky/src/App.tsx

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import './App.css';
2+
3+
import * as Form from '@radix-ui/react-form';
4+
import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons';
5+
import {
6+
Avatar,
7+
Box,
8+
Button,
9+
Card,
10+
Container,
11+
Flex,
12+
Heading,
13+
Section,
14+
Text,
15+
TextField,
16+
} from '@radix-ui/themes';
17+
import { useState } from 'react';
18+
19+
import { createClient } from './client/client';
20+
import { PetSchema } from './client/schemas.gen';
21+
import { addPet, getPetById, updatePet } from './client/sdk.gen';
22+
import type { Pet } from './client/types.gen';
23+
24+
const localClient = createClient({
25+
// set default base url for requests made by this client
26+
baseUrl: 'https://petstore3.swagger.io/api/v3',
27+
/**
28+
* Set default headers only for requests made by this client. This is to
29+
* demonstrate local clients and their configuration taking precedence over
30+
* internal service client.
31+
*/
32+
headers: {
33+
Authorization: 'Bearer <token_from_local_client>',
34+
},
35+
});
36+
37+
localClient.interceptors.request.use((request, options) => {
38+
// Middleware is great for adding authorization tokens to requests made to
39+
// protected paths. Headers are set randomly here to allow surfacing the
40+
// default headers, too.
41+
if (
42+
options.url === '/pet/{petId}' &&
43+
options.method === 'GET' &&
44+
Math.random() < 0.5
45+
) {
46+
request.headers.set('Authorization', 'Bearer <token_from_interceptor>');
47+
}
48+
return request;
49+
});
50+
51+
localClient.interceptors.error.use((error) => {
52+
console.log(error);
53+
return error;
54+
});
55+
56+
function App() {
57+
const [pet, setPet] = useState<Pet>();
58+
const [isRequiredNameError, setIsRequiredNameError] = useState(false);
59+
60+
const onAddPet = async (formData: FormData) => {
61+
// simple form field validation to demonstrate using schemas
62+
if (PetSchema.required.includes('name') && !formData.get('name')) {
63+
setIsRequiredNameError(true);
64+
return;
65+
}
66+
67+
const { data, error } = await addPet({
68+
body: {
69+
category: {
70+
id: 0,
71+
name: formData.get('category') as string,
72+
},
73+
id: 0,
74+
name: formData.get('name') as string,
75+
photoUrls: ['string'],
76+
status: 'available',
77+
tags: [
78+
{
79+
id: 0,
80+
name: 'string',
81+
},
82+
],
83+
},
84+
});
85+
if (error) {
86+
console.log(error);
87+
return;
88+
}
89+
setPet(data!);
90+
setIsRequiredNameError(false);
91+
};
92+
93+
const onGetPetById = async () => {
94+
const { data, error } = await getPetById({
95+
client: localClient,
96+
path: {
97+
// random id 1-10
98+
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
99+
},
100+
});
101+
if (error) {
102+
console.log(error);
103+
return;
104+
}
105+
setPet(data!);
106+
};
107+
108+
const onUpdatePet = async () => {
109+
const { data, error } = await updatePet({
110+
body: {
111+
category: {
112+
id: 0,
113+
name: 'Cats',
114+
},
115+
id: 2,
116+
name: 'Updated Kitty',
117+
photoUrls: ['string'],
118+
status: 'available',
119+
tags: [
120+
{
121+
id: 0,
122+
name: 'string',
123+
},
124+
],
125+
},
126+
// setting headers per request
127+
headers: {
128+
Authorization: 'Bearer <token_from_method>',
129+
},
130+
});
131+
if (error) {
132+
console.log(error);
133+
return;
134+
}
135+
setPet(data!);
136+
};
137+
138+
return (
139+
<Box
140+
style={{ background: 'var(--gray-a2)', borderRadius: 'var(--radius-3)' }}
141+
>
142+
<Container size="1">
143+
<Section size="1" />
144+
<Flex align="center">
145+
<a className="shrink-0" href="https://heyapi.dev/" target="_blank">
146+
<img
147+
src="https://heyapi.dev/logo.png"
148+
className="h-16 w-16 transition duration-300 will-change-auto"
149+
alt="Hey API logo"
150+
/>
151+
</a>
152+
<Heading>@hey-api/openapi-ts 🤝 Fetch API</Heading>
153+
</Flex>
154+
<Section size="1" />
155+
<Flex direction="column" gapY="2">
156+
<Box maxWidth="240px">
157+
<Card>
158+
<Flex gap="3" align="center">
159+
<Avatar
160+
size="3"
161+
src={pet?.photoUrls[0]}
162+
radius="full"
163+
fallback={pet?.name.slice(0, 1) ?? 'N'}
164+
/>
165+
<Box>
166+
<Text as="div" size="2" weight="bold">
167+
Name: {pet?.name ?? 'N/A'}
168+
</Text>
169+
<Text as="div" size="2" color="gray">
170+
Category: {pet?.category?.name ?? 'N/A'}
171+
</Text>
172+
</Box>
173+
</Flex>
174+
</Card>
175+
</Box>
176+
<Button onClick={onGetPetById}>
177+
<DownloadIcon /> Get Random Pet
178+
</Button>
179+
</Flex>
180+
<Section size="1" />
181+
<Flex direction="column" gapY="2">
182+
<Form.Root
183+
className="w-[260px]"
184+
onSubmit={(event) => {
185+
event.preventDefault();
186+
onAddPet(new FormData(event.currentTarget));
187+
}}
188+
>
189+
<Form.Field className="grid mb-[10px]" name="email">
190+
<div className="flex items-baseline justify-between">
191+
<Form.Label className="text-[15px] font-medium leading-[35px] text-white">
192+
Name
193+
</Form.Label>
194+
{isRequiredNameError && (
195+
<Form.Message className="text-[13px] text-white opacity-[0.8]">
196+
Please enter a name
197+
</Form.Message>
198+
)}
199+
</div>
200+
<Form.Control asChild>
201+
<TextField.Root placeholder="Kitty" name="name" type="text" />
202+
</Form.Control>
203+
</Form.Field>
204+
<Form.Field className="grid mb-[10px]" name="question">
205+
<div className="flex items-baseline justify-between">
206+
<Form.Label className="text-[15px] font-medium leading-[35px] text-white">
207+
Category
208+
</Form.Label>
209+
<Form.Message
210+
className="text-[13px] text-white opacity-[0.8]"
211+
match="valueMissing"
212+
>
213+
Please enter a category
214+
</Form.Message>
215+
</div>
216+
<Form.Control asChild>
217+
<TextField.Root
218+
placeholder="Cats"
219+
name="category"
220+
type="text"
221+
required
222+
/>
223+
</Form.Control>
224+
</Form.Field>
225+
<Flex gapX="2">
226+
<Form.Submit asChild>
227+
<Button type="submit">
228+
<PlusIcon /> Add Pet
229+
</Button>
230+
</Form.Submit>
231+
<Button onClick={onUpdatePet} type="button">
232+
<ReloadIcon /> Update Pet
233+
</Button>
234+
</Flex>
235+
</Form.Root>
236+
</Flex>
237+
<Section size="1" />
238+
</Container>
239+
</Box>
240+
);
241+
}
242+
243+
export default App;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
import {
4+
type ClientOptions,
5+
type Config,
6+
createClient,
7+
createConfig,
8+
} from './client';
9+
import type { ClientOptions as ClientOptions2 } from './types.gen';
10+
11+
/**
12+
* The `createClientConfig()` function will be called on client initialization
13+
* and the returned object will become the client's initial configuration.
14+
*
15+
* You may want to initialize your client this way instead of calling
16+
* `setConfig()`. This is useful for example if you're using Next.js
17+
* to ensure your client always has the correct values.
18+
*/
19+
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
20+
override?: Config<ClientOptions & T>,
21+
) => Config<Required<ClientOptions> & T>;
22+
23+
export const client = createClient(
24+
createConfig<ClientOptions2>({
25+
baseUrl: 'https://petstore3.swagger.io/api/v3',
26+
}),
27+
);

0 commit comments

Comments
 (0)