Skip to content

Commit f0539e8

Browse files
committed
feat: 🎸 added sdk handling kebab-case paths, fixed cli params
1 parent b2aaa94 commit f0539e8

File tree

4 files changed

+98
-20
lines changed

4 files changed

+98
-20
lines changed

‎packages/cli/src/cli/index.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ const main = async () => {
3939
description: cmd.description(),
4040
})),
4141
});
42+
await program.parseAsync([process.argv[0], process.argv[1], chosenCommand]);
43+
} else {
44+
await program.parseAsync(process.argv);
4245
}
43-
44-
await program.parseAsync([process.argv[0], process.argv[1], chosenCommand]);
4546
} catch (e) {
4647
handleError(e);
4748
if (e instanceof Error) {

‎packages/cli/src/codegen/openapi/generator.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,34 @@ export class OpenapiRequestGenerator {
117117
let currentLevel = schemaTree;
118118
// eslint-disable-next-line no-restricted-syntax
119119
for (const segment of segments) {
120-
const key = segment.startsWith(":") ? `$${segment.slice(1)}` : segment;
120+
let key: string;
121+
if (segment.startsWith(":")) {
122+
// Parameter segment - keep the $ prefix
123+
key = `$${segment.slice(1)}`;
124+
} else if (segment.includes("-")) {
125+
// Convert kebab-case to camelCase for path segments
126+
key = segment.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
127+
} else {
128+
key = segment;
129+
}
130+
121131
if (!currentLevel[key]) {
122132
currentLevel[key] = {};
123133
}
124134
currentLevel = currentLevel[key];
125135
}
126-
currentLevel[method.toLowerCase()] = requestInstanceType;
136+
// Prefix method names with $
137+
currentLevel[`$${method.toLowerCase()}`] = requestInstanceType;
127138
});
128139

129140
const sdkSchema = `export type SdkSchema<Client extends ClientInstance> = {\n${formatSchema(schemaTree)}\n}`;
130141

131142
const createSdkFn = `
132-
export const createSdk = <Client extends ClientInstance>(client: Client) => {
133-
return coreCreateSdk<Client, SdkSchema<Client>>(client);
143+
144+
export type { Components };
145+
146+
export const createSdk = <Client extends ClientInstance>(client: Client, options?: Parameters<typeof coreCreateSdk>[1] | undefined) => {
147+
return coreCreateSdk<Client, SdkSchema<Client>>(client, options);
134148
};
135149
`;
136150

‎packages/core/__tests__/features/sdk/sdk.base.spec.ts‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,25 @@ type TestClient = Client<Error, HttpAdapterType>;
1111

1212
type TestSchema = {
1313
users: {
14-
get: RequestInstance<{
14+
$get: RequestInstance<{
1515
client: TestClient;
1616
queryParams: { page: number; limit: number };
1717
endpoint: "/users";
1818
}>;
1919
$userId: {
20-
get: RequestInstance<{
20+
$get: RequestInstance<{
2121
client: TestClient;
2222
response: { id: string; name: string };
2323
endpoint: "/users/:userId";
2424
}>;
25-
delete: RequestInstance<{
25+
$delete: RequestInstance<{
2626
client: TestClient;
2727
response: { id: string; name: string };
2828
endpoint: "/users/:userId";
2929
}>;
3030
posts: {
3131
$postId: {
32-
delete: RequestInstance<{
32+
$delete: RequestInstance<{
3333
client: TestClient;
3434
response: { id: string; title: string };
3535
endpoint: "/users/:userId/posts/:postId";
@@ -38,6 +38,12 @@ type TestSchema = {
3838
};
3939
};
4040
};
41+
acceptInvitation: {
42+
$post: RequestInstance<{
43+
client: TestClient;
44+
endpoint: "/accept-invitation";
45+
}>;
46+
};
4147
};
4248

4349
describe("SDK [ Base ]", () => {
@@ -60,7 +66,7 @@ describe("SDK [ Base ]", () => {
6066
const sdk = createSdk<TestClient, TestSchema>(client);
6167
expect(sdk).toBeDefined();
6268

63-
const request = sdk.users.$userId.get;
69+
const request = sdk.users.$userId.$get;
6470

6571
expect(request).toBeInstanceOf(Request);
6672
expect(request.method).toBe("GET");
@@ -71,7 +77,7 @@ describe("SDK [ Base ]", () => {
7177
const sdk = createSdk<TestClient, TestSchema>(client);
7278
expect(sdk).toBeDefined();
7379

74-
const request = sdk.users.$userId.posts.$postId.delete;
80+
const request = sdk.users.$userId.posts.$postId.$delete;
7581

7682
expect(request).toBeInstanceOf(Request);
7783
expect(request.method).toBe("DELETE");
@@ -82,7 +88,7 @@ describe("SDK [ Base ]", () => {
8288
const sdk = createSdk<TestClient, TestSchema>(client);
8389
expect(sdk).toBeDefined();
8490

85-
const request = sdk.users.$userId.get.setParams({ userId: "1" });
91+
const request = sdk.users.$userId.$get.setParams({ userId: "1" });
8692

8793
expect(request).toBeInstanceOf(Request);
8894
expect(request.endpoint).toBe("/users/1");
@@ -93,10 +99,32 @@ describe("SDK [ Base ]", () => {
9399
const sdk = createSdk<TestClient, TestSchema>(client);
94100
expect(sdk).toBeDefined();
95101

96-
const request = sdk.users.$userId.posts.$postId.delete.setParams({ userId: "1", postId: "2" });
102+
const request = sdk.users.$userId.posts.$postId.$delete.setParams({ userId: "1", postId: "2" });
97103

98104
expect(request).toBeInstanceOf(Request);
99105
expect(request.endpoint).toBe("/users/1/posts/2");
100106
expect(request.params).toEqual({ userId: "1", postId: "2" });
101107
});
108+
109+
it("should handle kebab-case to camelCase conversion", () => {
110+
const sdk = createSdk<TestClient, TestSchema>(client, { camelCaseToKebabCase: true });
111+
expect(sdk).toBeDefined();
112+
113+
const request = sdk.acceptInvitation.$post;
114+
115+
expect(request).toBeInstanceOf(Request);
116+
expect(request.method).toBe("POST");
117+
expect(request.endpoint).toBe("/accept-invitation");
118+
});
119+
120+
it("should not handle kebab-case to camelCase conversion", () => {
121+
const sdk = createSdk<TestClient, TestSchema>(client, { camelCaseToKebabCase: false });
122+
expect(sdk).toBeDefined();
123+
124+
const request = sdk.acceptInvitation.$post;
125+
126+
expect(request).toBeInstanceOf(Request);
127+
expect(request.method).toBe("POST");
128+
expect(request.endpoint).toBe("/acceptInvitation");
129+
});
102130
});

‎packages/core/src/sdk/sdk.ts‎

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,51 @@ export type RecursiveSchemaType = Record<
55
any
66
>;
77

8-
const createRecursiveProxy = (client: ClientInstance, path: string[]): any => {
8+
export type CreateSdkOptions = {
9+
/** @default true */
10+
camelCaseToKebabCase?: boolean;
11+
/** @default (method) => method.toUpperCase() */
12+
methodTransform?: (method: string) => string;
13+
};
14+
15+
const getMethod = (key: string, options?: CreateSdkOptions) => {
16+
const { methodTransform = (method: string) => method.toUpperCase() } = options ?? {};
17+
return methodTransform(key);
18+
};
19+
20+
const createRecursiveProxy = (client: ClientInstance, path: string[], options?: CreateSdkOptions): any => {
921
// eslint-disable-next-line @typescript-eslint/no-empty-function
1022
return new Proxy(() => {}, {
1123
get: (_target, key: string) => {
1224
if (typeof key === "symbol" || key === "inspect") {
1325
return undefined;
1426
}
1527

16-
// Assume the key is a method for the current path
28+
// Check if this is a method (starts with $) or a path segment
29+
let isMethod = false;
30+
let methodName = key;
31+
let pathSegment = key;
32+
33+
if (key.startsWith("$")) {
34+
// This could be either a method or a parameter
35+
// Try to determine by checking if it's a terminal access (method)
36+
// For now, assume it's a method and strip the $ prefix
37+
isMethod = true;
38+
methodName = key.slice(1);
39+
pathSegment = `:${key.slice(1)}`; // Convert to parameter format for path building
40+
} else if (options?.camelCaseToKebabCase) {
41+
// Convert camelCase to kebab-case for path segments if option is enabled
42+
pathSegment = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
43+
}
44+
45+
// Always create a request assuming this is a method call
1746
const endpoint = `/${path.join("/")}`;
18-
const method = key.toUpperCase();
47+
const method = getMethod(isMethod ? methodName : key, options);
1948
const request = client.createRequest()({ endpoint, method });
2049

2150
// But also, assume the key is a new path segment for a deeper call
22-
const newPath = [...path, key.startsWith("$") ? `:${key.slice(1)}` : key];
23-
const deeperProxy = createRecursiveProxy(client, newPath);
51+
const newPath = [...path, pathSegment];
52+
const deeperProxy = createRecursiveProxy(client, newPath, options);
2453

2554
// Return a new proxy that wraps both the request and the deeper proxy
2655
return new Proxy(request, {
@@ -39,6 +68,12 @@ const createRecursiveProxy = (client: ClientInstance, path: string[]): any => {
3968

4069
export const createSdk = <Client extends ClientInstance, RecursiveSchema extends RecursiveSchemaType>(
4170
client: Client,
71+
options?: CreateSdkOptions,
4272
): RecursiveSchema => {
43-
return createRecursiveProxy(client, []) as RecursiveSchema;
73+
const {
74+
camelCaseToKebabCase = true,
75+
methodTransform = (method: string) => method.toUpperCase(),
76+
...rest
77+
} = options ?? {};
78+
return createRecursiveProxy(client, [], { camelCaseToKebabCase, methodTransform, ...rest }) as RecursiveSchema;
4479
};

0 commit comments

Comments
 (0)