Skip to content

Commit 318175c

Browse files
authored
Merge pull request #1787 from hey-api/chore/config-platform
chore: add config options for platform api
2 parents 3a3ff61 + c422015 commit 318175c

File tree

9 files changed

+368
-53
lines changed

9 files changed

+368
-53
lines changed

.changeset/itchy-plums-explain.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: support Hey API platform input arguments

.changeset/ten-spiders-act.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: handle raw OpenAPI specification input
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { compileInputPath } from '../createClient';
4+
5+
describe('compileInputPath', () => {
6+
it('with raw OpenAPI specification', () => {
7+
const path = compileInputPath({
8+
path: {
9+
info: {
10+
version: '1.0.0',
11+
},
12+
openapi: '3.1.0',
13+
},
14+
});
15+
expect(path).toEqual({
16+
path: {
17+
info: {
18+
version: '1.0.0',
19+
},
20+
openapi: '3.1.0',
21+
},
22+
});
23+
});
24+
25+
it('with arbitrary string', () => {
26+
const path = compileInputPath({
27+
path: 'path/to/openapi.json',
28+
});
29+
expect(path).toEqual({
30+
path: 'path/to/openapi.json',
31+
});
32+
});
33+
34+
it('with platform string', () => {
35+
const path = compileInputPath({
36+
path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0',
37+
});
38+
expect(path).toEqual({
39+
branch: 'main',
40+
commit_sha: 'sha',
41+
organization: 'foo',
42+
path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0',
43+
project: 'bar',
44+
tags: ['a', 'b', 'c'],
45+
version: '1.0.0',
46+
});
47+
});
48+
49+
it('with platform arguments', () => {
50+
const path = compileInputPath({
51+
branch: 'main',
52+
commit_sha: 'sha',
53+
organization: 'foo',
54+
path: '',
55+
project: 'bar',
56+
tags: ['a', 'b', 'c'],
57+
version: '1.0.0',
58+
});
59+
expect(path).toEqual({
60+
branch: 'main',
61+
commit_sha: 'sha',
62+
organization: 'foo',
63+
path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0',
64+
project: 'bar',
65+
tags: ['a', 'b', 'c'],
66+
version: '1.0.0',
67+
});
68+
});
69+
70+
it('loads API key from HEY_API_TOKEN', () => {
71+
process.env.HEY_API_TOKEN = 'foo';
72+
const path = compileInputPath({
73+
path: 'https://get.heyapi.dev/foo/bar',
74+
});
75+
delete process.env.HEY_API_TOKEN;
76+
expect(path).toEqual({
77+
api_key: 'foo',
78+
organization: 'foo',
79+
path: 'https://get.heyapi.dev/foo/bar?api_key=foo',
80+
project: 'bar',
81+
});
82+
});
83+
});

packages/openapi-ts/src/index.test.ts renamed to packages/openapi-ts/src/__tests__/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it } from 'vitest';
22

3-
import { createClient } from './index';
3+
import { createClient } from '../index';
44

