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.

feat: support bulk callback for patch.schemas

- Updated type definition to allow function signature in addition to Record
- Implemented bulk callback for OpenAPI v3 schemas
- Implemented bulk callback for OpenAPI v2 (swagger) definitions
- Added comprehensive tests for both v2 and v3 specs
- Tests cover basic callbacks, mutations, version extraction, and invalid schema handling

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+311 -19
+31 -9
packages/shared/src/config/parser/patch.ts
··· 125 125 * use cases include fixing incorrect data types, removing unwanted 126 126 * properties, adding missing fields, or standardizing date/time formats. 127 127 * 128 + * Can be: 129 + * - `Record<string, fn>`: Patch specific named schemas 130 + * - `function`: Bulk callback receives `(name, schema)` for every schema 131 + * 128 132 * @example 129 133 * ```js 134 + * // Named schemas 130 135 * schemas: { 131 136 * Foo: (schema) => { 132 137 * // convert date-time format to timestamp ··· 146 151 * delete schema.properties.internalField; 147 152 * } 148 153 * } 154 + * 155 + * // Bulk callback for all schemas 156 + * schemas: (name, schema) => { 157 + * const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 158 + * if (match) { 159 + * schema.description = (schema.description || '') + 160 + * `\n@version ${match[1]}.${match[2]}.${match[3]}`; 161 + * } 162 + * } 149 163 * ``` 150 164 */ 151 - schemas?: Record< 152 - string, 153 - ( 154 - schema: 155 - | OpenApiSchemaObject.V2_0_X 156 - | OpenApiSchemaObject.V3_0_X 157 - | OpenApiSchemaObject.V3_1_X, 158 - ) => void 159 - >; 165 + schemas?: 166 + | Record< 167 + string, 168 + ( 169 + schema: 170 + | OpenApiSchemaObject.V2_0_X 171 + | OpenApiSchemaObject.V3_0_X 172 + | OpenApiSchemaObject.V3_1_X, 173 + ) => void 174 + > 175 + | (( 176 + name: string, 177 + schema: 178 + | OpenApiSchemaObject.V2_0_X 179 + | OpenApiSchemaObject.V3_0_X 180 + | OpenApiSchemaObject.V3_1_X, 181 + ) => void); 160 182 /** 161 183 * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 162 184 * Useful for normalizing or overriding the version value before further processing.
+254
packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts
··· 857 857 expect(versionFn).toHaveBeenCalledOnce(); 858 858 expect(spec.openapi).toBe('patched-3.1.0'); 859 859 }); 860 + 861 + it('calls bulk callback function for all schemas', async () => { 862 + const fn = vi.fn(); 863 + 864 + const spec: OpenApi.V3_1_X = { 865 + ...specMetadataV3, 866 + components: { 867 + schemas: { 868 + Bar: { 869 + type: 'object', 870 + }, 871 + Foo: { 872 + type: 'string', 873 + }, 874 + Qux: { 875 + type: 'number', 876 + }, 877 + }, 878 + }, 879 + }; 880 + 881 + await patchOpenApiSpec({ 882 + patchOptions: { 883 + schemas: fn, 884 + }, 885 + spec, 886 + }); 887 + 888 + expect(fn).toHaveBeenCalledTimes(3); 889 + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); 890 + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); 891 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); 892 + }); 893 + 894 + it('bulk callback mutates all schemas', async () => { 895 + const spec: OpenApi.V3_1_X = { 896 + ...specMetadataV3, 897 + components: { 898 + schemas: { 899 + Bar: { 900 + description: 'Bar schema', 901 + type: 'object', 902 + }, 903 + Foo: { 904 + description: 'Foo schema', 905 + type: 'string', 906 + }, 907 + }, 908 + }, 909 + }; 910 + 911 + await patchOpenApiSpec({ 912 + patchOptions: { 913 + schemas: (name, schema) => { 914 + schema.description = `${schema.description} - patched`; 915 + }, 916 + }, 917 + spec, 918 + }); 919 + 920 + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - patched'); 921 + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - patched'); 922 + }); 923 + 924 + it('bulk callback can extract version from schema name', async () => { 925 + const spec: OpenApi.V3_1_X = { 926 + ...specMetadataV3, 927 + components: { 928 + schemas: { 929 + OtherSchema: { 930 + type: 'string', 931 + }, 932 + ServiceRoot_v1_20_0_ServiceRoot: { 933 + description: 'Service root', 934 + type: 'object', 935 + }, 936 + User_v2_3_1_User: { 937 + description: 'User object', 938 + type: 'object', 939 + }, 940 + }, 941 + }, 942 + }; 943 + 944 + await patchOpenApiSpec({ 945 + patchOptions: { 946 + schemas: (name, schema) => { 947 + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 948 + if (match) { 949 + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; 950 + } 951 + }, 952 + }, 953 + spec, 954 + }); 955 + 956 + expect(spec.components?.schemas?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( 957 + 'Service root\n@version 1.20.0', 958 + ); 959 + expect(spec.components?.schemas?.User_v2_3_1_User!.description).toBe( 960 + 'User object\n@version 2.3.1', 961 + ); 962 + expect(spec.components?.schemas?.OtherSchema!.description).toBeUndefined(); 963 + }); 964 + 965 + it('bulk callback skips invalid schemas', async () => { 966 + const fn = vi.fn(); 967 + 968 + const spec: OpenApi.V3_1_X = { 969 + ...specMetadataV3, 970 + components: { 971 + schemas: { 972 + Bar: 123 as any, 973 + Baz: 'invalid' as any, 974 + Foo: null as any, 975 + Qux: { 976 + type: 'string', 977 + }, 978 + }, 979 + }, 980 + }; 981 + 982 + await patchOpenApiSpec({ 983 + patchOptions: { 984 + schemas: fn, 985 + }, 986 + spec, 987 + }); 988 + 989 + expect(fn).toHaveBeenCalledOnce(); 990 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); 991 + }); 860 992 }); 861 993 862 994 describe('OpenAPI v2', () => { ··· 1047 1179 }); 1048 1180 expect(versionFn).toHaveBeenCalledOnce(); 1049 1181 expect(spec.swagger).toBe('patched-2.0'); 1182 + }); 1183 + 1184 + it('calls bulk callback function for all schemas', async () => { 1185 + const fn = vi.fn(); 1186 + 1187 + const spec: OpenApi.V2_0_X = { 1188 + ...specMetadataV2, 1189 + definitions: { 1190 + Bar: { 1191 + type: 'object', 1192 + }, 1193 + Foo: { 1194 + type: 'string', 1195 + }, 1196 + Qux: { 1197 + type: 'number', 1198 + }, 1199 + }, 1200 + }; 1201 + 1202 + await patchOpenApiSpec({ 1203 + patchOptions: { 1204 + schemas: fn, 1205 + }, 1206 + spec, 1207 + }); 1208 + 1209 + expect(fn).toHaveBeenCalledTimes(3); 1210 + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); 1211 + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); 1212 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); 1213 + }); 1214 + 1215 + it('bulk callback mutates all schemas', async () => { 1216 + const spec: OpenApi.V2_0_X = { 1217 + ...specMetadataV2, 1218 + definitions: { 1219 + Bar: { 1220 + description: 'Bar schema', 1221 + type: 'object', 1222 + }, 1223 + Foo: { 1224 + description: 'Foo schema', 1225 + type: 'string', 1226 + }, 1227 + }, 1228 + }; 1229 + 1230 + await patchOpenApiSpec({ 1231 + patchOptions: { 1232 + schemas: (name, schema) => { 1233 + schema.description = `${schema.description} - patched`; 1234 + }, 1235 + }, 1236 + spec, 1237 + }); 1238 + 1239 + expect(spec.definitions?.Bar!.description).toBe('Bar schema - patched'); 1240 + expect(spec.definitions?.Foo!.description).toBe('Foo schema - patched'); 1241 + }); 1242 + 1243 + it('bulk callback can extract version from schema name', async () => { 1244 + const spec: OpenApi.V2_0_X = { 1245 + ...specMetadataV2, 1246 + definitions: { 1247 + OtherSchema: { 1248 + type: 'string', 1249 + }, 1250 + ServiceRoot_v1_20_0_ServiceRoot: { 1251 + description: 'Service root', 1252 + type: 'object', 1253 + }, 1254 + User_v2_3_1_User: { 1255 + description: 'User object', 1256 + type: 'object', 1257 + }, 1258 + }, 1259 + }; 1260 + 1261 + await patchOpenApiSpec({ 1262 + patchOptions: { 1263 + schemas: (name, schema) => { 1264 + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 1265 + if (match) { 1266 + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; 1267 + } 1268 + }, 1269 + }, 1270 + spec, 1271 + }); 1272 + 1273 + expect(spec.definitions?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( 1274 + 'Service root\n@version 1.20.0', 1275 + ); 1276 + expect(spec.definitions?.User_v2_3_1_User!.description).toBe('User object\n@version 2.3.1'); 1277 + expect(spec.definitions?.OtherSchema!.description).toBeUndefined(); 1278 + }); 1279 + 1280 + it('bulk callback skips invalid schemas', async () => { 1281 + const fn = vi.fn(); 1282 + 1283 + const spec: OpenApi.V2_0_X = { 1284 + ...specMetadataV2, 1285 + definitions: { 1286 + Bar: 123 as any, 1287 + Baz: 'invalid' as any, 1288 + Foo: null as any, 1289 + Qux: { 1290 + type: 'string', 1291 + }, 1292 + }, 1293 + }; 1294 + 1295 + await patchOpenApiSpec({ 1296 + patchOptions: { 1297 + schemas: fn, 1298 + }, 1299 + spec, 1300 + }); 1301 + 1302 + expect(fn).toHaveBeenCalledOnce(); 1303 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); 1050 1304 }); 1051 1305 }); 1052 1306
+26 -10
packages/shared/src/openApi/shared/utils/patch.ts
··· 37 37 } 38 38 39 39 if (patchOptions.schemas && spec.definitions) { 40 - for (const key in patchOptions.schemas) { 41 - const schema = spec.definitions[key]; 42 - if (!schema || typeof schema !== 'object') continue; 40 + if (typeof patchOptions.schemas === 'function') { 41 + for (const [key, schema] of Object.entries(spec.definitions)) { 42 + if (schema && typeof schema === 'object') { 43 + patchOptions.schemas(key, schema); 44 + } 45 + } 46 + } else { 47 + for (const key in patchOptions.schemas) { 48 + const schema = spec.definitions[key]; 49 + if (!schema || typeof schema !== 'object') continue; 43 50 44 - const patchFn = patchOptions.schemas[key]!; 45 - patchFn(schema); 51 + const patchFn = patchOptions.schemas[key]!; 52 + patchFn(schema); 53 + } 46 54 } 47 55 } 48 56 ··· 80 88 81 89 if (spec.components) { 82 90 if (patchOptions.schemas && spec.components.schemas) { 83 - for (const key in patchOptions.schemas) { 84 - const schema = spec.components.schemas[key]; 85 - if (!schema || typeof schema !== 'object') continue; 91 + if (typeof patchOptions.schemas === 'function') { 92 + for (const [key, schema] of Object.entries(spec.components.schemas)) { 93 + if (schema && typeof schema === 'object') { 94 + patchOptions.schemas(key, schema as Parameters<typeof patchOptions.schemas>[1]); 95 + } 96 + } 97 + } else { 98 + for (const key in patchOptions.schemas) { 99 + const schema = spec.components.schemas[key]; 100 + if (!schema || typeof schema !== 'object') continue; 86 101 87 - const patchFn = patchOptions.schemas[key]!; 88 - patchFn(schema as Parameters<typeof patchFn>[0]); 102 + const patchFn = patchOptions.schemas[key]!; 103 + patchFn(schema as Parameters<typeof patchFn>[0]); 104 + } 89 105 } 90 106 } 91 107