Skip to content

Commit

Permalink
feat(core): add mode include/exclude to input filters option
Browse files Browse the repository at this point in the history
  • Loading branch information
jbcam committed Nov 8, 2024
1 parent c1851f1 commit 2856d13
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 20 deletions.
24 changes: 24 additions & 0 deletions docs/src/pages/reference/configuration/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ Default Value: `{}`.

If specified, Orval only generates the endpoints after applying the filter.

#### mode

Type: `String`.

Valid values: `include`, `exclude`.

Default Value: `include`.

Combined with `tags` or `schemas`, this setting determines whether to include or exclude the specified items.
For instance, the example below generates endpoints that do not contain the tag `pets`.

```js
module.exports = {
petstore: {
input: {
filters: {
mode: 'exclude',
tags: ['pets'],
},
},
},
};
```

#### tags

Type: Array of `string` or `RegExp`.
Expand Down
98 changes: 98 additions & 0 deletions packages/core/src/generators/schema-definition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from 'vitest';
import type { ContextSpecs, InputFiltersOption, SchemasObject } from '../types';
import { generateSchemasDefinition } from './schema-definition';

describe('generateSchemasDefinition', () => {
const context: ContextSpecs = {
specKey: 'testSpec',
output: {
override: {
useNativeEnums: false,
},
},
target: 'typescript',
specs: {},
};

it('should return an empty array if schemas are empty', () => {
const result = generateSchemasDefinition({}, context, 'Suffix');
expect(result).toEqual([]);
});

it('should generate schemas without filters', () => {
const schemas: SchemasObject = {
TestSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
};

const result = generateSchemasDefinition(schemas, context, 'Suffix');
expect(result).toHaveLength(1);
expect(result[0].name).toBe('TestSchemaSuffix');
});

it('should generate schemas with include filters', () => {
const schemas: SchemasObject = {
TestSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
AnotherSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
};

const filters: InputFiltersOption = {
schemas: ['TestSchema'],
mode: 'include',
};

const result = generateSchemasDefinition(
schemas,
context,
'Suffix',
filters,
);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('TestSchemaSuffix');
});

it('should generate schemas with exclude filters', () => {
const schemas: SchemasObject = {
TestSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
AnotherSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
};

const filters: InputFiltersOption = {
schemas: ['TestSchema'],
mode: 'exclude',
};

const result = generateSchemasDefinition(
schemas,
context,
'Suffix',
filters,
);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('AnotherSchemaSuffix');
});
});
24 changes: 17 additions & 7 deletions packages/core/src/generators/schema-definition.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import isEmpty from 'lodash.isempty';
import { SchemaObject, SchemasObject } from 'openapi3-ts/oas30';
import type { SchemaObject, SchemasObject } from 'openapi3-ts/oas30';
import { getEnum, resolveDiscriminators } from '../getters';
import { resolveRef, resolveValue } from '../resolvers';
import { ContextSpecs, GeneratorSchema, InputFiltersOption } from '../types';
import type {
ContextSpecs,
GeneratorSchema,
InputFiltersOption,
} from '../types';
import {
upath,
isReference,
isString,
jsDoc,
pascal,
sanitize,
isString,
upath,
} from '../utils';
import { generateInterface } from './interface';

