fork of hey-api/openapi-ts because I need some additional things

feat(parser): input supports Scalar API Registry with scalar: prefix

Lubos ce602fed 572bba34

+411 -73
+5
.changeset/twelve-mice-cheer.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + feat(parser): input supports Scalar API Registry with `scalar:` prefix
+11
docs/openapi-ts/configuration/input.md
··· 104 104 105 105 We also provide shorthands for other registries: 106 106 107 + ::: details Scalar 108 + Prefix your input with `scalar:` to use the Scalar API Registry. 109 + 110 + ```js [long] 111 + export default { 112 + input: 'scalar:@scalar/access-service', // [!code ++] 113 + }; 114 + ``` 115 + 116 + ::: 117 + 107 118 ::: details ReadMe 108 119 Prefix your input with `readme:` to use the ReadMe API Registry. 109 120
+18 -6
examples/openapi-ts-axios/src/client/client/client.ts examples/openapi-ts-axios/src/client/client/client.gen.ts
··· 1 - import type { AxiosError, RawAxiosRequestHeaders } from 'axios'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; 2 4 import axios from 'axios'; 3 5 4 - import type { Client, Config } from './types'; 6 + import type { Client, Config } from './types.gen'; 5 7 import { 6 8 buildUrl, 7 9 createConfig, 8 10 mergeConfigs, 9 11 mergeHeaders, 10 12 setAuthParams, 11 - } from './utils'; 13 + } from './utils.gen'; 12 14 13 15 export const createClient = (config: Config = {}): Client => { 14 16 let _config = mergeConfigs(createConfig(), config); 15 17 16 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 - const { auth, ...configWithoutAuth } = _config; 18 - const instance = axios.create(configWithoutAuth); 18 + let instance: AxiosInstance; 19 + 20 + if (_config.axios && !('Axios' in _config.axios)) { 21 + instance = _config.axios; 22 + } else { 23 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 + const { auth, ...configWithoutAuth } = _config; 25 + instance = axios.create(configWithoutAuth); 26 + } 19 27 20 28 const getConfig = (): Config => ({ ..._config }); 21 29 ··· 44 52 ...opts, 45 53 security: opts.security, 46 54 }); 55 + } 56 + 57 + if (opts.requestValidator) { 58 + await opts.requestValidator(opts); 47 59 } 48 60 49 61 if (opts.body && opts.bodySerializer) {
+9 -7
examples/openapi-ts-axios/src/client/client/index.ts
··· 1 - export type { Auth } from '../core/auth'; 2 - export type { QuerySerializerOptions } from '../core/bodySerializer'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 3 5 export { 4 6 formDataBodySerializer, 5 7 jsonBodySerializer, 6 8 urlSearchParamsBodySerializer, 7 - } from '../core/bodySerializer'; 8 - export { buildClientParams } from '../core/params'; 9 - export { createClient } from './client'; 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { createClient } from './client.gen'; 10 12 export type { 11 13 Client, 12 14 ClientOptions, ··· 17 19 RequestOptions, 18 20 RequestResult, 19 21 TDataShape, 20 - } from './types'; 21 - export { createConfig } from './utils'; 22 + } from './types.gen'; 23 + export { createConfig } from './utils.gen';
+12 -6
examples/openapi-ts-axios/src/client/client/types.ts examples/openapi-ts-axios/src/client/client/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 import type { 2 4 AxiosError, 3 5 AxiosInstance, 6 + AxiosRequestHeaders, 4 7 AxiosResponse, 5 8 AxiosStatic, 6 9 CreateAxiosDefaults, 7 10 } from 'axios'; 8 11 9 - import type { Auth } from '../core/auth'; 10 - import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 12 + import type { Auth } from '../core/auth.gen'; 13 + import type { 14 + Client as CoreClient, 15 + Config as CoreConfig, 16 + } from '../core/types.gen'; 11 17 12 18 export interface Config<T extends ClientOptions = ClientOptions> 13 19 extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>, 14 20 CoreConfig { 15 21 /** 16 - * Axios implementation. You can use this option to provide a custom 17 - * Axios instance. 22 + * Axios implementation. You can use this option to provide either an 23 + * `AxiosStatic` or an `AxiosInstance`. 18 24 * 19 25 * @default axios 20 26 */ 21 - axios?: AxiosStatic; 27 + axios?: AxiosStatic | AxiosInstance; 22 28 /** 23 29 * Base URL for all requests made by this client. 24 30 */ ··· 30 36 * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 31 37 */ 32 38 headers?: 33 - | CreateAxiosDefaults['headers'] 39 + | AxiosRequestHeaders 34 40 | Record< 35 41 string, 36 42 | string
+37 -7
examples/openapi-ts-axios/src/client/client/utils.ts examples/openapi-ts-axios/src/client/client/utils.gen.ts
··· 1 - import { getAuthToken } from '../core/auth'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { getAuthToken } from '../core/auth.gen'; 2 4 import type { 3 5 QuerySerializer, 4 6 QuerySerializerOptions, 5 - } from '../core/bodySerializer'; 6 - import type { ArraySeparatorStyle } from '../core/pathSerializer'; 7 + } from '../core/bodySerializer.gen'; 8 + import type { ArraySeparatorStyle } from '../core/pathSerializer.gen'; 7 9 import { 8 10 serializeArrayParam, 9 11 serializeObjectParam, 10 12 serializePrimitiveParam, 11 - } from '../core/pathSerializer'; 12 - import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + } from '../core/pathSerializer.gen'; 14 + import type { 15 + Client, 16 + ClientOptions, 17 + Config, 18 + RequestOptions, 19 + } from './types.gen'; 13 20 14 21 interface PathSerializer { 15 22 path: Record<string, unknown>; ··· 138 145 return querySerializer; 139 146 }; 140 147 148 + const checkForExistence = ( 149 + options: Pick<RequestOptions, 'auth' | 'query'> & { 150 + headers: Record<any, unknown>; 151 + }, 152 + name?: string, 153 + ): boolean => { 154 + if (!name) { 155 + return false; 156 + } 157 + if (name in options.headers || options.query?.[name]) { 158 + return true; 159 + } 160 + if ( 161 + 'Cookie' in options.headers && 162 + options.headers['Cookie'] && 163 + typeof options.headers['Cookie'] === 'string' 164 + ) { 165 + return options.headers['Cookie'].includes(`${name}=`); 166 + } 167 + return false; 168 + }; 169 + 141 170 export const setAuthParams = async ({ 142 171 security, 143 172 ...options ··· 146 175 headers: Record<any, unknown>; 147 176 }) => { 148 177 for (const auth of security) { 178 + if (checkForExistence(options, auth.name)) { 179 + continue; 180 + } 149 181 const token = await getAuthToken(auth, options.auth); 150 182 151 183 if (!token) { ··· 175 207 options.headers[name] = token; 176 208 break; 177 209 } 178 - 179 - return; 180 210 } 181 211 }; 182 212
+2
examples/openapi-ts-axios/src/client/core/auth.ts examples/openapi-ts-axios/src/client/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 export type AuthToken = string | undefined; 2 4 3 5 export interface Auth {
+15 -7
examples/openapi-ts-axios/src/client/core/bodySerializer.ts examples/openapi-ts-axios/src/client/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 import type { 2 4 ArrayStyle, 3 5 ObjectStyle, 4 6 SerializerOptions, 5 - } from './pathSerializer'; 7 + } from './pathSerializer.gen'; 6 8 7 9 export type QuerySerializer = (query: Record<string, unknown>) => string; 8 10 ··· 14 16 object?: SerializerOptions<ObjectStyle>; 15 17 } 16 18 17 - const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 19 + const serializeFormDataPair = ( 20 + data: FormData, 21 + key: string, 22 + value: unknown, 23 + ): void => { 18 24 if (typeof value === 'string' || value instanceof Blob) { 19 25 data.append(key, value); 26 + } else if (value instanceof Date) { 27 + data.append(key, value.toISOString()); 20 28 } else { 21 29 data.append(key, JSON.stringify(value)); 22 30 } ··· 26 34 data: URLSearchParams, 27 35 key: string, 28 36 value: unknown, 29 - ) => { 37 + ): void => { 30 38 if (typeof value === 'string') { 31 39 data.append(key, value); 32 40 } else { ··· 37 45 export const formDataBodySerializer = { 38 46 bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 47 body: T, 40 - ) => { 48 + ): FormData => { 41 49 const data = new FormData(); 42 50 43 51 Object.entries(body).forEach(([key, value]) => { ··· 56 64 }; 57 65 58 66 export const jsonBodySerializer = { 59 - bodySerializer: <T>(body: T) => 60 - JSON.stringify(body, (key, value) => 67 + bodySerializer: <T>(body: T): string => 68 + JSON.stringify(body, (_key, value) => 61 69 typeof value === 'bigint' ? value.toString() : value, 62 70 ), 63 71 }; ··· 65 73 export const urlSearchParamsBodySerializer = { 66 74 bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 75 body: T, 68 - ) => { 76 + ): string => { 69 77 const data = new URLSearchParams(); 70 78 71 79 Object.entries(body).forEach(([key, value]) => {
+12
examples/openapi-ts-axios/src/client/core/params.ts examples/openapi-ts-axios/src/client/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 type Slot = 'body' | 'headers' | 'path' | 'query'; 2 4 3 5 export type Field = 4 6 | { 5 7 in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 6 11 key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 7 16 map?: string; 8 17 } 9 18 | { 10 19 in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 11 23 key?: string; 12 24 map?: string; 13 25 };
+2
examples/openapi-ts-axios/src/client/core/pathSerializer.ts examples/openapi-ts-axios/src/client/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 1 3 interface SerializeOptions<T> 2 4 extends SerializePrimitiveOptions, 3 5 SerializerOptions<T> {}
+24 -2
examples/openapi-ts-axios/src/client/core/types.ts examples/openapi-ts-axios/src/client/core/types.gen.ts
··· 1 - import type { Auth, AuthToken } from './auth'; 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 2 4 import type { 3 5 BodySerializer, 4 6 QuerySerializer, 5 7 QuerySerializerOptions, 6 - } from './bodySerializer'; 8 + } from './bodySerializer.gen'; 7 9 8 10 export interface Client< 9 11 RequestFn = never, ··· 85 87 */ 86 88 querySerializer?: QuerySerializer | QuerySerializerOptions; 87 89 /** 90 + * A function validating request data. This is useful if you want to ensure 91 + * the request conforms to the desired shape, so it can be safely sent to 92 + * the server. 93 + */ 94 + requestValidator?: (data: unknown) => Promise<unknown>; 95 + /** 88 96 * A function transforming response data before it's returned. This is useful 89 97 * for post-processing data, e.g. converting ISO strings into Date objects. 90 98 */ ··· 96 104 */ 97 105 responseValidator?: (data: unknown) => Promise<unknown>; 98 106 } 107 + 108 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 109 + ? true 110 + : [T] extends [never | undefined] 111 + ? [undefined] extends [T] 112 + ? false 113 + : true 114 + : false; 115 + 116 + export type OmitNever<T extends Record<string, unknown>> = { 117 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true 118 + ? never 119 + : K]: T[K]; 120 + };
+5 -5
examples/openapi-ts-axios/src/client/sdk.gen.ts
··· 138 138 * Multiple status values can be provided with comma separated strings. 139 139 */ 140 140 export const findPetsByStatus = <ThrowOnError extends boolean = false>( 141 - options?: Options<FindPetsByStatusData, ThrowOnError>, 141 + options: Options<FindPetsByStatusData, ThrowOnError>, 142 142 ) => 143 - (options?.client ?? _heyApiClient).get< 143 + (options.client ?? _heyApiClient).get< 144 144 FindPetsByStatusResponses, 145 145 FindPetsByStatusErrors, 146 146 ThrowOnError ··· 161 161 * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 162 162 */ 163 163 export const findPetsByTags = <ThrowOnError extends boolean = false>( 164 - options?: Options<FindPetsByTagsData, ThrowOnError>, 164 + options: Options<FindPetsByTagsData, ThrowOnError>, 165 165 ) => 166 - (options?.client ?? _heyApiClient).get< 166 + (options.client ?? _heyApiClient).get< 167 167 FindPetsByTagsResponses, 168 168 FindPetsByTagsErrors, 169 169 ThrowOnError ··· 410 410 LoginUserErrors, 411 411 ThrowOnError 412 412 >({ 413 - responseType: 'blob', 413 + responseType: 'json', 414 414 url: '/user/login', 415 415 ...options, 416 416 });
+5 -5
examples/openapi-ts-axios/src/client/types.gen.ts
··· 136 136 export type FindPetsByStatusData = { 137 137 body?: never; 138 138 path?: never; 139 - query?: { 139 + query: { 140 140 /** 141 141 * Status values that need to be considered for filter 142 142 */ 143 - status?: 'available' | 'pending' | 'sold'; 143 + status: 'available' | 'pending' | 'sold'; 144 144 }; 145 145 url: '/pet/findByStatus'; 146 146 }; ··· 169 169 export type FindPetsByTagsData = { 170 170 body?: never; 171 171 path?: never; 172 - query?: { 172 + query: { 173 173 /** 174 174 * Tags to filter by 175 175 */ 176 - tags?: Array<string>; 176 + tags: Array<string>; 177 177 }; 178 178 url: '/pet/findByTags'; 179 179 }; ··· 560 560 /** 561 561 * successful operation 562 562 */ 563 - 200: Blob | File; 563 + 200: string; 564 564 }; 565 565 566 566 export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses];
+2
packages/openapi-ts-tests/main/test/openapi-ts.config.ts
··· 39 39 // 'full.yaml', 40 40 // 'validators-circular-ref.json', 41 41 ), 42 + // https://registry.scalar.com/@lubos-heyapi-dev-team/apis/demo-api-scalar-galaxy/latest?format=json 43 + // path: 'scalar:@lubos-heyapi-dev-team/demo-api-scalar-galaxy', 42 44 // path: 'hey-api/backend', 43 45 // path: 'hey-api/backend?branch=main&version=1.0.0', 44 46 // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0',
+3 -19
packages/openapi-ts/src/config/input.ts
··· 1 1 import type { Config, UserConfig } from '../types/config'; 2 2 import type { Input } from '../types/input'; 3 - import { 4 - heyApiRegistryBaseUrl, 5 - inputToHeyApiPath, 6 - } from '../utils/input/heyApi'; 7 - import { inputToReadmePath } from '../utils/input/readme'; 3 + import { inputToApiRegistry } from '../utils/input'; 4 + import { heyApiRegistryBaseUrl } from '../utils/input/heyApi'; 8 5 9 6 const defaultWatch: Config['input']['watch'] = { 10 7 enabled: false, ··· 69 66 } 70 67 71 68 if (typeof input.path === 'string') { 72 - if (input.path.startsWith('readme:')) { 73 - input.path = inputToReadmePath(input.path); 74 - } else if (!input.path.startsWith('.')) { 75 - if (input.path.startsWith(heyApiRegistryBaseUrl)) { 76 - input.path = input.path.slice(heyApiRegistryBaseUrl.length + 1); 77 - input.path = inputToHeyApiPath(input as Input & { path: string }); 78 - } else { 79 - const parts = input.path.split('/'); 80 - const cleanParts = parts.filter(Boolean); 81 - if (parts.length === 2 && cleanParts.length === 2) { 82 - input.path = inputToHeyApiPath(input as Input & { path: string }); 83 - } 84 - } 85 - } 69 + inputToApiRegistry(input as Input & { path: string }); 86 70 } 87 71 88 72 if (
+1
packages/openapi-ts/src/types/config.d.ts
··· 33 33 | `${string}/${string}` 34 34 | `readme:@${string}/${string}#${string}` 35 35 | `readme:${string}` 36 + | `scalar:@${string}/${string}` 36 37 | (string & {}) 37 38 | (Record<string, unknown> & { path?: never }) 38 39 | Input;
+1
packages/openapi-ts/src/types/input.d.ts
··· 49 49 | `${string}/${string}` 50 50 | `readme:@${string}/${string}#${string}` 51 51 | `readme:${string}` 52 + | `scalar:@${string}/${string}` 52 53 | (string & {}) 53 54 | Record<string, unknown>; 54 55 /**
+136
packages/openapi-ts/src/utils/input/__tests__/scalar.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { 4 + getRegistryUrl, 5 + inputToScalarPath, 6 + type Parsed, 7 + parseShorthand, 8 + } from '../scalar'; 9 + 10 + describe('readme utils', () => { 11 + describe('parseShorthand', () => { 12 + it('should parse full format with organization and project', () => { 13 + const result = parseShorthand('@myorg/myproject'); 14 + expect(result).toEqual({ 15 + organization: '@myorg', 16 + project: 'myproject', 17 + }); 18 + }); 19 + 20 + it('should parse organization and project with hyphens', () => { 21 + const result = parseShorthand('@my-org/my-project'); 22 + expect(result).toEqual({ 23 + organization: '@my-org', 24 + project: 'my-project', 25 + }); 26 + }); 27 + 28 + it('should throw error for invalid formats', () => { 29 + expect(() => parseShorthand('')).toThrow( 30 + 'Invalid Scalar shorthand format', 31 + ); 32 + expect(() => parseShorthand('@org')).toThrow( 33 + 'Invalid Scalar shorthand format', 34 + ); 35 + expect(() => parseShorthand('@org/project#')).toThrow( 36 + 'Invalid Scalar shorthand format', 37 + ); 38 + expect(() => parseShorthand('https://example.com')).toThrow( 39 + 'Invalid Scalar shorthand format', 40 + ); 41 + }); 42 + 43 + it('should throw error for invalid UUID characters', () => { 44 + expect(() => parseShorthand('abc@123')).toThrow( 45 + 'Invalid Scalar shorthand format', 46 + ); 47 + expect(() => parseShorthand('abc/123')).toThrow( 48 + 'Invalid Scalar shorthand format', 49 + ); 50 + expect(() => parseShorthand('abc#123')).toThrow( 51 + 'Invalid Scalar shorthand format', 52 + ); 53 + expect(() => parseShorthand('abc 123')).toThrow( 54 + 'Invalid Scalar shorthand format', 55 + ); 56 + }); 57 + 58 + it('should handle empty UUID', () => { 59 + expect(() => parseShorthand('@org/project#')).toThrow( 60 + 'Invalid Scalar shorthand format', 61 + ); 62 + }); 63 + }); 64 + 65 + describe('getRegistryUrl', () => { 66 + it('should generate correct URL', () => { 67 + expect(getRegistryUrl('@foo', 'bar')).toBe( 68 + 'https://registry.scalar.com/@foo/apis/bar/latest?format=json', 69 + ); 70 + expect(getRegistryUrl('@foo-with-hyphens', 'bar')).toBe( 71 + 'https://registry.scalar.com/@foo-with-hyphens/apis/bar/latest?format=json', 72 + ); 73 + }); 74 + }); 75 + 76 + describe('inputToScalarPath', () => { 77 + it('should transform full format to API URL', () => { 78 + const result = inputToScalarPath('scalar:@foo/bar'); 79 + expect(result).toBe( 80 + 'https://registry.scalar.com/@foo/apis/bar/latest?format=json', 81 + ); 82 + }); 83 + 84 + it('should throw error for invalid inputs', () => { 85 + expect(() => inputToScalarPath('invalid')).toThrow( 86 + 'Invalid Scalar shorthand format', 87 + ); 88 + expect(() => inputToScalarPath('')).toThrow( 89 + 'Invalid Scalar shorthand format', 90 + ); 91 + }); 92 + }); 93 + 94 + describe('integration scenarios', () => { 95 + const validInputs: ReadonlyArray<{ expected: Parsed; input: string }> = [ 96 + { 97 + expected: { organization: '@org', project: 'proj' }, 98 + input: '@org/proj', 99 + }, 100 + { 101 + expected: { 102 + organization: '@my-org', 103 + project: 'my-project', 104 + }, 105 + input: '@my-org/my-project', 106 + }, 107 + ]; 108 + 109 + it.each(validInputs)( 110 + 'should handle $input correctly', 111 + ({ expected, input }) => { 112 + expect(parseShorthand(input)).toEqual(expected); 113 + expect(inputToScalarPath(`scalar:${input}`)).toBe( 114 + `https://registry.scalar.com/${expected.organization}/apis/${expected.project}/latest?format=json`, 115 + ); 116 + }, 117 + ); 118 + 119 + const invalidInputs = [ 120 + '', 121 + '@', 122 + '@org', 123 + '@org/', 124 + 'uuid with spaces', 125 + 'uuid@invalid', 126 + 'uuid/invalid', 127 + 'uuid#invalid', 128 + 'https://example.com', 129 + './local-file.yaml', 130 + ]; 131 + 132 + it.each(invalidInputs)('should reject invalid input: %s', (input) => { 133 + expect(() => parseShorthand(input)).toThrow(); 134 + }); 135 + }); 136 + });
+5 -5
packages/openapi-ts/src/utils/input/heyApi.ts
··· 8 8 export const heyApiRegistryBaseUrl = 'https://get.heyapi.dev'; 9 9 10 10 /** 11 - * Generates the Hey API Registry URL for a given UUID. 11 + * Creates a full Hey API Registry URL. 12 12 * 13 - * @param organization - Organization slug 14 - * @param project - Project slug 13 + * @param organization - Hey API organization slug 14 + * @param project - Hey API project slug 15 15 * @param queryParams - Optional query parameters 16 - * @returns The full API registry URL 16 + * @returns The full Hey API registry URL. 17 17 */ 18 18 export const getRegistryUrl = ( 19 19 organization: string, ··· 63 63 } 64 64 65 65 if (!project) { 66 - throw new Error('The Hey API organization cannot be empty.'); 66 + throw new Error('The Hey API project cannot be empty.'); 67 67 } 68 68 69 69 const result: Parsed = {
+36
packages/openapi-ts/src/utils/input/index.ts
··· 1 + import type { Input } from '../../types/input'; 2 + import { heyApiRegistryBaseUrl, inputToHeyApiPath } from './heyApi'; 3 + import { inputToReadmePath } from './readme'; 4 + import { inputToScalarPath } from './scalar'; 5 + 6 + export const inputToApiRegistry = ( 7 + input: Input & { 8 + path: string; 9 + }, 10 + ) => { 11 + if (input.path.startsWith('readme:')) { 12 + input.path = inputToReadmePath(input.path); 13 + return; 14 + } 15 + 16 + if (input.path.startsWith('scalar:')) { 17 + input.path = inputToScalarPath(input.path); 18 + return; 19 + } 20 + 21 + if (input.path.startsWith('.')) { 22 + return; 23 + } 24 + 25 + if (input.path.startsWith(heyApiRegistryBaseUrl)) { 26 + input.path = input.path.slice(heyApiRegistryBaseUrl.length + 1); 27 + input.path = inputToHeyApiPath(input as Input & { path: string }); 28 + return; 29 + } 30 + 31 + const parts = input.path.split('/'); 32 + const cleanParts = parts.filter(Boolean); 33 + if (parts.length === 2 && cleanParts.length === 2) { 34 + input.path = inputToHeyApiPath(input as Input & { path: string }); 35 + } 36 + };
+4 -4
packages/openapi-ts/src/utils/input/readme.ts
··· 4 4 const registryRegExp = /^(@([\w-]+)\/([\w\-.]+)#)?([\w-]+)$/; 5 5 6 6 /** 7 - * Generates the Hey API Registry URL for a given UUID. 7 + * Creates a full ReadMe API Registry URL. 8 8 * 9 - * @param uuid - The Hey API Registry UUID 10 - * @returns The full API registry URL 9 + * @param uuid - ReadMe UUID 10 + * @returns The full ReadMe API registry URL. 11 11 */ 12 12 export const getRegistryUrl = (uuid: string): string => 13 13 `https://dash.readme.com/api/v1/api-registry/${uuid}`; ··· 39 39 const [, , organization, project, uuid] = match; 40 40 41 41 if (!uuid) { 42 - throw new Error('The ReadMe shorthand UUID cannot be empty.'); 42 + throw new Error('The ReadMe UUID cannot be empty.'); 43 43 } 44 44 45 45 const result: Parsed = {
+66
packages/openapi-ts/src/utils/input/scalar.ts
··· 1 + // Regular expression to match Scalar API Registry input formats: 2 + // - @{organization}/{project} 3 + const registryRegExp = /^(@[\w-]+)\/([\w.-]+)$/; 4 + 5 + /** 6 + * Creates a full Scalar API Registry URL. 7 + * 8 + * @param organization - Scalar organization slug 9 + * @param project - Scalar project slug 10 + * @returns The full Scalar API registry URL. 11 + */ 12 + export const getRegistryUrl = (organization: string, project: string): string => 13 + `https://registry.scalar.com/${organization}/apis/${project}/latest?format=json`; 14 + 15 + export interface Parsed { 16 + organization: string; 17 + project: string; 18 + } 19 + 20 + const namespace = 'scalar'; 21 + 22 + /** 23 + * Parses a Scalar input string and extracts components. 24 + * 25 + * @param shorthand - Scalar format string (@org/project) 26 + * @returns Parsed Scalar input components 27 + * @throws Error if the input format is invalid 28 + */ 29 + export const parseShorthand = (shorthand: string): Parsed => { 30 + const match = shorthand.match(registryRegExp); 31 + 32 + if (!match) { 33 + throw new Error( 34 + `Invalid Scalar shorthand format. Expected "${namespace}:@organization/project", received: ${namespace}:${shorthand}`, 35 + ); 36 + } 37 + 38 + const [, organization, project] = match; 39 + 40 + if (!organization) { 41 + throw new Error('The Scalar organization cannot be empty.'); 42 + } 43 + 44 + if (!project) { 45 + throw new Error('The Scalar project cannot be empty.'); 46 + } 47 + 48 + const result: Parsed = { 49 + organization, 50 + project, 51 + }; 52 + 53 + return result; 54 + }; 55 + 56 + /** 57 + * Transforms a Scalar shorthand string to the corresponding API URL. 58 + * 59 + * @param input - Scalar format string 60 + * @returns The Scalar API Registry URL 61 + */ 62 + export const inputToScalarPath = (input: string): string => { 63 + const shorthand = input.slice(`${namespace}:`.length); 64 + const parsed = parseShorthand(shorthand); 65 + return getRegistryUrl(parsed.organization, parsed.project); 66 + };