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

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2485 from bombillazo/feat/readme-api-registry-support

Add ReadMe API Registry input support

authored by

Lubos and committed by
GitHub
4bc4dd13 726cae72

+661 -121
+5
.changeset/soft-wolves-develop.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + feat(parser): input supports ReadMe API Registry with `readme:` prefix
+36
docs/openapi-ts/configuration/input.md
··· 52 52 If you use an HTTPS URL with a self-signed certificate in development, you will need to set [`NODE_TLS_REJECT_UNAUTHORIZED=0`](https://github.com/hey-api/openapi-ts/issues/276#issuecomment-2043143501) in your environment. 53 53 ::: 54 54 55 + ### ReadMe API Registry 56 + 57 + You can use ReadMe API Registry UUIDs to fetch OpenAPI specifications directly from ReadMe's platform. This is useful when API providers use ReadMe as their source of truth for API documentation. 58 + 59 + ::: code-group 60 + 61 + ```js [simple format] 62 + export default { 63 + input: 'readme:abc123def456', // [!code ++] 64 + }; 65 + ``` 66 + 67 + ```js [full format] 68 + export default { 69 + input: 'readme:@organization/project#abc123def456', // [!code ++] 70 + }; 71 + ``` 72 + 73 + ```js [object format] 74 + export default { 75 + input: { 76 + path: 'readme:abc123def456', // [!code ++] 77 + // ...other options 78 + }, 79 + }; 80 + ``` 81 + 82 + ::: 83 + 84 + The ReadMe input formats are: 85 + 86 + - `readme:uuid` - Simple format using only the UUID 87 + - `readme:@organization/project#uuid` - Full format including organization and project names 88 + 89 + Both formats will fetch the OpenAPI specification from `https://dash.readme.com/api/v1/api-registry/{uuid}`. 90 + 55 91 ### Hey API Platform options 56 92 57 93 You might want to use the [Hey API Platform](/openapi-ts/integrations) to store your specifications. If you do so, the `input` object provides options to help with constructing the correct URL.
+116 -116
examples/openapi-ts-openai/src/client/types.gen.ts
··· 194 194 } & ErrorEvent); 195 195 196 196 export const AssistantSupportedModels = { 197 + GPT_3_5_TURBO: 'gpt-3.5-turbo', 198 + GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', 199 + GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', 200 + GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', 201 + GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', 202 + GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', 203 + GPT_4: 'gpt-4', 197 204 GPT_4O: 'gpt-4o', 198 205 GPT_4O_2024_05_13: 'gpt-4o-2024-05-13', 199 206 GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', 200 207 GPT_4O_2024_11_20: 'gpt-4o-2024-11-20', 201 - GPT_4_1: 'gpt-4.1', 202 208 GPT_4O_MINI: 'gpt-4o-mini', 203 - GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', 204 209 GPT_4O_MINI_2024_07_18: 'gpt-4o-mini-2024-07-18', 205 - GPT_4_1_MINI: 'gpt-4.1-mini', 206 210 GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', 207 - GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', 208 - GPT_4: 'gpt-4', 209 - GPT_4_1_NANO: 'gpt-4.1-nano', 210 211 GPT_4_0314: 'gpt-4-0314', 211 - GPT_5: 'gpt-5', 212 212 GPT_4_0613: 'gpt-4-0613', 213 - GPT_5_2025_08_07: 'gpt-5-2025-08-07', 214 - GPT_3_5_TURBO: 'gpt-3.5-turbo', 215 - GPT_5_MINI: 'gpt-5-mini', 216 - GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', 217 - GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', 218 - GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', 219 - GPT_5_NANO: 'gpt-5-nano', 220 - GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', 221 - GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', 222 - GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', 223 - GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', 224 - GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', 213 + GPT_4_1: 'gpt-4.1', 225 214 GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', 226 - O1: 'o1', 215 + GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', 216 + GPT_4_1_MINI: 'gpt-4.1-mini', 217 + GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', 218 + GPT_4_1_NANO: 'gpt-4.1-nano', 219 + GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', 227 220 GPT_4_32K: 'gpt-4-32k', 228 - O3_MINI: 'o3-mini', 229 221 GPT_4_32K_0314: 'gpt-4-32k-0314', 230 - O3_MINI_2025_01_31: 'o3-mini-2025-01-31', 231 222 GPT_4_32K_0613: 'gpt-4-32k-0613', 232 - O1_2024_12_17: 'o1-2024-12-17', 233 223 GPT_4_5_PREVIEW: 'gpt-4.5-preview', 234 224 GPT_4_5_PREVIEW_2025_02_27: 'gpt-4.5-preview-2025-02-27', 235 225 GPT_4_TURBO: 'gpt-4-turbo', 236 226 GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', 237 227 GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', 238 228 GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', 229 + GPT_5: 'gpt-5', 230 + GPT_5_2025_08_07: 'gpt-5-2025-08-07', 231 + GPT_5_MINI: 'gpt-5-mini', 232 + GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', 233 + GPT_5_NANO: 'gpt-5-nano', 234 + GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', 235 + O1: 'o1', 236 + O1_2024_12_17: 'o1-2024-12-17', 237 + O3_MINI: 'o3-mini', 238 + O3_MINI_2025_01_31: 'o3-mini-2025-01-31', 239 239 } as const; 240 240 241 241 export type AssistantSupportedModels = ··· 702 702 /** 703 703 * The details for events with this `type`. 704 704 */ 705 - 'user.added'?: { 705 + 'rate_limit.updated'?: { 706 706 /** 707 - * The user ID. 707 + * The payload used to update the rate limits. 708 708 */ 709 - id?: string; 710 - /** 711 - * The payload used to add the user to the project. 712 - */ 713 - data?: { 709 + changes_requested?: { 710 + /** 711 + * The maximum batch input tokens per day. Only relevant for certain models. 712 + */ 713 + batch_1_day_max_input_tokens?: number; 714 + /** 715 + * The maximum audio megabytes per minute. Only relevant for certain models. 716 + */ 717 + max_audio_megabytes_per_1_minute?: number; 718 + /** 719 + * The maximum images per minute. Only relevant for certain models. 720 + */ 721 + max_images_per_1_minute?: number; 722 + /** 723 + * The maximum requests per day. Only relevant for certain models. 724 + */ 725 + max_requests_per_1_day?: number; 726 + /** 727 + * The maximum requests per minute. 728 + */ 729 + max_requests_per_1_minute?: number; 714 730 /** 715 - * The role of the user. Is either `owner` or `member`. 731 + * The maximum tokens per minute. 716 732 */ 717 - role?: string; 733 + max_tokens_per_1_minute?: number; 718 734 }; 735 + /** 736 + * The rate limit ID 737 + */ 738 + id?: string; 719 739 }; 720 740 /** 721 741 * The details for events with this `type`. 722 742 */ 723 - 'user.updated'?: { 724 - /** 725 - * The project ID. 726 - */ 727 - id?: string; 743 + 'service_account.created'?: { 728 744 /** 729 - * The payload used to update the user. 745 + * The payload used to create the service account. 730 746 */ 731 - changes_requested?: { 747 + data?: { 732 748 /** 733 - * The role of the user. Is either `owner` or `member`. 749 + * The role of the service account. Is either `owner` or `member`. 734 750 */ 735 751 role?: string; 736 752 }; 753 + /** 754 + * The service account ID. 755 + */ 756 + id?: string; 737 757 }; 738 758 /** 739 759 * The details for events with this `type`. ··· 762 782 */ 763 783 id?: string; 764 784 }; 785 + type: AuditLogEventType; 765 786 /** 766 787 * The details for events with this `type`. 767 788 */ 768 - 'rate_limit.updated'?: { 789 + 'user.added'?: { 769 790 /** 770 - * The payload used to update the rate limits. 791 + * The payload used to add the user to the project. 771 792 */ 772 - changes_requested?: { 793 + data?: { 773 794 /** 774 - * The maximum requests per minute. 795 + * The role of the user. Is either `owner` or `member`. 775 796 */ 776 - max_requests_per_1_minute?: number; 777 - /** 778 - * The maximum tokens per minute. 779 - */ 780 - max_tokens_per_1_minute?: number; 781 - /** 782 - * The maximum images per minute. Only relevant for certain models. 783 - */ 784 - max_images_per_1_minute?: number; 785 - /** 786 - * The maximum audio megabytes per minute. Only relevant for certain models. 787 - */ 788 - max_audio_megabytes_per_1_minute?: number; 789 - /** 790 - * The maximum requests per day. Only relevant for certain models. 791 - */ 792 - max_requests_per_1_day?: number; 793 - /** 794 - * The maximum batch input tokens per day. Only relevant for certain models. 795 - */ 796 - batch_1_day_max_input_tokens?: number; 797 + role?: string; 797 798 }; 798 799 /** 799 - * The rate limit ID 800 + * The user ID. 800 801 */ 801 802 id?: string; 802 803 }; 803 - type: AuditLogEventType; 804 804 /** 805 805 * The details for events with this `type`. 806 806 */ 807 - 'service_account.created'?: { 808 - /** 809 - * The payload used to create the service account. 810 - */ 811 - data?: { 812 - /** 813 - * The role of the service account. Is either `owner` or `member`. 814 - */ 815 - role?: string; 816 - }; 807 + 'user.deleted'?: { 817 808 /** 818 - * The service account ID. 809 + * The user ID. 819 810 */ 820 811 id?: string; 821 812 }; 822 813 /** 823 814 * The details for events with this `type`. 824 815 */ 825 - 'user.deleted'?: { 816 + 'user.updated'?: { 826 817 /** 827 - * The user ID. 818 + * The payload used to update the user. 819 + */ 820 + changes_requested?: { 821 + /** 822 + * The role of the user. Is either `owner` or `member`. 823 + */ 824 + role?: string; 825 + }; 826 + /** 827 + * The project ID. 828 828 */ 829 829 id?: string; 830 830 }; ··· 18135 18135 } & MessageDeltaContentImageUrlObject); 18136 18136 18137 18137 export const ChatModel = { 18138 - GPT_4_1: 'gpt-4.1', 18139 - GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', 18140 - GPT_4_1_MINI: 'gpt-4.1-mini', 18141 - GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', 18142 - GPT_4_1_NANO: 'gpt-4.1-nano', 18138 + CHATGPT_4O_LATEST: 'chatgpt-4o-latest', 18139 + CODEX_MINI_LATEST: 'codex-mini-latest', 18140 + GPT_3_5_TURBO: 'gpt-3.5-turbo', 18141 + GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', 18142 + GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', 18143 + GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', 18144 + GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', 18145 + GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', 18146 + GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', 18147 + GPT_4: 'gpt-4', 18143 18148 GPT_4O: 'gpt-4o', 18144 - GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', 18145 - GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', 18146 - GPT_5: 'gpt-5', 18147 18149 GPT_4O_2024_05_13: 'gpt-4o-2024-05-13', 18148 - GPT_5_2025_08_07: 'gpt-5-2025-08-07', 18150 + GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', 18149 18151 GPT_4O_2024_11_20: 'gpt-4o-2024-11-20', 18150 - GPT_5_CHAT_LATEST: 'gpt-5-chat-latest', 18151 18152 GPT_4O_AUDIO_PREVIEW: 'gpt-4o-audio-preview', 18152 - GPT_5_MINI: 'gpt-5-mini', 18153 18153 GPT_4O_AUDIO_PREVIEW_2024_10_01: 'gpt-4o-audio-preview-2024-10-01', 18154 - GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', 18155 18154 GPT_4O_AUDIO_PREVIEW_2024_12_17: 'gpt-4o-audio-preview-2024-12-17', 18156 - GPT_5_NANO: 'gpt-5-nano', 18157 18155 GPT_4O_AUDIO_PREVIEW_2025_06_03: 'gpt-4o-audio-preview-2025-06-03', 18158 - GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', 18159 - CHATGPT_4O_LATEST: 'chatgpt-4o-latest', 18160 - O1: 'o1', 18161 - CODEX_MINI_LATEST: 'codex-mini-latest', 18162 - O1_2024_12_17: 'o1-2024-12-17', 18163 18156 GPT_4O_MINI: 'gpt-4o-mini', 18164 - O1_MINI: 'o1-mini', 18165 18157 GPT_4O_MINI_2024_07_18: 'gpt-4o-mini-2024-07-18', 18166 - O3: 'o3', 18167 - GPT_4: 'gpt-4', 18168 - O3_2025_04_16: 'o3-2025-04-16', 18169 18158 GPT_4O_MINI_AUDIO_PREVIEW: 'gpt-4o-mini-audio-preview', 18170 - O4_MINI: 'o4-mini', 18171 18159 GPT_4O_MINI_AUDIO_PREVIEW_2024_12_17: 'gpt-4o-mini-audio-preview-2024-12-17', 18172 - O4_MINI_2025_04_16: 'o4-mini-2025-04-16', 18173 - GPT_3_5_TURBO: 'gpt-3.5-turbo', 18174 - O3_MINI: 'o3-mini', 18175 - GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', 18176 - O3_MINI_2025_01_31: 'o3-mini-2025-01-31', 18177 - GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', 18178 - O1_PREVIEW: 'o1-preview', 18179 - GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', 18180 - O1_PREVIEW_2024_09_12: 'o1-preview-2024-09-12', 18181 - GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', 18182 - O1_MINI_2024_09_12: 'o1-mini-2024-09-12', 18183 - GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', 18184 18160 GPT_4O_MINI_SEARCH_PREVIEW: 'gpt-4o-mini-search-preview', 18185 - GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', 18186 18161 GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11: 18187 18162 'gpt-4o-mini-search-preview-2025-03-11', 18188 18163 GPT_4O_SEARCH_PREVIEW: 'gpt-4o-search-preview', ··· 18190 18165 GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', 18191 18166 GPT_4_0314: 'gpt-4-0314', 18192 18167 GPT_4_0613: 'gpt-4-0613', 18168 + GPT_4_1: 'gpt-4.1', 18193 18169 GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', 18170 + GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', 18171 + GPT_4_1_MINI: 'gpt-4.1-mini', 18172 + GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', 18173 + GPT_4_1_NANO: 'gpt-4.1-nano', 18174 + GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', 18194 18175 GPT_4_32K: 'gpt-4-32k', 18195 18176 GPT_4_32K_0314: 'gpt-4-32k-0314', 18196 18177 GPT_4_32K_0613: 'gpt-4-32k-0613', ··· 18198 18179 GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', 18199 18180 GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', 18200 18181 GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', 18182 + GPT_5: 'gpt-5', 18183 + GPT_5_2025_08_07: 'gpt-5-2025-08-07', 18184 + GPT_5_CHAT_LATEST: 'gpt-5-chat-latest', 18185 + GPT_5_MINI: 'gpt-5-mini', 18186 + GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', 18187 + GPT_5_NANO: 'gpt-5-nano', 18188 + GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', 18189 + O1: 'o1', 18190 + O1_2024_12_17: 'o1-2024-12-17', 18191 + O1_MINI: 'o1-mini', 18192 + O1_MINI_2024_09_12: 'o1-mini-2024-09-12', 18193 + O1_PREVIEW: 'o1-preview', 18194 + O1_PREVIEW_2024_09_12: 'o1-preview-2024-09-12', 18195 + O3: 'o3', 18196 + O3_2025_04_16: 'o3-2025-04-16', 18197 + O3_MINI: 'o3-mini', 18198 + O3_MINI_2025_01_31: 'o3-mini-2025-01-31', 18199 + O4_MINI: 'o4-mini', 18200 + O4_MINI_2025_04_16: 'o4-mini-2025-04-16', 18201 18201 } as const; 18202 18202 18203 18203 export type ChatModel = (typeof ChatModel)[keyof typeof ChatModel];
+204
packages/openapi-ts/src/config/__tests__/input.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import type { UserConfig } from '../../types/config'; 4 + import { getInput } from '../input'; 5 + 6 + describe('input config', () => { 7 + describe('getInput', () => { 8 + it('should handle string input', () => { 9 + const userConfig: UserConfig = { 10 + input: 'https://example.com/openapi.yaml', 11 + output: 'src/client', 12 + }; 13 + 14 + const result = getInput(userConfig); 15 + expect(result.path).toBe('https://example.com/openapi.yaml'); 16 + }); 17 + 18 + it('should transform ReadMe simple format input', () => { 19 + const userConfig: UserConfig = { 20 + input: 'readme:abc123', 21 + output: 'src/client', 22 + }; 23 + 24 + const result = getInput(userConfig); 25 + expect(result.path).toBe( 26 + 'https://dash.readme.com/api/v1/api-registry/abc123', 27 + ); 28 + }); 29 + 30 + it('should transform ReadMe full format input', () => { 31 + const userConfig: UserConfig = { 32 + input: 'readme:@myorg/myproject#uuid123', 33 + output: 'src/client', 34 + }; 35 + 36 + const result = getInput(userConfig); 37 + expect(result.path).toBe( 38 + 'https://dash.readme.com/api/v1/api-registry/uuid123', 39 + ); 40 + }); 41 + 42 + it('should handle ReadMe input with hyphens', () => { 43 + const userConfig: UserConfig = { 44 + input: 'readme:@my-org/my-project#test-uuid-123', 45 + output: 'src/client', 46 + }; 47 + 48 + const result = getInput(userConfig); 49 + expect(result.path).toBe( 50 + 'https://dash.readme.com/api/v1/api-registry/test-uuid-123', 51 + ); 52 + }); 53 + 54 + it('should handle object input with ReadMe path', () => { 55 + const userConfig: UserConfig = { 56 + input: { 57 + fetch: { 58 + headers: { 59 + Authorization: 'Bearer token', 60 + }, 61 + }, 62 + path: 'readme:abc123', 63 + }, 64 + output: 'src/client', 65 + }; 66 + 67 + const result = getInput(userConfig); 68 + expect(result.path).toBe( 69 + 'https://dash.readme.com/api/v1/api-registry/abc123', 70 + ); 71 + }); 72 + 73 + it('should handle object input with ReadMe full format path', () => { 74 + const userConfig: UserConfig = { 75 + input: { 76 + path: 'readme:@org/project#uuid', 77 + watch: true, 78 + }, 79 + output: 'src/client', 80 + }; 81 + 82 + const result = getInput(userConfig); 83 + expect(result.path).toBe( 84 + 'https://dash.readme.com/api/v1/api-registry/uuid', 85 + ); 86 + expect(result.watch.enabled).toBe(true); 87 + }); 88 + 89 + it('should handle HeyAPI input format (existing functionality)', () => { 90 + const userConfig: UserConfig = { 91 + input: { 92 + organization: 'myorg', 93 + project: 'myproject', 94 + }, 95 + output: 'src/client', 96 + }; 97 + 98 + const result = getInput(userConfig); 99 + expect(result.path).toBe('https://get.heyapi.dev'); 100 + }); 101 + 102 + it('should handle object input (existing functionality)', () => { 103 + const userConfig: UserConfig = { 104 + input: { 105 + info: { title: 'Test API', version: '1.0.0' }, 106 + openapi: '3.0.0', 107 + }, 108 + output: 'src/client', 109 + }; 110 + 111 + const result = getInput(userConfig); 112 + expect(result.path).toEqual({ 113 + info: { title: 'Test API', version: '1.0.0' }, 114 + openapi: '3.0.0', 115 + }); 116 + }); 117 + 118 + it('should not transform non-ReadMe string inputs', () => { 119 + const inputs = [ 120 + 'https://example.com/openapi.yaml', 121 + './local-file.yaml', 122 + '/absolute/path/to/file.json', 123 + 'file.yaml', 124 + ]; 125 + 126 + inputs.forEach((input) => { 127 + const userConfig: UserConfig = { input, output: 'src/client' }; 128 + const result = getInput(userConfig); 129 + expect(result.path).toBe(input); 130 + }); 131 + }); 132 + 133 + it('should handle watch options with ReadMe inputs', () => { 134 + const userConfig: UserConfig = { 135 + input: 'readme:abc123', 136 + output: 'src/client', 137 + watch: { 138 + enabled: true, 139 + interval: 2000, 140 + }, 141 + }; 142 + 143 + const result = getInput(userConfig); 144 + expect(result.path).toBe( 145 + 'https://dash.readme.com/api/v1/api-registry/abc123', 146 + ); 147 + expect(result.watch.enabled).toBe(true); 148 + expect(result.watch.interval).toBe(2000); 149 + }); 150 + 151 + it('should preserve other input object properties when transforming ReadMe path', () => { 152 + const userConfig: UserConfig = { 153 + input: { 154 + fetch: { 155 + headers: { 'X-Custom': 'value' }, 156 + }, 157 + path: 'readme:test123', 158 + watch: { enabled: true, interval: 1500 }, 159 + }, 160 + output: 'src/client', 161 + }; 162 + 163 + const result = getInput(userConfig); 164 + expect(result.path).toBe( 165 + 'https://dash.readme.com/api/v1/api-registry/test123', 166 + ); 167 + // Note: fetch options are preserved in the input object, not in the result 168 + // The watch options should be processed separately 169 + expect(result.watch.enabled).toBe(true); 170 + expect(result.watch.interval).toBe(1500); 171 + }); 172 + }); 173 + 174 + describe('error handling', () => { 175 + it('should throw error for invalid ReadMe format', () => { 176 + const userConfig: UserConfig = { 177 + input: 'readme:', 178 + output: 'src/client', 179 + }; 180 + 181 + expect(() => getInput(userConfig)).toThrow('Invalid ReadMe input format'); 182 + }); 183 + 184 + it('should throw error for invalid ReadMe UUID', () => { 185 + const userConfig: UserConfig = { 186 + input: 'readme:invalid uuid with spaces', 187 + output: 'src/client', 188 + }; 189 + 190 + expect(() => getInput(userConfig)).toThrow('Invalid ReadMe input format'); 191 + }); 192 + 193 + it('should throw error for invalid ReadMe format in object input', () => { 194 + const userConfig: UserConfig = { 195 + input: { 196 + path: 'readme:@org/project', 197 + }, 198 + output: 'src/client', 199 + }; 200 + 201 + expect(() => getInput(userConfig)).toThrow('Invalid ReadMe input format'); 202 + }); 203 + }); 204 + });
+12 -1
packages/openapi-ts/src/config/input.ts
··· 1 1 import type { Config, UserConfig } from '../types/config'; 2 + import { isReadmeInput, transformReadmeInput } from '../utils/readme'; 2 3 3 4 const defaultWatch: Config['input']['watch'] = { 4 5 enabled: false, ··· 38 39 }; 39 40 40 41 if (typeof userConfig.input === 'string') { 41 - input.path = userConfig.input; 42 + // Handle ReadMe input format transformation 43 + if (isReadmeInput(userConfig.input)) { 44 + input.path = transformReadmeInput(userConfig.input); 45 + } else { 46 + input.path = userConfig.input; 47 + } 42 48 } else if ( 43 49 userConfig.input && 44 50 (userConfig.input.path !== undefined || ··· 50 56 path: 'https://get.heyapi.dev', 51 57 ...userConfig.input, 52 58 }; 59 + 60 + // Handle ReadMe input format transformation when path is specified 61 + if (typeof input.path === 'string' && isReadmeInput(input.path)) { 62 + input.path = transformReadmeInput(input.path); 63 + } 53 64 54 65 // watch only remote files 55 66 if (input.watch !== undefined) {
+8 -2
packages/openapi-ts/src/types/config.d.ts
··· 18 18 */ 19 19 dryRun?: boolean; 20 20 /** 21 - * Path to the OpenAPI specification. This can be either local or remote path. 21 + * Path to the OpenAPI specification. This can be either: 22 + * - local file 23 + * - remote path 24 + * - ReadMe API Registry UUID (full and simplified formats) 25 + * 22 26 * Both JSON and YAML file formats are supported. You can also pass the parsed 23 27 * object directly if you're fetching the file yourself. 24 28 * 25 29 * Alternatively, you can define a configuration object with more options. 26 30 */ 27 31 input: 28 - | 'https://get.heyapi.dev/<organization>/<project>' 32 + | `https://get.heyapi.dev/${string}/${string}` 33 + | `readme:@${string}/${string}#${string}` 34 + | `readme:${string}` 29 35 | (string & {}) 30 36 | (Record<string, unknown> & { path?: never }) 31 37 | Input;
+8 -2
packages/openapi-ts/src/types/input.d.ts
··· 36 36 */ 37 37 organization?: string; 38 38 /** 39 - * Path to the OpenAPI specification. This can be either local or remote path. 39 + * Path to the OpenAPI specification. This can be either: 40 + * - local file 41 + * - remote path 42 + * - ReadMe API Registry UUID (full and simplified formats) 43 + * 40 44 * Both JSON and YAML file formats are supported. You can also pass the parsed 41 45 * object directly if you're fetching the file yourself. 42 46 */ 43 47 path?: 44 - | 'https://get.heyapi.dev/<organization>/<project>' 48 + | `https://get.heyapi.dev/${string}/${string}` 49 + | `readme:@${string}/${string}#${string}` 50 + | `readme:${string}` 45 51 | (string & {}) 46 52 | Record<string, unknown>; 47 53 /**
+199
packages/openapi-ts/src/utils/__tests__/readme.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { 4 + getReadmeApiUrl, 5 + isReadmeInput, 6 + parseReadmeInput, 7 + type ReadmeInput, 8 + transformReadmeInput, 9 + } from '../readme'; 10 + 11 + describe('readme utils', () => { 12 + describe('isReadmeInput', () => { 13 + it('should return true for valid ReadMe formats', () => { 14 + expect(isReadmeInput('readme:abc123')).toBe(true); 15 + expect(isReadmeInput('readme:@org/project#uuid123')).toBe(true); 16 + expect(isReadmeInput('readme:test-uuid-with-hyphens')).toBe(true); 17 + }); 18 + 19 + it('should return false for non-ReadMe inputs', () => { 20 + expect(isReadmeInput('https://example.com')).toBe(false); 21 + expect(isReadmeInput('./local-file.yaml')).toBe(false); 22 + expect(isReadmeInput('random-string')).toBe(false); 23 + expect(isReadmeInput('')).toBe(false); 24 + expect(isReadmeInput('readme')).toBe(false); 25 + expect(isReadmeInput('readmeabc123')).toBe(false); 26 + }); 27 + 28 + it('should handle non-string inputs', () => { 29 + expect(isReadmeInput(123 as any)).toBe(false); 30 + expect(isReadmeInput(null as any)).toBe(false); 31 + expect(isReadmeInput(undefined as any)).toBe(false); 32 + expect(isReadmeInput({} as any)).toBe(false); 33 + }); 34 + }); 35 + 36 + describe('parseReadmeInput', () => { 37 + it('should parse simple UUID format', () => { 38 + const result = parseReadmeInput('readme:abc123'); 39 + expect(result).toEqual({ uuid: 'abc123' }); 40 + }); 41 + 42 + it('should parse UUID with hyphens', () => { 43 + const result = parseReadmeInput('readme:test-uuid-123'); 44 + expect(result).toEqual({ uuid: 'test-uuid-123' }); 45 + }); 46 + 47 + it('should parse full format with organization and project', () => { 48 + const result = parseReadmeInput('readme:@myorg/myproject#uuid123'); 49 + expect(result).toEqual({ 50 + organization: 'myorg', 51 + project: 'myproject', 52 + uuid: 'uuid123', 53 + }); 54 + }); 55 + 56 + it('should parse organization and project with hyphens', () => { 57 + const result = parseReadmeInput('readme:@my-org/my-project#test-uuid'); 58 + expect(result).toEqual({ 59 + organization: 'my-org', 60 + project: 'my-project', 61 + uuid: 'test-uuid', 62 + }); 63 + }); 64 + 65 + it('should throw error for invalid formats', () => { 66 + expect(() => parseReadmeInput('readme:')).toThrow( 67 + 'Invalid ReadMe input format', 68 + ); 69 + expect(() => parseReadmeInput('readme:@org')).toThrow( 70 + 'Invalid ReadMe input format', 71 + ); 72 + expect(() => parseReadmeInput('readme:@org/project')).toThrow( 73 + 'Invalid ReadMe input format', 74 + ); 75 + expect(() => parseReadmeInput('readme:@org/project#')).toThrow( 76 + 'Invalid ReadMe input format', 77 + ); 78 + expect(() => parseReadmeInput('https://example.com')).toThrow( 79 + 'Invalid ReadMe input format', 80 + ); 81 + }); 82 + 83 + it('should throw error for invalid UUID characters', () => { 84 + expect(() => parseReadmeInput('readme:abc@123')).toThrow( 85 + 'Invalid ReadMe input format', 86 + ); 87 + expect(() => parseReadmeInput('readme:abc/123')).toThrow( 88 + 'Invalid ReadMe input format', 89 + ); 90 + expect(() => parseReadmeInput('readme:abc#123')).toThrow( 91 + 'Invalid ReadMe input format', 92 + ); 93 + expect(() => parseReadmeInput('readme:abc 123')).toThrow( 94 + 'Invalid ReadMe input format', 95 + ); 96 + }); 97 + 98 + it('should handle empty UUID', () => { 99 + expect(() => parseReadmeInput('readme:@org/project#')).toThrow( 100 + 'Invalid ReadMe input format', 101 + ); 102 + }); 103 + }); 104 + 105 + describe('getReadmeApiUrl', () => { 106 + it('should generate correct API URL', () => { 107 + expect(getReadmeApiUrl('abc123')).toBe( 108 + 'https://dash.readme.com/api/v1/api-registry/abc123', 109 + ); 110 + expect(getReadmeApiUrl('test-uuid-with-hyphens')).toBe( 111 + 'https://dash.readme.com/api/v1/api-registry/test-uuid-with-hyphens', 112 + ); 113 + }); 114 + }); 115 + 116 + describe('transformReadmeInput', () => { 117 + it('should transform simple UUID format to API URL', () => { 118 + const result = transformReadmeInput('readme:abc123'); 119 + expect(result).toBe('https://dash.readme.com/api/v1/api-registry/abc123'); 120 + }); 121 + 122 + it('should transform full format to API URL', () => { 123 + const result = transformReadmeInput('readme:@myorg/myproject#uuid123'); 124 + expect(result).toBe( 125 + 'https://dash.readme.com/api/v1/api-registry/uuid123', 126 + ); 127 + }); 128 + 129 + it('should throw error for invalid inputs', () => { 130 + expect(() => transformReadmeInput('invalid')).toThrow( 131 + 'Invalid ReadMe input format', 132 + ); 133 + expect(() => transformReadmeInput('readme:')).toThrow( 134 + 'Invalid ReadMe input format', 135 + ); 136 + }); 137 + }); 138 + 139 + describe('integration scenarios', () => { 140 + const validInputs: Array<{ expected: ReadmeInput; input: string }> = [ 141 + { expected: { uuid: 'simple123' }, input: 'readme:simple123' }, 142 + { 143 + expected: { uuid: 'uuid-with-hyphens' }, 144 + input: 'readme:uuid-with-hyphens', 145 + }, 146 + { expected: { uuid: 'UUID123' }, input: 'readme:UUID123' }, 147 + { 148 + expected: { organization: 'org', project: 'proj', uuid: 'uuid' }, 149 + input: 'readme:@org/proj#uuid', 150 + }, 151 + { 152 + expected: { 153 + organization: 'my-org', 154 + project: 'my-project', 155 + uuid: 'my-uuid', 156 + }, 157 + input: 'readme:@my-org/my-project#my-uuid', 158 + }, 159 + ]; 160 + 161 + it.each(validInputs)( 162 + 'should handle $input correctly', 163 + ({ expected, input }) => { 164 + expect(isReadmeInput(input)).toBe(true); 165 + expect(parseReadmeInput(input)).toEqual(expected); 166 + expect(transformReadmeInput(input)).toBe( 167 + `https://dash.readme.com/api/v1/api-registry/${expected.uuid}`, 168 + ); 169 + }, 170 + ); 171 + 172 + const invalidInputs = [ 173 + 'readme:', 174 + 'readme:@', 175 + 'readme:@org', 176 + 'readme:@org/', 177 + 'readme:@org/proj', 178 + 'readme:@org/proj#', 179 + 'readme:uuid with spaces', 180 + 'readme:uuid@invalid', 181 + 'readme:uuid/invalid', 182 + 'readme:uuid#invalid', 183 + 'https://example.com', 184 + './local-file.yaml', 185 + 'random-string', 186 + '', 187 + ]; 188 + 189 + it.each(invalidInputs)('should reject invalid input: %s', (input) => { 190 + if (isReadmeInput(input)) { 191 + expect(() => parseReadmeInput(input)).toThrow(); 192 + } else { 193 + expect(() => parseReadmeInput(input)).toThrow( 194 + 'Invalid ReadMe input format', 195 + ); 196 + } 197 + }); 198 + }); 199 + });
+73
packages/openapi-ts/src/utils/readme.ts
··· 1 + // Regular expression to match ReadMe input formats: 2 + // readme:@organization/project#uuid or readme:uuid 3 + const readmeInputRegExp = /^readme:(?:@([\w-]+)\/([\w-]+)#)?([\w-]+)$/; 4 + 5 + export interface ReadmeInput { 6 + organization?: string; 7 + project?: string; 8 + uuid: string; 9 + } 10 + 11 + /** 12 + * Checks if the input string is a ReadMe format 13 + * @param input - The input string to check 14 + * @returns true if the input matches ReadMe format patterns 15 + */ 16 + export const isReadmeInput = (input: string): boolean => 17 + typeof input === 'string' && input.startsWith('readme:'); 18 + 19 + /** 20 + * Parses a ReadMe input string and extracts components 21 + * @param input - ReadMe format string (readme:@org/project#uuid or readme:uuid) 22 + * @returns Parsed ReadMe input components 23 + * @throws Error if the input format is invalid 24 + */ 25 + export const parseReadmeInput = (input: string): ReadmeInput => { 26 + if (!isReadmeInput(input)) { 27 + throw new Error( 28 + `Invalid ReadMe input format. Expected "readme:@organization/project#uuid" or "readme:uuid", received: ${input}`, 29 + ); 30 + } 31 + 32 + const match = input.match(readmeInputRegExp); 33 + 34 + if (!match) { 35 + throw new Error( 36 + `Invalid ReadMe input format. Expected "readme:@organization/project#uuid" or "readme:uuid", received: ${input}`, 37 + ); 38 + } 39 + 40 + const [, organization, project, uuid] = match; 41 + 42 + // Validate UUID format (basic validation for alphanumeric + hyphens) 43 + if (!uuid || !/^[\w-]+$/.test(uuid)) { 44 + throw new Error(`Invalid UUID format: ${uuid}`); 45 + } 46 + 47 + const result: ReadmeInput = { uuid }; 48 + 49 + if (organization && project) { 50 + result.organization = organization; 51 + result.project = project; 52 + } 53 + 54 + return result; 55 + }; 56 + 57 + /** 58 + * Generates the ReadMe API Registry URL for a given UUID 59 + * @param uuid - The ReadMe API Registry UUID 60 + * @returns The full API URL 61 + */ 62 + export const getReadmeApiUrl = (uuid: string): string => 63 + `https://dash.readme.com/api/v1/api-registry/${uuid}`; 64 + 65 + /** 66 + * Transforms a ReadMe input string to the corresponding API URL 67 + * @param input - ReadMe format string 68 + * @returns The ReadMe API Registry URL 69 + */ 70 + export const transformReadmeInput = (input: string): string => { 71 + const parsed = parseReadmeInput(input); 72 + return getReadmeApiUrl(parsed.uuid); 73 + };