Expand All @@ -22,19 +26,25 @@ export const generateSchemasDefinition = (
schemas: SchemasObject = {},
context: ContextSpecs,
suffix: string,
schemasFilters?: InputFiltersOption['schemas'],
filters?: InputFiltersOption,
): GeneratorSchema[] => {
if (isEmpty(schemas)) {
return [];
}

const transformedSchemas = resolveDiscriminators(schemas, context);

let generateSchemas = Object.entries(transformedSchemas);
if (schemasFilters) {
if (filters) {
const schemasFilters = filters.schemas || [];
const mode = filters.mode || 'include';

generateSchemas = generateSchemas.filter(([schemaName]) => {
return schemasFilters.some((filter) =>
const isMatch = schemasFilters.some((filter) =>
isString(filter) ? filter === schemaName : filter.test(schemaName),
);

return mode === 'include' ? isMatch : !isMatch;
});
}

Expand Down
55 changes: 51 additions & 4 deletions packages/core/src/generators/verbs-options.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { _filteredVerbs } from './verbs-options';
import { describe, expect, it } from 'vitest';
import type { NormalizedInputOptions } from '../types';
import { _filteredVerbs } from './verbs-options';

describe('_filteredVerbs', () => {
it('should return all verbs if filters.tags is undefined', () => {
Expand Down Expand Up @@ -33,7 +34,7 @@ describe('_filteredVerbs', () => {
},
};

const filters = {
const filters: NormalizedInputOptions['filters'] = {
tags: ['tag1'],
};

Expand All @@ -42,7 +43,7 @@ describe('_filteredVerbs', () => {
);
});

it('should verbs that match the regex filter', () => {
it('should return verbs that match the regex filter', () => {
const verbs = {
get: {
tags: ['tag1', 'tag2'],
Expand All @@ -54,12 +55,58 @@ describe('_filteredVerbs', () => {
},
};

const filters = {
const filters: NormalizedInputOptions['filters'] = {
tags: [/tag1/],
};

expect(_filteredVerbs(verbs, filters)).toEqual(
Object.entries({ get: verbs.get }),
);
});

describe('filters.mode', () => {
it('should return verbs that match the tag filter', () => {
const verbs = {
get: {
tags: ['tag1', 'tag2'],
responses: {},
},
post: {
tags: ['tag3', 'tag4'],
responses: {},
},
};

const filters: NormalizedInputOptions['filters'] = {
tags: ['tag1'],
mode: 'include',
};

expect(_filteredVerbs(verbs, filters)).toEqual(
Object.entries({ get: verbs.get }),
);
});

it('should return verbs that do not match the tag filter', () => {
const verbs = {
get: {
tags: ['tag1', 'tag2'],
responses: {},
},
post: {
tags: ['tag3', 'tag4'],
responses: {},
},
};

const filters: NormalizedInputOptions['filters'] = {
tags: ['tag1'],
mode: 'exclude',
};

expect(_filteredVerbs(verbs, filters)).toEqual(
Object.entries({ post: verbs.post }),
);
});
});
});
13 changes: 9 additions & 4 deletions packages/core/src/generators/verbs-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
ComponentsObject,
OperationObject,
ParameterObject,
Expand All @@ -14,7 +14,7 @@ import {
getQueryParams,
getResponse,
} from '../getters';
import {
import type {
ContextSpecs,
GeneratorVerbOptions,
GeneratorVerbsOptions,
Expand Down Expand Up @@ -259,15 +259,20 @@ export const _filteredVerbs = (
return Object.entries(verbs);
}

const filterTags = filters.tags || [];
const filterMode = filters.mode || 'include';

return Object.entries(verbs).filter(
([_verb, operation]: [string, OperationObject]) => {
const operationTags = operation.tags || [];
const filterTags = filters.tags || [];
return operationTags.some((tag) =>

const isMatch = operationTags.some((tag) =>
filterTags.some((filterTag) =>
filterTag instanceof RegExp ? filterTag.test(tag) : filterTag === tag,
),
);

return filterMode === 'exclude' ? !isMatch : isMatch;
},
);
};
9 changes: 5 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import {
import type SwaggerParser from '@apidevtools/swagger-parser';
import type { allLocales } from '@faker-js/faker';
import type {
InfoObject,
OpenAPIObject,
OperationObject,
Expand All @@ -10,8 +11,7 @@ import {
SchemaObject,
} from 'openapi3-ts/oas30';
// @ts-ignore // FIXME when running `yarn test` getting `orval:test: ../core/src/types.ts(12,34): error TS7016: Could not find a declaration file for module 'swagger2openapi'. '/home/maxim/orval/node_modules/swagger2openapi/index.js' implicitly has an 'any' type.`
import swagger2openapi from 'swagger2openapi';
import type { allLocales } from '@faker-js/faker';
import type swagger2openapi from 'swagger2openapi';

export interface Options {
output?: string | OutputOptions;
Expand Down Expand Up @@ -193,6 +193,7 @@ export type SwaggerParserOptions = Omit<SwaggerParser.Options, 'validate'> & {
};

export type InputFiltersOption = {
mode?: 'include' | 'exclude';
tags?: (string | RegExp)[];
schemas?: (string | RegExp)[];
};
Expand Down
2 changes: 1 addition & 1 deletion packages/orval/src/import-open-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const getApiSchemas = ({
parsedSchemas,
context,
output.override.components.schemas.suffix,
input.filters?.schemas,
input.filters,
);

const responseDefinition = generateComponentDefinition(
Expand Down
11 changes: 11 additions & 0 deletions tests/configs/default.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ export default defineConfig({
},
output: '../generated/default/petstore-filter/endpoints.ts',
},
'petstore-filter-exlude': {
input: {
target: '../specifications/petstore.yaml',
filters: {
mode: 'exclude',
tags: ['health'],
schemas: ['Error', /Cat/],
},
},
output: '../generated/default/petstore-filter-exclude/endpoints.ts',
},
'petstore-transfomer': {
output: {
target: '../generated/default/petstore-transformer/endpoints.ts',
Expand Down

0 comments on commit 2856d13

Please sign in to comment.