55
describe('index', () => {
66
it('parses v2 without issues', async () => {

packages/openapi-ts/src/createClient.ts

Lines changed: 153 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,150 @@ import type { Templates } from './utils/handlebars';
1313
import { Performance } from './utils/performance';
1414
import { postProcessClient } from './utils/postprocess';
1515

16+
const isPlatformPath = (path: string) =>
17+
path.startsWith('https://get.heyapi.dev');
18+
19+
export const compileInputPath = (input: Config['input']) => {
20+
const result: Pick<
21+
Partial<Config['input']>,
22+
| 'api_key'
23+
| 'branch'
24+
| 'commit_sha'
25+
| 'organization'
26+
| 'project'
27+
| 'tags'
28+
| 'version'
29+
> &
30+
Pick<Required<Config['input']>, 'path'> = {
31+
path: '',
32+
};
33+
34+
if (
35+
input.path &&
36+
(typeof input.path !== 'string' || !isPlatformPath(input.path))
37+
) {
38+
result.path = input.path;
39+
return result;
40+
}
41+
42+
const [basePath, baseQuery] = input.path.split('?');
43+
const queryParts = (baseQuery || '').split('&');
44+
const queryPath = queryParts.map((part) => part.split('='));
45+
46+
let path = basePath || '';
47+
if (path.endsWith('/')) {
48+
path = path.slice(0, path.length - 1);
49+
}
50+
51+
const [, pathUrl] = path.split('://');
52+
const [baseUrl, organization, project] = (pathUrl || '').split('/');
53+
result.organization = organization || input.organization;
54+
result.project = project || input.project;
55+
56+
const queryParams: Array<string> = [];
57+
58+
const kApiKey = 'api_key';
59+
result.api_key =
60+
queryPath.find(([key]) => key === kApiKey)?.[1] ||
61+
input.api_key ||
62+
process.env.HEY_API_TOKEN;
63+
if (result.api_key) {
64+
queryParams.push(`${kApiKey}=${result.api_key}`);
65+
}
66+
67+
const kBranch = 'branch';
68+
result.branch =
69+
queryPath.find(([key]) => key === kBranch)?.[1] || input.branch;
70+
if (result.branch) {
71+
queryParams.push(`${kBranch}=${result.branch}`);
72+
}
73+
74+
const kCommitSha = 'commit_sha';
75+
result.commit_sha =
76+
queryPath.find(([key]) => key === kCommitSha)?.[1] || input.commit_sha;
77+
if (result.commit_sha) {
78+
queryParams.push(`${kCommitSha}=${result.commit_sha}`);
79+
}
80+
81+
const kTags = 'tags';
82+
result.tags =
83+
queryPath.find(([key]) => key === kTags)?.[1]?.split(',') || input.tags;
84+
if (result.tags?.length) {
85+
queryParams.push(`${kTags}=${result.tags.join(',')}`);
86+
}
87+
88+
const kVersion = 'version';
89+
result.version =
90+
queryPath.find(([key]) => key === kVersion)?.[1] || input.version;
91+
if (result.version) {
92+
queryParams.push(`${kVersion}=${result.version}`);
93+
}
94+
95+
if (!result.organization) {
96+
throw new Error(
97+
'🚫 missing organization - from which Hey API platform organization do you want to generate your output?',
98+
);
99+
}
100+
101+
if (!result.project) {
102+
throw new Error(
103+
'🚫 missing project - from which Hey API platform project do you want to generate your output?',
104+
);
105+
}
106+
107+
const query = queryParams.join('&');
108+
const platformUrl = baseUrl || 'get.heyapi.dev';
109+
const compiledPath = `https://${[platformUrl, result.organization, result.project].join('/')}`;
110+
result.path = query ? `${compiledPath}?${query}` : compiledPath;
111+
112+
return result;
113+
};
114+
115+
const logInputPath = ({
116+
config,
117+
inputPath,
118+
watch,
119+
}: {
120+
config: Config;
121+
inputPath: ReturnType<typeof compileInputPath>;
122+
watch?: boolean;
123+
}) => {
124+
if (config.logs.level === 'silent') {
125+
return;
126+
}
127+
128+
if (watch) {
129+
console.clear();
130+
}
131+
132+
const baseString = watch
133+
? 'Input changed, generating from'
134+
: 'Generating from';
135+
136+
if (typeof inputPath.path === 'string') {
137+
const baseInput = isPlatformPath(inputPath.path)
138+
? `${inputPath.organization}/${inputPath.project}`
139+
: inputPath.path;
140+
console.log(`⏳ ${baseString} ${baseInput}`);
141+
if (isPlatformPath(inputPath.path)) {
142+
if (inputPath.branch) {
143+
console.log(`branch: ${inputPath.branch}`);
144+
}
145+
if (inputPath.commit_sha) {
146+
console.log(`commit: ${inputPath.commit_sha}`);
147+
}
148+
if (inputPath.tags?.length) {
149+
console.log(`tags: ${inputPath.tags.join(', ')}`);
150+
}
151+
if (inputPath.version) {
152+
console.log(`version: ${inputPath.version}`);
153+
}
154+
}
155+
} else {
156+
console.log(`⏳ ${baseString} raw OpenAPI specification`);
157+
}
158+
};
159+
16160
export const createClient = async ({
17161
config,
18162
templates,
@@ -22,14 +166,20 @@ export const createClient = async ({
22166
templates: Templates;
23167
watch?: WatchValues;
24168
}) => {
25-
const inputPath = config.input.path;
169+
const inputPath = compileInputPath(config.input);
26170
const timeout = config.watch.timeout;
27171

28172
const watch: WatchValues = _watch || { headers: new Headers() };
29173

174+
logInputPath({
175+
config,
176+
inputPath,
177+
watch: Boolean(_watch),
178+
});
179+
30180
Performance.start('spec');
31181
const { data, error, response } = await getSpec({
32-
inputPath,
182+
inputPath: inputPath.path,
33183
timeout,
34184
watch,
35185
});
@@ -48,15 +198,6 @@ export const createClient = async ({
48198
let context: IR.Context | undefined;
49199

50200
if (data) {
51-
if (config.logs.level !== 'silent') {
52-
if (_watch) {
53-
console.clear();
54-
console.log(`⏳ Input changed, generating from ${inputPath}`);
55-
} else {
56-
console.log(`⏳ Generating from ${inputPath}`);
57-
}
58-
}
59-
60201
Performance.start('parser');
61202
if (
62203
config.experimentalParser &&
@@ -95,7 +236,7 @@ export const createClient = async ({
95236
Performance.end('postprocess');
96237
}
97238

98-
if (config.watch.enabled && typeof inputPath === 'string') {
239+
if (config.watch.enabled && typeof inputPath.path === 'string') {
99240
setTimeout(() => {
100241
createClient({ config, templates, watch });
101242
}, config.watch.interval);

packages/openapi-ts/src/getSpec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ export const getSpec = async ({
3030
watch: WatchValues;
3131
}): Promise<SpecResponse | SpecError> => {
3232
const refParser = new $RefParser();
33-
const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath });
33+
// TODO: patch @hey-api/json-schema-ref-parser to correctly handle raw spec
34+
const resolvedInput =
35+
typeof inputPath === 'string'
36+
? getResolvedInput({ pathOrUrlOrSchema: inputPath })
37+
: ({
38+
path: '',
39+
schema: inputPath,
40+
type: 'json',
41+
} as const);
3442

3543
let arrayBuffer: ArrayBuffer | undefined;
3644
// boolean signals whether the file has **definitely** changed

packages/openapi-ts/src/initConfigs.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ const getInput = (userConfig: UserConfig): Config['input'] => {
2828
};
2929
if (typeof userConfig.input === 'string') {
3030
input.path = userConfig.input;
31-
} else if (userConfig.input && userConfig.input.path) {
31+
} else if (
32+
userConfig.input &&
33+
(userConfig.input.path || userConfig.input.organization)
34+
) {
3235
input = {
3336
...input,
37+
path: 'https://get.heyapi.dev',
3438
...userConfig.input,
3539
};
3640
} else {
3741
input = {
3842
...input,
39-
path: userConfig.input,
43+
path: userConfig.input as Record<string, unknown>,
4044
};
4145
}
4246
return input;

0 commit comments

Comments
 (0)