Skip to content

Commit a889c3c

Browse files
committed
fix(parser): add format to deduplcation strategy
1 parent ab8cede commit a889c3c

File tree

8 files changed

+113
-25
lines changed

8 files changed

+113
-25
lines changed

.changeset/poor-stingrays-remember.md

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+
fix(parser): do not mark schemas as duplicate if they have different format

packages/openapi-ts-tests/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,10 @@ export const vApiVVersionODataControllerCountResponse = vModelFromZendesk;
10891089
/**
10901090
* foo in method
10911091
*/
1092-
export const vGetApiVbyApiVersionSimpleOperationParameterFooParam = v.string();
1092+
export const vGetApiVbyApiVersionSimpleOperationParameterFooParam = v.union([
1093+
v.string(),
1094+
v.pipe(v.string(), v.uuid())
1095+
]);
10931096

10941097
/**
10951098
* Response is a simple number

packages/openapi-ts-tests/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,10 @@ export const zGetApiVbyApiVersionSimpleOperationData = z.object({
11101110
body: z.never().optional(),
11111111
headers: z.never().optional(),
11121112
path: z.object({
1113-
foo_param: z.string()
1113+
foo_param: z.union([
1114+
z.string(),
1115+
z.string().uuid()
1116+
])
11141117
}),
11151118
query: z.never().optional()
11161119
});

packages/openapi-ts-tests/test/openapi-ts.config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default defineConfig(() => {
5757
// 'invalid',
5858
// 'servers-entry.yaml',
5959
// ),
60-
path: path.resolve(__dirname, 'spec', '3.1.x', 'full.yaml'),
60+
path: path.resolve(__dirname, 'spec', '3.1.x', 'validators.yaml'),
6161
// path: path.resolve(__dirname, 'spec', 'v3-transforms.json'),
6262
// path: 'http://localhost:4000/',
6363
// path: 'https://get.heyapi.dev/',
@@ -124,13 +124,13 @@ export default defineConfig(() => {
124124
// responseStyle: 'data',
125125
// throwOnError: true,
126126
// transformer: '@hey-api/transformers',
127-
transformer: true,
128-
validator: 'valibot',
127+
// transformer: true,
128+
validator: 'zod',
129129
},
130130
{
131131
// bigInt: true,
132132
dates: true,
133-
name: '@hey-api/transformers',
133+
// name: '@hey-api/transformers',
134134
},
135135
{
136136
enums: 'typescript+namespace',
@@ -152,14 +152,14 @@ export default defineConfig(() => {
152152
infiniteQueryKeyNameBuilder: '{{name}}IQK',
153153
infiniteQueryOptionsNameBuilder: '{{name}}InfiniteQuery',
154154
mutationOptionsNameBuilder: '{{name}}MutationOptions',
155-
name: '@tanstack/react-query',
155+
// name: '@tanstack/react-query',
156156
queryKeyNameBuilder: '{{name}}QK',
157157
queryOptionsNameBuilder: '{{name}}Query',
158158
},
159159
{
160160
// comments: false,
161161
// exportFromIndex: true,
162-
name: 'valibot',
162+
// name: 'valibot',
163163
},
164164
{
165165
// case: 'snake_case',
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { deduplicateSchema } from '../schema';
4+
import type { IR } from '../types';
5+
6+
describe('deduplicateSchema', () => {
7+
const scenarios: ReadonlyArray<{
8+
description: string;
9+
detectFormat?: boolean;
10+
result: IR.SchemaObject;
11+
schema: IR.SchemaObject;
12+
}> = [
13+
{
14+
description: 'keeps multiple strings if they have different formats',
15+
result: {
16+
items: [
17+
{
18+
format: 'uuid',
19+
type: 'string',
20+
},
21+
{
22+
type: 'string',
23+
},
24+
],
25+
logicalOperator: 'or',
26+
},
27+
schema: {
28+
items: [
29+
{
30+
format: 'uuid',
31+
type: 'string',
32+
},
33+
{
34+
type: 'string',
35+
},
36+
],
37+
logicalOperator: 'or',
38+
},
39+
},
40+
{
41+
description:
42+
'discards duplicate strings if they have different formats and `detectFormat` is `false`',
43+
detectFormat: false,
44+
result: {
45+
format: 'uuid',
46+
type: 'string',
47+
},
48+
schema: {
49+
items: [
50+
{
51+
format: 'uuid',
52+
type: 'string',
53+
},
54+
{
55+
type: 'string',
56+
},
57+
],
58+
logicalOperator: 'or',
59+
},
60+
},
61+
];
62+
63+
it.each(scenarios)('$description', ({ detectFormat, result, schema }) => {
64+
expect(deduplicateSchema({ detectFormat, schema })).toEqual(result);
65+
});
66+
});

packages/openapi-ts/src/ir/parser.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import type { IR } from './types';
77
export const parseIR = async ({ context }: { context: IR.Context }) => {
88
await context.broadcast('before');
99

10-
for (const server of context.ir.servers ?? []) {
11-
await context.broadcast('server', { server });
10+
if (context.ir.servers) {
11+
for (const server of context.ir.servers) {
12+
await context.broadcast('server', { server });
13+
}
1214
}
1315

1416
if (context.ir.components) {

packages/openapi-ts/src/ir/schema.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { IR } from './types';
44
* Ensure we don't produce redundant types, e.g. string | string.
55
*/
66
export const deduplicateSchema = <T extends IR.SchemaObject>({
7+
detectFormat = true,
78
schema,
89
}: {
10+
detectFormat?: boolean;
911
schema: T;
1012
}): T => {
1113
if (!schema.items) {
@@ -35,7 +37,12 @@ export const deduplicateSchema = <T extends IR.SchemaObject>({
3537
) {
3638
// const needs namespace to handle empty string values, otherwise
3739
// fallback would equal an actual value and we would skip an item
38-
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`;
40+
const constant = item.const !== undefined ? `const-${item.const}` : '';
41+
const format =
42+
item.format !== undefined && detectFormat
43+
? `format-${item.format}`
44+
: '';
45+
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${constant}${format}`;
3946
if (!typeIds.includes(typeId)) {
4047
typeIds.push(typeId);
4148
uniqueItems.push(item);
@@ -46,28 +53,29 @@ export const deduplicateSchema = <T extends IR.SchemaObject>({
4653
uniqueItems.push(item);
4754
}
4855

49-
schema.items = uniqueItems;
56+
let result = { ...schema };
57+
result.items = uniqueItems;
5058

5159
if (
52-
schema.items.length <= 1 &&
53-
schema.type !== 'array' &&
54-
schema.type !== 'enum' &&
55-
schema.type !== 'tuple'
60+
result.items.length <= 1 &&
61+
result.type !== 'array' &&
62+
result.type !== 'enum' &&
63+
result.type !== 'tuple'
5664
) {
5765
// bring the only item up to clean up the schema
58-
const liftedSchema = schema.items[0];
59-
delete schema.logicalOperator;
60-
delete schema.items;
61-
schema = {
62-
...schema,
66+
const liftedSchema = result.items[0];
67+
delete result.logicalOperator;
68+
delete result.items;
69+
result = {
70+
...result,
6371
...liftedSchema,
6472
};
6573
}
6674

6775
// exclude unknown if it's the only type left
68-
if (schema.type === 'unknown') {
76+
if (result.type === 'unknown') {
6977
return {} as T;
7078
}
7179

72-
return schema;
80+
return result;
7381
};

packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ const arrayTypeToIdentifier = ({
367367
);
368368
}
369369

370-
schema = deduplicateSchema({ schema });
370+
schema = deduplicateSchema({ detectFormat: false, schema });
371371

372372
const itemTypes: Array<ts.TypeNode> = [];
373373

@@ -859,6 +859,7 @@ const irParametersToIrSchema = ({
859859
const parameter = parameters[key]!;
860860

861861
properties[parameter.name] = deduplicateSchema({
862+
detectFormat: false,
862863
schema: parameter.schema,
863864
});
864865

@@ -1233,7 +1234,7 @@ export const schemaToType = ({
12331234
state,
12341235
});
12351236
} else if (schema.items) {
1236-
schema = deduplicateSchema({ schema });
1237+
schema = deduplicateSchema({ detectFormat: false, schema });
12371238
if (schema.items) {
12381239
const itemTypes: Array<ts.TypeNode> = [];
12391240

0 commit comments

Comments
 (0)