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.

fix: include orphans when explicitly requested

Lubos 8e2c4b1f bb6c4919

+321 -80
+6
.changeset/bright-rooms-tease.md
··· 1 + --- 2 + "@hey-api/shared": patch 3 + "@hey-api/openapi-ts": patch 4 + --- 5 + 6 + **parser**: fix: keep orphans when explicitly included in filters
+133
packages/shared/src/openApi/shared/utils/__tests__/filter.test.ts
··· 1 + import type { Logger } from '@hey-api/codegen-core'; 2 + 3 + import type { ResourceMetadata } from '../../graph/meta'; 4 + import { createFilteredDependencies, type Filters } from '../filter'; 5 + 6 + const loggerStub = { 7 + timeEvent: () => ({ timeEnd: () => {} }), 8 + } as unknown as Logger; 9 + 10 + function createFilters(): Filters { 11 + return { 12 + deprecated: true, 13 + operations: { 14 + exclude: new Set(), 15 + include: new Set(), 16 + }, 17 + orphans: false, 18 + parameters: { 19 + exclude: new Set(), 20 + include: new Set(), 21 + }, 22 + preserveOrder: false, 23 + requestBodies: { 24 + exclude: new Set(), 25 + include: new Set(), 26 + }, 27 + responses: { 28 + exclude: new Set(), 29 + include: new Set(), 30 + }, 31 + schemas: { 32 + exclude: new Set(), 33 + include: new Set(), 34 + }, 35 + tags: { 36 + exclude: new Set(), 37 + include: new Set(), 38 + }, 39 + }; 40 + } 41 + 42 + function createResourceMetadata(): ResourceMetadata { 43 + return { 44 + operations: new Map([ 45 + [ 46 + 'operation/GET /v1/foo', 47 + { 48 + dependencies: new Set(['response/UsedResponse']), 49 + deprecated: false, 50 + tags: new Set(), 51 + }, 52 + ], 53 + ]), 54 + parameters: new Map(), 55 + requestBodies: new Map([ 56 + [ 57 + 'body/IncludedBody', 58 + { 59 + dependencies: new Set(['schema/Baz']), 60 + deprecated: false, 61 + }, 62 + ], 63 + ]), 64 + responses: new Map([ 65 + [ 66 + 'response/UsedResponse', 67 + { 68 + dependencies: new Set(), 69 + deprecated: false, 70 + }, 71 + ], 72 + ]), 73 + schemas: new Map([ 74 + [ 75 + 'schema/Foo', 76 + { 77 + dependencies: new Set(['schema/Baz']), 78 + deprecated: false, 79 + }, 80 + ], 81 + [ 82 + 'schema/Baz', 83 + { 84 + dependencies: new Set(), 85 + deprecated: false, 86 + }, 87 + ], 88 + ]), 89 + }; 90 + } 91 + 92 + describe('createFilteredDependencies', () => { 93 + it('keeps explicitly included schemas and their dependencies when dropping orphans', () => { 94 + const filters = createFilters(); 95 + filters.schemas.include.add('schema/Foo'); 96 + 97 + const { schemas } = createFilteredDependencies({ 98 + filters, 99 + logger: loggerStub, 100 + resourceMetadata: createResourceMetadata(), 101 + }); 102 + 103 + expect(schemas).toEqual(new Set(['schema/Foo', 'schema/Baz'])); 104 + }); 105 + 106 + it('keeps explicitly included request bodies and their schema dependencies when dropping orphans', () => { 107 + const filters = createFilters(); 108 + filters.requestBodies.include.add('body/IncludedBody'); 109 + 110 + const { requestBodies, schemas } = createFilteredDependencies({ 111 + filters, 112 + logger: loggerStub, 113 + resourceMetadata: createResourceMetadata(), 114 + }); 115 + 116 + expect(requestBodies).toEqual(new Set(['body/IncludedBody'])); 117 + expect(schemas).toEqual(new Set(['schema/Baz'])); 118 + }); 119 + 120 + it('prioritizes excludes when the same schema is explicitly included and excluded', () => { 121 + const filters = createFilters(); 122 + filters.schemas.include.add('schema/Foo'); 123 + filters.schemas.exclude.add('schema/Foo'); 124 + 125 + const { schemas } = createFilteredDependencies({ 126 + filters, 127 + logger: loggerStub, 128 + resourceMetadata: createResourceMetadata(), 129 + }); 130 + 131 + expect(schemas).toEqual(new Set()); 132 + }); 133 + });
+182 -80
packages/shared/src/openApi/shared/utils/filter.ts
··· 11 11 12 12 const namespaceNeedle = '/'; 13 13 14 - export const addNamespace = (namespace: FilterNamespace, value: string = ''): string => 15 - `${namespace}${namespaceNeedle}${value}`; 14 + export function addNamespace(namespace: FilterNamespace, value: string = ''): string { 15 + return `${namespace}${namespaceNeedle}${value}`; 16 + } 16 17 17 - export const removeNamespace = ( 18 - key: string, 19 - ): { 18 + export function removeNamespace(key: string): { 20 19 name: string; 21 20 namespace: FilterNamespace; 22 - } => { 21 + } { 23 22 const index = key.indexOf(namespaceNeedle); 24 23 const name = key.slice(index + 1); 25 24 return { 26 25 name, 27 26 namespace: key.slice(0, index)! as FilterNamespace, 28 27 }; 29 - }; 28 + } 30 29 31 30 /** 32 31 * Converts reference strings from OpenAPI $ref keywords into namespaces. 33 32 * 34 33 * @example '#/components/schemas/Foo' -> 'schema' 35 34 */ 36 - export const stringToNamespace = (value: string): FilterNamespace => { 35 + export function stringToNamespace(value: string): FilterNamespace { 37 36 switch (value) { 38 37 case 'parameters': 39 38 return 'parameter'; ··· 47 46 default: 48 47 return 'unknown'; 49 48 } 50 - }; 49 + } 50 + 51 + function getResourceDependencies( 52 + key: string, 53 + resourceMetadata: ResourceMetadata, 54 + ): Set<string> | undefined { 55 + const { namespace } = removeNamespace(key); 56 + if (namespace === 'body') { 57 + return resourceMetadata.requestBodies.get(key)?.dependencies; 58 + } 59 + if (namespace === 'operation') { 60 + return resourceMetadata.operations.get(key)?.dependencies; 61 + } 62 + if (namespace === 'parameter') { 63 + return resourceMetadata.parameters.get(key)?.dependencies; 64 + } 65 + if (namespace === 'response') { 66 + return resourceMetadata.responses.get(key)?.dependencies; 67 + } 68 + if (namespace === 'schema') { 69 + return resourceMetadata.schemas.get(key)?.dependencies; 70 + } 71 + } 51 72 52 73 type FiltersConfigToState<T> = { 53 74 [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U> ··· 64 85 set: Set<string>; 65 86 } 66 87 67 - const createFiltersSetAndRegExps = ( 88 + function createFiltersSetAndRegExps( 68 89 type: FilterNamespace, 69 90 filters: ReadonlyArray<string> | undefined, 70 - ): SetAndRegExps => { 91 + ): SetAndRegExps { 71 92 const keys: Array<string> = []; 72 93 const regexps: Array<RegExp> = []; 73 94 if (filters) { ··· 83 104 regexps, 84 105 set: new Set(keys), 85 106 }; 86 - }; 107 + } 87 108 88 109 interface CollectFiltersSetFromRegExps { 89 110 excludeOperations: SetAndRegExps; ··· 98 119 includeSchemas: SetAndRegExps; 99 120 } 100 121 101 - const collectFiltersSetFromRegExpsOpenApiV2 = ({ 122 + function collectFiltersSetFromRegExpsOpenApiV2({ 102 123 excludeOperations, 103 124 excludeSchemas, 104 125 includeOperations, ··· 106 127 spec, 107 128 }: CollectFiltersSetFromRegExps & { 108 129 spec: OpenApi.V2_0_X; 109 - }) => { 130 + }): void { 110 131 if ((excludeOperations.regexps.length || includeOperations.regexps.length) && spec.paths) { 111 132 for (const entry of Object.entries(spec.paths)) { 112 133 const path = entry[0] as keyof OpenAPIV3_1.PathsObject; ··· 142 163 } 143 164 } 144 165 } 145 - }; 166 + } 146 167 147 - const collectFiltersSetFromRegExpsOpenApiV3 = ({ 168 + function collectFiltersSetFromRegExpsOpenApiV3({ 148 169 excludeOperations, 149 170 excludeParameters, 150 171 excludeRequestBodies, ··· 158 179 spec, 159 180 }: CollectFiltersSetFromRegExps & { 160 181 spec: OpenApi.V3_0_X | OpenApi.V3_1_X; 161 - }) => { 182 + }): void { 162 183 if ((excludeOperations.regexps.length || includeOperations.regexps.length) && spec.paths) { 163 184 for (const entry of Object.entries(spec.paths)) { 164 185 const path = entry[0] as keyof OpenAPIV3_1.PathsObject; ··· 237 258 } 238 259 } 239 260 } 240 - }; 261 + } 241 262 242 - const collectFiltersSetFromRegExps = ({ 263 + function collectFiltersSetFromRegExps({ 243 264 spec, 244 265 ...filters 245 266 }: CollectFiltersSetFromRegExps & { 246 267 spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X; 247 - }): void => { 268 + }): void { 248 269 if ('swagger' in spec) { 249 270 collectFiltersSetFromRegExpsOpenApiV2({ ...filters, spec }); 250 271 } else { 251 272 collectFiltersSetFromRegExpsOpenApiV3({ ...filters, spec }); 252 273 } 253 - }; 274 + } 254 275 255 - export const createFilters = ( 276 + export function createFilters( 256 277 config: Parser['filters'], 257 278 spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, 258 279 logger: Logger, 259 - ): Filters => { 280 + ): Filters { 260 281 const eventCreateFilters = logger.timeEvent('create-filters'); 261 282 const excludeOperations = createFiltersSetAndRegExps('operation', config?.operations?.exclude); 262 283 const includeOperations = createFiltersSetAndRegExps('operation', config?.operations?.include); ··· 314 335 }; 315 336 eventCreateFilters.timeEnd(); 316 337 return filters; 317 - }; 338 + } 318 339 319 - export const hasFilters = (config: Parser['filters']): boolean => { 340 + export function hasFilters(config: Parser['filters']): boolean { 320 341 if (!config) { 321 342 return false; 322 343 } ··· 340 361 config.tags?.exclude?.length || 341 362 config.tags?.include?.length, 342 363 ); 343 - }; 364 + } 344 365 345 366 /** 346 367 * Collect operations that satisfy the include/exclude filters and schema dependencies. 368 + * 369 + * Must be called after dropping components. 347 370 */ 348 - const collectOperations = ({ 371 + function collectOperations({ 349 372 filters, 350 373 parameters, 351 374 requestBodies, ··· 361 384 schemas: Set<string>; 362 385 }): { 363 386 operations: Set<string>; 364 - } => { 387 + } { 365 388 const finalSet = new Set<string>(); 366 389 const initialSet = filters.operations.include.size 367 390 ? filters.operations.include ··· 423 446 finalSet.add(key); 424 447 } 425 448 return { operations: finalSet }; 426 - }; 449 + } 427 450 428 451 /** 429 452 * Collect parameters that satisfy the include/exclude filters and schema dependencies. 430 453 */ 431 - const collectParameters = ({ 454 + function collectParameters({ 432 455 filters, 433 456 resourceMetadata, 434 457 schemas, ··· 438 461 schemas: Set<string>; 439 462 }): { 440 463 parameters: Set<string>; 441 - } => { 464 + } { 442 465 const finalSet = new Set<string>(); 443 466 const initialSet = filters.parameters.include.size 444 467 ? filters.parameters.include ··· 490 513 } 491 514 } 492 515 return { parameters: finalSet }; 493 - }; 516 + } 494 517 495 518 /** 496 519 * Collect request bodies that satisfy the include/exclude filters and schema dependencies. 497 520 */ 498 - const collectRequestBodies = ({ 521 + function collectRequestBodies({ 499 522 filters, 500 523 resourceMetadata, 501 524 schemas, ··· 505 528 schemas: Set<string>; 506 529 }): { 507 530 requestBodies: Set<string>; 508 - } => { 531 + } { 509 532 const finalSet = new Set<string>(); 510 533 const initialSet = filters.requestBodies.include.size 511 534 ? filters.requestBodies.include ··· 557 580 } 558 581 } 559 582 return { requestBodies: finalSet }; 560 - }; 583 + } 561 584 562 585 /** 563 586 * Collect responses that satisfy the include/exclude filters and schema dependencies. 564 587 */ 565 - const collectResponses = ({ 588 + function collectResponses({ 566 589 filters, 567 590 resourceMetadata, 568 591 schemas, ··· 572 595 schemas: Set<string>; 573 596 }): { 574 597 responses: Set<string>; 575 - } => { 598 + } { 576 599 const finalSet = new Set<string>(); 577 600 const initialSet = filters.responses.include.size 578 601 ? filters.responses.include ··· 624 647 } 625 648 } 626 649 return { responses: finalSet }; 627 - }; 650 + } 628 651 629 652 /** 630 653 * Collect schemas that satisfy the include/exclude filters. 631 654 */ 632 - const collectSchemas = ({ 655 + function collectSchemas({ 633 656 filters, 634 657 resourceMetadata, 635 658 }: { ··· 637 660 resourceMetadata: ResourceMetadata; 638 661 }): { 639 662 schemas: Set<string>; 640 - } => { 663 + } { 641 664 const finalSet = new Set<string>(); 642 665 const initialSet = filters.schemas.include.size 643 666 ? filters.schemas.include ··· 679 702 } 680 703 } 681 704 return { schemas: finalSet }; 682 - }; 705 + } 683 706 684 707 /** 685 708 * Drop parameters that depend on already excluded parameters. 686 709 */ 687 - const dropExcludedParameters = ({ 710 + function dropExcludedParameters({ 688 711 filters, 689 712 parameters, 690 713 resourceMetadata, ··· 692 715 filters: Filters; 693 716 parameters: Set<string>; 694 717 resourceMetadata: ResourceMetadata; 695 - }): void => { 718 + }): void { 696 719 if (!filters.parameters.exclude.size) { 697 720 return; 698 721 } ··· 711 734 } 712 735 } 713 736 } 714 - }; 737 + } 715 738 716 739 /** 717 740 * Drop request bodies that depend on already excluded request bodies. 718 741 */ 719 - const dropExcludedRequestBodies = ({ 742 + function dropExcludedRequestBodies({ 720 743 filters, 721 744 requestBodies, 722 745 resourceMetadata, ··· 724 747 filters: Filters; 725 748 requestBodies: Set<string>; 726 749 resourceMetadata: ResourceMetadata; 727 - }): void => { 750 + }): void { 728 751 if (!filters.requestBodies.exclude.size) { 729 752 return; 730 753 } ··· 743 766 } 744 767 } 745 768 } 746 - }; 769 + } 747 770 748 771 /** 749 772 * Drop responses that depend on already excluded responses. 750 773 */ 751 - const dropExcludedResponses = ({ 774 + function dropExcludedResponses({ 752 775 filters, 753 776 resourceMetadata, 754 777 responses, ··· 756 779 filters: Filters; 757 780 resourceMetadata: ResourceMetadata; 758 781 responses: Set<string>; 759 - }): void => { 782 + }): void { 760 783 if (!filters.responses.exclude.size) { 761 784 return; 762 785 } ··· 775 798 } 776 799 } 777 800 } 778 - }; 801 + } 779 802 780 803 /** 781 804 * Drop schemas that depend on already excluded schemas. 782 805 */ 783 - const dropExcludedSchemas = ({ 806 + function dropExcludedSchemas({ 784 807 filters, 785 808 resourceMetadata, 786 809 schemas, ··· 788 811 filters: Filters; 789 812 resourceMetadata: ResourceMetadata; 790 813 schemas: Set<string>; 791 - }): void => { 814 + }): void { 792 815 if (!filters.schemas.exclude.size) { 793 816 return; 794 817 } ··· 807 830 } 808 831 } 809 832 } 810 - }; 833 + } 811 834 812 - const dropOrphans = ({ 835 + function dropOrphans({ 836 + includedDependencies, 813 837 operationDependencies, 814 838 parameters, 815 839 requestBodies, 816 840 responses, 817 841 schemas, 818 842 }: { 843 + includedDependencies: Set<string>; 819 844 operationDependencies: Set<string>; 820 845 parameters: Set<string>; 821 846 requestBodies: Set<string>; 822 847 responses: Set<string>; 823 848 schemas: Set<string>; 824 - }) => { 849 + }): void { 825 850 for (const key of schemas) { 826 - if (!operationDependencies.has(key)) { 851 + if (!operationDependencies.has(key) && !includedDependencies.has(key)) { 827 852 schemas.delete(key); 828 853 } 829 854 } 830 855 for (const key of parameters) { 831 - if (!operationDependencies.has(key)) { 856 + if (!operationDependencies.has(key) && !includedDependencies.has(key)) { 832 857 parameters.delete(key); 833 858 } 834 859 } 835 860 for (const key of requestBodies) { 836 - if (!operationDependencies.has(key)) { 861 + if (!operationDependencies.has(key) && !includedDependencies.has(key)) { 837 862 requestBodies.delete(key); 838 863 } 839 864 } 840 865 for (const key of responses) { 841 - if (!operationDependencies.has(key)) { 866 + if (!operationDependencies.has(key) && !includedDependencies.has(key)) { 842 867 responses.delete(key); 843 868 } 844 869 } 845 - }; 870 + } 871 + 872 + function collectDependencies({ 873 + resourceMetadata, 874 + seeds, 875 + }: { 876 + resourceMetadata: ResourceMetadata; 877 + seeds: Set<string>; 878 + }): { 879 + dependencies: Set<string>; 880 + } { 881 + const finalSet = new Set<string>(); 882 + const stack = [...seeds]; 883 + while (stack.length) { 884 + const key = stack.pop()!; 885 + 886 + if (finalSet.has(key)) { 887 + continue; 888 + } 889 + 890 + finalSet.add(key); 891 + const dependencies = getResourceDependencies(key, resourceMetadata); 892 + 893 + if (!dependencies?.size) { 894 + continue; 895 + } 896 + 897 + for (const dependency of dependencies) { 898 + if (!finalSet.has(dependency)) { 899 + stack.push(dependency); 900 + } 901 + } 902 + } 903 + return { dependencies: finalSet }; 904 + } 905 + 906 + function collectExplicitDependencies({ 907 + filters, 908 + resourceMetadata, 909 + }: { 910 + filters: Filters; 911 + resourceMetadata: ResourceMetadata; 912 + }): { 913 + explicitDependencies: Set<string>; 914 + } { 915 + // Exclude wins over include. Start from non-excluded include seeds. 916 + const seeds = new Set<string>(); 917 + 918 + for (const key of filters.parameters.include) { 919 + if (!filters.parameters.exclude.has(key)) { 920 + seeds.add(key); 921 + } 922 + } 923 + for (const key of filters.requestBodies.include) { 924 + if (!filters.requestBodies.exclude.has(key)) { 925 + seeds.add(key); 926 + } 927 + } 928 + for (const key of filters.responses.include) { 929 + if (!filters.responses.exclude.has(key)) { 930 + seeds.add(key); 931 + } 932 + } 933 + for (const key of filters.schemas.include) { 934 + if (!filters.schemas.exclude.has(key)) { 935 + seeds.add(key); 936 + } 937 + } 938 + 939 + const { dependencies } = collectDependencies({ resourceMetadata, seeds }); 940 + 941 + // Exclude wins for transitive dependencies as well. 942 + for (const key of dependencies) { 943 + const { namespace } = removeNamespace(key); 944 + if ( 945 + (namespace === 'body' && filters.requestBodies.exclude.has(key)) || 946 + (namespace === 'parameter' && filters.parameters.exclude.has(key)) || 947 + (namespace === 'response' && filters.responses.exclude.has(key)) || 948 + (namespace === 'schema' && filters.schemas.exclude.has(key)) 949 + ) { 950 + dependencies.delete(key); 951 + } 952 + } 953 + 954 + return { explicitDependencies: dependencies }; 955 + } 846 956 847 - const collectOperationDependencies = ({ 957 + function collectOperationDependencies({ 848 958 operations, 849 959 resourceMetadata, 850 960 }: { ··· 852 962 resourceMetadata: ResourceMetadata; 853 963 }): { 854 964 operationDependencies: Set<string>; 855 - } => { 965 + } { 856 966 const finalSet = new Set<string>(); 857 967 const initialSet = new Set( 858 968 [...operations].flatMap((key) => [ ··· 868 978 } 869 979 870 980 finalSet.add(key); 871 - 872 - const { namespace } = removeNamespace(key); 873 - let dependencies: Set<string> | undefined; 874 - if (namespace === 'body') { 875 - dependencies = resourceMetadata.requestBodies.get(key)?.dependencies; 876 - } else if (namespace === 'operation') { 877 - dependencies = resourceMetadata.operations.get(key)?.dependencies; 878 - } else if (namespace === 'parameter') { 879 - dependencies = resourceMetadata.parameters.get(key)?.dependencies; 880 - } else if (namespace === 'response') { 881 - dependencies = resourceMetadata.responses.get(key)?.dependencies; 882 - } else if (namespace === 'schema') { 883 - dependencies = resourceMetadata.schemas.get(key)?.dependencies; 884 - } 981 + const dependencies = getResourceDependencies(key, resourceMetadata); 885 982 886 983 if (!dependencies?.size) { 887 984 continue; ··· 894 991 } 895 992 } 896 993 return { operationDependencies: finalSet }; 897 - }; 994 + } 898 995 899 - export const createFilteredDependencies = ({ 996 + export function createFilteredDependencies({ 900 997 filters, 901 998 logger, 902 999 resourceMetadata, ··· 910 1007 requestBodies: Set<string>; 911 1008 responses: Set<string>; 912 1009 schemas: Set<string>; 913 - } => { 1010 + } { 914 1011 const eventCreateFilteredDependencies = logger.timeEvent('create-filtered-dependencies'); 915 1012 const { schemas } = collectSchemas({ filters, resourceMetadata }); 916 1013 const { parameters } = collectParameters({ ··· 949 1046 operations, 950 1047 resourceMetadata, 951 1048 }); 1049 + const { explicitDependencies } = collectExplicitDependencies({ 1050 + filters, 1051 + resourceMetadata, 1052 + }); 952 1053 dropOrphans({ 1054 + includedDependencies: explicitDependencies, 953 1055 operationDependencies, 954 1056 parameters, 955 1057 requestBodies, ··· 966 1068 responses, 967 1069 schemas, 968 1070 }; 969 - }; 1071 + }